Started attempting to split up core handler

This commit is contained in:
WatDuhHekBro 2021-04-01 05:44:44 -05:00
parent df3e4e8e6e
commit 9adc5eea6e
8 changed files with 186 additions and 86 deletions

View File

@ -3,5 +3,8 @@ module.exports = {
testMatch: ["**/*.test.+(ts|tsx)"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest"
}
},
// The environment is the DOM by default, so discord.js fails to load because it's calling a Node-specific function.
// https://github.com/discordjs/discord.js/issues/3971#issuecomment-602010271
testEnvironment: "node"
};

View File

@ -0,0 +1,28 @@
import {client} from "../index";
import {botHasPermission} from "./libd";
import {Permissions, Message} from "discord.js";
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
export const unreactEventListeners: Map<string, (emote: string, id: string) => void> = new Map();
// Attached to the client, there can be one event listener attached to a message ID which is executed if present.
client.on("messageReactionRemove", (reaction, user) => {
const canDeleteEmotes = botHasPermission(reaction.message.guild, Permissions.FLAGS.MANAGE_MESSAGES);
if (!canDeleteEmotes) {
const callback = unreactEventListeners.get(reaction.message.id);
callback && callback(reaction.emoji.name, user.id);
}
});
// A list of "channel-message" and callback pairs. Also, I imagine that the callback will be much more maintainable when discord.js v13 comes out with a dedicated message.referencedMessage property.
// Also, I'm defining it here instead of the message event because the load order screws up if you export it from there. Yeah... I'm starting to notice just how much technical debt has been built up. The command handler needs to be modularized and refactored sooner rather than later. Define all constants in one area then grab from there.
export const replyEventListeners = new Map<string, (message: Message) => void>();
client.on("message", (message) => {
// If there's an inline reply, fire off that event listener (if it exists).
if (message.reference) {
const reference = message.reference;
replyEventListeners.get(`${reference.channelID}-${reference.messageID}`)?.(message);
}
});

View File

