mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Compare commits
3 commits
86ccb74ac2
...
974985586d
Author | SHA1 | Date | |
---|---|---|---|
|
974985586d | ||
|
945102b7cf | ||
|
3ef487c4a4 |
43 changed files with 502 additions and 567 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,7 +1,6 @@
|
||||||
# Specific to this repository
|
# Specific to this repository
|
||||||
dist/
|
dist/
|
||||||
data/*
|
data/
|
||||||
!data/endpoints.json
|
|
||||||
tmp/
|
tmp/
|
||||||
test*
|
test*
|
||||||
!test/
|
!test/
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {User} from "discord.js";
|
import {User} from "discord.js";
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {random} from "../../core/lib";
|
import {random} from "../../core/lib";
|
||||||
import {parseVars} from "../../core/libd";
|
import {parseVars} from "../../core/lib";
|
||||||
|
|
||||||
const cookies = [
|
const cookies = [
|
||||||
`has given %target% a chocolate chip cookie!`,
|
`has given %target% a chocolate chip cookie!`,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {isAuthorized, getMoneyEmbed} from "./subcommands/eco-utils";
|
import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils";
|
||||||
import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./subcommands/eco-core";
|
import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core";
|
||||||
import {BuyCommand, ShopCommand} from "./subcommands/eco-shop";
|
import {BuyCommand, ShopCommand} from "./modules/eco-shop";
|
||||||
import {MondayCommand} from "./subcommands/eco-extras";
|
import {MondayCommand} from "./modules/eco-extras";
|
||||||
import {callMemberByUsername} from "../../core/libd";
|
import {callMemberByUsername} from "../../core/libd";
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {URL} from "url";
|
import {URL} from "url";
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {getContent} from "../../core/libd";
|
import {getContent} from "../../core/lib";
|
||||||
|
|
||||||
const endpoints: {sfw: {[key: string]: string}} = {
|
const endpoints: {sfw: {[key: string]: string}} = {
|
||||||
sfw: {
|
sfw: {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/// @ts-nocheck
|
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {getContent} from "../../core/libd";
|
import {getContent} from "../../core/lib";
|
||||||
|
import {URL} from "url";
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
description: "OwO-ifies the input.",
|
description: "OwO-ifies the input.",
|
||||||
async run($) {
|
async run($) {
|
||||||
let url = new URL(`https://nekos.life/api/v2/owoify?text=${$.args.join(" ")}`);
|
let url = new URL(`https://nekos.life/api/v2/owoify?text=${$.args.join(" ")}`);
|
||||||
const content = await getContent(url.toString());
|
const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}.
|
||||||
$.channel.send(content.owo);
|
$.channel.send(content.owo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Command, {handler} from "../core/command";
|
import Command, {handler} from "../../core/command";
|
||||||
import {botHasPermission, clean} from "../core/libd";
|
import {clean} from "../../core/lib";
|
||||||
import {Config, Storage} from "../core/structures";
|
import {botHasPermission} from "../../core/libd";
|
||||||
import {getPermissionLevel, getPermissionName} from "../core/permissions";
|
import {Config, Storage} from "../../core/structures";
|
||||||
|
import {getPermissionLevel, getPermissionName} from "../../core/permissions";
|
||||||
import {Permissions} from "discord.js";
|
import {Permissions} from "discord.js";
|
||||||
import * as discord from "discord.js";
|
import {logs} from "../../globals";
|
||||||
import {logs} from "../globals";
|
|
||||||
|
|
||||||
function getLogBuffer(type: string) {
|
function getLogBuffer(type: string) {
|
||||||
return {
|
return {
|
|
@ -1,7 +1,7 @@
|
||||||
import Command from "../core/command";
|
import Command from "../../core/command";
|
||||||
import {toTitleCase} from "../core/lib";
|
import {toTitleCase} from "../../core/lib";
|
||||||
import {loadableCommands, categories} from "../core/command";
|
import {loadableCommands, categories} from "../../core/command";
|
||||||
import {getPermissionName} from "../core/permissions";
|
import {getPermissionName} from "../../core/permissions";
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
description: "Lists all commands. If a command is specified, their arguments are listed as well.",
|
description: "Lists all commands. If a command is specified, their arguments are listed as well.",
|
|
@ -1,5 +1,5 @@
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {queryClosestEmoteByName} from "./subcommands/emote-utils";
|
import {queryClosestEmoteByName} from "./modules/emote-utils";
|
||||||
import {botHasPermission} from "../../core/libd";
|
import {botHasPermission} from "../../core/libd";
|
||||||
import {Permissions} from "discord.js";
|
import {Permissions} from "discord.js";
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import {MessageEmbed, version as djsversion} from "discord.js";
|
import {MessageEmbed, version as djsversion} from "discord.js";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import Command from "../core/command";
|
import Command from "../../core/command";
|
||||||
import {formatBytes, trimArray, getMemberByUsername} from "../core/libd";
|
import {formatBytes, trimArray} from "../../core/lib";
|
||||||
import {verificationLevels, filterLevels, regions} from "../defs/info";
|
import {getMemberByUsername} from "../../core/libd";
|
||||||
|
import {verificationLevels, filterLevels, regions} from "../../defs/info";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import utc from "moment";
|
import utc from "moment";
|
||||||
import {Guild} from "discord.js";
|
import {Guild} from "discord.js";
|
|
@ -1,6 +1,6 @@
|
||||||
import Command from "../../core/command";
|
import Command from "../../core/command";
|
||||||
import {Message, Channel, TextChannel} from "discord.js";
|
import {Message, Channel, TextChannel} from "discord.js";
|
||||||
import {queryClosestEmoteByName} from "./subcommands/emote-utils";
|
import {queryClosestEmoteByName} from "./modules/emote-utils";
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
description:
|
description:
|
|
@ -1,5 +1,5 @@
|
||||||
import Command, {handler} from "../core/command";
|
import Command, {handler} from "../../core/command";
|
||||||
import {pluralise} from "../core/lib";
|
import {pluralise} from "../../core/lib";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Collection, TextChannel} from "discord.js";
|
import {Collection, TextChannel} from "discord.js";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {parseVars} from "./libd";
|
import {parseVars} from "./lib";
|
||||||
import {Collection} from "discord.js";
|
import {Collection} from "discord.js";
|
||||||
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
|
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
|
||||||
import {getPrefix} from "../core/structures";
|
import {getPrefix} from "../core/structures";
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import {Client, ClientEvents, Constants} from "discord.js";
|
|
||||||
import Storage from "./storage";
|
|
||||||
|
|
||||||
interface EventOptions<K extends keyof ClientEvents> {
|
|
||||||
readonly on?: (...args: ClientEvents[K]) => void;
|
|
||||||
readonly once?: (...args: ClientEvents[K]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Event<K extends keyof ClientEvents> {
|
|
||||||
private readonly on?: (...args: ClientEvents[K]) => void;
|
|
||||||
private readonly once?: (...args: ClientEvents[K]) => void;
|
|
||||||
|
|
||||||
constructor(options: EventOptions<K>) {
|
|
||||||
this.on = options.on;
|
|
||||||
this.once = options.once;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For this function, I'm going to assume that the event is used with the correct arguments and that the event tag is checked in "storage.ts".
|
|
||||||
public attach(client: Client, event: K) {
|
|
||||||
if (this.on) client.on(event, this.on);
|
|
||||||
if (this.once) client.once(event, this.once);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadEvents(client: Client) {
|
|
||||||
for (const file of Storage.open("dist/events", (filename: string) => filename.endsWith(".js"))) {
|
|
||||||
const header = file.substring(0, file.indexOf(".js"));
|
|
||||||
const event = (await import(`../events/${header}`)).default;
|
|
||||||
|
|
||||||
if ((Object.values(Constants.Events) as string[]).includes(header)) {
|
|
||||||
event.attach(client, header);
|
|
||||||
console.log(`Loading Event: ${header}`);
|
|
||||||
} else
|
|
||||||
console.warn(
|
|
||||||
`"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
158
src/core/handler.ts
Normal file
158
src/core/handler.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
client.on("message", async (message) => {
|
||||||
|
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).
|
||||||
|
// The pattern here has an optional space at the end to capture that and make it not mess with the header and args.
|
||||||
|
const matches = message.content.match(new RegExp(`^<@!?${clientUser.id}> ?`));
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
prefix = matches[0];
|
||||||
|
exitEarly = false;
|
||||||
|
usesBotSpecificPrefix = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't start with the current normal prefix or the bot-specific unique prefix, exit the thread of execution early.
|
||||||
|
// Inline replies should still be captured here because if it doesn't exit early, two characters for a two-length prefix would still trigger commands.
|
||||||
|
if (exitEarly) return;
|
||||||
|
|
||||||
|
const [header, ...args] = message.content.substring(prefix.length).split(/ +/);
|
||||||
|
|
||||||
|
// If the message is just the prefix itself, move onto this block.
|
||||||
|
if (header === "" && args.length === 0) {
|
||||||
|
// I moved the bot-specific prefix to a separate conditional block to separate the logic.
|
||||||
|
// And because it listens for the mention as a prefix instead of a free-form mention, inline replies (probably) shouldn't ever trigger this unintentionally.
|
||||||
|
if (usesBotSpecificPrefix) {
|
||||||
|
message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${originalPrefix}\`.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commands.has(header)) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.channel.type === "text" &&
|
||||||
|
!message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES)
|
||||||
|
) {
|
||||||
|
let status;
|
||||||
|
|
||||||
|
if (message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
|
||||||
|
status =
|
||||||
|
"Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended.";
|
||||||
|
else
|
||||||
|
status =
|
||||||
|
"Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong.";
|
||||||
|
|
||||||
|
return message.author.send(
|
||||||
|
`I don't have permission to send messages in ${message.channel.toString()}. ${status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.member)
|
||||||
|
return console.warn("This command was likely called from a DM channel meaning the member object is null.");
|
||||||
|
|
||||||
|
if (!hasPermission(message.member, permLevel)) {
|
||||||
|
const userPermLevel = getPermissionLevel(message.member);
|
||||||
|
return message.channel.send(
|
||||||
|
`You don't have access to this command! Your permission level is \`${getPermissionName(
|
||||||
|
userPermLevel
|
||||||
|
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
|
||||||
|
permLevel
|
||||||
|
)}\` (${permLevel}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEndpoint) return message.channel.send("Too many arguments!");
|
||||||
|
|
||||||
|
// Execute with dynamic library attached. //
|
||||||
|
// The purpose of using $.bind($) is to clone the function so as to not modify the original $.
|
||||||
|
// The cloned function doesn't copy the properties, so Object.assign() is used.
|
||||||
|
// Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
|
||||||
|
command.execute({
|
||||||
|
args: params,
|
||||||
|
author: message.author,
|
||||||
|
channel: message.channel,
|
||||||
|
client: message.client,
|
||||||
|
guild: message.guild,
|
||||||
|
member: message.member,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.once("ready", () => {
|
||||||
|
if (client.user) {
|
||||||
|
console.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);
|
||||||
|
client.user.setActivity({
|
||||||
|
type: "LISTENING",
|
||||||
|
name: `${Config.prefix}help`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
165
src/core/lib.ts
165
src/core/lib.ts
|
@ -1,4 +1,169 @@
|
||||||
// Library for pure functions
|
// Library for pure functions
|
||||||
|
import {get} from "https";
|
||||||
|
import FileManager from "./storage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a command by spaces while accounting for quotes which capture string arguments.
|
||||||
|
* - `\"` = `"`
|
||||||
|
* - `\\` = `\`
|
||||||
|
*/
|
||||||
|
export function parseArgs(line: string): string[] {
|
||||||
|
let result = [];
|
||||||
|
let selection = "";
|
||||||
|
let inString = false;
|
||||||
|
let isEscaped = false;
|
||||||
|
|
||||||
|
for (let c of line) {
|
||||||
|
if (isEscaped) {
|
||||||
|
if (['"', "\\"].includes(c)) selection += c;
|
||||||
|
else selection += "\\" + c;
|
||||||
|
|
||||||
|
isEscaped = false;
|
||||||
|
} else if (c === "\\") isEscaped = true;
|
||||||
|
else if (c === '"') inString = !inString;
|
||||||
|
else if (c === " " && !inString) {
|
||||||
|
result.push(selection);
|
||||||
|
selection = "";
|
||||||
|
} else selection += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.length > 0) result.push(selection);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to store a template string with variable markers and parse it later.
|
||||||
|
* - Use `%name%` for variables
|
||||||
|
* - `%%` = `%`
|
||||||
|
* - If the invalid token is null/undefined, nothing is changed.
|
||||||
|
*/
|
||||||
|
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string | null = ""): string {
|
||||||
|
let result = "";
|
||||||
|
let inVariable = false;
|
||||||
|
let token = "";
|
||||||
|
|
||||||
|
for (const c of line) {
|
||||||
|
if (c === "%") {
|
||||||
|
if (inVariable) {
|
||||||
|
if (token === "") result += "%";
|
||||||
|
else {
|
||||||
|
if (token in definitions) result += definitions[token];
|
||||||
|
else if (invalid === null) result += `%${token}%`;
|
||||||
|
else result += invalid;
|
||||||
|
|
||||||
|
token = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inVariable = !inVariable;
|
||||||
|
} else if (inVariable) token += c;
|
||||||
|
else result += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isType(value: any, type: any): boolean {
|
||||||
|
if (value === undefined && type === undefined) return true;
|
||||||
|
else if (value === null && type === null) return true;
|
||||||
|
else return value !== undefined && value !== null && value.constructor === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
|
||||||
|
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
|
||||||
|
* If at any point the value doesn't match the data structure provided, the fallback is returned.
|
||||||
|
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
|
||||||
|
*/
|
||||||
|
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T {
|
||||||
|
if (isArray && isType(value, Array)) {
|
||||||
|
for (let item of value) if (!isType(item, type)) return fallback;
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
if (isType(value, type)) return value;
|
||||||
|
else return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clean(text: any) {
|
||||||
|
if (typeof text === "string")
|
||||||
|
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
|
||||||
|
else return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trimArray(arr: any, maxLen = 10) {
|
||||||
|
if (arr.length > maxLen) {
|
||||||
|
const len = arr.length - maxLen;
|
||||||
|
arr = arr.slice(0, maxLen);
|
||||||
|
arr.push(`${len} more...`);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes: any) {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContent(url: string): Promise<{url: string}> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
get(url, (res) => {
|
||||||
|
const {statusCode} = res;
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
res.resume();
|
||||||
|
reject(`Request failed. Status code: ${statusCode}`);
|
||||||
|
}
|
||||||
|
res.setEncoding("utf8");
|
||||||
|
let rawData = "";
|
||||||
|
res.on("data", (chunk: string) => {
|
||||||
|
rawData += chunk;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(rawData);
|
||||||
|
resolve(parsedData);
|
||||||
|
} catch (e) {
|
||||||
|
reject(`Error: ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on("error", (err: {message: any}) => {
|
||||||
|
reject(`Error: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenericJSON {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class GenericStructure {
|
||||||
|
private __meta__ = "generic";
|
||||||
|
|
||||||
|
constructor(tag?: string) {
|
||||||
|
this.__meta__ = tag || this.__meta__;
|
||||||
|
}
|
||||||
|
|
||||||
|
public save(asynchronous = true) {
|
||||||
|
const tag = this.__meta__;
|
||||||
|
/// @ts-ignore
|
||||||
|
delete this.__meta__;
|
||||||
|
FileManager.write(tag, this, asynchronous);
|
||||||
|
this.__meta__ = tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
|
||||||
|
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
|
||||||
|
export const Random = {
|
||||||
|
num: (min: number, max: number) => Math.random() * (max - min) + min,
|
||||||
|
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
|
||||||
|
chance: (decimal: number) => Math.random() < decimal,
|
||||||
|
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
|
||||||
|
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pluralises a word and chooses a suffix attached to the root provided.
|
* Pluralises a word and chooses a suffix attached to the root provided.
|
||||||
|
|
201
src/core/libd.ts
201
src/core/libd.ts
|
@ -9,37 +9,25 @@ import {
|
||||||
NewsChannel,
|
NewsChannel,
|
||||||
MessageOptions
|
MessageOptions
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import {get} from "https";
|
|
||||||
import FileManager from "./storage";
|
|
||||||
import {eventListeners} from "../events/messageReactionRemove";
|
|
||||||
import {client} from "../index";
|
import {client} from "../index";
|
||||||
import {EmoteRegistryDump} from "./structures";
|
|
||||||
|
// 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 function botHasPermission(guild: Guild | null, permission: number): boolean {
|
export function botHasPermission(guild: Guild | null, permission: number): boolean {
|
||||||
return !!guild?.me?.hasPermission(permission);
|
return !!guild?.me?.hasPermission(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateGlobalEmoteRegistry(): void {
|
|
||||||
const data: EmoteRegistryDump = {version: 1, list: []};
|
|
||||||
|
|
||||||
for (const guild of client.guilds.cache.values()) {
|
|
||||||
for (const emote of guild.emojis.cache.values()) {
|
|
||||||
data.list.push({
|
|
||||||
ref: emote.name,
|
|
||||||
id: emote.id,
|
|
||||||
name: emote.name,
|
|
||||||
requires_colons: emote.requiresColons || false,
|
|
||||||
animated: emote.animated,
|
|
||||||
url: emote.url,
|
|
||||||
guild_id: emote.guild.name,
|
|
||||||
guild_name: emote.guild.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileManager.write("emote-registry", data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
|
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
|
||||||
|
|
||||||
// Pagination function that allows for customization via a callback.
|
// Pagination function that allows for customization via a callback.
|
||||||
|
@ -274,166 +262,3 @@ export async function callMemberByUsername(
|
||||||
else send(`Couldn't find a user by the name of \`${username}\`!`);
|
else send(`Couldn't find a user by the name of \`${username}\`!`);
|
||||||
} else send("You must execute this command in a server!");
|
} else send("You must execute this command in a server!");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits a command by spaces while accounting for quotes which capture string arguments.
|
|
||||||
* - `\"` = `"`
|
|
||||||
* - `\\` = `\`
|
|
||||||
*/
|
|
||||||
export function parseArgs(line: string): string[] {
|
|
||||||
let result = [];
|
|
||||||
let selection = "";
|
|
||||||
let inString = false;
|
|
||||||
let isEscaped = false;
|
|
||||||
|
|
||||||
for (let c of line) {
|
|
||||||
if (isEscaped) {
|
|
||||||
if (['"', "\\"].includes(c)) selection += c;
|
|
||||||
else selection += "\\" + c;
|
|
||||||
|
|
||||||
isEscaped = false;
|
|
||||||
} else if (c === "\\") isEscaped = true;
|
|
||||||
else if (c === '"') inString = !inString;
|
|
||||||
else if (c === " " && !inString) {
|
|
||||||
result.push(selection);
|
|
||||||
selection = "";
|
|
||||||
} else selection += c;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selection.length > 0) result.push(selection);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows you to store a template string with variable markers and parse it later.
|
|
||||||
* - Use `%name%` for variables
|
|
||||||
* - `%%` = `%`
|
|
||||||
* - If the invalid token is null/undefined, nothing is changed.
|
|
||||||
*/
|
|
||||||
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string | null = ""): string {
|
|
||||||
let result = "";
|
|
||||||
let inVariable = false;
|
|
||||||
let token = "";
|
|
||||||
|
|
||||||
for (const c of line) {
|
|
||||||
if (c === "%") {
|
|
||||||
if (inVariable) {
|
|
||||||
if (token === "") result += "%";
|
|
||||||
else {
|
|
||||||
if (token in definitions) result += definitions[token];
|
|
||||||
else if (invalid === null) result += `%${token}%`;
|
|
||||||
else result += invalid;
|
|
||||||
|
|
||||||
token = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inVariable = !inVariable;
|
|
||||||
} else if (inVariable) token += c;
|
|
||||||
else result += c;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isType(value: any, type: any): boolean {
|
|
||||||
if (value === undefined && type === undefined) return true;
|
|
||||||
else if (value === null && type === null) return true;
|
|
||||||
else return value !== undefined && value !== null && value.constructor === type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
|
|
||||||
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
|
|
||||||
* If at any point the value doesn't match the data structure provided, the fallback is returned.
|
|
||||||
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
|
|
||||||
*/
|
|
||||||
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T {
|
|
||||||
if (isArray && isType(value, Array)) {
|
|
||||||
for (let item of value) if (!isType(item, type)) return fallback;
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
if (isType(value, type)) return value;
|
|
||||||
else return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clean(text: any) {
|
|
||||||
if (typeof text === "string")
|
|
||||||
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
|
|
||||||
else return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function trimArray(arr: any, maxLen = 10) {
|
|
||||||
if (arr.length > maxLen) {
|
|
||||||
const len = arr.length - maxLen;
|
|
||||||
arr = arr.slice(0, maxLen);
|
|
||||||
arr.push(`${len} more...`);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBytes(bytes: any) {
|
|
||||||
if (bytes === 0) return "0 Bytes";
|
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getContent(url: string): Promise<{url: string}> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
get(url, (res) => {
|
|
||||||
const {statusCode} = res;
|
|
||||||
if (statusCode !== 200) {
|
|
||||||
res.resume();
|
|
||||||
reject(`Request failed. Status code: ${statusCode}`);
|
|
||||||
}
|
|
||||||
res.setEncoding("utf8");
|
|
||||||
let rawData = "";
|
|
||||||
res.on("data", (chunk: string) => {
|
|
||||||
rawData += chunk;
|
|
||||||
});
|
|
||||||
res.on("end", () => {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(rawData);
|
|
||||||
resolve(parsedData);
|
|
||||||
} catch (e) {
|
|
||||||
reject(`Error: ${e.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on("error", (err: {message: any}) => {
|
|
||||||
reject(`Error: ${err.message}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GenericJSON {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class GenericStructure {
|
|
||||||
private __meta__ = "generic";
|
|
||||||
|
|
||||||
constructor(tag?: string) {
|
|
||||||
this.__meta__ = tag || this.__meta__;
|
|
||||||
}
|
|
||||||
|
|
||||||
public save(asynchronous = true) {
|
|
||||||
const tag = this.__meta__;
|
|
||||||
/// @ts-ignore
|
|
||||||
delete this.__meta__;
|
|
||||||
FileManager.write(tag, this, asynchronous);
|
|
||||||
this.__meta__ = tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
|
|
||||||
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
|
|
||||||
export const Random = {
|
|
||||||
num: (min: number, max: number) => Math.random() * (max - min) + min,
|
|
||||||
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
|
|
||||||
chance: (decimal: number) => Math.random() < decimal,
|
|
||||||
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
|
|
||||||
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
|
|
||||||
};
|
|
||||||
|
|
|
@ -63,6 +63,6 @@ export function getPermissionLevel(member: GuildMember): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPermissionName(level: number) {
|
export function getPermissionName(level: number) {
|
||||||
if (level > length || length < 0) return "N/A";
|
if (level > length || level < 0) return "N/A";
|
||||||
else return PermissionLevels[level].name;
|
else return PermissionLevels[level].name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import FileManager from "./storage";
|
import FileManager from "./storage";
|
||||||
import {select, GenericJSON, GenericStructure} from "./libd";
|
import {select, GenericJSON, GenericStructure} from "./lib";
|
||||||
import {watch} from "fs";
|
import {watch} from "fs";
|
||||||
import {Guild as DiscordGuild, Snowflake} from "discord.js";
|
import {Guild as DiscordGuild, Snowflake} from "discord.js";
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {client} from "../index";
|
|
||||||
import * as discord from "discord.js";
|
|
||||||
|
|
||||||
export default new Event<"channelCreate">({
|
|
||||||
async on(channel) {
|
|
||||||
const botGuilds = client.guilds;
|
|
||||||
if (channel instanceof discord.GuildChannel) {
|
|
||||||
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
|
||||||
console.log(`Channel created in '${createdGuild.name}' called '#${channel.name}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,13 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {client} from "../index";
|
|
||||||
import * as discord from "discord.js";
|
|
||||||
|
|
||||||
export default new Event<"channelDelete">({
|
|
||||||
async on(channel) {
|
|
||||||
const botGuilds = client.guilds;
|
|
||||||
if (channel instanceof discord.GuildChannel) {
|
|
||||||
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
|
||||||
console.log(`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"emojiCreate">({
|
|
||||||
on(emote) {
|
|
||||||
console.log(`Updated emote registry. ${emote.name}`);
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"emojiDelete">({
|
|
||||||
on() {
|
|
||||||
console.log("Updated emote registry.");
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"emojiUpdate">({
|
|
||||||
on() {
|
|
||||||
console.log("Updated emote registry.");
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"guildCreate">({
|
|
||||||
on() {
|
|
||||||
console.log("Updated emote registry.");
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"guildDelete">({
|
|
||||||
on() {
|
|
||||||
console.log("Updated emote registry.");
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,149 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
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";
|
|
||||||
|
|
||||||
export default new Event<"message">({
|
|
||||||
async on(message) {
|
|
||||||
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).
|
|
||||||
// The pattern here has an optional space at the end to capture that and make it not mess with the header and args.
|
|
||||||
const matches = message.content.match(new RegExp(`^<@!?${clientUser.id}> ?`));
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
prefix = matches[0];
|
|
||||||
exitEarly = false;
|
|
||||||
usesBotSpecificPrefix = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it doesn't start with the current normal prefix or the bot-specific unique prefix, exit the thread of execution early.
|
|
||||||
// Inline replies should still be captured here because if it doesn't exit early, two characters for a two-length prefix would still trigger commands.
|
|
||||||
if (exitEarly) return;
|
|
||||||
|
|
||||||
const [header, ...args] = message.content.substring(prefix.length).split(/ +/);
|
|
||||||
|
|
||||||
// If the message is just the prefix itself, move onto this block.
|
|
||||||
if (header === "" && args.length === 0) {
|
|
||||||
// I moved the bot-specific prefix to a separate conditional block to separate the logic.
|
|
||||||
// And because it listens for the mention as a prefix instead of a free-form mention, inline replies (probably) shouldn't ever trigger this unintentionally.
|
|
||||||
if (usesBotSpecificPrefix) {
|
|
||||||
message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${originalPrefix}\`.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!commands.has(header)) return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.channel.type === "text" &&
|
|
||||||
!message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES)
|
|
||||||
) {
|
|
||||||
let status;
|
|
||||||
|
|
||||||
if (message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
|
|
||||||
status =
|
|
||||||
"Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended.";
|
|
||||||
else
|
|
||||||
status =
|
|
||||||
"Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong.";
|
|
||||||
|
|
||||||
return message.author.send(
|
|
||||||
`I don't have permission to send messages in ${message.channel.toString()}. ${status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.member)
|
|
||||||
return console.warn("This command was likely called from a DM channel meaning the member object is null.");
|
|
||||||
|
|
||||||
if (!hasPermission(message.member, permLevel)) {
|
|
||||||
const userPermLevel = getPermissionLevel(message.member);
|
|
||||||
return message.channel.send(
|
|
||||||
`You don't have access to this command! Your permission level is \`${getPermissionName(
|
|
||||||
userPermLevel
|
|
||||||
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
|
|
||||||
permLevel
|
|
||||||
)}\` (${permLevel}).`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEndpoint) return message.channel.send("Too many arguments!");
|
|
||||||
|
|
||||||
// Execute with dynamic library attached. //
|
|
||||||
// The purpose of using $.bind($) is to clone the function so as to not modify the original $.
|
|
||||||
// The cloned function doesn't copy the properties, so Object.assign() is used.
|
|
||||||
// Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
|
|
||||||
command.execute({
|
|
||||||
args: params,
|
|
||||||
author: message.author,
|
|
||||||
channel: message.channel,
|
|
||||||
client: message.client,
|
|
||||||
guild: message.guild,
|
|
||||||
member: message.member,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,18 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {Permissions} from "discord.js";
|
|
||||||
import {botHasPermission} from "../core/libd";
|
|
||||||
|
|
||||||
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
|
|
||||||
export 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.
|
|
||||||
export default new Event<"messageReactionRemove">({
|
|
||||||
on(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
import Event from "../core/event";
|
|
||||||
import {client} from "../index";
|
|
||||||
import {Config} from "../core/structures";
|
|
||||||
import {updateGlobalEmoteRegistry} from "../core/libd";
|
|
||||||
|
|
||||||
export default new Event<"ready">({
|
|
||||||
once() {
|
|
||||||
if (client.user) {
|
|
||||||
console.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);
|
|
||||||
client.user.setActivity({
|
|
||||||
type: "LISTENING",
|
|
||||||
name: `${Config.prefix}help`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateGlobalEmoteRegistry();
|
|
||||||
}
|
|
||||||
});
|
|
65
src/index.ts
65
src/index.ts
|
@ -1,65 +1,20 @@
|
||||||
|
// Bootstrapping Section //
|
||||||
import "./globals";
|
import "./globals";
|
||||||
import * as discord from "discord.js";
|
import {Client} from "discord.js";
|
||||||
import setup from "./setup";
|
import setup from "./setup";
|
||||||
import {Config} from "./core/structures";
|
import {Config} from "./core/structures";
|
||||||
import {loadEvents} from "./core/event";
|
|
||||||
import "discord.js-lavalink-lib";
|
|
||||||
import LavalinkMusic from "discord.js-lavalink-lib";
|
|
||||||
|
|
||||||
declare module "discord.js" {
|
|
||||||
interface Presence {
|
|
||||||
patch(data: any): void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The terrible hacks were written by none other than The Noble Programmer On The White PC.
|
|
||||||
|
|
||||||
// NOTE: Terrible hack ahead!!! In order to reduce the memory usage of the bot
|
|
||||||
// we only store the information from presences that we actually end up using,
|
|
||||||
// which currently is only the (online/idle/dnd/offline/...) status (see
|
|
||||||
// `src/commands/info.ts`). What data is retrieved from the `data` object
|
|
||||||
// (which contains the data received from the Gateway) and how can be seen
|
|
||||||
// here:
|
|
||||||
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/structures/Presence.js#L81-L110>.
|
|
||||||
const oldPresencePatch = discord.Presence.prototype.patch;
|
|
||||||
discord.Presence.prototype.patch = function patch(data: any) {
|
|
||||||
oldPresencePatch.call(this, {status: data.status});
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is here in order to make it much less of a headache to access the client from other files.
|
// This is here in order to make it much less of a headache to access the client from other files.
|
||||||
// This of course won't actually do anything until the setup process is complete and it logs in.
|
// This of course won't actually do anything until the setup process is complete and it logs in.
|
||||||
export const client = new discord.Client();
|
export const client = new Client();
|
||||||
|
|
||||||
// NOTE: Terrible hack continued!!! Unfortunately we can't receive the presence
|
// Send the login request to Discord's API and then load modules while waiting for it.
|
||||||
// data at all when the GUILD_PRESENCES intent is disabled, so while we do
|
|
||||||
// waste network bandwidth and the CPU time for decoding the incoming packets,
|
|
||||||
// the function which handles those packets is NOP-ed out, which, among other
|
|
||||||
// things, skips the code which caches the referenced users in the packet. See
|
|
||||||
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/client/actions/PresenceUpdate.js#L7-L41>.
|
|
||||||
(client["actions"] as any)["PresenceUpdate"].handle = () => {};
|
|
||||||
|
|
||||||
(client as any).music = LavalinkMusic(client, {
|
|
||||||
lavalink: {
|
|
||||||
restnode: {
|
|
||||||
host: "localhost",
|
|
||||||
port: 2333,
|
|
||||||
password: "youshallnotpass"
|
|
||||||
},
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
host: "localhost",
|
|
||||||
port: 2333,
|
|
||||||
password: "youshallnotpass"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
prefix: Config.prefix,
|
|
||||||
helpCmd: "mhelp",
|
|
||||||
admins: ["717352467280691331"]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Command loading will start as soon as an instance of "core/command" is loaded, which is loaded during "events/message".
|
|
||||||
setup.init().then(() => {
|
setup.init().then(() => {
|
||||||
loadEvents(client);
|
|
||||||
client.login(Config.token).catch(setup.again);
|
client.login(Config.token).catch(setup.again);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 "./modules/lavalink";
|
||||||
|
import "./modules/emoteRegistry";
|
||||||
|
import "./modules/channelListener";
|
||||||
|
|
20
src/modules/channelListener.ts
Normal file
20
src/modules/channelListener.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {client} from "../index";
|
||||||
|
import {GuildChannel} from "discord.js";
|
||||||
|
|
||||||
|
client.on("channelCreate", async (channel) => {
|
||||||
|
const botGuilds = client.guilds;
|
||||||
|
|
||||||
|
if (channel instanceof GuildChannel) {
|
||||||
|
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
||||||
|
console.log(`Channel created in '${createdGuild.name}' called '#${channel.name}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("channelDelete", async (channel) => {
|
||||||
|
const botGuilds = client.guilds;
|
||||||
|
|
||||||
|
if (channel instanceof GuildChannel) {
|
||||||
|
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
||||||
|
console.log(`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`);
|
||||||
|
}
|
||||||
|
});
|
53
src/modules/emoteRegistry.ts
Normal file
53
src/modules/emoteRegistry.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import {client} from "../index";
|
||||||
|
import FileManager from "../core/storage";
|
||||||
|
import {EmoteRegistryDump} from "../core/structures";
|
||||||
|
|
||||||
|
function updateGlobalEmoteRegistry(): void {
|
||||||
|
const data: EmoteRegistryDump = {version: 1, list: []};
|
||||||
|
|
||||||
|
for (const guild of client.guilds.cache.values()) {
|
||||||
|
for (const emote of guild.emojis.cache.values()) {
|
||||||
|
data.list.push({
|
||||||
|
ref: emote.name,
|
||||||
|
id: emote.id,
|
||||||
|
name: emote.name,
|
||||||
|
requires_colons: emote.requiresColons || false,
|
||||||
|
animated: emote.animated,
|
||||||
|
url: emote.url,
|
||||||
|
guild_id: emote.guild.name,
|
||||||
|
guild_name: emote.guild.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileManager.write("emote-registry", data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.on("emojiCreate", (emote) => {
|
||||||
|
console.log(`Updated emote registry. ${emote.name}`);
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("emojiDelete", () => {
|
||||||
|
console.log("Updated emote registry.");
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("emojiUpdate", () => {
|
||||||
|
console.log("Updated emote registry.");
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("guildCreate", () => {
|
||||||
|
console.log("Updated emote registry.");
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("guildDelete", () => {
|
||||||
|
console.log("Updated emote registry.");
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("ready", () => {
|
||||||
|
updateGlobalEmoteRegistry();
|
||||||
|
});
|
52
src/modules/lavalink.ts
Normal file
52
src/modules/lavalink.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {Presence} from "discord.js";
|
||||||
|
import LavalinkMusic from "discord.js-lavalink-lib";
|
||||||
|
import {Config} from "../core/structures";
|
||||||
|
import {client} from "../index";
|
||||||
|
|
||||||
|
declare module "discord.js" {
|
||||||
|
interface Presence {
|
||||||
|
patch(data: any): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The terrible hacks were written by none other than The Noble Programmer On The White PC.
|
||||||
|
|
||||||
|
// NOTE: Terrible hack ahead!!! In order to reduce the memory usage of the bot
|
||||||
|
// we only store the information from presences that we actually end up using,
|
||||||
|
// which currently is only the (online/idle/dnd/offline/...) status (see
|
||||||
|
// `src/commands/info.ts`). What data is retrieved from the `data` object
|
||||||
|
// (which contains the data received from the Gateway) and how can be seen
|
||||||
|
// here:
|
||||||
|
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/structures/Presence.js#L81-L110>.
|
||||||
|
const oldPresencePatch = Presence.prototype.patch;
|
||||||
|
Presence.prototype.patch = function patch(data: any) {
|
||||||
|
oldPresencePatch.call(this, {status: data.status});
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: Terrible hack continued!!! Unfortunately we can't receive the presence
|
||||||
|
// data at all when the GUILD_PRESENCES intent is disabled, so while we do
|
||||||
|
// waste network bandwidth and the CPU time for decoding the incoming packets,
|
||||||
|
// the function which handles those packets is NOP-ed out, which, among other
|
||||||
|
// things, skips the code which caches the referenced users in the packet. See
|
||||||
|
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/client/actions/PresenceUpdate.js#L7-L41>.
|
||||||
|
(client["actions"] as any)["PresenceUpdate"].handle = () => {};
|
||||||
|
|
||||||
|
(client as any).music = LavalinkMusic(client, {
|
||||||
|
lavalink: {
|
||||||
|
restnode: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 2333,
|
||||||
|
password: "youshallnotpass"
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
host: "localhost",
|
||||||
|
port: 2333,
|
||||||
|
password: "youshallnotpass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
prefix: Config.prefix,
|
||||||
|
helpCmd: "mhelp",
|
||||||
|
admins: ["717352467280691331"]
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue