aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bot/src/api/routes/quotes.js138
-rw-r--r--bot/src/api/server.js2
-rw-r--r--bot/src/commands/about.js9
-rw-r--r--bot/src/commands/quote.js25
-rw-r--r--bot/src/commands/serverinfo.js6
-rw-r--r--bot/src/commands/stats.js15
-rw-r--r--bot/src/init.js16
-rw-r--r--web/src/app/quotes/page.js67
8 files changed, 195 insertions, 83 deletions
diff --git a/bot/src/api/routes/quotes.js b/bot/src/api/routes/quotes.js
index f362d7e..1b89f67 100644
--- a/bot/src/api/routes/quotes.js
+++ b/bot/src/api/routes/quotes.js
@@ -2,70 +2,88 @@ import { Router } from 'express';
import { pendingQuote, quote as newQuote } from '../../models/quote.js';
import { verifyToken } from './auth.js';
-export const quoteRouter = Router();
+export function quoteRouter(client) {
+ const router = Router();
-quoteRouter.get('/quotes/pending', verifyToken, async (req, res) => {
- try {
- const quotes = await pendingQuote.findAll();
- res.json(quotes);
- } catch (error) {
- console.error('Error fetching quotes:', error);
- res.status(500).send({ message: 'Internal Server Error' });
- }
-});
-
-quoteRouter.post('/quotes/add', verifyToken, async (req, res) => {
- const { author, authorImage, quote, year, submitterID } = req.body;
- try {
- await newQuote.create({
- author: author,
- authorImage: authorImage,
- quote: quote,
- year: year,
- submitter: submitterID
- });
- res.status(200).send({ message: 'Added a new quote' });
- } catch (error) {
- console.error('Something went wrong:', error);
- res.status(500).send({ message: 'Internal Server Error' });
- }
-});
+ router.get('/quotes/pending', verifyToken, async (req, res) => {
+ try {
+ const quotes = await pendingQuote.findAll();
+ res.json(quotes);
+ } catch (error) {
+ console.error('Error fetching quotes:', error);
+ res.status(500).send({ message: 'Internal Server Error' });
+ }
+ });
-quoteRouter.post('/quotes/approve', verifyToken, async (req, res) => {
- const { id } = req.body;
- try {
- const quote = await pendingQuote.findByPk(id);
- if (quote) {
+ router.post('/quotes/add', verifyToken, async (req, res) => {
+ const { author, authorImage, quote, year, submitterID } = req.body;
+ try {
await newQuote.create({
- author: quote.author,
- authorImage: quote.authorImage,
- quote: quote.quote,
- year: quote.year,
- submitter: quote.submitterID
+ author: author,
+ authorImage: authorImage,
+ quote: quote,
+ year: year,
+ submitter: submitterID
});
- await pendingQuote.destroy({ where: { id } });
- res.status(200).send({ message: 'Quote approved' });
- } else {
- res.status(404).send({ message: 'Quote not found' });
+ res.status(200).send({ message: 'Added a new quote' });
+ } catch (error) {
+ console.error('Something went wrong:', error);
+ res.status(500).send({ message: 'Internal Server Error' });
+ }
+ });
+
+ router.post('/quotes/approve', verifyToken, async (req, res) => {
+ const { id } = req.body;
+ try {
+ const quote = await pendingQuote.findByPk(id);
+ if (quote) {
+ await newQuote.create({
+ author: quote.author,
+ authorImage: quote.authorImage,
+ quote: quote.quote,
+ year: quote.year,
+ submitter: quote.submitterID
+ });
+
+ await pendingQuote.destroy({ where: {id} });
+ res.status(200).send({ message: 'Quote approved' });
+ } else {
+ res.status(404).send({ message: 'Quote not found '});
+ }
+ } catch (error) {
+ console.error('Error approving quote:', error);
+ res.status(500).send({ message: 'Internal Server Error' });
}
- } catch (error) {
- console.error('Error approving quote:', error);
- res.status(500).send({ message: 'Internal Server Error' });
- }
-});
+ });
+
+ router.post('/quotes/reject', verifyToken, async (req, res) => {
+ const { id } = req.body;
+ try {
+ const quote = await pendingQuote.findByPk(id);
+ if (quote) {
+ await pendingQuote.destroy({ where: {id} });
-quoteRouter.post('/quotes/reject', verifyToken, async (req, res) => {
- const { id } = req.body;
- try {
- const quote = await pendingQuote.findByPk(id);
- if (quote) {
- await pendingQuote.destroy({ where: { id } });
- res.status(200).send({ message: 'Quote rejected' });
- } else {
- res.status(404).send({ message: 'Quote not found' });
+ if (!req.body.silent) {
+ client.users.fetch(quote.submitterID).then((user) => {
+ if (req.body.reason) {
+ user.send(`Hello ${user.displayName},\nYour quote was rejected for the following reason:\n\`\`\`\n${req.body.reason}\n\`\`\``);
+ } else {
+ user.send(`Hello ${user.displayName},\nYour quote was rejected.`);
+ }
+ }).catch((err) => {
+ console.error('Error sending rejection message:', err);
+ });
+ }
+
+ res.status(200).send({ message: 'Quote rejected', reason: req.body.reason });
+ } else {
+ res.status(404).send({ message: 'Quote not found' });
+ }
+ } catch (error) {
+ console.error('Error rejecting quote:', error);
+ res.status(500).send({ message: 'Internal Server Error' });
}
- } catch (error) {
- console.error('Error rejecting quote:', error);
- res.status(500).send({ message: 'Internal Server Error' });
- }
-});
+ });
+
+ return router;
+}
diff --git a/bot/src/api/server.js b/bot/src/api/server.js
index 379f410..53b29d4 100644
--- a/bot/src/api/server.js
+++ b/bot/src/api/server.js
@@ -14,7 +14,7 @@ export const apiServer = (client) => {
app.use(cors()); // Allow cross-origin requests
app.use(express.json());
- app.use('/api', quoteRouter);
+ app.use('/api', quoteRouter(client));
app.use('/api', settingsRouter(client));
app.use('/api', authRouter());
diff --git a/bot/src/commands/about.js b/bot/src/commands/about.js
index 22b3441..177ac47 100644
--- a/bot/src/commands/about.js
+++ b/bot/src/commands/about.js
@@ -23,12 +23,13 @@ export default {
{ name: 'License', value: 'GNU General Public License v3.0' },
{ name: 'Contributors', value:
'- <@297201585090723841> (Uptime command from 2.x)\n' +
- '- <@236279900728721409> (Eval command from 2.x)' }
+ '- <@236279900728721409> (Eval command from 2.x)'
+ }
)
.setFooter({ text: '© Copyright 2017-2025 Andrew Lee & contributors' })
.setColor(abEmbedColour);
- let Buttons = new ActionRowBuilder()
+ let aboutButtons = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setStyle(ButtonStyle.Link)
@@ -37,13 +38,13 @@ export default {
new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setLabel('Invite AleeBot')
- .setURL('https://discord.com/oauth2/authorize?client_id=282547024547545109'),
+ .setURL(`https://discord.com/oauth2/authorize?client_id=${interaction.client.user.id}`),
new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setLabel('Join Andrew Lee Projects')
.setURL('https://discord.gg/EFhRDqG')
);
- return await interaction.reply({ embeds: [aboutEmbed], components: [Buttons] });
+ return await interaction.reply({ embeds: [aboutEmbed], components: [aboutButtons] });
}
};
diff --git a/bot/src/commands/quote.js b/bot/src/commands/quote.js
index 55b8cbc..9c409f9 100644
--- a/bot/src/commands/quote.js
+++ b/bot/src/commands/quote.js
@@ -50,6 +50,7 @@ export default {
.setCustomId('authorImage')
.setLabel('Submit the image of the author')
.setMaxLength(100)
+ .setMinLength(4)
.setPlaceholder('Image URL (512x512) or (128x128)')
.setStyle(TextInputStyle.Short);
@@ -57,6 +58,7 @@ export default {
.setCustomId('quote')
.setLabel('Enter the quote')
.setMaxLength(200)
+ .setMinLength(5)
.setPlaceholder('Quote')
.setStyle(TextInputStyle.Paragraph);
@@ -85,6 +87,29 @@ export default {
const quote = modalInteraction.fields.getTextInputValue('quote');
const year = modalInteraction.fields.getTextInputValue('year');
+ try {
+ new URL(authorImage);
+ } catch {
+ return modalInteraction.reply({
+ content: 'Error: Author image must be a valid URL.',
+ flags: MessageFlags.Ephemeral
+ });
+ }
+
+ if (!authorImage.match(/\.(jpeg|jpg|png|webp)$/i)) {
+ return modalInteraction.reply({
+ content: 'Error: Author image URL must end with a valid image extension (jpeg, jpg, png, webp).',
+ flags: MessageFlags.Ephemeral
+ });
+ }
+
+ if (isNaN(year) || year.trim() === '' || !Number.isInteger(Number(year))) {
+ return modalInteraction.reply({
+ content: 'Error: Year must be a number.',
+ flags: MessageFlags.Ephemeral
+ });
+ }
+
await pendingQuote.create({
author: author,
authorImage: authorImage,
diff --git a/bot/src/commands/serverinfo.js b/bot/src/commands/serverinfo.js
index 3f7bd72..7ebf6ea 100644
--- a/bot/src/commands/serverinfo.js
+++ b/bot/src/commands/serverinfo.js
@@ -22,10 +22,10 @@ export default {
.addFields(
{ name: 'Main Information', value: `**Server Name:** ${interaction.guild.name}\n**Server ID:** ${interaction.guild.id}\n**Server Owner:** ${guildOwner.user.username}`},
{ name: 'Join Dates', value: `**Created At:** ${interaction.guild.createdAt.toUTCString()}\n**AleeBot Joined:** ${interaction.guild.joinedAt.toUTCString()}`},
- { name: 'Total Channels (without threads)', value: `${interaction.guild.channels.channelCountWithoutThreads}` },
+ { name: 'Total Channels (without threads)', value: interaction.guild.channels.channelCountWithoutThreads.toString() },
// { name: 'Channels', value: listedChannels.join(' ') },
- { name: 'Total Members (with bots)', value: `${interaction.guild.memberCount}` },
- { name: 'Total Members (without bots)', value: `${memberCountNoBots}` }
+ { name: 'Total Members (with bots)', value: interaction.guild.memberCount.toString() },
+ { name: 'Total Members (without bots)', value: memberCountNoBots.toString() }
)
.setColor(abEmbedColour);
return await interaction.reply({ embeds: [serverEmbed] });
diff --git a/bot/src/commands/stats.js b/bot/src/commands/stats.js
index 9504386..6924874 100644
--- a/bot/src/commands/stats.js
+++ b/bot/src/commands/stats.js
@@ -1,21 +1,26 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import { commandUsages } from '../models/command-usages.js';
import { abEmbedColour } from '../storage/consts.js';
+import { quote } from '../models/quote.js';
export default {
data: new SlashCommandBuilder()
.setName('stats')
- .setDescription('Shows how many times you executed a command.'),
+ .setDescription('Shows statistics of your interaction with AleeBot.'),
async execute(interaction) {
- let cmdUsage = await commandUsages.findAll({ where: { userID: interaction.user.id } });
+ const cmdUsage = await commandUsages.findAll({ where: { userID: interaction.user.id } });
+ const quoteSubmitted = await quote.findAll({ where: { submitter: interaction.user.id } });
+
const totalCommands = cmdUsage.length;
const guildCommands = cmdUsage.filter(cmd => cmd.guildID === interaction.guild.id).length;
+ const totalQuotes = quoteSubmitted.length;
const statsEmbed = new EmbedBuilder()
- .setAuthor({ name: `Stats for ${interaction.user.username}`, iconURL: interaction.client.user.avatarURL() })
+ .setAuthor({ name: `AleeBot Stats for ${interaction.user.displayName}`, iconURL: interaction.client.user.avatarURL() })
.addFields(
- { name: 'Total Commands Executed', value: totalCommands.toString() },
- { name: 'Total Commands Executed in this Guild', value: guildCommands.toString() }
+ { name: 'Total Commands Executed (Global)', value: totalCommands.toString() },
+ { name: 'Total Commands Executed (Guild)', value: guildCommands.toString() },
+ { name: 'Total Quotes Submitted', value: totalQuotes.toString() }
)
.setColor(abEmbedColour);
diff --git a/bot/src/init.js b/bot/src/init.js
index 3557757..45b9a41 100644
--- a/bot/src/init.js
+++ b/bot/src/init.js
@@ -5,11 +5,15 @@ import { command } from './handlers/command.js';
//import { deployCommands } from './util/deploy.js';
export async function init(client) {
- if (process.env.NODE_ENV === 'development') {
- await syncDB();
+ try {
+ if (process.env.NODE_ENV === 'development') {
+ await syncDB();
+ }
+ //deployCommands().then(() => console.log('[>] Deployed commands'));
+ await apiServer(client);
+ await event(client).then(() => console.log('[>] Event module loaded'));
+ await command(client).then(() => console.log('[>] Command module loaded'));
+ } catch (e) {
+ console.error(e);
}
- //deployCommands().then(() => console.log('[>] Deployed commands'));
- await apiServer(client);
- await event(client).then(() => console.log('[>] Event module loaded'));
- await command(client).then(() => console.log('[>] Command module loaded'));
}
diff --git a/web/src/app/quotes/page.js b/web/src/app/quotes/page.js
index 0dde1ec..3ba4ea3 100644
--- a/web/src/app/quotes/page.js
+++ b/web/src/app/quotes/page.js
@@ -9,6 +9,8 @@ export default function Quotes() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [message, setMessage] = useState(null);
+ const [rejectReason, setRejectReason] = useState('');
+ const [quoteToReject, setQuoteToReject] = useState(null);
const [formData, setFormData] = useState({
author: '',
authorImage: '',
@@ -16,6 +18,7 @@ export default function Quotes() {
year: '',
submitterID: ''
});
+ const [silentReject, setSilentReject] = useState(false);
useEffect(() => {
fetchPendingQuotes();
@@ -114,14 +117,28 @@ export default function Quotes() {
}
};
- const handleRejectQuote = async (id) => {
+ const openRejectModal = (id) => {
+ setQuoteToReject(id);
+ setRejectReason('');
+ };
+
+ const closeRejectModal = () => {
+ setQuoteToReject(null);
+ setRejectReason('');
+ };
+
+ const handleRejectQuote = async () => {
try {
const response = await fetchWithAuth('/api/quotes/reject', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify({ id })
+ body: JSON.stringify({
+ id: quoteToReject,
+ reason: rejectReason,
+ silent: silentReject
+ })
});
if (!response.ok) {
@@ -133,6 +150,9 @@ export default function Quotes() {
text: 'Quote rejected successfully'
});
+ // Close modal
+ closeRejectModal();
+
// Refresh quotes
fetchPendingQuotes();
} catch (err) {
@@ -202,7 +222,6 @@ export default function Quotes() {
</div>
)}
-
<h1 className="text-3xl">Pending Quotes</h1>
{loading && <p>Loading quotes...</p>}
@@ -228,7 +247,7 @@ export default function Quotes() {
Approve
</button>
<button
- onClick={() => handleRejectQuote(quote.id)}
+ onClick={() => openRejectModal(quote.id)}
className="bg-red-600 hover:bg-red-500 text-white py-1 px-3 rounded"
>
Reject
@@ -237,6 +256,46 @@ export default function Quotes() {
</Card>
))}
</div>
+
+ {/* Rejection Modal */}
+ {quoteToReject && (
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
+ <div className="bg-gray-800 p-6 rounded-lg w-full max-w-md">
+ <h2 className="text-xl mb-4">Reject Quote</h2>
+ <textarea
+ className="w-full p-2 bg-gray-700 rounded mb-4"
+ placeholder="Reason for rejection (optional)"
+ value={rejectReason}
+ onChange={(e) => setRejectReason(e.target.value)}
+ rows="4"
+ />
+ <div className="flex items-center mb-4">
+ <input
+ type="checkbox"
+ id="silentReject"
+ checked={silentReject}
+ onChange={(e) => setSilentReject(e.target.checked)}
+ className="mr-2"
+ />
+ <label htmlFor="silentReject">Reject silently (don&#39;t notify user)</label>
+ </div>
+ <div className="flex justify-end gap-3">
+ <button
+ onClick={closeRejectModal}
+ className="bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleRejectQuote}
+ className="bg-red-600 hover:bg-red-500 text-white py-2 px-4 rounded"
+ >
+ Reject
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
</div>
</>
);