Skip to content

Commit 8f1c37e

Browse files
committed
feat(messageUpdate.js): refactor emoji processing logic to improve clarity and efficiency by separating add and remove functionalities
refactor(index.js): restructure command and event loading into separate functions for better organization and maintainability
1 parent 0ebca2d commit 8f1c37e

File tree

2 files changed

+140
-98
lines changed

2 files changed

+140
-98
lines changed

src/events/messageUpdate.js

Lines changed: 69 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,90 @@
11
import { Events } from 'discord.js';
2-
import {
3-
getGuildInfo,
4-
addEmojiRecords,
5-
deleteEmojiRecords,
6-
insertGuild,
7-
} from '../helpers/mongodbModel.js';
8-
import {
9-
createEmojiRecord,
10-
extractEmojis,
11-
getUserOpt,
12-
shouldProcessMessage,
13-
} from '../helpers/utilities.js';
2+
import { getGuildInfo, addEmojiRecords, deleteEmojiRecords, insertGuild } from '../helpers/mongodbModel.js';
3+
import { createEmojiRecord, extractEmojis, getUserOpt, shouldProcessMessage } from '../helpers/utilities.js';
144

15-
async function processEmojis(message, action) {
16-
const guildId = message.guildId;
17-
const messageAuthorId = message.author.id;
18-
const emojis = extractEmojis(message);
19-
const emojiRecords = [];
5+
const tally = ids =>
6+
ids.reduce((acc, id) => { acc[id] = (acc[id] || 0) + 1; return acc; }, {});
207

21-
for (const emoji of emojis) {
22-
const guildEmoji = await message.guild.emojis.fetch(emoji[3]).catch(() => null);
23-
if (!guildEmoji) continue;
24-
const emojiRecord = createEmojiRecord(
25-
guildId,
26-
message.id,
27-
guildEmoji.id,
28-
messageAuthorId,
29-
message.createdAt,
30-
'message'
8+
const diffList = (fromCount, toCount) =>
9+
Object.entries(fromCount)
10+
.flatMap(([id, cnt]) =>
11+
Array(Math.max(cnt - (toCount[id] || 0), 0)).fill(id)
3112
);
32-
emojiRecords.push(emojiRecord);
33-
}
3413

35-
if (emojiRecords.length === 0) return false;
14+
const fetchEmoji = (guild, id) =>
15+
guild.emojis.cache.get(id) || guild.emojis.fetch(id).catch(() => null);
3616

37-
if (action === 'add') {
38-
await addEmojiRecords(message.client.db, emojiRecords);
39-
} else if (action === 'delete') {
40-
const deleteFilter = { $or: emojiRecords };
41-
await deleteEmojiRecords(message.client.db, deleteFilter);
42-
}
17+
async function addEmojis(db, guild, message, ids) {
18+
if (!ids.length) return;
19+
const records = (
20+
await Promise.all(
21+
ids.map(async id => {
22+
const emoji = await fetchEmoji(guild, id);
23+
return emoji && createEmojiRecord(
24+
message.guildId,
25+
message.id,
26+
emoji.id,
27+
message.author.id,
28+
message.createdAt,
29+
'message'
30+
);
31+
})
32+
)
33+
).filter(Boolean);
34+
if (records.length) await addEmojiRecords(db, records);
4335
}
4436

45-
async function processMessageUpdate(oldMessage, newMessage) {
46-
if(newMessage.partial) await newMessage.fetch();
47-
48-
const guildInfo = await getGuildInfo(newMessage.client.db, newMessage.guild);
49-
const userOpt = await getUserOpt(guildInfo, newMessage.author.id);
50-
51-
if (shouldProcessMessage(oldMessage, guildInfo, userOpt)) {
52-
await processEmojis(oldMessage, 'delete');
53-
}
54-
55-
if (shouldProcessMessage(newMessage, guildInfo, userOpt)) {
56-
await processEmojis(newMessage, 'add');
57-
}
37+
async function removeEmojis(db, guild, message, ids) {
38+
if (!ids.length) return;
39+
const records = (
40+
await Promise.all(
41+
ids.map(async id => {
42+
const emoji = await fetchEmoji(guild, id);
43+
return emoji && createEmojiRecord(
44+
message.guildId,
45+
message.id,
46+
emoji.id,
47+
message.author.id,
48+
message.createdAt,
49+
'message'
50+
);
51+
})
52+
)
53+
).filter(Boolean);
54+
if (records.length) await deleteEmojiRecords(db, { $or: records });
5855
}
5956

6057
export default {
6158
name: Events.MessageUpdate,
6259
async execute(oldMessage, newMessage) {
6360
try {
64-
await processMessageUpdate(oldMessage, newMessage);
61+
if (newMessage.partial) await newMessage.fetch();
62+
const { client, guild, author } = newMessage;
63+
const guildInfo = await getGuildInfo(client.db, guild);
64+
const userOpt = await getUserOpt(guildInfo, author.id);
65+
66+
const oldIds =
67+
shouldProcessMessage(oldMessage, guildInfo, userOpt)
68+
? extractEmojis(oldMessage).map(e => e[3])
69+
: [];
70+
const newIds =
71+
shouldProcessMessage(newMessage, guildInfo, userOpt)
72+
? extractEmojis(newMessage).map(e => e[3])
73+
: [];
74+
75+
const toAdd = diffList(tally(newIds), tally(oldIds));
76+
const toRemove = diffList(tally(oldIds), tally(newIds));
77+
78+
await Promise.all([
79+
addEmojis(client.db, guild, newMessage, toAdd),
80+
removeEmojis(client.db, guild, newMessage, toRemove)
81+
]);
6582
} catch (error) {
66-
if (error.message == `Cannot read properties of null (reading 'usersOpt')`) {
83+
if (error.message.includes('usersOpt')) {
6784
await insertGuild(newMessage.client.db, newMessage.guild);
6885
} else {
6986
console.error(Events.MessageUpdate, error);
7087
}
7188
}
72-
},
89+
}
7390
};

src/index.js

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import fs from 'fs';
1+
import { promises as fs } from 'fs';
22
import path from 'path';
3-
import * as url from 'url';
3+
import { fileURLToPath, pathToFileURL } from 'url';
44
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js';
5-
import * as dotenv from 'dotenv';
5+
import dotenv from 'dotenv';
66
import { MongoClient } from 'mongodb';
77

88
dotenv.config();
9-
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
109

10+
// Resolve __dirname in ESM
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12+
13+
// Extend Client to hold our db and commands collection
14+
/**
15+
* @typedef {import('discord.js').Client & { commands: Collection<string, any>, db?: import('mongodb').Db }} BotClient
16+
*/
17+
18+
/** @type {BotClient} */
1119
const client = new Client({
1220
intents: [
1321
GatewayIntentBits.Guilds,
@@ -18,53 +26,70 @@ const client = new Client({
1826
],
1927
partials: [Partials.Message, Partials.User, Partials.Channel, Partials.Reaction],
2028
});
21-
22-
// Commands
2329
client.commands = new Collection();
24-
const commandsPath = path.join(__dirname, 'commands');
25-
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js'));
26-
(async () => {
27-
for (const file of commandFiles) {
28-
const filePath = path.join(commandsPath, file);
29-
const command = (await import(url.pathToFileURL(filePath))).default;
30-
if ('data' in command && 'execute' in command) {
31-
client.commands.set(command.data.name, command);
32-
}
33-
else {
34-
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
35-
}
36-
}
37-
})();
3830

39-
// Events
40-
const eventsPath = path.join(__dirname, 'events');
41-
const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith('.js'));
42-
(async () => {
43-
for (const file of eventFiles) {
44-
const filePath = path.join(eventsPath, file);
45-
const event = (await import(url.pathToFileURL(filePath))).default;
46-
if (event.once) {
47-
client.once(event.name, (...args) => event.execute(...args));
48-
}
49-
else {
50-
client.on(event.name, (...args) => event.execute(...args));
51-
}
52-
}
53-
})();
31+
/**
32+
* Load and register command modules
33+
*/
34+
async function loadCommands() {
35+
const commandsDir = path.join(__dirname, 'commands');
36+
const files = (await fs.readdir(commandsDir)).filter(f => f.endsWith('.js'));
37+
await Promise.all(
38+
files.map(async file => {
39+
const filePath = path.join(commandsDir, file);
40+
const { default: cmd } = await import(pathToFileURL(filePath).href);
41+
if (cmd?.data?.name && typeof cmd.execute === 'function') {
42+
client.commands.set(cmd.data.name, cmd);
43+
} else {
44+
console.warn(`[WARNING] ${file} is missing a valid data or execute export.`);
45+
}
46+
})
47+
);
48+
}
5449

55-
// MongoDB
56-
const mongoClient = new MongoClient(process.env.MONGODB_URI);
57-
(async () => {
50+
/**
51+
* Load and attach event handlers
52+
*/
53+
async function loadEvents() {
54+
const eventsDir = path.join(__dirname, 'events');
55+
const files = (await fs.readdir(eventsDir)).filter(f => f.endsWith('.js'));
56+
await Promise.all(
57+
files.map(async file => {
58+
const filePath = path.join(eventsDir, file);
59+
const { default: evt } = await import(pathToFileURL(filePath).href);
60+
const listener = (...args) => evt.execute(...args).catch(console.error);
61+
if (evt.once) client.once(evt.name, listener);
62+
else client.on(evt.name, listener);
63+
})
64+
);
65+
}
66+
67+
/**
68+
* Main initialization
69+
*/
70+
async function init() {
5871
try {
72+
await loadCommands();
73+
await loadEvents();
74+
75+
const mongoClient = new MongoClient(process.env.MONGODB_URI, { useUnifiedTopology: true });
5976
await mongoClient.connect();
77+
client.db = mongoClient.db('data');
6078

61-
const db = mongoClient.db('data');
62-
client.db = db;
79+
await client.login(process.env.BOT_TOKEN);
6380

64-
client.login(process.env.BOT_TOKEN);
81+
// Graceful shutdown
82+
const cleanExit = async () => {
83+
console.log('Shutting down...');
84+
await mongoClient.close();
85+
process.exit(0);
86+
};
87+
process.on('SIGINT', cleanExit);
88+
process.on('SIGTERM', cleanExit);
89+
} catch (err) {
90+
console.error('❌ Initialization error:', err);
91+
process.exit(1);
6592
}
66-
catch (error) {
67-
console.error('Error connecting to MongoDB:', error);
68-
mongoClient.close();
69-
}
70-
})();
93+
}
94+
95+
init();

0 commit comments

Comments
 (0)