Advantages of refactoring
LifeAdmin-cmd
Posted on January 8, 2023
In this Blog article I want to talk about the advantages of refactoring code, which I will demonstrate on an example of the public repository 'WhatsAppToDiscord' from FKLC.
I recently used this Discord bot for a personal project and even though the bot is mostly used for a very specific use case I think the code is easy enough to follow since the purpose of the bot is rather simple. The bot basically is a bridge between WhatsApp and Discord and based on the WhatsApp Web version. You can find the repository here.
For my example we are going to look into the 'whatsapp_manager.js' file, located in the src directory.
const { default: makeWASocket, fetchLatestBaileysVersion } = require('@adiwajshing/baileys');
const waUtils = require('./whatsapp_utils');
const dcUtils = require('./discord_utils');
const state = require('./state');
let authState;
let saveState;
const connectToWhatsApp = async (retry = 1) => {
const controlChannel = await state.getControlChannel();
const { version } = await fetchLatestBaileysVersion();
const client = makeWASocket({
version,
printQRInTerminal: false,
auth: authState,
logger: state.logger,
markOnlineOnConnect: false,
});
client.contacts = state.contacts;
client.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
await waUtils.sendQR(qr);
}
if (connection === 'close') {
state.logger.error(lastDisconnect.error);
if (retry <= 3) {
await controlChannel.send(`WhatsApp connection failed! Trying to reconnect! Retry #${retry}`);
await connectToWhatsApp(retry + 1);
} else if (retry <= 5) {
const delay = (retry - 3) * 10;
await controlChannel.send(`WhatsApp connection failed! Waiting ${delay} seconds before trying to reconnect! Retry #${retry}.`);
await new Promise((resolve) => {
setTimeout(resolve, delay * 1000);
});
await connectToWhatsApp(retry + 1);
} else {
await controlChannel.send('Failed connecting 5 times. Please rescan the QR code.');
await module.exports.start(true);
}
} else if (connection === 'open') {
state.waClient = client;
// eslint-disable-next-line no-param-reassign
retry = 1;
await controlChannel.send('WhatsApp connection successfully opened!');
}
});
client.ev.on('creds.update', saveState);
['chats.set', 'contacts.set', 'chats.upsert', 'chats.update', 'contacts.upsert', 'contacts.update', 'groups.upsert', 'groups.update'].forEach((eventName) => client.ev.on(eventName, waUtils.updateContacts));
client.ev.on('messages.upsert', async (update) => {
if (update.type === 'notify') {
for await (const message of update.messages) {
if (state.settings.Whitelist.length && !state.settings.Whitelist.includes(message.key.remoteJid)) {
return;
}
if (state.startTime > message.messageTimestamp) {
return;
}
if (!['conversation', 'extendedTextMessage', 'imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'].some((el) => Object.keys(message.message || {}).includes(el))) {
return;
}
await new Promise((resolve) => {
state.dcClient.emit('whatsappMessage', message, resolve);
});
}
}
});
client.ev.on('messages.reaction', async (reactions) => {
for await (const reaction of reactions) {
if (state.settings.Whitelist.length && !state.settings.Whitelist.includes(reaction.key.remoteJid)) {
return;
}
if (state.startTime > reaction.messageTimestamp) {
return;
}
await new Promise((resolve) => {
state.dcClient.emit('whatsappReaction', reaction, resolve);
});
}
});
client.ev.on('discordMessage', async (message) => {
const jid = dcUtils.channelIdToJid(message.channel.id);
if (!jid) {
if (!state.settings.Categories.includes(message.channel?.parent?.id)) {
return;
}
message.channel.send("Couldn't find the user. Restart the bot, or manually delete this channel and start a new chat using the `start` command.");
return;
}
const content = {};
const options = {};
if (state.settings.UploadAttachments) {
await Promise.all([...message.attachments.values()].map((attachment) => client.sendMessage(jid, waUtils.createDocumentContent(attachment))));
if (!message.content) {
return;
}
content.text = message.content;
} else {
content.text = [message.content, ...message.attachments.map((el) => el.url)].join(' ');
}
if (state.settings.DiscordPrefix) {
content.text = `[${message.member?.nickname || message.author.username}] ${content.text}`;
}
if (message.reference) {
options.quoted = await waUtils.createQuoteMessage(message);
if (options.quoted == null) {
message.channel.send("Couldn't find the message quoted. You can only reply to messages received after the bot went online. Sending the message without the quoted message.");
}
}
state.lastMessages[message.id] = (await client.sendMessage(jid, content, options)).key.id;
});
client.ev.on('discordReaction', async ({ reaction, removed }) => {
const jid = dcUtils.channelIdToJid(reaction.message.channelId);
if (!jid) {
reaction.message.channel.send("Couldn't find the user. Restart the bot, or manually delete this channel and start a new chat using the `start` command.");
return;
}
const key = {
id: state.lastMessages[reaction.message.id],
fromMe: reaction.message.webhookId == null || reaction.message.author.username === 'You',
remoteJid: jid,
};
if (jid.endsWith('@g.us')) {
key.participant = waUtils.nameToJid(reaction.message.author.username);
}
const messageId = (
await client.sendMessage(jid, {
react: {
text: removed ? '' : reaction.emoji.name,
key,
},
})
).key.id;
state.lastMessages[messageId] = true;
});
};
module.exports = {
start: async (newSession = false) => {
({ authState, saveState } = await waUtils.useStorageAuthState(newSession));
await connectToWhatsApp();
},
};
The code exports a function called start that establishes a connection to WhatsApp using the makeWASocket function from the @adiwajshing/baileys module. It sets up event listeners for various events emitted by the client object returned by makeWASocket, such as connection.update, creds.update, messages.upsert, messages.reaction, discordMessage, and discordReaction. The start function also sets up an event listener for the connection.update event that attempts to reconnect to WhatsApp if the connection is lost and handles failures to reconnect after multiple attempts.
And that is exactly what we want to refactor:
The start function currently sets up everything on it's own, but we want to split the code up in order to make it more readable and understandable, reuse- and maintainable and more testable since it can be called seperately.
So let's get to it: Here is my refactored code, try to find find and understand the refactoring before you continue to read.
const { sendQR, updateContacts } = require('./whatsapp_utils');
const dcUtils = require('./discord_utils');
const state = require('./state');
const handleWaEvents = (client) => {
client.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
await sendQR(qr);
}
if (connection === 'close') {
state.logger.error(lastDisconnect.error);
} else if (connection === 'open') {
state.waClient = client;
await state.getControlChannel().send('WhatsApp connection successfully opened!');
}
});
client.ev.on('creds.update', state.saveAuth);
['chats.set', 'contacts.set', 'chats.upsert', 'chats.update', 'contacts.upsert', 'contacts.update', 'groups.upsert', 'groups.update'].forEach((eventName) => client.ev.on(eventName, updateContacts));
client.ev.on('messages.upsert', async (update) => {
if (update.type === 'notify') {
for await (const message of update.messages) {
if (state.settings.Whitelist.length && !state.settings.Whitelist.includes(message.key.remoteJid)) {
return;
}
if (state.startTime > message.messageTimestamp) {
return;
}
if (!['conversation', 'extendedTextMessage', 'imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'].some((el) => Object.keys(message.message || {}).includes(el))) {
return;
}
await new Promise((resolve) => {
state.dcClient.emit('whatsappMessage', message, resolve);
});
}
}
});
client.ev.on('messages.reaction', async (reactions) => {
for await (const reaction of reactions) {
if (state.settings.Whitelist.length && !state.settings.Whitelist.includes(reaction.key.remoteJid)) {
return;
}
if (state.startTime > reaction.messageTimestamp) {
return;
}
await new Promise((resolve) => {
state.dcClient.emit('whatsappReaction', reaction, resolve);
});
}
});
client.ev.on('discordMessage', async (message) => {
const jid = dcUtils.getJid(message);
if (!jid) return;
try {
await client.sendText(jid, message.content);
} catch (error) {
state.logger.error(error);
}
});
client.ev.on('discordReaction', async (reaction) => {
const jid = dcUtils.getJid(reaction);
if (!jid) return;
try {
await client.sendText(jid, `${reaction.member.displayName} added a reaction to a message: ${reaction.emoji.name}`);
} catch (error) {
state.logger.error(error);
}
});
};
module.exports = handleWaEvents;
So for explanation: the 'problem' we're trying to solve here is a deisgn issue, so it is important to note that the code wored before and that it should still work after the refactoring. So you might have seen what was changed, and if not then here is the solution and a explanation.
The code above was basically a single function file but instead of that now I refactored the earlier mentioned event listener that was included in the start function. So I refactored the code by extracting the event handling code for WhatsApp messages and reactions into a separate function called 'handleWaEvents'. This function takes in a client object as a parameter and attaches event listeners to it for various events.
By moving this code to a separate function, it becomes easier to reuse this functionality and keeps the 'connectToWhatsApp' function focused on establishing a connection to WhatsApp. It also makes the code more organized and easier to read and maintain.
Conslusion:
In my opinion refactoring is one of the most important good practices out there and therefore should be a part of every code base you're planning to work on
TLDR: Refactoring code makes it easier to understand and maintain while keeping or even extending the functionality.
Posted on January 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024