@ -1,38 +1,43 @@
import {client} from "../index";
import Command, {loadableCommands} from "../core/command";
import {hasPermission, getPermissionLevel, getPermissionName} from "../core/permissions";
import {Permissions} from "discord.js";
import {getPrefix} from "../core/structures";
import {replyEventListeners} from "../core/libd";
import quote from "../modules/message_embed";
import {Config} from "../core/structures";
import Command, {loadableCommands} from "./command";
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
import {Permissions, Message} from "discord.js";
import {getPrefix} from "./structures";
import {Config} from "./structures";
///////////
// Steps //
///////////
// 1. Someone sends a message in chat.
// 2. Check if bot, then load commands.
// 3. Check if "<prefix>...". If not, check if "@<bot>...". Resolve prefix and cropped message (if possible).
// 4. Test if bot has permission to send messages.
// 5. Once confirmed as a command, resolve the subcommand.
// 6. Check permission level and whether or not it's an endpoint.
// 7. Execute command if all successful.
// For custom message events that want to cancel this one on certain conditions.
const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot];
export function addInterceptRule(handler: (message: Message) => boolean) {
interceptRules.push(handler);
}
client.on("message", async (message) => {
for (const shouldIntercept of interceptRules) {
if (shouldIntercept(message)) {
return;
}
}
const commands = await loadableCommands;
if (message.content.toLowerCase().includes("remember to drink water")) {
message.react("🚱");
}
// Message Setup //
if (message.author.bot) return;
// If there's an inline reply, fire off that event listener (if it exists).
if (message.reference) {
const reference = message.reference;
replyEventListeners.get(`${reference.channelID}-${reference.messageID}`)?.(message);
}
let prefix = getPrefix(message.guild);
const originalPrefix = prefix;
let exitEarly = !message.content.startsWith(prefix);
const clientUser = message.client.user;
let usesBotSpecificPrefix = false;
if (!message.content.startsWith(prefix)) {
return quote(message);
}
// If the client user exists, check if it starts with the bot-specific prefix.
if (clientUser) {
// If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other).
@ -87,34 +92,8 @@ client.on("message", async (message) => {
);
// Subcommand Recursion //
let command = commands.get(header);
if (!command) return console.warn(`Command "${header}" was called but for some reason it's still undefined!`);
const params: any[] = [];
let isEndpoint = false;
let permLevel = command.permission ?? 0;
for (let param of args) {
if (command.endpoint) {
if (command.subcommands.size > 0 || command.user || command.number || command.any)
console.warn(`An endpoint cannot have subcommands! Check ${originalPrefix}${header} again.`);
isEndpoint = true;
break;
}
const type = command.resolve(param);
command = command.get(param);
permLevel = command.permission ?? permLevel;
if (type === Command.TYPES.USER) {
const id = param.match(/\d+/g)![0];
try {
params.push(await message.client.users.fetch(id));
} catch (error) {
return message.channel.send(`No user found by the ID \`${id}\`!`);
}
} else if (type === Command.TYPES.NUMBER) params.push(Number(param));
else if (type !== Command.TYPES.SUBCOMMAND) params.push(param);
}
let command = commands.get(header)!;
//resolveSubcommand()
if (!message.member)
return console.warn("This command was likely called from a DM channel meaning the member object is null.");
@ -147,6 +126,40 @@ client.on("message", async (message) => {
});
});
// Takes a base command and a list of string parameters and returns:
// - The resolved subcommand
// - The resolved parameters
// - Whether or not an endpoint has been broken
// - The permission level required
async function resolveSubcommand(command: Command, args: string[]): [Command, any[], boolean, number] {
const params: any[] = [];
let isEndpoint = false;
let permLevel = command.permission ?? 0;
for (const param of args) {
if (command.endpoint) {
if (command.subcommands.size > 0 || command.user || command.number || command.any)
console.warn("An endpoint cannot have subcommands!");
isEndpoint = true;
break;
}
const type = command.resolve(param);
command = command.get(param);
permLevel = command.permission ?? permLevel;
if (type === Command.TYPES.USER) {
const id = param.match(/\d+/g)![0];
try {
params.push(await message.client.users.fetch(id));
} catch (error) {
return message.channel.send(`No user found by the ID \`${id}\`!`);
}
} else if (type === Command.TYPES.NUMBER) params.push(Number(param));
else if (type !== Command.TYPES.SUBCOMMAND) params.push(param);
}
}
client.once("ready", () => {
if (client.user) {
console.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);

View File

@ -9,20 +9,9 @@ import {
NewsChannel,
MessageOptions
} from "discord.js";
import {client} from "../index";
import {unreactEventListeners, replyEventListeners} from "./eventListeners";
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
const eventListeners: Map<string, (emote: string, id: string) => void> = new Map();
// Attached to the client, there can be one event listener attached to a message ID which is executed if present.
client.on("messageReactionRemove", (reaction, user) => {
const canDeleteEmotes = botHasPermission(reaction.message.guild, Permissions.FLAGS.MANAGE_MESSAGES);
if (!canDeleteEmotes) {
const callback = eventListeners.get(reaction.message.id);
callback && callback(reaction.emoji.name, user.id);
}
});
export type SingleMessageOptions = MessageOptions & {split?: false};
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
@ -36,7 +25,7 @@ export async function paginate(
channel: TextChannel | DMChannel | NewsChannel,
senderID: string,
total: number,
callback: (page: number, hasMultiplePages: boolean) => MessageOptions & {split?: false},
callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
duration = 60000
) {
const hasMultiplePages = total > 1;
@ -70,7 +59,7 @@ export async function paginate(
// Listen for reactions and call the handler.
let backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_EMOJI);
eventListeners.set(message.id, handle);
unreactEventListeners.set(message.id, handle);
const collector = message.createReactionCollector(
(reaction, user) => {
if (user.id === senderID) {
@ -91,7 +80,7 @@ export async function paginate(
// When time's up, remove the bot's own reactions.
collector.on("end", () => {
eventListeners.delete(message.id);
unreactEventListeners.delete(message.id);
backwardsReaction.users.remove(message.author);
forwardsReaction.users.remove(message.author);
});
@ -127,10 +116,6 @@ export async function prompt(message: Message, senderID: string, onConfirm: () =
if (!isDeleted) message.delete();
}
// A list of "channel-message" and callback pairs. Also, I imagine that the callback will be much more maintainable when discord.js v13 comes out with a dedicated message.referencedMessage property.
// Also, I'm defining it here instead of the message event because the load order screws up if you export it from there. Yeah... I'm starting to notice just how much technical debt has been built up. The command handler needs to be modularized and refactored sooner rather than later. Define all constants in one area then grab from there.
export const replyEventListeners = new Map<string, (message: Message) => void>();
// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
// If the reply is rejected, reply with an error message (when stable support comes from discord.js).
// Append "\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" in the future? And also the "you can now reply to this message" edit.

View File

@ -15,7 +15,10 @@ setup.init().then(() => {
// Initialize Modules //
import "./core/handler"; // Command loading will start as soon as an instance of "core/command" is loaded, which is loaded in "core/handler".
import "./core/eventListeners";
import "./modules/presence";
import "./modules/lavalink";
import "./modules/emoteRegistry";
import "./modules/channelListener";
import "./modules/intercept";
import "./modules/messageEmbed";

7
src/modules/intercept.ts Normal file
View File

@ -0,0 +1,7 @@
import {client} from "../index";
client.on("message", (message) => {
if (message.content.toLowerCase().includes("remember to drink water")) {
message.react("🚱");
}
});

View File

@ -0,0 +1,55 @@
jest.useFakeTimers();
import {strict as assert} from "assert";
//import {extractFirstMessageLink} from "./messageEmbed";
/*describe("modules/messageEmbed", () => {
describe("extractFirstMessageLink()", () => {
const guildID = "802906483866631183";
const channelID = "681747101169682147"
const messageID = "996363055050949479";
const post = `channels/${guildID}/${channelID}/${messageID}`;
const commonUrl = `https://discord.com/channels/${post}`;
const combined = [guildID, channelID, messageID];
it('should return work and extract correctly on an isolated link', () => {
const result = extractFirstMessageLink(commonUrl);
assert.deepStrictEqual(result, combined);
})
it('should return work and extract correctly on a link within a message', () => {
const result = extractFirstMessageLink(`sample text${commonUrl}, more sample text`);
assert.deepStrictEqual(result, combined);
})
it('should return null on "!link"', () => {
const result = extractFirstMessageLink(`just some !${commonUrl} text`);
assert.strictEqual(result, null);
})
it('should return null on "<link>"', () => {
const result = extractFirstMessageLink(`just some <${commonUrl}> text`);
assert.strictEqual(result, null);
})
it('should return work and extract correctly on "<link"', () => {
const result = extractFirstMessageLink(`just some <${commonUrl} text`);
assert.deepStrictEqual(result, combined);
})
it('should return work and extract correctly on "link>"', () => {
const result = extractFirstMessageLink(`just some ${commonUrl}> text`);
assert.deepStrictEqual(result, combined);
})
it('should return work and extract correctly on a canary link', () => {
const result = extractFirstMessageLink(`https://canary.discord.com/${post}`);
assert.deepStrictEqual(result, combined);
})
})
});*/
describe("placeholder", () => {
it("placeholder", async () => {
assert.strictEqual(1, 1);
});
});

View File

@ -1,19 +1,14 @@
import {client} from "..";
import {Message, TextChannel, APIMessage, MessageEmbed} from "discord.js";
import {client} from "../index";
import {TextChannel, APIMessage, MessageEmbed} from "discord.js";
import {getPrefix} from "../core/structures";
import {DiscordAPIError} from "discord.js";
export default async function quote(message: Message) {
if (message.author.bot) return;
// const message_link_regex = message.content.match(/(!)?https?:\/\/\w+\.com\/channels\/(\d+)\/(\d+)\/(\d+)/)
const message_link_regex = message.content.match(
/([<!]?)https?:\/\/(?:ptb\.|canary\.|)discord(?:app)?\.com\/channels\/(\d+)\/(\d+)\/(\d+)(>?)/
);
if (message_link_regex == null) return;
const [, char, guildID, channelID, messageID] = message_link_regex;
if (char || message.content.startsWith(getPrefix(message.guild))) return;
client.on("message", async (message) => {
// Only execute if the message is from a user and isn't a command.
if (message.content.startsWith(getPrefix(message.guild)) || message.author.bot) return;
const messageLink = extractFirstMessageLink(message.content);
if (!messageLink) return;
const [guildID, channelID, messageID] = messageLink;
try {
const channel = client.guilds.cache.get(guildID)?.channels.cache.get(channelID) as TextChannel;
@ -58,4 +53,15 @@ export default async function quote(message: Message) {
}
return console.error(error);
}
});
export function extractFirstMessageLink(message: string): [string, string, string] | null {
const messageLinkMatch = message.match(
/([!<])?https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})(>)?/
);
if (messageLinkMatch === null) return null;
const [, leftToken, guildID, channelID, messageID, rightToken] = messageLinkMatch;
// "!link" and "<link>" will cancel the embed request.
if (leftToken === "!" || (leftToken === "<" && rightToken === ">")) return null;
else return [guildID, channelID, messageID];
}