Formatting Preview Alpha

This commit is contained in:
WatDuhHekBro 2020-12-14 19:44:28 -06:00
parent 98e47e3796
commit 39f89a9f63
47 changed files with 4714 additions and 4581 deletions

View File

@ -1,6 +1,6 @@
name: 'CodeQL Config' name: "CodeQL Config"
queries: queries:
- uses: security-and-quality - uses: security-and-quality
paths: paths:
- dist - dist

View File

@ -1,41 +1,41 @@
name: 'CodeQL' name: "CodeQL"
on: on:
push: push:
branches: [typescript] branches: [typescript]
pull_request: pull_request:
branches: [typescript] branches: [typescript]
schedule: schedule:
- cron: '0 5 * * 1' - cron: "0 5 * * 1"
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- run: git checkout HEAD^2 - run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
- name: Setup Node.JS - name: Setup Node.JS
uses: actions/setup-node@v2-beta uses: actions/setup-node@v2-beta
with: with:
node-version: '12' node-version: "12"
- run: npm ci - run: npm ci
- name: Build codebase - name: Build codebase
run: npm run build run: npm run build
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
languages: javascript languages: javascript
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v1

View File

@ -1,25 +1,25 @@
name: Build Docker Image + Push name: Build Docker Image + Push
on: on:
push: push:
branches: branches:
- typescript - typescript
- docker - docker
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Docker BuildX - name: Install Docker BuildX
id: buildx id: buildx
uses: crazy-max/ghaction-docker-buildx@v1 uses: crazy-max/ghaction-docker-buildx@v1
with: with:
buildx-version: latest buildx-version: latest
- name: Login to Docker Hub - name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Build the image - name: Build the image
run: | run: |
docker buildx build \ docker buildx build \
--tag keanucode/travbot-v3:latest \ --tag keanucode/travbot-v3:latest \
--platform linux/amd64,linux/arm/v7,linux/arm64 --push . --platform linux/amd64,linux/arm/v7,linux/arm64 --push .

View File

@ -1,3 +1,10 @@
# Specific to prettier (so it doesn't throw a bunch of errors when running "npm run format")
.dockerignore
.gitignore
.prettierignore
Dockerfile
LICENSE
# Specific to this repository # Specific to this repository
dist/ dist/
data/ data/

View File

@ -12,9 +12,9 @@ Thank you for coming on this journey with me, but it is time to put the big chan
Special thanks to: Special thanks to:
- Lexi Sother (TravBot v2, structural overhaul. Reviewing PRs.) - Lexi Sother (TravBot v2, structural overhaul. Reviewing PRs.)
- WatDuhHekBro (a _lot_ of contributions to TravBot v2) - WatDuhHekBro (a _lot_ of contributions to TravBot v2)
- Zeehondie (Ideas for various commands.) - Zeehondie (Ideas for various commands.)
### License ### License

View File

@ -17,3 +17,4 @@
- ...update the [changelog](CHANGELOG.md) and any other necessary docs. - ...update the [changelog](CHANGELOG.md) and any other necessary docs.
- ...update the version numbers in `package.json` and `package-lock.json`. - ...update the version numbers in `package.json` and `package-lock.json`.
- ...make sure the test suite passes by running `npm test`. - ...make sure the test suite passes by running `npm test`.
- ...format the code by running `npm run format`.

2948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,42 @@
{ {
"name": "d.js-v12-bot", "name": "d.js-v12-bot",
"version": "0.0.1", "version": "0.0.1",
"description": "A Discord bot built on Discord.JS v12", "description": "A Discord bot built on Discord.JS v12",
"main": "dist/index.js", "main": "dist/index.js",
"private": true, "private": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.0", "chalk": "^4.1.0",
"discord.js": "^12.4.0", "discord.js": "^12.4.0",
"discord.js-lavalink-lib": "^0.1.7", "discord.js-lavalink-lib": "^0.1.7",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"moment": "^2.29.1", "moment": "^2.29.1",
"ms": "^2.1.2", "ms": "^2.1.2",
"os": "^0.1.1" "os": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/inquirer": "^6.5.0", "@types/inquirer": "^6.5.0",
"@types/mocha": "^8.0.3", "@types/mocha": "^8.0.3",
"@types/ms": "^0.7.31", "@types/ms": "^0.7.31",
"@types/node": "^14.14.2", "@types/node": "^14.14.2",
"@types/ws": "^7.2.7", "@types/ws": "^7.2.7",
"mocha": "^8.2.0", "mocha": "^8.2.0",
"prettier": "2.1.2", "prettier": "2.1.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"tsc-watch": "^4.2.9", "tsc-watch": "^4.2.9",
"typescript": "^3.9.7" "typescript": "^3.9.7"
}, },
"scripts": { "scripts": {
"build": "tsc && npm prune --production", "build": "tsc && npm prune --production",
"start": "node dist/index.js", "start": "node dist/index.js",
"once": "tsc && npm start", "once": "tsc && npm start",
"dev": "tsc-watch --onSuccess \"node dist/index.js dev\"", "dev": "tsc-watch --onSuccess \"node dist/index.js dev\"",
"test": "mocha --require ts-node/register --extension ts --recursive", "test": "mocha --require ts-node/register --extension ts --recursive",
"format": "prettier --write **/*" "format": "prettier --write **/*"
}, },
"keywords": [ "keywords": [
"discord.js", "discord.js",
"bot" "bot"
], ],
"author": "Keanu Timmermans", "author": "Keanu Timmermans",
"license": "MIT" "license": "MIT"
} }

View File

@ -1,14 +1,14 @@
module.exports = { module.exports = {
printWidth: 80, printWidth: 80,
tabWidth: 2, tabWidth: 4,
useTabs: false, useTabs: false,
semi: true, semi: true,
singleQuote: true, singleQuote: false,
quoteProps: 'as-needed', quoteProps: "as-needed",
jsxSingleQuote: false, jsxSingleQuote: false,
trailingComma: 'all', trailingComma: "none",
bracketSpacing: true, bracketSpacing: false,
jsxBracketSameLine: true, jsxBracketSameLine: false,
arrowParens: 'always', arrowParens: "always",
endOfLine: 'auto', endOfLine: "lf"
}; };

View File

@ -1,228 +1,237 @@
import Command from '../core/command'; import Command from "../core/command";
import { CommonLibrary, logs, botHasPermission, clean } from '../core/lib'; import {CommonLibrary, logs, botHasPermission, clean} from "../core/lib";
import { Config, Storage } from '../core/structures'; import {Config, Storage} from "../core/structures";
import { PermissionNames, getPermissionLevel } from '../core/permissions'; import {PermissionNames, getPermissionLevel} from "../core/permissions";
import { Permissions } from 'discord.js'; import {Permissions} from "discord.js";
import * as discord from 'discord.js'; import * as discord from "discord.js";
function getLogBuffer(type: string) { function getLogBuffer(type: string) {
return { return {
files: [ files: [
{ {
attachment: Buffer.alloc(logs[type].length, logs[type]), attachment: Buffer.alloc(logs[type].length, logs[type]),
name: `${Date.now()}.${type}.log`, name: `${Date.now()}.${type}.log`
}, }
], ]
}; };
} }
const activities = ['playing', 'listening', 'streaming', 'watching']; const activities = ["playing", "listening", "streaming", "watching"];
const statuses = ['online', 'idle', 'dnd', 'invisible']; const statuses = ["online", "idle", "dnd", "invisible"];
export default new Command({ export default new Command({
description: description:
"An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.", "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
if (!$.member) if (!$.member)
return $.channel.send( return $.channel.send(
"Couldn't find a member object for you! Did you make sure you used this in a server?", "Couldn't find a member object for you! Did you make sure you used this in a server?"
); );
const permLevel = getPermissionLevel($.member); const permLevel = getPermissionLevel($.member);
$.channel.send( $.channel.send(
`${$.author.toString()}, your permission level is \`${ `${$.author.toString()}, your permission level is \`${
PermissionNames[permLevel] PermissionNames[permLevel]
}\` (${permLevel}).`, }\` (${permLevel}).`
); );
}, },
subcommands: { subcommands: {
set: new Command({ set: new Command({
description: 'Set different per-guild settings for the bot.', description: "Set different per-guild settings for the bot.",
run: 'You have to specify the option you want to set.', run: "You have to specify the option you want to set.",
permission: Command.PERMISSIONS.ADMIN, permission: Command.PERMISSIONS.ADMIN,
subcommands: { subcommands: {
prefix: new Command({ prefix: new Command({
description: description:
'Set a custom prefix for your guild. Removes your custom prefix if none is provided.', "Set a custom prefix for your guild. Removes your custom prefix if none is provided.",
usage: '(<prefix>)', usage: "(<prefix>)",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
Storage.getGuild($.guild?.id || 'N/A').prefix = null; Storage.getGuild($.guild?.id || "N/A").prefix = null;
Storage.save(); Storage.save();
$.channel.send( $.channel.send(
`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`, `The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`
); );
}, },
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
Storage.getGuild($.guild?.id || 'N/A').prefix = $.args[0]; Storage.getGuild($.guild?.id || "N/A").prefix =
Storage.save(); $.args[0];
$.channel.send( Storage.save();
`The custom prefix for this guild is now \`${$.args[0]}\`.`, $.channel.send(
); `The custom prefix for this guild is now \`${$.args[0]}\`.`
}, );
}), }
}), })
}, })
}), }
diag: new Command({ }),
description: 'Requests a debug log with the "info" verbosity level.', diag: new Command({
permission: Command.PERMISSIONS.BOT_SUPPORT, description:
async run($: CommonLibrary): Promise<any> { 'Requests a debug log with the "info" verbosity level.',
$.channel.send(getLogBuffer('info')); permission: Command.PERMISSIONS.BOT_SUPPORT,
}, async run($: CommonLibrary): Promise<any> {
any: new Command({ $.channel.send(getLogBuffer("info"));
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys( },
logs, any: new Command({
).join(', ')}]\``, description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(
async run($: CommonLibrary): Promise<any> { logs
const type = $.args[0]; ).join(", ")}]\``,
async run($: CommonLibrary): Promise<any> {
if (type in logs) $.channel.send(getLogBuffer(type)); const type = $.args[0];
else
$.channel.send( if (type in logs) $.channel.send(getLogBuffer(type));
`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys( else
logs, $.channel.send(
).join(', ')}]\`.`, `Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(
); logs
}, ).join(", ")}]\`.`
}), );
}), }
status: new Command({ })
description: "Changes the bot's status.", }),
permission: Command.PERMISSIONS.BOT_SUPPORT, status: new Command({
async run($: CommonLibrary): Promise<any> { description: "Changes the bot's status.",
$.channel.send('Setting status to `online`...'); permission: Command.PERMISSIONS.BOT_SUPPORT,
}, async run($: CommonLibrary): Promise<any> {
any: new Command({ $.channel.send("Setting status to `online`...");
description: `Select a status to set to. Available statuses: \`[${statuses.join( },
', ', any: new Command({
)}]\`.`, description: `Select a status to set to. Available statuses: \`[${statuses.join(
async run($: CommonLibrary): Promise<any> { ", "
if (!statuses.includes($.args[0])) )}]\`.`,
return $.channel.send("That status doesn't exist!"); async run($: CommonLibrary): Promise<any> {
else { if (!statuses.includes($.args[0]))
$.client.user?.setStatus($.args[0]); return $.channel.send("That status doesn't exist!");
$.channel.send(`Setting status to \`${$.args[0]}\`...`); else {
} $.client.user?.setStatus($.args[0]);
}, $.channel.send(`Setting status to \`${$.args[0]}\`...`);
}), }
}), }
purge: new Command({ })
description: 'Purges bot messages.', }),
permission: Command.PERMISSIONS.BOT_SUPPORT, purge: new Command({
async run($: CommonLibrary): Promise<any> { description: "Purges bot messages.",
if ($.message.channel instanceof discord.DMChannel) { permission: Command.PERMISSIONS.BOT_SUPPORT,
return; async run($: CommonLibrary): Promise<any> {
} if ($.message.channel instanceof discord.DMChannel) {
$.message.delete(); return;
const msgs = await $.channel.messages.fetch({ }
limit: 100, $.message.delete();
}); const msgs = await $.channel.messages.fetch({
const travMessages = msgs.filter( limit: 100
(m) => m.author.id === $.client.user?.id, });
); const travMessages = msgs.filter(
(m) => m.author.id === $.client.user?.id
await $.message.channel );
.send(`Found ${travMessages.size} messages to delete.`)
.then((m) => await $.message.channel
m.delete({ .send(`Found ${travMessages.size} messages to delete.`)
timeout: 5000, .then((m) =>
}), m.delete({
); timeout: 5000
await $.message.channel.bulkDelete(travMessages); })
}, );
}), await $.message.channel.bulkDelete(travMessages);
clear: new Command({ }
description: "Clears a given amount of messages.", }),
usage: "<amount>", clear: new Command({
run: "A number was not provided.", description: "Clears a given amount of messages.",
number: new Command({ usage: "<amount>",
description: "Amount of messages to delete.", run: "A number was not provided.",
async run($: CommonLibrary): Promise<any> { number: new Command({
$.message.delete(); description: "Amount of messages to delete.",
const fetched = await $.channel.messages.fetch({ async run($: CommonLibrary): Promise<any> {
limit: $.args[0], $.message.delete();
}); const fetched = await $.channel.messages.fetch({
/// @ts-ignore limit: $.args[0]
$.channel.bulkDelete(fetched) });
.catch((error: any) => $.channel.send(`Error: ${error}`)); $.channel
} /// @ts-ignore
}) .bulkDelete(fetched)
}), .catch((error: any) =>
eval: new Command({ $.channel.send(`Error: ${error}`)
description: 'Evaluate code.', );
usage: '<code>', }
permission: Command.PERMISSIONS.BOT_OWNER, })
async run($: CommonLibrary): Promise<any> { }),
try { eval: new Command({
const code = $.args.join(' '); description: "Evaluate code.",
let evaled = eval(code); usage: "<code>",
permission: Command.PERMISSIONS.BOT_OWNER,
if (typeof evaled !== 'string') async run($: CommonLibrary): Promise<any> {
evaled = require('util').inspect(evaled); try {
$.channel.send(clean(evaled), { code: 'x1' }); const code = $.args.join(" ");
} catch (err) { let evaled = eval(code);
$.channel.send(`\`ERROR\` \`\`\`x1\n${clean(err)}\n\`\`\``);
} if (typeof evaled !== "string")
}, evaled = require("util").inspect(evaled);
}), $.channel.send(clean(evaled), {code: "x1"});
nick: new Command({ } catch (err) {
description: "Change the bot's nickname.", $.channel.send(`\`ERROR\` \`\`\`x1\n${clean(err)}\n\`\`\``);
permission: Command.PERMISSIONS.BOT_SUPPORT, }
async run($: CommonLibrary): Promise<any> { }
const nickName = $.args.join(' '); }),
const trav = $.guild?.members.cache.find( nick: new Command({
(member) => member.id === $.client.user?.id, description: "Change the bot's nickname.",
); permission: Command.PERMISSIONS.BOT_SUPPORT,
await trav?.setNickname(nickName); async run($: CommonLibrary): Promise<any> {
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)) const nickName = $.args.join(" ");
$.message.delete({ timeout: 5000 }).catch($.handler.bind($)); const trav = $.guild?.members.cache.find(
$.channel (member) => member.id === $.client.user?.id
.send(`Nickname set to \`${nickName}\``) );
.then((m) => m.delete({ timeout: 5000 })); await trav?.setNickname(nickName);
}, if (
}), botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)
guilds: new Command({ )
description: 'Shows a list of all guilds the bot is a member of.', $.message.delete({timeout: 5000}).catch($.handler.bind($));
permission: Command.PERMISSIONS.BOT_SUPPORT, $.channel
async run($: CommonLibrary): Promise<any> { .send(`Nickname set to \`${nickName}\``)
const guildList = $.client.guilds.cache.array().map((e) => e.name); .then((m) => m.delete({timeout: 5000}));
$.channel.send(guildList); }
}, }),
}), guilds: new Command({
activity: new Command({ description: "Shows a list of all guilds the bot is a member of.",
description: 'Set the activity of the bot.', permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: Command.PERMISSIONS.BOT_SUPPORT, async run($: CommonLibrary): Promise<any> {
usage: '<type> <string>', const guildList = $.client.guilds.cache
async run($: CommonLibrary): Promise<any> { .array()
$.client.user?.setActivity('.help', { .map((e) => e.name);
type: 'LISTENING', $.channel.send(guildList);
}); }
$.channel.send('Activity set to default.'); }),
}, activity: new Command({
any: new Command({ description: "Set the activity of the bot.",
description: `Select an activity type to set. Available levels: \`[${activities.join( permission: Command.PERMISSIONS.BOT_SUPPORT,
', ', usage: "<type> <string>",
)}]\``, async run($: CommonLibrary): Promise<any> {
async run($: CommonLibrary): Promise<any> { $.client.user?.setActivity(".help", {
const type = $.args[0]; type: "LISTENING"
});
if (activities.includes(type)) { $.channel.send("Activity set to default.");
$.client.user?.setActivity($.args.slice(1).join(' '), { },
type: $.args[0].toUpperCase(), any: new Command({
}); description: `Select an activity type to set. Available levels: \`[${activities.join(
$.channel.send( ", "
`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args )}]\``,
.slice(1) async run($: CommonLibrary): Promise<any> {
.join(' ')}\`.`, const type = $.args[0];
);
} else if (activities.includes(type)) {
$.channel.send( $.client.user?.setActivity($.args.slice(1).join(" "), {
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join( type: $.args[0].toUpperCase()
', ', });
)}]\`.`, $.channel.send(
); `Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args
}, .slice(1)
}), .join(" ")}\`.`
}), );
}, } else
}); $.channel.send(
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join(
", "
)}]\`.`
);
}
})
})
}
});

View File

@ -1,38 +1,39 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
const responses = [ const responses = [
'Most likely,', "Most likely,",
'It is certain,', "It is certain,",
'It is decidedly so,', "It is decidedly so,",
'Without a doubt,', "Without a doubt,",
'Definitely,', "Definitely,",
'You may rely on it,', "You may rely on it,",
'As I see it, yes,', "As I see it, yes,",
'Outlook good,', "Outlook good,",
'Yes,', "Yes,",
'Signs point to yes,', "Signs point to yes,",
'Reply hazy, try again,', "Reply hazy, try again,",
'Ask again later,', "Ask again later,",
'Better not tell you now,', "Better not tell you now,",
'Cannot predict now,', "Cannot predict now,",
'Concentrate and ask again,', "Concentrate and ask again,",
"Don't count on it,", "Don't count on it,",
'My reply is no,', "My reply is no,",
'My sources say no,', "My sources say no,",
'Outlook not so good,', "Outlook not so good,",
'Very doubtful,', "Very doubtful,"
]; ];
export default new Command({ export default new Command({
description: 'Answers your question in an 8-ball manner.', description: "Answers your question in an 8-ball manner.",
endpoint: false, endpoint: false,
usage: '<question>', usage: "<question>",
run: 'Please provide a question.', run: "Please provide a question.",
any: new Command({ any: new Command({
description: 'Question to ask the 8-ball.', description: "Question to ask the 8-ball.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const sender = $.message.author; const sender = $.message.author;
$.channel.send($(responses).random() + ` <@${sender.id}>`); $.channel.send($(responses).random() + ` <@${sender.id}>`);
}, }
}), })
}); });

View File

@ -1,50 +1,54 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Gives specified user a cookie.", description: "Gives specified user a cookie.",
usage: "['all'/@user]", usage: "['all'/@user]",
run: ":cookie: Here's a cookie!", run: ":cookie: Here's a cookie!",
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
if ($.args[0] == "all") return $.channel.send(`${$.author} gave everybody a cookie!`) if ($.args[0] == "all")
} return $.channel.send(`${$.author} gave everybody a cookie!`);
}), }
user: new Command({ }),
description: "User to give cookie to.", user: new Command({
async run($: CommonLibrary): Promise<any> { description: "User to give cookie to.",
const sender = $.author; async run($: CommonLibrary): Promise<any> {
const mention = $.message.mentions.users.first(); const sender = $.author;
if (!mention) return; const mention = $.message.mentions.users.first();
const cookies = [
`has given <@${mention.id}> a chocolate chip cookie!`, if (!mention) return;
`has given <@${mention.id}> a soft homemade oatmeal cookie!`,
`has given <@${mention.id}> a plain, dry, old cookie. It was the last one in the bag. Gross.`, const cookies = [
`gives <@${mention.id}> a sugar cookie. What, no frosting and sprinkles? 0/10 would not touch.`, `has given <@${mention.id}> a chocolate chip cookie!`,
`gives <@${mention.id}> a chocolate chip cookie. Oh wait, those are raisins. Bleck!`, `has given <@${mention.id}> a soft homemade oatmeal cookie!`,
`gives <@${mention.id}> an enormous cookie. Poking it gives you more cookies. Weird.`, `has given <@${mention.id}> a plain, dry, old cookie. It was the last one in the bag. Gross.`,
`gives <@${mention.id}> a fortune cookie. It reads "Why aren't you working on any projects?"`, `gives <@${mention.id}> a sugar cookie. What, no frosting and sprinkles? 0/10 would not touch.`,
`gives <@${mention.id}> a fortune cookie. It reads "Give that special someone a compliment"`, `gives <@${mention.id}> a chocolate chip cookie. Oh wait, those are raisins. Bleck!`,
`gives <@${mention.id}> a fortune cookie. It reads "Take a risk!"`, `gives <@${mention.id}> an enormous cookie. Poking it gives you more cookies. Weird.`,
`gives <@${mention.id}> a fortune cookie. It reads "Go outside."`, `gives <@${mention.id}> a fortune cookie. It reads "Why aren't you working on any projects?"`,
`gives <@${mention.id}> a fortune cookie. It reads "Don't forget to eat your veggies!"`, `gives <@${mention.id}> a fortune cookie. It reads "Give that special someone a compliment"`,
`gives <@${mention.id}> a fortune cookie. It reads "Do you even lift?"`, `gives <@${mention.id}> a fortune cookie. It reads "Take a risk!"`,
`gives <@${mention.id}> a fortune cookie. It reads "m808 pls"`, `gives <@${mention.id}> a fortune cookie. It reads "Go outside."`,
`gives <@${mention.id}> a fortune cookie. It reads "If you move your hips, you'll get all the ladies."`, `gives <@${mention.id}> a fortune cookie. It reads "Don't forget to eat your veggies!"`,
`gives <@${mention.id}> a fortune cookie. It reads "I love you."`, `gives <@${mention.id}> a fortune cookie. It reads "Do you even lift?"`,
`gives <@${mention.id}> a Golden Cookie. You can't eat it because it is made of gold. Dammit.`, `gives <@${mention.id}> a fortune cookie. It reads "m808 pls"`,
`gives <@${mention.id}> an Oreo cookie with a glass of milk!`, `gives <@${mention.id}> a fortune cookie. It reads "If you move your hips, you'll get all the ladies."`,
`gives <@${mention.id}> a rainbow cookie made with love :heart:`, `gives <@${mention.id}> a fortune cookie. It reads "I love you."`,
`gives <@${mention.id}> an old cookie that was left out in the rain, it's moldy.`, `gives <@${mention.id}> a Golden Cookie. You can't eat it because it is made of gold. Dammit.`,
`bakes <@${mention.id}> fresh cookies, it smells amazing.`, `gives <@${mention.id}> an Oreo cookie with a glass of milk!`,
]; `gives <@${mention.id}> a rainbow cookie made with love :heart:`,
if (mention.id == sender.id) `gives <@${mention.id}> an old cookie that was left out in the rain, it's moldy.`,
return $.channel.send("You can't give yourself cookies!"); `bakes <@${mention.id}> fresh cookies, it smells amazing.`
$.channel.send( ];
`:cookie: <@${sender.id}> ` +
cookies[Math.floor(Math.random() * cookies.length)], if (mention.id == sender.id)
); return $.channel.send("You can't give yourself cookies!");
} $.channel.send(
}) `:cookie: <@${sender.id}> ` +
}) cookies[Math.floor(Math.random() * cookies.length)]
);
}
})
});

View File

@ -1,35 +1,36 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { isAuthorized, getMoneyEmbed } from './subcommands/eco-utils'; import {isAuthorized, getMoneyEmbed} from "./subcommands/eco-utils";
import { DailyCommand, PayCommand, GuildCommand } from './subcommands/eco-core'; import {DailyCommand, PayCommand, GuildCommand} from "./subcommands/eco-core";
import { BuyCommand, ShopCommand } from './subcommands/eco-shop'; import {BuyCommand, ShopCommand} from "./subcommands/eco-shop";
export default new Command({ export default new Command({
description: 'Economy command for Monika.', description: "Economy command for Monika.",
async run({guild, channel, author}) {
async run({ guild, channel, author }) { if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(author));
if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(author));
},
subcommands: {
daily: DailyCommand,
pay: PayCommand,
guild: GuildCommand,
buy: BuyCommand,
shop: ShopCommand,
},
user: new Command({
description:
'See how much money someone else has by using their user ID or pinging them.',
async run({ guild, channel, args }) {
if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(args[0]));
}, },
}), subcommands: {
any: new Command({ daily: DailyCommand,
description: 'See how much money someone else has by using their username.', pay: PayCommand,
async run({ guild, channel, args, callMemberByUsername, message }) { guild: GuildCommand,
if (isAuthorized(guild, channel)) buy: BuyCommand,
callMemberByUsername(message, args.join(' '), (member) => { shop: ShopCommand
channel.send(getMoneyEmbed(member.user));
});
}, },
}), user: new Command({
description:
"See how much money someone else has by using their user ID or pinging them.",
async run({guild, channel, args}) {
if (isAuthorized(guild, channel))
channel.send(getMoneyEmbed(args[0]));
}
}),
any: new Command({
description:
"See how much money someone else has by using their username.",
async run({guild, channel, args, callMemberByUsername, message}) {
if (isAuthorized(guild, channel))
callMemberByUsername(message, args.join(" "), (member) => {
channel.send(getMoneyEmbed(member.user));
});
}
})
}); });

View File

@ -1,26 +1,31 @@
/// @ts-nocheck /// @ts-nocheck
import { URL } from 'url' import {URL} from "url";
import FileManager from '../../core/storage'; import FileManager from "../../core/storage";
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary, getContent } from '../../core/lib'; import {CommonLibrary, getContent} from "../../core/lib";
const endpoints = FileManager.read('endpoints'); const endpoints = FileManager.read("endpoints");
export default new Command({ export default new Command({
description: 'Provides you with a random image with the selected argument.', description: "Provides you with a random image with the selected argument.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
console.log(endpoints.sfw) console.log(endpoints.sfw);
$.channel.send(`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(', ')}]\`.`) $.channel.send(
}, `Please provide an image type. Available arguments:\n\`[${Object.keys(
any: new Command({ endpoints.sfw
description: "Image type to send.", ).join(", ")}]\`.`
async run($: CommonLibrary): Promise<any> { );
if (!($.args[0] in endpoints.sfw)) },
return $.channel.send("Couldn't find that endpoint!"); any: new Command({
let baseURL = 'https://nekos.life/api/v2'; description: "Image type to send.",
let url = new URL(`${baseURL}${endpoints.sfw[$.args[0]]}`); async run($: CommonLibrary): Promise<any> {
const content = await getContent(url.toString()) if (!($.args[0] in endpoints.sfw))
$.channel.send(content.url) return $.channel.send("Couldn't find that endpoint!");
},
}) let baseURL = "https://nekos.life/api/v2";
}); let url = new URL(`${baseURL}${endpoints.sfw[$.args[0]]}`);
const content = await getContent(url.toString());
$.channel.send(content.url);
}
})
});

View File

@ -1,68 +1,69 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Sends random ok message.", description: "Sends random ok message.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const responses = [ const responses = [
'boomer', "boomer",
'zoomer', "zoomer",
'the last generationer', "the last generationer",
'the last airbender', "the last airbender",
'fire nation', "fire nation",
'fire lord', "fire lord",
'guy fieri', "guy fieri",
'guy from final fight', "guy from final fight",
'haggar', "haggar",
'Max Thunder from Streets of Rage 2', "Max Thunder from Streets of Rage 2",
'police guy who fires bazookas', "police guy who fires bazookas",
'Mr. X', "Mr. X",
'Leon Its Wrong If Its Not Ada Wong S. Kennedy.', "Leon Its Wrong If Its Not Ada Wong S. Kennedy.",
'Jill', "Jill",
'JFK', "JFK",
'george bush', "george bush",
'obama', "obama",
'the world', "the world",
'copy of scott pilgrim vs the world', "copy of scott pilgrim vs the world",
'ok', "ok",
'ko', "ko",
'Hot Daddy Venomous', "Hot Daddy Venomous",
'big daddy', "big daddy",
'John Cena', "John Cena",
'BubbleSpurJarJarBinks', "BubbleSpurJarJarBinks",
'T-Series', "T-Series",
'pewdiepie', "pewdiepie",
'markiplier', "markiplier",
'jacksepticeye', "jacksepticeye",
'vanossgaming', "vanossgaming",
'miniladd', "miniladd",
'Traves', "Traves",
'Wilbur Soot', "Wilbur Soot",
'sootrhianna', "sootrhianna",
'person with tiny ears', "person with tiny ears",
'anti-rabbit', "anti-rabbit",
'homo sapiens', "homo sapiens",
'homo', "homo",
'cute kitty', "cute kitty",
'ugly kitty', "ugly kitty",
'sadness', "sadness",
'doomer', "doomer",
'gloomer', "gloomer",
'bloomer', "bloomer",
'edgelord', "edgelord",
'weeb', "weeb",
"m'lady", "m'lady",
'Mr. Crabs', "Mr. Crabs",
'hand', "hand",
'lahoma', "lahoma",
'big man', "big man",
'fox', "fox",
'pear', "pear",
'cat', "cat",
'large man', "large man"
]; ];
$.channel.send(
'ok ' + responses[Math.floor(Math.random() * responses.length)], $.channel.send(
); "ok " + responses[Math.floor(Math.random() * responses.length)]
} );
}) }
});

View File

@ -1,12 +1,14 @@
/// @ts-nocheck /// @ts-nocheck
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary, getContent } from '../../core/lib'; import {CommonLibrary, getContent} from "../../core/lib";
export default new Command({ export default new Command({
description: 'OwO-ifies the input.', description: "OwO-ifies the input.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
let url = new URL(`https://nekos.life/api/v2/owoify?text=${$.args.join(' ')}`); let url = new URL(
const content = await getContent(url.toString()); `https://nekos.life/api/v2/owoify?text=${$.args.join(" ")}`
$.channel.send(content.owo); );
}, const content = await getContent(url.toString());
}); $.channel.send(content.owo);
}
});

View File

@ -1,28 +1,28 @@
import { MessageEmbed } from 'discord.js'; import {MessageEmbed} from "discord.js";
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: 'Create a poll.', description: "Create a poll.",
usage: '<question>', usage: "<question>",
run: 'Please provide a question.', run: "Please provide a question.",
any: new Command({ any: new Command({
description: 'Question for the poll.', description: "Question for the poll.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const embed = new MessageEmbed() const embed = new MessageEmbed()
.setAuthor( .setAuthor(
`Poll created by ${$.message.author.username}`, `Poll created by ${$.message.author.username}`,
$.message.guild?.iconURL({ dynamic: true }) ?? undefined, $.message.guild?.iconURL({dynamic: true}) ?? undefined
) )
.setColor(0xffffff) .setColor(0xffffff)
.setFooter('React to vote.') .setFooter("React to vote.")
.setDescription($.args.join(' ')); .setDescription($.args.join(" "));
const msg = await $.channel.send(embed); const msg = await $.channel.send(embed);
await msg.react('✅'); await msg.react("✅");
await msg.react('⛔'); await msg.react("⛔");
$.message.delete({ $.message.delete({
timeout: 1000, timeout: 1000
}); });
}, }
}), })
}); });

View File

@ -1,180 +1,182 @@
import Command from '../../../core/command'; import Command from "../../../core/command";
import $ from '../../../core/lib'; import $ from "../../../core/lib";
import { Storage } from '../../../core/structures'; import {Storage} from "../../../core/structures";
import { isAuthorized, getMoneyEmbed, getSendEmbed } from './eco-utils'; import {isAuthorized, getMoneyEmbed, getSendEmbed} from "./eco-utils";
export const DailyCommand = new Command({ export const DailyCommand = new Command({
description: description:
'Pick up your daily Mons. The cooldown is per user and every 22 hours to allow for some leeway.', "Pick up your daily Mons. The cooldown is per user and every 22 hours to allow for some leeway.",
async run({ author, channel, guild }) { async run({author, channel, guild}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
const user = Storage.getUser(author.id); const user = Storage.getUser(author.id);
const now = Date.now(); const now = Date.now();
if (now - user.lastReceived >= 79200000) { if (now - user.lastReceived >= 79200000) {
user.money++; user.money++;
user.lastReceived = now; user.lastReceived = now;
Storage.save(); Storage.save();
channel.send({ channel.send({
embed: { embed: {
title: 'Daily Reward', title: "Daily Reward",
description: 'You received 1 Mon!', description: "You received 1 Mon!",
color: 0xf1c40f, color: 0xf1c40f
}, }
}); });
} else } else
channel.send({ channel.send({
embed: { embed: {
title: 'Daily Reward', title: "Daily Reward",
description: `It's too soon to pick up your daily credits. You have about ${( description: `It's too soon to pick up your daily credits. You have about ${(
(user.lastReceived + 79200000 - now) / (user.lastReceived + 79200000 - now) /
3600000 3600000
).toFixed(1)} hours to go.`, ).toFixed(1)} hours to go.`,
color: 0xf1c40f, color: 0xf1c40f
}, }
}); });
}
} }
},
}); });
export const GuildCommand = new Command({ export const GuildCommand = new Command({
description: 'See the richest players.', description: "See the richest players.",
async run({ guild, channel, client }) { async run({guild, channel, client}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
const users = Storage.users; const users = Storage.users;
const ids = Object.keys(users); const ids = Object.keys(users);
ids.sort((a, b) => users[b].money - users[a].money); ids.sort((a, b) => users[b].money - users[a].money);
const fields = []; const fields = [];
for (let i = 0, limit = Math.min(10, ids.length); i < limit; i++) { for (let i = 0, limit = Math.min(10, ids.length); i < limit; i++) {
const id = ids[i]; const id = ids[i];
const user = await client.users.fetch(id); const user = await client.users.fetch(id);
fields.push({ fields.push({
name: `#${i + 1}. ${user.username}#${user.discriminator}`, name: `#${i + 1}. ${user.username}#${user.discriminator}`,
value: $(users[id].money).pluralise('credit', 's'), value: $(users[id].money).pluralise("credit", "s")
}); });
} }
channel.send({ channel.send({
embed: { embed: {
title: 'Top 10 Richest Players', title: "Top 10 Richest Players",
color: '#ffff00', color: "#ffff00",
fields: fields, fields: fields
}, }
}); });
}
} }
},
}); });
export const PayCommand = new Command({ export const PayCommand = new Command({
description: 'Send money to someone.', description: "Send money to someone.",
usage: '<user> <amount>', usage: "<user> <amount>",
run: 'Who are you sending this money to?', run: "Who are you sending this money to?",
user: new Command({ user: new Command({
run: "You need to enter an amount you're sending!", run: "You need to enter an amount you're sending!",
number: new Command({ number: new Command({
async run({ args, author, channel, guild }): Promise<any> { async run({args, author, channel, guild}): Promise<any> {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
const amount = Math.floor(args[1]); const amount = Math.floor(args[1]);
const sender = Storage.getUser(author.id); const sender = Storage.getUser(author.id);
const target = args[0]; const target = args[0];
const receiver = Storage.getUser(target.id); const receiver = Storage.getUser(target.id);
if (amount <= 0) if (amount <= 0)
return channel.send('You must send at least one Mon!'); return channel.send("You must send at least one Mon!");
else if (sender.money < amount) else if (sender.money < amount)
return channel.send( return channel.send(
"You don't have enough Mons for that.", "You don't have enough Mons for that.",
getMoneyEmbed(author), getMoneyEmbed(author)
); );
else if (target.id === author.id) else if (target.id === author.id)
return channel.send("You can't send Mons to yourself!"); return channel.send("You can't send Mons to yourself!");
else if (target.bot && process.argv[2] !== 'dev') else if (target.bot && process.argv[2] !== "dev")
return channel.send("You can't send Mons to a bot!"); return channel.send("You can't send Mons to a bot!");
sender.money -= amount; sender.money -= amount;
receiver.money += amount; receiver.money += amount;
Storage.save(); Storage.save();
return channel.send(getSendEmbed(author, target, amount)); return channel.send(getSendEmbed(author, target, amount));
} }
}, }
})
}), }),
}), number: new Command({
number: new Command({ run: "You must use the format `money send <user> <amount>`!"
run: 'You must use the format `money send <user> <amount>`!', }),
}), any: new Command({
any: new Command({ async run({args, author, channel, guild, prompt}) {
async run({ args, author, channel, guild, prompt }) { if (isAuthorized(guild, channel)) {
if (isAuthorized(guild, channel)) { const last = args.pop();
const last = args.pop();
if (!/\d+/g.test(last) && args.length === 0) if (!/\d+/g.test(last) && args.length === 0)
return channel.send("You need to enter an amount you're sending!"); return channel.send(
"You need to enter an amount you're sending!"
);
const amount = Math.floor(last); const amount = Math.floor(last);
const sender = Storage.getUser(author.id); const sender = Storage.getUser(author.id);
if (amount <= 0) if (amount <= 0)
return channel.send('You must send at least one credit!'); return channel.send("You must send at least one credit!");
else if (sender.money < amount) else if (sender.money < amount)
return channel.send( return channel.send(
"You don't have enough money to do that!", "You don't have enough money to do that!",
getMoneyEmbed(author), getMoneyEmbed(author)
); );
else if (!guild) else if (!guild)
return channel.send( return channel.send(
'You have to use this in a server if you want to send money with a username!', "You have to use this in a server if you want to send money with a username!"
); );
const username = args.join(' '); const username = args.join(" ");
const member = ( const member = (
await guild.members.fetch({ await guild.members.fetch({
query: username, query: username,
limit: 1, limit: 1
}) })
).first(); ).first();
if (!member) if (!member)
return channel.send( return channel.send(
`Couldn't find a user by the name of \`${username}\`! If you want to send money to someone in a different server, you have to use their user ID!`, `Couldn't find a user by the name of \`${username}\`! If you want to send money to someone in a different server, you have to use their user ID!`
); );
else if (member.user.id === author.id) else if (member.user.id === author.id)
return channel.send("You can't send money to yourself!"); return channel.send("You can't send money to yourself!");
else if (member.user.bot && process.argv[2] !== 'dev') else if (member.user.bot && process.argv[2] !== "dev")
return channel.send("You can't send money to a bot!"); return channel.send("You can't send money to a bot!");
const target = member.user; const target = member.user;
return prompt( return prompt(
await channel.send( await channel.send(
`Are you sure you want to send ${$(amount).pluralise( `Are you sure you want to send ${$(amount).pluralise(
'credit', "credit",
's', "s"
)} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`, )} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`,
{ {
embed: { embed: {
color: '#ffff00', color: "#ffff00",
author: { author: {
name: `${target.username}#${target.discriminator}`, name: `${target.username}#${target.discriminator}`,
icon_url: target.displayAvatarURL({ icon_url: target.displayAvatarURL({
format: 'png', format: "png",
dynamic: true, dynamic: true
}), })
}, }
}, }
}, }
), ),
author.id, author.id,
() => { () => {
const receiver = Storage.getUser(target.id); const receiver = Storage.getUser(target.id);
sender.money -= amount; sender.money -= amount;
receiver.money += amount; receiver.money += amount;
Storage.save(); Storage.save();
channel.send(getSendEmbed(author, target, amount)); channel.send(getSendEmbed(author, target, amount));
}, }
); );
} }
}, }
}), })
}); });

View File

@ -1,73 +1,73 @@
import { Message } from 'discord.js'; import {Message} from "discord.js";
import $ from '../../../core/lib'; import $ from "../../../core/lib";
export interface ShopItem { export interface ShopItem {
cost: number; cost: number;
title: string; title: string;
description: string; description: string;
usage: string; usage: string;
run(message: Message, cost: number, amount: number): void; run(message: Message, cost: number, amount: number): void;
} }
export const ShopItems: ShopItem[] = [ export const ShopItems: ShopItem[] = [
{ {
cost: 1, cost: 1,
title: 'Hug', title: "Hug",
description: 'Hug Monika.', description: "Hug Monika.",
usage: 'hug', usage: "hug",
run(message, cost) { run(message, cost) {
message.channel.send( message.channel.send(
`Transaction of ${cost} Mon completed successfully. <@394808963356688394>`, `Transaction of ${cost} Mon completed successfully. <@394808963356688394>`
); );
}
}, },
}, {
{ cost: 2,
cost: 2, title: "Handholding",
title: 'Handholding', description: "Hold Monika's hand.",
description: "Hold Monika's hand.", usage: "handhold",
usage: 'handhold', run(message, cost) {
run(message, cost) { message.channel.send(
message.channel.send( `Transaction of ${cost} Mons completed successfully. <@394808963356688394>`
`Transaction of ${cost} Mons completed successfully. <@394808963356688394>`, );
); }
}, },
}, {
{ cost: 1,
cost: 1, title: "Cute",
title: 'Cute', description: "Calls Monika cute.",
description: 'Calls Monika cute.', usage: "cute",
usage: 'cute', run(message) {
run(message) { message.channel.send("<:MoniCheeseBlushRed:637513137083383826>");
message.channel.send('<:MoniCheeseBlushRed:637513137083383826>'); }
}, },
}, {
{ cost: 3,
cost: 3, title: "Laser Bridge",
title: 'Laser Bridge', description: "Buys what is technically a laser bridge.",
description: 'Buys what is technically a laser bridge.', usage: "laser bridge",
usage: 'laser bridge', run(message) {
run(message) { message.channel.send($(lines).random(), {
message.channel.send($(lines).random(), { files: [
files: [ {
{ attachment:
attachment: "https://raw.githubusercontent.com/keanuplayz/TravBot/dev/assets/TheUltimateLaser.gif"
'https://raw.githubusercontent.com/keanuplayz/TravBot/dev/assets/TheUltimateLaser.gif', }
}, ]
], });
}); }
}, }
},
]; ];
const lines = [ const lines = [
"It's technically a laser bridge. No refunds.", "It's technically a laser bridge. No refunds.",
'You want a laser bridge? You got one!', "You want a laser bridge? You got one!",
"Now what'd they say about building bridges... Oh wait, looks like I nuked the planet again. Whoops!", "Now what'd they say about building bridges... Oh wait, looks like I nuked the planet again. Whoops!",
'I saw this redhead the other day who was so excited to buy what I was selling. Needless to say, she was not very happy with me afterward.', "I saw this redhead the other day who was so excited to buy what I was selling. Needless to say, she was not very happy with me afterward.",
"Sorry, but you'll have to wait until the Laser Bridge Builder leaves early access.", "Sorry, but you'll have to wait until the Laser Bridge Builder leaves early access.",
'Thank you for your purchase! For you see, this is the legendary laser of obliteration that has been defended and preserved for countless generations!', "Thank you for your purchase! For you see, this is the legendary laser of obliteration that has been defended and preserved for countless generations!",
'They say that a certain troll dwells under this laser bridge, waiting for an unlucky person to fall for th- I mean- Thank you for your purchase!', "They say that a certain troll dwells under this laser bridge, waiting for an unlucky person to fall for th- I mean- Thank you for your purchase!",
"Buy?! Hah! How about our new rental service for just under $9.99 a month? But wait, there's more! For just $99.99, you can rent this laser bridge for an entire year and save 16.67% as opposed to renting it monthly!", "Buy?! Hah! How about our new rental service for just under $9.99 a month? But wait, there's more! For just $99.99, you can rent this laser bridge for an entire year and save 16.67% as opposed to renting it monthly!",
'Good choice. Owning a laser bridge is the penultimate experience that all true seekers strive for!', "Good choice. Owning a laser bridge is the penultimate experience that all true seekers strive for!",
'I can already imagine the reviews...\n"9/10 needs more lasers"', 'I can already imagine the reviews...\n"9/10 needs more lasers"'
]; ];

View File

@ -1,100 +1,99 @@
import Command from '../../../core/command'; import Command from "../../../core/command";
import $ from '../../../core/lib'; import $ from "../../../core/lib";
import { Storage, getPrefix } from '../../../core/structures'; import {Storage, getPrefix} from "../../../core/structures";
import { isAuthorized, getMoneyEmbed, getSendEmbed } from './eco-utils'; import {isAuthorized} from "./eco-utils";
import { ShopItems, ShopItem } from './eco-shop-items'; import {ShopItems, ShopItem} from "./eco-shop-items";
import { EmbedField } from 'discord.js'; import {EmbedField} from "discord.js";
export const ShopCommand = new Command({ export const ShopCommand = new Command({
description: 'Displays the list of items you can buy in the shop.', description: "Displays the list of items you can buy in the shop.",
async run({ guild, channel, author }) { async run({guild, channel, author}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
function getShopEmbed(selection: ShopItem[], title = 'Shop') { function getShopEmbed(selection: ShopItem[], title = "Shop") {
const fields: EmbedField[] = []; const fields: EmbedField[] = [];
for (const item of selection) for (const item of selection)
fields.push({ fields.push({
name: `**${item.title}** (${getPrefix(guild)}eco buy ${ name: `**${item.title}** (${getPrefix(guild)}eco buy ${
item.usage item.usage
})`, })`,
value: `${item.description} Costs ${$(item.cost).pluralise( value: `${item.description} Costs ${$(
'Mon', item.cost
's', ).pluralise("Mon", "s")}.`,
)}.`, inline: false
inline: false, });
});
return { return {
embed: { embed: {
color: 0xf1c40f, color: 0xf1c40f,
title: title, title: title,
fields: fields, fields: fields,
footer: { footer: {
text: 'Mon Shop | TravBot Services', text: "Mon Shop | TravBot Services"
}, }
}, }
}; };
} }
// In case there's just one page, omit unnecessary details. // In case there's just one page, omit unnecessary details.
if (ShopItems.length <= 5) channel.send(getShopEmbed(ShopItems)); if (ShopItems.length <= 5) channel.send(getShopEmbed(ShopItems));
else { else {
const shopPages = $(ShopItems).split(5); const shopPages = $(ShopItems).split(5);
const pageAmount = shopPages.length; const pageAmount = shopPages.length;
const msg = await channel.send( const msg = await channel.send(
getShopEmbed(shopPages[0], `Shop (Page 1 of ${pageAmount})`), getShopEmbed(shopPages[0], `Shop (Page 1 of ${pageAmount})`)
); );
$.paginate(msg, author.id, pageAmount, (page) => { $.paginate(msg, author.id, pageAmount, (page) => {
msg.edit( msg.edit(
getShopEmbed( getShopEmbed(
shopPages[page], shopPages[page],
`Shop (Page ${page + 1} of ${pageAmount})`, `Shop (Page ${page + 1} of ${pageAmount})`
), )
); );
}); });
} }
}
} }
},
}); });
export const BuyCommand = new Command({ export const BuyCommand = new Command({
description: 'Buys an item from the shop.', description: "Buys an item from the shop.",
usage: '<item>', usage: "<item>",
async run({ guild, channel, args, message, author }) { async run({guild, channel, args, message, author}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
let found = false; let found = false;
let amount = 1; // The amount the user is buying. let amount = 1; // The amount the user is buying.
// For now, no shop items support being bought multiple times. Uncomment these 2 lines when it's supported/needed. // For now, no shop items support being bought multiple times. Uncomment these 2 lines when it's supported/needed.
//if (/\d+/g.test(args[args.length - 1])) //if (/\d+/g.test(args[args.length - 1]))
//amount = parseInt(args.pop()); //amount = parseInt(args.pop());
let requested = args.join(' '); // The item the user is buying. let requested = args.join(" "); // The item the user is buying.
for (let item of ShopItems) { for (let item of ShopItems) {
if (item.usage === requested) { if (item.usage === requested) {
const user = Storage.getUser(author.id); const user = Storage.getUser(author.id);
const cost = item.cost * amount; const cost = item.cost * amount;
if (cost > user.money) { if (cost > user.money) {
channel.send('Not enough Mons!'); channel.send("Not enough Mons!");
} else { } else {
user.money -= cost; user.money -= cost;
Storage.save(); Storage.save();
item.run(message, cost, amount); item.run(message, cost, amount);
} }
found = true; found = true;
break; break;
}
}
if (!found)
channel.send(
`There's no item in the shop that goes by \`${requested}\`!`
);
} }
}
if (!found)
channel.send(
`There's no item in the shop that goes by \`${requested}\`!`,
);
} }
},
}); });

View File

@ -1,81 +1,87 @@
import $ from '../../../core/lib'; import $ from "../../../core/lib";
import { Storage } from '../../../core/structures'; import {Storage} from "../../../core/structures";
import { User, Guild, TextChannel, DMChannel, NewsChannel } from 'discord.js'; import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js";
export function getMoneyEmbed(user: User): object { export function getMoneyEmbed(user: User): object {
const profile = Storage.getUser(user.id); const profile = Storage.getUser(user.id);
return { return {
embed: { embed: {
color: 0xffff00, color: 0xffff00,
author: { author: {
name: user.username, name: user.username,
icon_url: user.displayAvatarURL({ icon_url: user.displayAvatarURL({
format: 'png', format: "png",
dynamic: true, dynamic: true
}), })
}, },
fields: [ fields: [
{ {
name: 'Balance', name: "Balance",
value: $(profile.money).pluralise('credit', 's'), value: $(profile.money).pluralise("credit", "s")
}, }
], ]
}, }
}; };
} }
export function getSendEmbed( export function getSendEmbed(
sender: User, sender: User,
receiver: User, receiver: User,
amount: number, amount: number
): object { ): object {
return { return {
embed: { embed: {
color: 0xffff00, color: 0xffff00,
author: { author: {
name: sender.username, name: sender.username,
icon_url: sender.displayAvatarURL({ icon_url: sender.displayAvatarURL({
format: 'png', format: "png",
dynamic: true, dynamic: true
}), })
}, },
title: 'Transaction', title: "Transaction",
description: `${sender.toString()} has sent ${$(amount).pluralise( description: `${sender.toString()} has sent ${$(amount).pluralise(
'credit', "credit",
's', "s"
)} to ${receiver.toString()}!`, )} to ${receiver.toString()}!`,
fields: [ fields: [
{ {
name: `Sender: ${sender.username}#${sender.discriminator}`, name: `Sender: ${sender.username}#${sender.discriminator}`,
value: $(Storage.getUser(sender.id).money).pluralise('credit', 's'), value: $(Storage.getUser(sender.id).money).pluralise(
}, "credit",
{ "s"
name: `Receiver: ${receiver.username}#${receiver.discriminator}`, )
value: $(Storage.getUser(receiver.id).money).pluralise('credit', 's'), },
}, {
], name: `Receiver: ${receiver.username}#${receiver.discriminator}`,
footer: { value: $(Storage.getUser(receiver.id).money).pluralise(
text: receiver.username, "credit",
icon_url: receiver.displayAvatarURL({ "s"
format: 'png', )
dynamic: true, }
}), ],
}, footer: {
}, text: receiver.username,
}; icon_url: receiver.displayAvatarURL({
format: "png",
dynamic: true
})
}
}
};
} }
export function isAuthorized( export function isAuthorized(
guild: Guild | null, guild: Guild | null,
channel: TextChannel | DMChannel | NewsChannel, channel: TextChannel | DMChannel | NewsChannel
): boolean { ): boolean {
if (guild?.id === '637512823676600330' || process.argv[2] === 'dev') if (guild?.id === "637512823676600330" || process.argv[2] === "dev")
return true; return true;
else { else {
channel.send( channel.send(
"Sorry, this command can only be used in Monika's emote server.", "Sorry, this command can only be used in Monika's emote server."
); );
return false; return false;
} }
} }

View File

@ -1,145 +1,153 @@
import Command from '../core/command'; import Command from "../core/command";
import { CommonLibrary } from '../core/lib'; import {CommonLibrary} from "../core/lib";
import { loadCommands, categories } from '../core/command'; import {loadCommands, categories} from "../core/command";
import { PermissionNames } from '../core/permissions'; import {PermissionNames} from "../core/permissions";
export default new Command({ export default new Command({
description: description:
'Lists all commands. If a command is specified, their arguments are listed as well.', "Lists all commands. If a command is specified, their arguments are listed as well.",
usage: '([command, [subcommand/type], ...])', usage: "([command, [subcommand/type], ...])",
aliases: ['h'], aliases: ["h"],
async run($: CommonLibrary): Promise<any> {
const commands = await loadCommands();
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``;
for (const [category, headers] of categories) {
output += `\n\n===[ ${category} ]===`;
for (const header of headers) {
if (header !== 'test') {
const command = commands.get(header);
if (!command)
return $.warn(
`Command "${header}" of category "${category}" unexpectedly doesn't exist!`,
);
output += `\n- \`${header}\`: ${command.description}`;
}
}
}
$.channel.send(output, { split: true });
},
any: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const commands = await loadCommands(); const commands = await loadCommands();
let header = $.args.shift() as string; let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``;
let command = commands.get(header);
if (!command || header === 'test') for (const [category, headers] of categories) {
return $.channel.send(`No command found by the name \`${header}\`!`); output += `\n\n===[ ${category} ]===`;
if (command.originalCommandName) header = command.originalCommandName; for (const header of headers) {
else $.warn(`originalCommandName isn't defined for ${header}?!`); if (header !== "test") {
const command = commands.get(header);
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; if (!command)
let usage = command.usage; return $.warn(
let invalid = false; `Command "${header}" of category "${category}" unexpectedly doesn't exist!`
);
let selectedCategory = 'Unknown'; output += `\n- \`${header}\`: ${command.description}`;
}
for (const [category, headers] of categories) { }
if (headers.includes(header)) {
if (selectedCategory !== 'Unknown')
$.warn(
`Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.`,
);
else selectedCategory = category;
}
}
for (const param of $.args) {
const type = command.resolve(param);
command = command.get(param);
permLevel = command.permission ?? permLevel;
switch (type) {
case Command.TYPES.SUBCOMMAND:
header += ` ${command.originalCommandName}`;
break;
case Command.TYPES.USER:
header += ' <user>';
break;
case Command.TYPES.NUMBER:
header += ' <number>';
break;
case Command.TYPES.ANY:
header += ' <any>';
break;
default:
header += ` ${param}`;
break;
} }
if (type === Command.TYPES.NONE) { $.channel.send(output, {split: true});
invalid = true;
break;
}
}
if (invalid)
return $.channel.send(`No command found by the name \`${header}\`!`);
let append = '';
if (usage === '') {
const list: string[] = [];
command.subcommands.forEach((subcmd, subtag) => {
// Don't capture duplicates generated from aliases.
if (subcmd.originalCommandName === subtag) {
const customUsage = subcmd.usage ? ` ${subcmd.usage}` : '';
list.push(
`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`,
);
}
});
const addDynamicType = (cmd: Command | null, type: string) => {
if (cmd) {
const customUsage = cmd.usage ? ` ${cmd.usage}` : '';
list.push(
`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`,
);
}
};
addDynamicType(command.user, 'user');
addDynamicType(command.number, 'number');
addDynamicType(command.any, 'any');
append =
'Usages:' + (list.length > 0 ? `\n${list.join('\n')}` : ' None.');
} else append = `Usage: \`${header} ${usage}\``;
let aliases = 'None';
if (command.aliases.length > 0) {
aliases = '';
for (let i = 0; i < command.aliases.length; i++) {
const alias = command.aliases[i];
aliases += `\`${alias}\``;
if (i !== command.aliases.length - 1) aliases += ', ';
}
}
$.channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`,
{ split: true },
);
}, },
}), any: new Command({
async run($: CommonLibrary): Promise<any> {
const commands = await loadCommands();
let header = $.args.shift() as string;
let command = commands.get(header);
if (!command || header === "test")
return $.channel.send(
`No command found by the name \`${header}\`!`
);
if (command.originalCommandName)
header = command.originalCommandName;
else $.warn(`originalCommandName isn't defined for ${header}?!`);
let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
let usage = command.usage;
let invalid = false;
let selectedCategory = "Unknown";
for (const [category, headers] of categories) {
if (headers.includes(header)) {
if (selectedCategory !== "Unknown")
$.warn(
`Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.`
);
else selectedCategory = category;
}
}
for (const param of $.args) {
const type = command.resolve(param);
command = command.get(param);
permLevel = command.permission ?? permLevel;
switch (type) {
case Command.TYPES.SUBCOMMAND:
header += ` ${command.originalCommandName}`;
break;
case Command.TYPES.USER:
header += " <user>";
break;
case Command.TYPES.NUMBER:
header += " <number>";
break;
case Command.TYPES.ANY:
header += " <any>";
break;
default:
header += ` ${param}`;
break;
}
if (type === Command.TYPES.NONE) {
invalid = true;
break;
}
}
if (invalid)
return $.channel.send(
`No command found by the name \`${header}\`!`
);
let append = "";
if (usage === "") {
const list: string[] = [];
command.subcommands.forEach((subcmd, subtag) => {
// Don't capture duplicates generated from aliases.
if (subcmd.originalCommandName === subtag) {
const customUsage = subcmd.usage
? ` ${subcmd.usage}`
: "";
list.push(
`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`
);
}
});
const addDynamicType = (cmd: Command | null, type: string) => {
if (cmd) {
const customUsage = cmd.usage ? ` ${cmd.usage}` : "";
list.push(
`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`
);
}
};
addDynamicType(command.user, "user");
addDynamicType(command.number, "number");
addDynamicType(command.any, "any");
append =
"Usages:" +
(list.length > 0 ? `\n${list.join("\n")}` : " None.");
} else append = `Usage: \`${header} ${usage}\``;
let aliases = "None";
if (command.aliases.length > 0) {
aliases = "";
for (let i = 0; i < command.aliases.length; i++) {
const alias = command.aliases[i];
aliases += `\`${alias}\``;
if (i !== command.aliases.length - 1) aliases += ", ";
}
}
$.channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`,
{split: true}
);
}
})
}); });

View File

@ -1,243 +1,288 @@
import { MessageEmbed, version as djsversion } from 'discord.js'; import {MessageEmbed, version as djsversion} from "discord.js";
/// @ts-ignore /// @ts-ignore
import { version } from '../../package.json'; import {version} from "../../package.json";
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 { CommonLibrary, formatBytes, trimArray } from '../core/lib'; import {CommonLibrary, formatBytes, trimArray} from "../core/lib";
import { verificationLevels, filterLevels, regions, flags } from '../defs/info'; import {verificationLevels, filterLevels, regions, flags} from "../defs/info";
import moment from 'moment'; import moment from "moment";
import utc from 'moment'; import utc from "moment";
export default new Command({ export default new Command({
description: description:
'Command to provide all sorts of info about the current server, a user, etc.', "Command to provide all sorts of info about the current server, a user, etc.",
run: 'Please provide an argument.\nFor help, run `%prefix%help info`.', run: "Please provide an argument.\nFor help, run `%prefix%help info`.",
subcommands: { subcommands: {
avatar: new Command({ avatar: new Command({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
usage: '(<user>)', usage: "(<user>)",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.channel.send( $.channel.send(
$.author.displayAvatarURL({ dynamic: true, size: 2048 }), $.author.displayAvatarURL({dynamic: true, size: 2048})
); );
}, },
user: new Command({ user: new Command({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.channel.send( $.channel.send(
$.args[0].displayAvatarURL({ dynamic: true, size: 2048 }), $.args[0].displayAvatarURL({
); dynamic: true,
}, size: 2048
}), })
}), );
}
bot: new Command({ })
description: 'Displays info about the bot.', }),
async run($: CommonLibrary): Promise<any> { bot: new Command({
const core = os.cpus()[0]; description: "Displays info about the bot.",
const embed = new MessageEmbed() async run($: CommonLibrary): Promise<any> {
.setThumbnail( const core = os.cpus()[0];
/// @ts-ignore const embed = new MessageEmbed()
$.client.user?.displayAvatarURL({ dynamic: true, size: 2048 }), .setThumbnail(
) /// @ts-ignore
.setColor($.guild?.me?.displayHexColor || 'BLUE') $.client.user?.displayAvatarURL({
.addField('General', [ dynamic: true,
`** Client:** ${$.client.user?.tag} (${$.client.user?.id})`, size: 2048
`** Servers:** ${$.client.guilds.cache.size.toLocaleString()}`, })
`** Users:** ${$.client.guilds.cache )
.reduce((a: any, b: { memberCount: any }) => a + b.memberCount, 0) .setColor($.guild?.me?.displayHexColor || "BLUE")
.toLocaleString()}`, .addField("General", [
`** Channels:** ${$.client.channels.cache.size.toLocaleString()}`, `** Client:** ${$.client.user?.tag} (${$.client.user?.id})`,
`** Creation Date:** ${utc($.client.user?.createdTimestamp).format( `** Servers:** ${$.client.guilds.cache.size.toLocaleString()}`,
'Do MMMM YYYY HH:mm:ss', `** Users:** ${$.client.guilds.cache
)}`, .reduce(
`** Node.JS:** ${process.version}`, (a: any, b: {memberCount: any}) =>
`** Version:** v${version}`, a + b.memberCount,
`** Discord.JS:** ${djsversion}`, 0
'\u200b', )
]) .toLocaleString()}`,
.addField('System', [ `** Channels:** ${$.client.channels.cache.size.toLocaleString()}`,
`** Platform:** ${process.platform}`, `** Creation Date:** ${utc(
`** Uptime:** ${ms(os.uptime() * 1000, { long: true })}`, $.client.user?.createdTimestamp
`** CPU:**`, ).format("Do MMMM YYYY HH:mm:ss")}`,
`\u3000 • Cores: ${os.cpus().length}`, `** Node.JS:** ${process.version}`,
`\u3000 • Model: ${core.model}`, `** Version:** v${version}`,
`\u3000 • Speed: ${core.speed}MHz`, `** Discord.JS:** ${djsversion}`,
`** Memory:**`, "\u200b"
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`, ])
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapTotal)}`, .addField("System", [
]) `** Platform:** ${process.platform}`,
.setTimestamp(); `** Uptime:** ${ms(os.uptime() * 1000, {
$.channel.send(embed); long: true
}, })}`,
}), `** CPU:**`,
`\u3000 • Cores: ${os.cpus().length}`,
guild: new Command({ `\u3000 • Model: ${core.model}`,
description: 'Displays info about the current guild.', `\u3000 • Speed: ${core.speed}MHz`,
async run($: CommonLibrary): Promise<any> { `** Memory:**`,
if ($.guild) { `\u3000 • Total: ${formatBytes(
const roles = $.guild.roles.cache process.memoryUsage().heapTotal
.sort((a, b) => b.position - a.position) )}`,
.map((role) => role.toString()); `\u3000 • Used: ${formatBytes(
const members = $.guild.members.cache; process.memoryUsage().heapTotal
const channels = $.guild.channels.cache; )}`
const emojis = $.guild.emojis.cache; ])
.setTimestamp();
const iconURL = $.guild.iconURL({ dynamic: true }); $.channel.send(embed);
const embed = new MessageEmbed() }
.setDescription(`**Guild information for __${$.guild.name}__**`) }),
.setColor('BLUE'); guild: new Command({
if (iconURL) description: "Displays info about the current guild.",
embed async run($: CommonLibrary): Promise<any> {
.setThumbnail(iconURL) if ($.guild) {
.addField('General', [ const roles = $.guild.roles.cache
`** Name:** ${$.guild.name}`, .sort((a, b) => b.position - a.position)
`** ID:** ${$.guild.id}`, .map((role) => role.toString());
`** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`, const members = $.guild.members.cache;
`** Region:** ${regions[$.guild.region]}`, const channels = $.guild.channels.cache;
`** Boost Tier:** ${ const emojis = $.guild.emojis.cache;
$.guild.premiumTier ? `Tier ${$.guild.premiumTier}` : 'None' const iconURL = $.guild.iconURL({dynamic: true});
}`, const embed = new MessageEmbed()
`** Explicit Filter:** ${ .setDescription(
filterLevels[$.guild.explicitContentFilter] `**Guild information for __${$.guild.name}__**`
}`, )
`** Verification Level:** ${ .setColor("BLUE");
verificationLevels[$.guild.verificationLevel] if (iconURL)
}`, embed
`** Time Created:** ${moment($.guild.createdTimestamp).format( .setThumbnail(iconURL)
'LT', .addField("General", [
)} ${moment($.guild.createdTimestamp).format('LL')} ${moment( `** Name:** ${$.guild.name}`,
$.guild.createdTimestamp, `** ID:** ${$.guild.id}`,
).fromNow()})`, `** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`,
'\u200b', `** Region:** ${regions[$.guild.region]}`,
]) `** Boost Tier:** ${
.addField('Statistics', [ $.guild.premiumTier
`** Role Count:** ${roles.length}`, ? `Tier ${$.guild.premiumTier}`
`** Emoji Count:** ${emojis.size}`, : "None"
`** Regular Emoji Count:** ${ }`,
emojis.filter((emoji) => !emoji.animated).size `** Explicit Filter:** ${
}`, filterLevels[$.guild.explicitContentFilter]
`** Animated Emoji Count:** ${ }`,
emojis.filter((emoji) => emoji.animated).size `** Verification Level:** ${
}`, verificationLevels[
`** Member Count:** ${$.guild.memberCount}`, $.guild.verificationLevel
`** Humans:** ${ ]
members.filter((member) => !member.user.bot).size }`,
}`, `** Time Created:** ${moment(
`** Bots:** ${ $.guild.createdTimestamp
members.filter((member) => member.user.bot).size ).format("LT")} ${moment(
}`, $.guild.createdTimestamp
`** Text Channels:** ${ ).format("LL")} ${moment(
channels.filter((channel) => channel.type === 'text').size $.guild.createdTimestamp
}`, ).fromNow()})`,
`** Voice Channels:** ${ "\u200b"
channels.filter((channel) => channel.type === 'voice').size ])
}`, .addField("Statistics", [
`** Boost Count:** ${$.guild.premiumSubscriptionCount || '0'}`, `** Role Count:** ${roles.length}`,
`\u200b`, `** Emoji Count:** ${emojis.size}`,
]) `** Regular Emoji Count:** ${
.addField('Presence', [ emojis.filter((emoji) => !emoji.animated)
`** Online:** ${ .size
members.filter( }`,
(member) => member.presence.status === 'online', `** Animated Emoji Count:** ${
).size emojis.filter((emoji) => emoji.animated)
}`, .size
`** Idle:** ${ }`,
members.filter((member) => member.presence.status === 'idle') `** Member Count:** ${$.guild.memberCount}`,
.size `** Humans:** ${
}`, members.filter((member) => !member.user.bot)
`** Do Not Disturb:** ${ .size
members.filter((member) => member.presence.status === 'dnd') }`,
.size `** Bots:** ${
}`, members.filter((member) => member.user.bot)
`** Offline:** ${ .size
members.filter( }`,
(member) => member.presence.status === 'offline', `** Text Channels:** ${
).size channels.filter(
}`, (channel) => channel.type === "text"
'\u200b', ).size
]) }`,
.addField( `** Voice Channels:** ${
`Roles [${roles.length - 1}]`, channels.filter(
roles.length < 10 (channel) => channel.type === "voice"
? roles.join(', ') ).size
: roles.length > 10 }`,
? trimArray(roles) `** Boost Count:** ${
: 'None', $.guild.premiumSubscriptionCount || "0"
) }`,
.setTimestamp(); `\u200b`
])
$.channel.send(embed); .addField("Presence", [
} else { `** Online:** ${
$.channel.send('Please execute this command in a guild.'); members.filter(
} (member) =>
}, member.presence.status === "online"
}), ).size
}, }`,
user: new Command({ `** Idle:** ${
description: 'Displays info about mentioned user.', members.filter(
async run($: CommonLibrary): Promise<any> { (member) =>
// Transforms the User object into a GuildMember object of the current guild. member.presence.status === "idle"
const member = $.guild?.members.resolve($.args[0]); ).size
}`,
if (!member) `** Do Not Disturb:** ${
return $.channel.send( members.filter(
'No member object was found by that user! Are you sure you used this command in a server?', (member) =>
); member.presence.status === "dnd"
).size
const roles = member.roles.cache }`,
.sort( `** Offline:** ${
(a: { position: number }, b: { position: number }) => members.filter(
b.position - a.position, (member) =>
) member.presence.status === "offline"
.map((role: { toString: () => any }) => role.toString()) ).size
.slice(0, -1); }`,
// @ts-ignore - Discord.js' typings seem to be outdated here. According to their v12 docs, it's User.fetchFlags() instead of User.flags. "\u200b"
const userFlags = ((await member.user.fetchFlags()) as UserFlags).toArray(); ])
.addField(
const embed = new MessageEmbed() `Roles [${roles.length - 1}]`,
.setThumbnail( roles.length < 10
member.user.displayAvatarURL({ dynamic: true, size: 512 }), ? roles.join(", ")
) : roles.length > 10
.setColor(member.displayHexColor || 'BLUE') ? trimArray(roles)
.addField('User', [ : "None"
`** Username:** ${member.user.username}`, )
`** Discriminator:** ${member.user.discriminator}`, .setTimestamp();
`** ID:** ${member.id}`,
`** Flags:** ${userFlags.length ? userFlags.join(', ') : 'None'}`, $.channel.send(embed);
`** Avatar:** [Link to avatar](${member.user.displayAvatarURL({ } else {
dynamic: true, $.channel.send("Please execute this command in a guild.");
})})`, }
`** Time Created:** ${moment(member.user.createdTimestamp).format( }
'LT', })
)} ${moment(member.user.createdTimestamp).format('LL')} ${moment( },
member.user.createdTimestamp, user: new Command({
).fromNow()}`, description: "Displays info about mentioned user.",
`** Status:** ${member.user.presence.status}`, async run($: CommonLibrary): Promise<any> {
`** Game:** ${ // Transforms the User object into a GuildMember object of the current guild.
member.user.presence.activities || 'Not playing a game.' const member = $.guild?.members.resolve($.args[0]);
}`,
]) if (!member)
.addField('Member', [ return $.channel.send(
`** Highest Role:** ${ "No member object was found by that user! Are you sure you used this command in a server?"
member.roles.highest.id === $.guild?.id );
? 'None'
: member.roles.highest.name const roles = member.roles.cache
}`, .sort(
`** Server Join Date:** ${moment(member.joinedAt).format('LL LTS')}`, (a: {position: number}, b: {position: number}) =>
`** Hoist Role:** ${ b.position - a.position
member.roles.hoist ? member.roles.hoist.name : 'None' )
}`, .map((role: {toString: () => any}) => role.toString())
`** Roles:** [${roles.length}]: ${ .slice(0, -1);
roles.length < 10 // @ts-ignore - Discord.js' typings seem to be outdated here. According to their v12 docs, it's User.fetchFlags() instead of User.flags.
? roles.join(', ') const userFlags = ((await member.user.fetchFlags()) as UserFlags).toArray();
: roles.length > 10
? this.client.utils.trimArray(roles) const embed = new MessageEmbed()
: 'None' .setThumbnail(
}`, member.user.displayAvatarURL({dynamic: true, size: 512})
]); )
$.channel.send(embed); .setColor(member.displayHexColor || "BLUE")
}, .addField("User", [
}), `** Username:** ${member.user.username}`,
}); `** Discriminator:** ${member.user.discriminator}`,
`** ID:** ${member.id}`,
`** Flags:** ${
userFlags.length ? userFlags.join(", ") : "None"
}`,
`** Avatar:** [Link to avatar](${member.user.displayAvatarURL(
{
dynamic: true
}
)})`,
`** Time Created:** ${moment(
member.user.createdTimestamp
).format("LT")} ${moment(
member.user.createdTimestamp
).format("LL")} ${moment(
member.user.createdTimestamp
).fromNow()}`,
`** Status:** ${member.user.presence.status}`,
`** Game:** ${
member.user.presence.activities || "Not playing a game."
}`
])
.addField("Member", [
`** Highest Role:** ${
member.roles.highest.id === $.guild?.id
? "None"
: member.roles.highest.name
}`,
`** Server Join Date:** ${moment(member.joinedAt).format(
"LL LTS"
)}`,
`** Hoist Role:** ${
member.roles.hoist ? member.roles.hoist.name : "None"
}`,
`** Roles:** [${roles.length}]: ${
roles.length < 10
? roles.join(", ")
: roles.length > 10
? this.client.utils.trimArray(roles)
: "None"
}`
]);
$.channel.send(embed);
}
})
});

View File

@ -1,191 +1,196 @@
import Command from '../core/command'; import Command from "../core/command";
import { CommonLibrary } from '../core/lib'; import {CommonLibrary} from "../core/lib";
import moment from 'moment'; import moment from "moment";
import { Collection, TextChannel } from 'discord.js'; import {Collection, TextChannel} from "discord.js";
const lastUsedTimestamps: { [id: string]: number } = {}; const lastUsedTimestamps: {[id: string]: number} = {};
export default new Command({ export default new Command({
description: description:
'Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.', "Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
if (!$.guild) if (!$.guild)
return $.channel.send(`You must use this command on a server!`); return $.channel.send(`You must use this command on a server!`);
// Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown. // Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown.
const startTime = Date.now(); const startTime = Date.now();
const cooldown = 86400000; // 24 hours const cooldown = 86400000; // 24 hours
const lastUsedTimestamp = lastUsedTimestamps[$.guild.id] ?? 0; const lastUsedTimestamp = lastUsedTimestamps[$.guild.id] ?? 0;
const difference = startTime - lastUsedTimestamp; const difference = startTime - lastUsedTimestamp;
const howLong = moment(startTime).to(lastUsedTimestamp + cooldown); const howLong = moment(startTime).to(lastUsedTimestamp + cooldown);
// If it's been less than an hour since the command was last used, prevent it from executing. // If it's been less than an hour since the command was last used, prevent it from executing.
if (difference < cooldown) if (difference < cooldown)
return $.channel.send( return $.channel.send(
`This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`, `This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`
); );
else lastUsedTimestamps[$.guild.id] = startTime; else lastUsedTimestamps[$.guild.id] = startTime;
const stats: { const stats: {
[id: string]: { [id: string]: {
name: string; name: string;
formatted: string; formatted: string;
users: number; users: number;
bots: number; bots: number;
}; };
} = {}; } = {};
let totalUserEmoteUsage = 0; let totalUserEmoteUsage = 0;
// IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise. // IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise.
const allTextChannelsInCurrentGuild = $.guild.channels.cache.filter( const allTextChannelsInCurrentGuild = $.guild.channels.cache.filter(
(channel) => channel.type === 'text' && channel.viewable, (channel) => channel.type === "text" && channel.viewable
) as Collection<string, TextChannel>; ) as Collection<string, TextChannel>;
let messagesSearched = 0; let messagesSearched = 0;
let channelsSearched = 0; let channelsSearched = 0;
let currentChannelName = ''; let currentChannelName = "";
const totalChannels = allTextChannelsInCurrentGuild.size; const totalChannels = allTextChannelsInCurrentGuild.size;
const statusMessage = await $.channel.send('Gathering emotes...'); const statusMessage = await $.channel.send("Gathering emotes...");
let warnings = 0; let warnings = 0;
$.channel.startTyping(); $.channel.startTyping();
// Initialize the emote stats object with every emote in the current guild. // Initialize the emote stats object with every emote in the current guild.
// The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with. // The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with.
for (let emote of $.guild.emojis.cache.values()) { for (let emote of $.guild.emojis.cache.values()) {
// If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit. // If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit.
stats[emote.id] = { stats[emote.id] = {
name: emote.name, name: emote.name,
formatted: `<${emote.animated ? 'a' : ''}:${emote.name}:${emote.id}>`, formatted: `<${emote.animated ? "a" : ""}:${emote.name}:${
users: 0, emote.id
bots: 0, }>`,
}; users: 0,
} bots: 0
};
const interval = setInterval(() => {
statusMessage.edit(
`Searching channel \`${currentChannelName}\`... (${messagesSearched} messages scanned, ${channelsSearched}/${totalChannels} channels scanned)`,
);
}, 5000);
for (const channel of allTextChannelsInCurrentGuild.values()) {
currentChannelName = channel.name;
let selected = channel.lastMessageID ?? $.message.id;
let continueLoop = true;
while (continueLoop) {
// Unfortunately, any kind of .fetch call is limited to 100 items at once by Discord's API.
const messages = await channel.messages.fetch({
limit: 100,
before: selected,
});
if (messages.size > 0) {
for (const msg of messages.values()) {
// It's very important to not capture an array of matches then do \d+ on each item because emote names can have numbers in them, causing the bot to not count them correctly.
const search = /<a?:.+?:(\d+?)>/g;
const text = msg.content;
let match: RegExpExecArray | null;
while ((match = search.exec(text))) {
const emoteID = match[1];
if (emoteID in stats) {
if (msg.author.bot) stats[emoteID].bots++;
else {
stats[emoteID].users++;
totalUserEmoteUsage++;
}
}
}
for (const reaction of msg.reactions.cache.values()) {
const emoteID = reaction.emoji.id;
let continueReactionLoop = true;
let lastUserID: string | undefined;
let userReactions = 0;
let botReactions = 0;
// An emote's ID will be null if it's a unicode emote.
if (emoteID && emoteID in stats) {
// There is a simple count property on a reaction, but that doesn't separate users from bots.
// So instead, I'll use that property to check for inconsistencies.
while (continueReactionLoop) {
// After logging users, it seems like the order is strictly numerical. As long as that stays consistent, this should work fine.
const users = await reaction.users.fetch({
limit: 100,
after: lastUserID,
});
if (users.size > 0) {
for (const user of users.values()) {
if (user.bot) {
stats[emoteID].bots++;
botReactions++;
} else {
stats[emoteID].users++;
totalUserEmoteUsage++;
userReactions++;
}
lastUserID = user.id;
}
} else {
// Then halt the loop and send warnings of any inconsistencies.
continueReactionLoop = false;
if (reaction.count !== userReactions + botReactions) {
$.warn(
`[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`,
);
warnings++;
}
}
}
}
}
selected = msg.id;
messagesSearched++;
}
} else {
continueLoop = false;
channelsSearched++;
} }
}
const interval = setInterval(() => {
statusMessage.edit(
`Searching channel \`${currentChannelName}\`... (${messagesSearched} messages scanned, ${channelsSearched}/${totalChannels} channels scanned)`
);
}, 5000);
for (const channel of allTextChannelsInCurrentGuild.values()) {
currentChannelName = channel.name;
let selected = channel.lastMessageID ?? $.message.id;
let continueLoop = true;
while (continueLoop) {
// Unfortunately, any kind of .fetch call is limited to 100 items at once by Discord's API.
const messages = await channel.messages.fetch({
limit: 100,
before: selected
});
if (messages.size > 0) {
for (const msg of messages.values()) {
// It's very important to not capture an array of matches then do \d+ on each item because emote names can have numbers in them, causing the bot to not count them correctly.
const search = /<a?:.+?:(\d+?)>/g;
const text = msg.content;
let match: RegExpExecArray | null;
while ((match = search.exec(text))) {
const emoteID = match[1];
if (emoteID in stats) {
if (msg.author.bot) stats[emoteID].bots++;
else {
stats[emoteID].users++;
totalUserEmoteUsage++;
}
}
}
for (const reaction of msg.reactions.cache.values()) {
const emoteID = reaction.emoji.id;
let continueReactionLoop = true;
let lastUserID: string | undefined;
let userReactions = 0;
let botReactions = 0;
// An emote's ID will be null if it's a unicode emote.
if (emoteID && emoteID in stats) {
// There is a simple count property on a reaction, but that doesn't separate users from bots.
// So instead, I'll use that property to check for inconsistencies.
while (continueReactionLoop) {
// After logging users, it seems like the order is strictly numerical. As long as that stays consistent, this should work fine.
const users = await reaction.users.fetch({
limit: 100,
after: lastUserID
});
if (users.size > 0) {
for (const user of users.values()) {
if (user.bot) {
stats[emoteID].bots++;
botReactions++;
} else {
stats[emoteID].users++;
totalUserEmoteUsage++;
userReactions++;
}
lastUserID = user.id;
}
} else {
// Then halt the loop and send warnings of any inconsistencies.
continueReactionLoop = false;
if (
reaction.count !==
userReactions + botReactions
) {
$.warn(
`[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`
);
warnings++;
}
}
}
}
}
selected = msg.id;
messagesSearched++;
}
} else {
continueLoop = false;
channelsSearched++;
}
}
}
// Mark the operation as ended.
const finishTime = Date.now();
clearInterval(interval);
statusMessage.edit(
`Finished operation in ${moment
.duration(finishTime - startTime)
.humanize()} with ${$(warnings).pluralise(
"inconsistenc",
"ies",
"y"
)}.`
);
$.log(`Finished operation in ${finishTime - startTime} ms.`);
$.channel.stopTyping();
// Display stats on emote usage.
// This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end.
let sortedEmoteIDs = Object.keys(stats).sort(
(a, b) => stats[b].users - stats[a].users
);
const lines: string[] = [];
let rank = 1;
// It's better to send all the lines at once rather than paginate the data because it's quite a memory-intensive task to search all the messages in a server for it, and I wouldn't want to activate the command again just to get to another page.
for (const emoteID of sortedEmoteIDs) {
const emote = stats[emoteID];
const botInfo = emote.bots > 0 ? ` (Bots: ${emote.bots})` : "";
lines.push(
`\`#${rank++}\` ${emote.formatted} x ${emote.users} - ${(
(emote.users / totalUserEmoteUsage) * 100 || 0
).toFixed(3)}%` + botInfo
);
}
$.channel.send(lines, {split: true}).catch($.handler.bind($));
} }
// Mark the operation as ended.
const finishTime = Date.now();
clearInterval(interval);
statusMessage.edit(
`Finished operation in ${moment
.duration(finishTime - startTime)
.humanize()} with ${$(warnings).pluralise(
'inconsistenc',
'ies',
'y',
)}.`,
);
$.log(`Finished operation in ${finishTime - startTime} ms.`);
$.channel.stopTyping();
// Display stats on emote usage.
// This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end.
let sortedEmoteIDs = Object.keys(stats).sort(
(a, b) => stats[b].users - stats[a].users,
);
const lines: string[] = [];
let rank = 1;
// It's better to send all the lines at once rather than paginate the data because it's quite a memory-intensive task to search all the messages in a server for it, and I wouldn't want to activate the command again just to get to another page.
for (const emoteID of sortedEmoteIDs) {
const emote = stats[emoteID];
const botInfo = emote.bots > 0 ? ` (Bots: ${emote.bots})` : '';
lines.push(
`\`#${rank++}\` ${emote.formatted} x ${emote.users} - ${(
(emote.users / totalUserEmoteUsage) * 100 || 0
).toFixed(3)}%` + botInfo,
);
}
$.channel.send(lines, { split: true }).catch($.handler.bind($));
},
}); });

View File

@ -1,29 +1,31 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Renames current voice channel.", description: "Renames current voice channel.",
usage: "<name>", usage: "<name>",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const voiceChannel = $.message.member?.voice.channel; const voiceChannel = $.message.member?.voice.channel;
if (!voiceChannel) if (!voiceChannel)
return $.channel.send('You are not in a voice channel.'); return $.channel.send("You are not in a voice channel.");
if (!$.guild?.me?.hasPermission('MANAGE_CHANNELS'))
return $.channel.send( if (!$.guild?.me?.hasPermission("MANAGE_CHANNELS"))
'I am lacking the required permissions to perform this action.', return $.channel.send(
); "I am lacking the required permissions to perform this action."
);
if ($.args.length === 0) if ($.args.length === 0)
return $.channel.send( return $.channel.send("Please provide a new voice channel name.");
'Please provide a new voice channel name.',
);
const changeVC = $.guild.channels.resolve(voiceChannel.id); const changeVC = $.guild.channels.resolve(voiceChannel.id);
$.channel $.channel
.send( .send(
`Changed channel name from "${voiceChannel}" to "${$.args.join( `Changed channel name from "${voiceChannel}" to "${$.args.join(
' ', " "
)}".`, )}".`
) )
/// @ts-ignore /// @ts-ignore
.then(changeVC?.setName($.args.join(' '))); .then(changeVC?.setName($.args.join(" ")));
} }
}) });

View File

@ -1,21 +1,21 @@
import { MessageEmbed } from 'discord.js'; import {MessageEmbed} from "discord.js";
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: 'Send the specified emote.', description: "Send the specified emote.",
run: 'Please provide a command name.', run: "Please provide a command name.",
any: new Command({ any: new Command({
description: 'The emote to send.', description: "The emote to send.",
usage: '<emote>', usage: "<emote>",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const search = $.args[0].toLowerCase(); const search = $.args[0].toLowerCase();
const emote = $.client.emojis.cache.find((emote) => const emote = $.client.emojis.cache.find((emote) =>
emote.name.toLowerCase().includes(search), emote.name.toLowerCase().includes(search)
); );
if (!emote) return $.channel.send("That's not a valid emote name!"); if (!emote) return $.channel.send("That's not a valid emote name!");
$.message.delete(); $.message.delete();
$.channel.send(`${emote}`); $.channel.send(`${emote}`);
}, }
}), })
}); });

View File

@ -1,32 +1,36 @@
import { MessageEmbed } from 'discord.js'; import {MessageEmbed} from "discord.js";
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Lists all emotes the bot has in it's registry,", description: "Lists all emotes the bot has in it's registry,",
endpoint: true, endpoint: true,
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const nsfw: string | string[] = []; const nsfw: string | string[] = [];
const pages = $.client.emojis.cache const pages = $.client.emojis.cache
.filter((x) => !nsfw.includes(x.guild.id), this) .filter((x) => !nsfw.includes(x.guild.id), this)
.array(); .array();
const pagesSplit = $(pages).split(20); const pagesSplit = $(pages).split(20);
$.log(pagesSplit); $.log(pagesSplit);
var embed = new MessageEmbed().setTitle('**Emoji list!**').setColor('AQUA'); var embed = new MessageEmbed()
let desc = ''; .setTitle("**Emoji list!**")
for (const emote of pagesSplit[0]) { .setColor("AQUA");
desc += `${emote} | ${emote.name}\n`; let desc = "";
}
embed.setDescription(desc);
const msg = await $.channel.send({ embed });
$.paginate(msg, $.author.id, pages.length, (page) => { for (const emote of pagesSplit[0]) {
let desc = ''; desc += `${emote} | ${emote.name}\n`;
for (const emote of pagesSplit[page]) { }
desc += `${emote} | ${emote.name}\n`;
} embed.setDescription(desc);
embed.setDescription(desc); const msg = await $.channel.send({embed});
msg.edit(embed);
}); $.paginate(msg, $.author.id, pages.length, (page) => {
}, let desc = "";
for (const emote of pagesSplit[page]) {
desc += `${emote} | ${emote.name}\n`;
}
embed.setDescription(desc);
msg.edit(embed);
});
}
}); });

View File

@ -1,68 +1,72 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: description:
'Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.', "Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.",
usage: 'react <emote name> (<message ID / distance>)', usage: "react <emote name> (<message ID / distance>)",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
let target; let target;
let distance = 1; let distance = 1;
if ($.args.length >= 2) { if ($.args.length >= 2) {
const last = $.args[$.args.length - 1]; const last = $.args[$.args.length - 1];
if (/\d{17,19}/g.test(last)) { if (/\d{17,19}/g.test(last)) {
try { try {
target = await $.channel.messages.fetch(last); target = await $.channel.messages.fetch(last);
} catch { } catch {
return $.channel.send( return $.channel.send(
`No valid message found by the ID \`${last}\`!`, `No valid message found by the ID \`${last}\`!`
); );
} }
$.args.pop();
} $.args.pop();
// The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this. }
else if (/^\d+$/g.test(last)) { // The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this.
distance = parseInt(last); else if (/^\d+$/g.test(last)) {
distance = parseInt(last);
if (distance >= 0 && distance <= 99) $.args.pop();
else return $.channel.send('Your distance must be between 0 and 99!'); if (distance >= 0 && distance <= 99) $.args.pop();
} else
} return $.channel.send(
"Your distance must be between 0 and 99!"
if (!target) { );
// Messages are ordered from latest to earliest. }
// You also have to add 1 as well because fetchMessages includes your own message. }
target = (
await $.message.channel.messages.fetch({ if (!target) {
limit: distance + 1, // Messages are ordered from latest to earliest.
}) // You also have to add 1 as well because fetchMessages includes your own message.
).last(); target = (
} await $.message.channel.messages.fetch({
limit: distance + 1
let anyEmoteIsValid = false; })
).last();
for (const search of $.args) { }
const emoji = $.client.emojis.cache.find(
(emoji) => emoji.name === search, let anyEmoteIsValid = false;
);
for (const search of $.args) {
if (emoji) { const emoji = $.client.emojis.cache.find(
// Call the delete function only once to avoid unnecessary errors. (emoji) => emoji.name === search
if (!anyEmoteIsValid && distance !== 0) $.message.delete(); );
anyEmoteIsValid = true;
const reaction = await target?.react(emoji); if (emoji) {
// Call the delete function only once to avoid unnecessary errors.
// This part is called with a promise because you don't want to wait 5 seconds between each reaction. if (!anyEmoteIsValid && distance !== 0) $.message.delete();
anyEmoteIsValid = true;
setTimeout(() => { const reaction = await target?.react(emoji);
/// @ts-ignore
reaction.users.remove($.client.user); // This part is called with a promise because you don't want to wait 5 seconds between each reaction.
}, 5000);
} setTimeout(() => {
} /// @ts-ignore
reaction.users.remove($.client.user);
if (!anyEmoteIsValid && !$.message.deleted) $.message.react('❓'); }, 5000);
}, }
}); }
if (!anyEmoteIsValid && !$.message.deleted) $.message.react("❓");
}
});

View File

@ -1,14 +1,14 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Repeats your message.", description: "Repeats your message.",
usage: "<message>", usage: "<message>",
run: "Please provide a message for me to say!", run: "Please provide a message for me to say!",
any: new Command({ any: new Command({
description: "Message to repeat.", description: "Message to repeat.",
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.channel.send(`*${$.author} says:*\n${$.args.join(' ')}`); $.channel.send(`*${$.author} says:*\n${$.args.join(" ")}`);
} }
}) })
}) });

View File

@ -1,25 +1,25 @@
import Command from '../../core/command'; import Command from "../../core/command";
import { CommonLibrary } from '../../core/lib'; import {CommonLibrary} from "../../core/lib";
import * as https from 'https'; import * as https from "https";
export default new Command({ export default new Command({
description: 'Shortens a given URL.', description: "Shortens a given URL.",
run: 'Please provide a URL.', run: "Please provide a URL.",
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
https.get( https.get(
'https://is.gd/create.php?format=simple&url=' + "https://is.gd/create.php?format=simple&url=" +
encodeURIComponent($.args[0]), encodeURIComponent($.args[0]),
function (res) { function (res) {
var body = ''; var body = "";
res.on('data', function (chunk) { res.on("data", function (chunk) {
body += chunk; body += chunk;
}); });
res.on('end', function () { res.on("end", function () {
$.channel.send(`<${body}>`); $.channel.send(`<${body}>`);
}); });
}, }
); );
}, }
}), })
}); });

View File

@ -1,155 +1,157 @@
import $, { isType, parseVars, CommonLibrary } from './lib'; import $, {isType, parseVars, CommonLibrary} from "./lib";
import { Collection } from 'discord.js'; import {Collection} from "discord.js";
import { generateHandler } from './storage'; import {generateHandler} from "./storage";
import { promises as ffs, existsSync, writeFile } from 'fs'; import {promises as ffs, existsSync, writeFile} from "fs";
import { PERMISSIONS } from './permissions'; import {PERMISSIONS} from "./permissions";
import { getPrefix } from '../core/structures'; import {getPrefix} from "../core/structures";
interface CommandOptions { interface CommandOptions {
description?: string; description?: string;
endpoint?: boolean; endpoint?: boolean;
usage?: string; usage?: string;
permission?: PERMISSIONS | null; permission?: PERMISSIONS | null;
aliases?: string[]; aliases?: string[];
run?: (($: CommonLibrary) => Promise<any>) | string; run?: (($: CommonLibrary) => Promise<any>) | string;
subcommands?: { [key: string]: Command }; subcommands?: {[key: string]: Command};
user?: Command; user?: Command;
number?: Command; number?: Command;
any?: Command; any?: Command;
} }
export enum TYPES { export enum TYPES {
SUBCOMMAND, SUBCOMMAND,
USER, USER,
NUMBER, NUMBER,
ANY, ANY,
NONE, NONE
} }
export default class Command { export default class Command {
public readonly description: string; public readonly description: string;
public readonly endpoint: boolean; public readonly endpoint: boolean;
public readonly usage: string; public readonly usage: string;
public readonly permission: PERMISSIONS | null; public readonly permission: PERMISSIONS | null;
public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases. public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName: string | null; // If the command is an alias, what's the original name? public originalCommandName: string | null; // If the command is an alias, what's the original name?
public run: (($: CommonLibrary) => Promise<any>) | string; public run: (($: CommonLibrary) => Promise<any>) | string;
public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to. public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
public user: Command | null; public user: Command | null;
public number: Command | null; public number: Command | null;
public any: Command | null; public any: Command | null;
public static readonly TYPES = TYPES; public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS; public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions) { constructor(options?: CommandOptions) {
this.description = options?.description || 'No description.'; this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false; this.endpoint = options?.endpoint || false;
this.usage = options?.usage || ''; this.usage = options?.usage || "";
this.permission = options?.permission ?? null; this.permission = options?.permission ?? null;
this.aliases = options?.aliases ?? []; this.aliases = options?.aliases ?? [];
this.originalCommandName = null; this.originalCommandName = null;
this.run = options?.run || 'No action was set on this command!'; this.run = options?.run || "No action was set on this command!";
this.subcommands = new Collection(); // Populate this collection after setting subcommands. this.subcommands = new Collection(); // Populate this collection after setting subcommands.
this.user = options?.user || null; this.user = options?.user || null;
this.number = options?.number || null; this.number = options?.number || null;
this.any = options?.any || null; this.any = options?.any || null;
if (options?.subcommands) { if (options?.subcommands) {
const baseSubcommands = Object.keys(options.subcommands); const baseSubcommands = Object.keys(options.subcommands);
// Loop once to set the base subcommands. // Loop once to set the base subcommands.
for (const name in options.subcommands) for (const name in options.subcommands)
this.subcommands.set(name, options.subcommands[name]); this.subcommands.set(name, options.subcommands[name]);
// Then loop again to make aliases point to the base subcommands and warn if something's not right. // Then loop again to make aliases point to the base subcommands and warn if something's not right.
// This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object. // This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object.
for (const name in options.subcommands) { for (const name in options.subcommands) {
const subcmd = options.subcommands[name]; const subcmd = options.subcommands[name];
subcmd.originalCommandName = name; subcmd.originalCommandName = name;
const aliases = subcmd.aliases; const aliases = subcmd.aliases;
for (const alias of aliases) { for (const alias of aliases) {
if (baseSubcommands.includes(alias)) if (baseSubcommands.includes(alias))
$.warn( $.warn(
`"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)`, `"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)`
); );
else if (this.subcommands.has(alias)) else if (this.subcommands.has(alias))
$.warn( $.warn(
`Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)`, `Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)`
); );
else this.subcommands.set(alias, subcmd); else this.subcommands.set(alias, subcmd);
}
}
} }
}
if (this.user && this.user.aliases.length > 0)
$.warn(
`There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
);
if (this.number && this.number.aliases.length > 0)
$.warn(
`There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
);
if (this.any && this.any.aliases.length > 0)
$.warn(
`There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
);
} }
if (this.user && this.user.aliases.length > 0) public execute($: CommonLibrary) {
$.warn( if (isType(this.run, String)) {
`There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`, $.channel.send(
); parseVars(
if (this.number && this.number.aliases.length > 0) this.run as string,
$.warn( {
`There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`, author: $.author.toString(),
); prefix: getPrefix($.guild)
if (this.any && this.any.aliases.length > 0) },
$.warn( "???"
`There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`, )
); );
} } else (this.run as Function)($).catch($.handler.bind($));
public execute($: CommonLibrary) {
if (isType(this.run, String)) {
$.channel.send(
parseVars(
this.run as string,
{
author: $.author.toString(),
prefix: getPrefix($.guild),
},
'???',
),
);
} else (this.run as Function)($).catch($.handler.bind($));
}
public resolve(param: string): TYPES {
if (this.subcommands.has(param)) return TYPES.SUBCOMMAND;
// Any Discord ID format will automatically format to a user ID.
else if (this.user && /\d{17,19}/.test(param)) return TYPES.USER;
// Disallow infinity and allow for 0.
else if (
this.number &&
(Number(param) || param === '0') &&
!param.includes('Infinity')
)
return TYPES.NUMBER;
else if (this.any) return TYPES.ANY;
else return TYPES.NONE;
}
public get(param: string): Command {
const type = this.resolve(param);
let command: Command;
switch (type) {
case TYPES.SUBCOMMAND:
command = this.subcommands.get(param) as Command;
break;
case TYPES.USER:
command = this.user as Command;
break;
case TYPES.NUMBER:
command = this.number as Command;
break;
case TYPES.ANY:
command = this.any as Command;
break;
default:
command = this;
break;
} }
return command; public resolve(param: string): TYPES {
} if (this.subcommands.has(param)) return TYPES.SUBCOMMAND;
// Any Discord ID format will automatically format to a user ID.
else if (this.user && /\d{17,19}/.test(param)) return TYPES.USER;
// Disallow infinity and allow for 0.
else if (
this.number &&
(Number(param) || param === "0") &&
!param.includes("Infinity")
)
return TYPES.NUMBER;
else if (this.any) return TYPES.ANY;
else return TYPES.NONE;
}
public get(param: string): Command {
const type = this.resolve(param);
let command: Command;
switch (type) {
case TYPES.SUBCOMMAND:
command = this.subcommands.get(param) as Command;
break;
case TYPES.USER:
command = this.user as Command;
break;
case TYPES.NUMBER:
command = this.number as Command;
break;
case TYPES.ANY:
command = this.any as Command;
break;
default:
command = this;
break;
}
return command;
}
} }
let commands: Collection<string, Command> | null = null; let commands: Collection<string, Command> | null = null;
@ -158,96 +160,95 @@ export const aliases: Collection<string, string> = new Collection(); // Top-leve
/** Returns the cache of the commands if it exists and searches the directory if not. */ /** Returns the cache of the commands if it exists and searches the directory if not. */
export async function loadCommands(): Promise<Collection<string, Command>> { export async function loadCommands(): Promise<Collection<string, Command>> {
if (commands) return commands; if (commands) return commands;
if (process.argv[2] === 'dev' && !existsSync('src/commands/test.ts')) if (process.argv[2] === "dev" && !existsSync("src/commands/test.ts"))
writeFile( writeFile(
'src/commands/test.ts', "src/commands/test.ts",
template, template,
generateHandler( generateHandler(
'"test.ts" (testing/template command) successfully generated.', '"test.ts" (testing/template command) successfully generated.'
), )
); );
commands = new Collection(); commands = new Collection();
const dir = await ffs.opendir('dist/commands'); const dir = await ffs.opendir("dist/commands");
const listMisc: string[] = []; const listMisc: string[] = [];
let selected; let selected;
// There will only be one level of directory searching (per category). // There will only be one level of directory searching (per category).
while ((selected = await dir.read())) { while ((selected = await dir.read())) {
if (selected.isDirectory()) { if (selected.isDirectory()) {
if (selected.name === 'subcommands') continue; if (selected.name === "subcommands") continue;
const subdir = await ffs.opendir(`dist/commands/${selected.name}`); const subdir = await ffs.opendir(`dist/commands/${selected.name}`);
const category = $(selected.name).toTitleCase(); const category = $(selected.name).toTitleCase();
const list: string[] = []; const list: string[] = [];
let cmd; let cmd;
while ((cmd = await subdir.read())) { while ((cmd = await subdir.read())) {
if (cmd.isDirectory()) { if (cmd.isDirectory()) {
if (cmd.name === 'subcommands') continue; if (cmd.name === "subcommands") continue;
else else
$.warn( $.warn(
`You can't have multiple levels of directories! From: "dist/commands/${cmd.name}"`, `You can't have multiple levels of directories! From: "dist/commands/${cmd.name}"`
); );
} else loadCommand(cmd.name, list, selected.name); } else loadCommand(cmd.name, list, selected.name);
} }
subdir.close(); subdir.close();
categories.set(category, list); categories.set(category, list);
} else loadCommand(selected.name, listMisc); } else loadCommand(selected.name, listMisc);
} }
dir.close(); dir.close();
categories.set('Miscellaneous', listMisc); categories.set("Miscellaneous", listMisc);
return commands; return commands;
} }
async function loadCommand( async function loadCommand(
filename: string, filename: string,
list: string[], list: string[],
category?: string, category?: string
) { ) {
if (!commands) if (!commands)
return $.error( return $.error(
`Function "loadCommand" was called without first initializing commands!`, `Function "loadCommand" was called without first initializing commands!`
);
const prefix = category ?? "";
const header = filename.substring(0, filename.indexOf(".js"));
const command = (await import(`../commands/${prefix}/${header}`))
.default as Command | undefined;
if (!command)
return $.warn(
`Command "${header}" has no default export which is a Command instance!`
);
command.originalCommandName = header;
list.push(header);
if (commands.has(header))
$.warn(
`Command "${header}" already exists! Make sure to make each command uniquely identifiable across categories!`
);
else commands.set(header, command);
for (const alias of command.aliases) {
if (commands.has(alias))
$.warn(
`Top-level alias "${alias}" from command "${header}" already exists either as a command or alias!`
);
else commands.set(alias, command);
}
$.log(
`Loading Command: ${header} (${
category ? $(category).toTitleCase() : "Miscellaneous"
})`
); );
const prefix = category ?? '';
const header = filename.substring(0, filename.indexOf('.js'));
const command = (await import(`../commands/${prefix}/${header}`)).default as
| Command
| undefined;
if (!command)
return $.warn(
`Command "${header}" has no default export which is a Command instance!`,
);
command.originalCommandName = header;
list.push(header);
if (commands.has(header))
$.warn(
`Command "${header}" already exists! Make sure to make each command uniquely identifiable across categories!`,
);
else commands.set(header, command);
for (const alias of command.aliases) {
if (commands.has(alias))
$.warn(
`Top-level alias "${alias}" from command "${header}" already exists either as a command or alias!`,
);
else commands.set(alias, command);
}
$.log(
`Loading Command: ${header} (${
category ? $(category).toTitleCase() : 'Miscellaneous'
})`,
);
} }
// The template should be built with a reductionist mentality. // The template should be built with a reductionist mentality.

View File

@ -1,41 +1,41 @@
import { Client, ClientEvents, Constants } from 'discord.js'; import {Client, ClientEvents, Constants} from "discord.js";
import Storage from './storage'; import Storage from "./storage";
import $ from './lib'; import $ from "./lib";
interface EventOptions<K extends keyof ClientEvents> { interface EventOptions<K extends keyof ClientEvents> {
readonly on?: (...args: ClientEvents[K]) => void; readonly on?: (...args: ClientEvents[K]) => void;
readonly once?: (...args: ClientEvents[K]) => void; readonly once?: (...args: ClientEvents[K]) => void;
} }
export default class Event<K extends keyof ClientEvents> { export default class Event<K extends keyof ClientEvents> {
private readonly on?: (...args: ClientEvents[K]) => void; private readonly on?: (...args: ClientEvents[K]) => void;
private readonly once?: (...args: ClientEvents[K]) => void; private readonly once?: (...args: ClientEvents[K]) => void;
constructor(options: EventOptions<K>) { constructor(options: EventOptions<K>) {
this.on = options.on; this.on = options.on;
this.once = options.once; 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". // 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) { public attach(client: Client, event: K) {
if (this.on) client.on(event, this.on); if (this.on) client.on(event, this.on);
if (this.once) client.once(event, this.once); if (this.once) client.once(event, this.once);
} }
} }
export async function loadEvents(client: Client) { export async function loadEvents(client: Client) {
for (const file of Storage.open('dist/events', (filename: string) => for (const file of Storage.open("dist/events", (filename: string) =>
filename.endsWith('.js'), filename.endsWith(".js")
)) { )) {
const header = file.substring(0, file.indexOf('.js')); const header = file.substring(0, file.indexOf(".js"));
const event = (await import(`../events/${header}`)).default; const event = (await import(`../events/${header}`)).default;
if ((Object.values(Constants.Events) as string[]).includes(header)) { if ((Object.values(Constants.Events) as string[]).includes(header)) {
event.attach(client, header); event.attach(client, header);
$.log(`Loading Event: ${header}`); $.log(`Loading Event: ${header}`);
} else } else
$.warn( $.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.)`, `"${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.)`
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +1,81 @@
import { GuildMember, Permissions } from 'discord.js'; import {GuildMember, Permissions} from "discord.js";
import { Config } from './structures'; import {Config} from "./structures";
import $ from './lib'; import $ from "./lib";
export enum PERMISSIONS { export enum PERMISSIONS {
NONE, NONE,
MOD, MOD,
ADMIN, ADMIN,
OWNER, OWNER,
BOT_SUPPORT, BOT_SUPPORT,
BOT_ADMIN, BOT_ADMIN,
BOT_OWNER, BOT_OWNER
} }
export const PermissionNames = [ export const PermissionNames = [
'User', "User",
'Moderator', "Moderator",
'Administrator', "Administrator",
'Server Owner', "Server Owner",
'Bot Support', "Bot Support",
'Bot Admin', "Bot Admin",
'Bot Owner', "Bot Owner"
]; ];
// Here is where you enter in the functions that check for permissions. // Here is where you enter in the functions that check for permissions.
const PermissionChecker: ((member: GuildMember) => boolean)[] = [ const PermissionChecker: ((member: GuildMember) => boolean)[] = [
// NONE // // NONE //
() => true, () => true,
// MOD // // MOD //
(member) => (member) =>
member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) || member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) || member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) || member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS), member.hasPermission(Permissions.FLAGS.BAN_MEMBERS),
// ADMIN // // ADMIN //
(member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR), (member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
// OWNER // // OWNER //
(member) => member.guild.ownerID === member.id, (member) => member.guild.ownerID === member.id,
// BOT_SUPPORT // // BOT_SUPPORT //
(member) => Config.support.includes(member.id), (member) => Config.support.includes(member.id),
// BOT_ADMIN // // BOT_ADMIN //
(member) => Config.admins.includes(member.id), (member) => Config.admins.includes(member.id),
// BOT_OWNER // // BOT_OWNER //
(member) => Config.owner === member.id, (member) => Config.owner === member.id
]; ];
// After checking the lengths of these three objects, use this as the length for consistency. // After checking the lengths of these three objects, use this as the length for consistency.
const length = Object.keys(PERMISSIONS).length / 2; const length = Object.keys(PERMISSIONS).length / 2;
export function hasPermission( export function hasPermission(
member: GuildMember, member: GuildMember,
permission: PERMISSIONS, permission: PERMISSIONS
): boolean { ): boolean {
for (let i = length - 1; i >= permission; i--) for (let i = length - 1; i >= permission; i--)
if (PermissionChecker[i](member)) return true; if (PermissionChecker[i](member)) return true;
return false; return false;
} }
export function getPermissionLevel(member: GuildMember): number { export function getPermissionLevel(member: GuildMember): number {
for (let i = length - 1; i >= 0; i--) for (let i = length - 1; i >= 0; i--)
if (PermissionChecker[i](member)) return i; if (PermissionChecker[i](member)) return i;
return 0; return 0;
} }
// Length Checking // Length Checking
(() => { (() => {
const lenNames = PermissionNames.length; const lenNames = PermissionNames.length;
const lenChecker = PermissionChecker.length; const lenChecker = PermissionChecker.length;
// By transitive property, lenNames and lenChecker have to be equal to each other as well. // By transitive property, lenNames and lenChecker have to be equal to each other as well.
if (length !== lenNames || length !== lenChecker) if (length !== lenNames || length !== lenChecker)
$.error( $.error(
`Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`, `Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`
); );
})(); })();

View File

@ -1,85 +1,87 @@
import fs from 'fs'; import fs from "fs";
import $ from './lib'; import $ from "./lib";
const Storage = { const Storage = {
read(header: string): object { read(header: string): object {
this.open('data'); this.open("data");
const path = `data/${header}.json`; const path = `data/${header}.json`;
let data = {}; let data = {};
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
const file = fs.readFileSync(path, 'utf-8'); const file = fs.readFileSync(path, "utf-8");
try { try {
data = JSON.parse(file); data = JSON.parse(file);
} catch (error) { } catch (error) {
if (process.argv[2] !== 'dev') { if (process.argv[2] !== "dev") {
$.warn( $.warn(
`Malformed JSON data (header: ${header}), backing it up.`, `Malformed JSON data (header: ${header}), backing it up.`,
file, file
); );
fs.writeFile( fs.writeFile(
`${path}.backup`, `${path}.backup`,
file, file,
generateHandler( generateHandler(
`Backup file of "${header}" successfully written as ${file}.`, `Backup file of "${header}" successfully written as ${file}.`
), )
); );
}
}
} }
}
return data;
},
write(header: string, data: object, asynchronous = true) {
this.open("data");
const path = `data/${header}.json`;
if (process.argv[2] === "dev" || header === "config") {
const result = JSON.stringify(data, null, "\t");
if (asynchronous)
fs.writeFile(
path,
result,
generateHandler(
`"${header}" sucessfully spaced and written.`
)
);
else fs.writeFileSync(path, result);
} else {
const result = JSON.stringify(data);
if (asynchronous)
fs.writeFile(
path,
result,
generateHandler(`"${header}" sucessfully written.`)
);
else fs.writeFileSync(path, result);
}
},
open(
path: string,
filter?: (value: string, index: number, array: string[]) => unknown
): string[] {
if (!fs.existsSync(path)) fs.mkdirSync(path);
let directory = fs.readdirSync(path);
if (filter) directory = directory.filter(filter);
return directory;
},
close(path: string) {
if (fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
} }
return data;
},
write(header: string, data: object, asynchronous = true) {
this.open('data');
const path = `data/${header}.json`;
if (process.argv[2] === 'dev' || header === 'config') {
const result = JSON.stringify(data, null, '\t');
if (asynchronous)
fs.writeFile(
path,
result,
generateHandler(`"${header}" sucessfully spaced and written.`),
);
else fs.writeFileSync(path, result);
} else {
const result = JSON.stringify(data);
if (asynchronous)
fs.writeFile(
path,
result,
generateHandler(`"${header}" sucessfully written.`),
);
else fs.writeFileSync(path, result);
}
},
open(
path: string,
filter?: (value: string, index: number, array: string[]) => unknown,
): string[] {
if (!fs.existsSync(path)) fs.mkdirSync(path);
let directory = fs.readdirSync(path);
if (filter) directory = directory.filter(filter);
return directory;
},
close(path: string) {
if (fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
},
}; };
export function generateHandler(message: string) { export function generateHandler(message: string) {
return (error: Error | null) => { return (error: Error | null) => {
if (error) $.error(error); if (error) $.error(error);
else $.debug(message); else $.debug(message);
}; };
} }
export default Storage; export default Storage;

View File

@ -1,112 +1,114 @@
import FileManager from './storage'; import FileManager from "./storage";
import $, { select, GenericJSON, GenericStructure } from './lib'; import $, {select, GenericJSON, GenericStructure} from "./lib";
import { watch } from 'fs'; import {watch} from "fs";
import { Guild as DiscordGuild } from 'discord.js'; import {Guild as DiscordGuild} from "discord.js";
class ConfigStructure extends GenericStructure { class ConfigStructure extends GenericStructure {
public token: string; public token: string;
public prefix: string; public prefix: string;
public owner: string; public owner: string;
public admins: string[]; public admins: string[];
public support: string[]; public support: string[];
constructor(data: GenericJSON) { constructor(data: GenericJSON) {
super('config'); super("config");
this.token = select(data.token, '<ENTER YOUR TOKEN HERE>', String); this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String);
this.prefix = select(data.prefix, '$', String); this.prefix = select(data.prefix, "$", String);
this.owner = select(data.owner, '', String); this.owner = select(data.owner, "", String);
this.admins = select(data.admins, [], String, true); this.admins = select(data.admins, [], String, true);
this.support = select(data.support, [], String, true); this.support = select(data.support, [], String, true);
} }
} }
class User { class User {
public money: number; public money: number;
public lastReceived: number; public lastReceived: number;
constructor(data?: GenericJSON) { constructor(data?: GenericJSON) {
this.money = select(data?.money, 0, Number); this.money = select(data?.money, 0, Number);
this.lastReceived = select(data?.lastReceived, -1, Number); this.lastReceived = select(data?.lastReceived, -1, Number);
} }
} }
class Guild { class Guild {
public prefix: string | null; public prefix: string | null;
constructor(data?: GenericJSON) { constructor(data?: GenericJSON) {
this.prefix = select(data?.prefix, null, String); this.prefix = select(data?.prefix, null, String);
} }
} }
class StorageStructure extends GenericStructure { class StorageStructure extends GenericStructure {
public users: { [id: string]: User }; public users: {[id: string]: User};
public guilds: { [id: string]: Guild }; public guilds: {[id: string]: Guild};
constructor(data: GenericJSON) { constructor(data: GenericJSON) {
super('storage'); super("storage");
this.users = {}; this.users = {};
this.guilds = {}; this.guilds = {};
for (let id in data.users) for (let id in data.users)
if (/\d{17,19}/g.test(id)) this.users[id] = new User(data.users[id]); if (/\d{17,19}/g.test(id))
this.users[id] = new User(data.users[id]);
for (let id in data.guilds) for (let id in data.guilds)
if (/\d{17,19}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]); if (/\d{17,19}/g.test(id))
} this.guilds[id] = new Guild(data.guilds[id]);
/** Gets a user's profile if they exist and generate one if not. */
public getUser(id: string): User {
if (!/\d{17,19}/g.test(id))
$.warn(
`"${id}" is not a valid user ID! It will be erased when the data loads again.`,
);
if (id in this.users) return this.users[id];
else {
const user = new User();
this.users[id] = user;
return user;
} }
}
/** Gets a guild's settings if they exist and generate one if not. */ /** Gets a user's profile if they exist and generate one if not. */
public getGuild(id: string): Guild { public getUser(id: string): User {
if (!/\d{17,19}/g.test(id)) if (!/\d{17,19}/g.test(id))
$.warn( $.warn(
`"${id}" is not a valid guild ID! It will be erased when the data loads again.`, `"${id}" is not a valid user ID! It will be erased when the data loads again.`
); );
if (id in this.guilds) return this.guilds[id]; if (id in this.users) return this.users[id];
else { else {
const guild = new Guild(); const user = new User();
this.guilds[id] = guild; this.users[id] = user;
return guild; return user;
}
}
/** Gets a guild's settings if they exist and generate one if not. */
public getGuild(id: string): Guild {
if (!/\d{17,19}/g.test(id))
$.warn(
`"${id}" is not a valid guild ID! It will be erased when the data loads again.`
);
if (id in this.guilds) return this.guilds[id];
else {
const guild = new Guild();
this.guilds[id] = guild;
return guild;
}
} }
}
} }
// Exports instances. Don't worry, importing it from different files will load the same instance. // Exports instances. Don't worry, importing it from different files will load the same instance.
export let Config = new ConfigStructure(FileManager.read('config')); export let Config = new ConfigStructure(FileManager.read("config"));
export let Storage = new StorageStructure(FileManager.read('storage')); export let Storage = new StorageStructure(FileManager.read("storage"));
// This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache. // This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache.
// However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues. // However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues.
if (process.argv[2] === 'dev') { if (process.argv[2] === "dev") {
watch('data', (event, filename) => { watch("data", (event, filename) => {
$.debug('File Watcher:', event, filename); $.debug("File Watcher:", event, filename);
const header = filename.substring(0, filename.indexOf('.json')); const header = filename.substring(0, filename.indexOf(".json"));
switch (header) { switch (header) {
case 'config': case "config":
Config = new ConfigStructure(FileManager.read('config')); Config = new ConfigStructure(FileManager.read("config"));
break; break;
case 'storage': case "storage":
Storage = new StorageStructure(FileManager.read('storage')); Storage = new StorageStructure(FileManager.read("storage"));
break; break;
} }
}); });
} }
export function getPrefix(guild: DiscordGuild | null): string { export function getPrefix(guild: DiscordGuild | null): string {
return Storage.getGuild(guild?.id || 'N/A').prefix ?? Config.prefix; return Storage.getGuild(guild?.id || "N/A").prefix ?? Config.prefix;
} }

View File

@ -1,87 +1,94 @@
export class GenericWrapper<T> { export class GenericWrapper<T> {
protected readonly value: T; protected readonly value: T;
public constructor(value: T) { public constructor(value: T) {
this.value = value; this.value = value;
} }
} }
export class NumberWrapper extends GenericWrapper<number> { export class NumberWrapper extends GenericWrapper<number> {
/** /**
* Pluralises a word and chooses a suffix attached to the root provided. * Pluralises a word and chooses a suffix attached to the root provided.
* - pluralise("credit", "s") = credit/credits * - pluralise("credit", "s") = credit/credits
* - pluralise("part", "ies", "y") = party/parties * - pluralise("part", "ies", "y") = party/parties
* - pluralise("sheep") = sheep * - pluralise("sheep") = sheep
*/ */
public pluralise( public pluralise(
word: string, word: string,
plural = '', plural = "",
singular = '', singular = "",
excludeNumber = false, excludeNumber = false
): string { ): string {
let result = excludeNumber ? '' : `${this.value} `; let result = excludeNumber ? "" : `${this.value} `;
if (this.value === 1) result += word + singular; if (this.value === 1) result += word + singular;
else result += word + plural; else result += word + plural;
return result; return result;
} }
/** /**
* Pluralises a word for changes. * Pluralises a word for changes.
* - (-1).pluraliseSigned() = '-1 credits' * - (-1).pluraliseSigned() = '-1 credits'
* - (0).pluraliseSigned() = '+0 credits' * - (0).pluraliseSigned() = '+0 credits'
* - (1).pluraliseSigned() = '+1 credit' * - (1).pluraliseSigned() = '+1 credit'
*/ */
public pluraliseSigned( public pluraliseSigned(
word: string, word: string,
plural = '', plural = "",
singular = '', singular = "",
excludeNumber = false, excludeNumber = false
): string { ): string {
const sign = this.value >= 0 ? '+' : ''; const sign = this.value >= 0 ? "+" : "";
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`; return `${sign}${this.pluralise(
} word,
plural,
singular,
excludeNumber
)}`;
}
} }
export class StringWrapper extends GenericWrapper<string> { export class StringWrapper extends GenericWrapper<string> {
public replaceAll(before: string, after: string): string { public replaceAll(before: string, after: string): string {
let result = this.value; let result = this.value;
while (result.indexOf(before) !== -1) while (result.indexOf(before) !== -1)
result = result.replace(before, after); result = result.replace(before, after);
return result; return result;
} }
public toTitleCase(): string { public toTitleCase(): string {
return this.value.replace( return this.value.replace(
/([^\W_]+[^\s-]*) */g, /([^\W_]+[^\s-]*) */g,
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(), (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
); );
} }
} }
export class ArrayWrapper<T> extends GenericWrapper<T[]> { export class ArrayWrapper<T> extends GenericWrapper<T[]> {
/** Returns a random element from this array. */ /** Returns a random element from this array. */
public random(): T { public random(): T {
return this.value[Math.floor(Math.random() * this.value.length)]; return this.value[Math.floor(Math.random() * this.value.length)];
} }
/** /**
* Splits up this array into a specified length. * Splits up this array into a specified length.
* `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]` * `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`
*/ */
public split(lengthOfEachSection: number): T[][] { public split(lengthOfEachSection: number): T[][] {
const amountOfSections = Math.ceil(this.value.length / lengthOfEachSection); const amountOfSections = Math.ceil(
const sections: T[][] = new Array(amountOfSections); this.value.length / lengthOfEachSection
);
const sections: T[][] = new Array(amountOfSections);
for (let index = 0; index < amountOfSections; index++) for (let index = 0; index < amountOfSections; index++)
sections[index] = this.value.slice( sections[index] = this.value.slice(
index * lengthOfEachSection, index * lengthOfEachSection,
(index + 1) * lengthOfEachSection, (index + 1) * lengthOfEachSection
); );
return sections; return sections;
} }
} }

View File

@ -1,45 +1,47 @@
// Flags a user can have. // Flags a user can have.
// They're basically your profile badges. // They're basically your profile badges.
export const flags: { [index: string]: any } = { export const flags: {[index: string]: any} = {
DISCORD_EMPLOYEE: 'Discord Employee', DISCORD_EMPLOYEE: "Discord Employee",
DISCORD_PARTNER: 'Discord Partner', DISCORD_PARTNER: "Discord Partner",
BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)', BUGHUNTER_LEVEL_1: "Bug Hunter (Level 1)",
BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)', BUGHUNTER_LEVEL_2: "Bug Hunter (Level 2)",
HYPESQUAD_EVENTS: 'HypeSquad Events', HYPESQUAD_EVENTS: "HypeSquad Events",
HOUSE_BRAVERY: 'House of Bravery', HOUSE_BRAVERY: "House of Bravery",
HOUSE_BRILLIANCE: 'House of Brilliance', HOUSE_BRILLIANCE: "House of Brilliance",
HOUSE_BALANCE: 'House of Balance', HOUSE_BALANCE: "House of Balance",
EARLY_SUPPORTER: 'Early Supporter', EARLY_SUPPORTER: "Early Supporter",
TEAM_USER: 'Team User', TEAM_USER: "Team User",
SYSTEM: 'System', SYSTEM: "System",
VERIFIED_BOT: 'Verified Bot', VERIFIED_BOT: "Verified Bot",
VERIFIED_DEVELOPER: 'Verified Bot Developer', VERIFIED_DEVELOPER: "Verified Bot Developer"
}; };
export const filterLevels: { [index: string]: any } = { export const filterLevels: {[index: string]: any} = {
DISABLED: 'Off', DISABLED: "Off",
MEMBERS_WITHOUT_ROLES: 'No Role', MEMBERS_WITHOUT_ROLES: "No Role",
ALL_MEMBERS: 'Everyone', ALL_MEMBERS: "Everyone"
}; };
export const verificationLevels: { [index: string]: any } = {
NONE: 'None', export const verificationLevels: {[index: string]: any} = {
LOW: 'Low', NONE: "None",
MEDIUM: 'Medium', LOW: "Low",
HIGH: '(╯°□°)╯︵ ┻━┻', MEDIUM: "Medium",
VERY_HIGH: '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', HIGH: "(╯°□°)╯︵ ┻━┻",
VERY_HIGH: "┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻"
}; };
export const regions: { [index: string]: any } = {
brazil: 'Brazil', export const regions: {[index: string]: any} = {
europe: 'Europe', brazil: "Brazil",
hongkong: 'Hong Kong', europe: "Europe",
india: 'India', hongkong: "Hong Kong",
japan: 'Japan', india: "India",
russia: 'Russia', japan: "Japan",
singapore: 'Singapore', russia: "Russia",
southafrica: 'South Africa', singapore: "Singapore",
sydney: 'Sydney', southafrica: "South Africa",
'us-central': 'US Central', sydney: "Sydney",
'us-east': 'US East', "us-central": "US Central",
'us-west': 'US West', "us-east": "US East",
'us-south': 'US South', "us-west": "US West",
"us-south": "US South"
}; };

View File

@ -1,16 +1,16 @@
import Event from '../core/event'; import Event from "../core/event";
import { client } from '../index'; import {client} from "../index";
import $ from '../core/lib'; import $ from "../core/lib";
import * as discord from 'discord.js'; import * as discord from "discord.js";
export default new Event<'channelCreate'>({ export default new Event<"channelCreate">({
async on(channel) { async on(channel) {
const botGuilds = client.guilds; const botGuilds = client.guilds;
if (channel instanceof discord.GuildChannel) { if (channel instanceof discord.GuildChannel) {
const createdGuild = await botGuilds.fetch(channel.guild.id); const createdGuild = await botGuilds.fetch(channel.guild.id);
$.log( $.log(
`Channel created in '${createdGuild.name}' called '#${channel.name}'`, `Channel created in '${createdGuild.name}' called '#${channel.name}'`
); );
}
} }
},
}); });

View File

@ -1,16 +1,16 @@
import Event from '../core/event'; import Event from "../core/event";
import { client } from '../index'; import {client} from "../index";
import $ from '../core/lib'; import $ from "../core/lib";
import * as discord from 'discord.js'; import * as discord from "discord.js";
export default new Event<'channelDelete'>({ export default new Event<"channelDelete">({
async on(channel) { async on(channel) {
const botGuilds = client.guilds; const botGuilds = client.guilds;
if (channel instanceof discord.GuildChannel) { if (channel instanceof discord.GuildChannel) {
const createdGuild = await botGuilds.fetch(channel.guild.id); const createdGuild = await botGuilds.fetch(channel.guild.id);
$.log( $.log(
`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`, `Channel deleted in '${createdGuild.name}' called '#${channel.name}'`
); );
}
} }
},
}); });

View File

@ -1,137 +1,143 @@
import Event from '../core/event'; import Event from "../core/event";
import Command, { loadCommands } from '../core/command'; import Command, {loadCommands} from "../core/command";
import { import {
hasPermission, hasPermission,
getPermissionLevel, getPermissionLevel,
PermissionNames, PermissionNames
} from '../core/permissions'; } from "../core/permissions";
import { Permissions, Collection } from 'discord.js'; import {Permissions, Collection} from "discord.js";
import { getPrefix } from '../core/structures'; import {getPrefix} from "../core/structures";
import $ from '../core/lib'; import $ from "../core/lib";
// It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional. // It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional.
let commands: Collection<string, Command> | null = null; let commands: Collection<string, Command> | null = null;
export default new Event<'message'>({ export default new Event<"message">({
async on(message) { async on(message) {
// Load commands if it hasn't already done so. Luckily, it's called once at most. // Load commands if it hasn't already done so. Luckily, it's called once at most.
if (!commands) commands = await loadCommands(); if (!commands) commands = await loadCommands();
// Message Setup // // Message Setup //
if (message.author.bot) return; if (message.author.bot) return;
const prefix = getPrefix(message.guild); const prefix = getPrefix(message.guild);
if (!message.content.startsWith(prefix)) { if (!message.content.startsWith(prefix)) {
if (message.client.user && message.mentions.has(message.client.user)) if (
message.channel.send( message.client.user &&
`${message.author.toString()}, my prefix on this guild is \`${prefix}\`.`, message.mentions.has(message.client.user)
); )
return; message.channel.send(
} `${message.author.toString()}, my prefix on this guild is \`${prefix}\`.`
);
const [header, ...args] = message.content return;
.substring(prefix.length)
.split(/ +/);
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}`,
);
}
$.log(
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`,
);
// Subcommand Recursion //
let command = commands.get(header);
if (!command)
return $.warn(
`Command "${header}" was called but for some reason it's still undefined!`,
);
const params: any[] = [];
let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
for (let param of args) {
if (command.endpoint) {
if (
command.subcommands.size > 0 ||
command.user ||
command.number ||
command.any
)
$.warn(
`An endpoint cannot have subcommands! Check ${prefix}${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); const [header, ...args] = message.content
.substring(prefix.length)
.split(/ +/);
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}`
);
}
$.log(
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`
);
// Subcommand Recursion //
let command = commands.get(header);
if (!command)
return $.warn(
`Command "${header}" was called but for some reason it's still undefined!`
);
const params: any[] = [];
let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
for (let param of args) {
if (command.endpoint) {
if (
command.subcommands.size > 0 ||
command.user ||
command.number ||
command.any
)
$.warn(
`An endpoint cannot have subcommands! Check ${prefix}${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 $.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 \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[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(
Object.assign(
$.bind($),
{
args: params,
author: message.author,
channel: message.channel,
client: message.client,
guild: message.guild,
member: message.member,
message: message
},
$
)
);
} }
if (!message.member)
return $.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 \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[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(
Object.assign(
$.bind($),
{
args: params,
author: message.author,
channel: message.channel,
client: message.client,
guild: message.guild,
member: message.member,
message: message,
},
$,
),
);
},
}); });

View File

@ -1,24 +1,24 @@
import Event from '../core/event'; import Event from "../core/event";
import { Permissions } from 'discord.js'; import {Permissions} from "discord.js";
import { botHasPermission } from '../core/lib'; import {botHasPermission} from "../core/lib";
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting. // A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
export const eventListeners: Map< export const eventListeners: Map<
string, string,
(emote: string, id: string) => void (emote: string, id: string) => void
> = new Map(); > = new Map();
// Attached to the client, there can be one event listener attached to a message ID which is executed if present. // 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'>({ export default new Event<"messageReactionRemove">({
on(reaction, user) { on(reaction, user) {
const canDeleteEmotes = botHasPermission( const canDeleteEmotes = botHasPermission(
reaction.message.guild, reaction.message.guild,
Permissions.FLAGS.MANAGE_MESSAGES, Permissions.FLAGS.MANAGE_MESSAGES
); );
if (!canDeleteEmotes) { if (!canDeleteEmotes) {
const callback = eventListeners.get(reaction.message.id); const callback = eventListeners.get(reaction.message.id);
callback && callback(reaction.emoji.name, user.id); callback && callback(reaction.emoji.name, user.id);
}
} }
},
}); });

View File

@ -1,18 +1,18 @@
import Event from '../core/event'; import Event from "../core/event";
import { client } from '../index'; import {client} from "../index";
import $ from '../core/lib'; import $ from "../core/lib";
import { Config } from '../core/structures'; import {Config} from "../core/structures";
export default new Event<'ready'>({ export default new Event<"ready">({
once() { once() {
if (client.user) { if (client.user) {
$.ready( $.ready(
`Logged in as ${client.user.username}#${client.user.discriminator}.`, `Logged in as ${client.user.username}#${client.user.discriminator}.`
); );
client.user.setActivity({ client.user.setActivity({
type: 'LISTENING', type: "LISTENING",
name: `${Config.prefix}help`, name: `${Config.prefix}help`
}); });
}
} }
},
}); });

View File

@ -1,38 +1,38 @@
import { Client } 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 { loadCommands } from './core/command'; import {loadCommands} from "./core/command";
import { loadEvents } from './core/event'; import {loadEvents} from "./core/event";
import 'discord.js-lavalink-lib'; import "discord.js-lavalink-lib";
import LavalinkMusic from 'discord.js-lavalink-lib'; import LavalinkMusic from "discord.js-lavalink-lib";
// 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 Client(); export const client = new Client();
(client as any).music = LavalinkMusic(client, { (client as any).music = LavalinkMusic(client, {
lavalink: { lavalink: {
restnode: { restnode: {
host: 'localhost', host: "localhost",
port: 2333, port: 2333,
password: 'youshallnotpass', password: "youshallnotpass"
},
nodes: [
{
host: "localhost",
port: 2333,
password: "youshallnotpass"
}
]
}, },
nodes: [ prefix: "!!",
{ helpCmd: "mhelp",
host: 'localhost', admins: ["717352467280691331"]
port: 2333,
password: 'youshallnotpass',
},
],
},
prefix: '!!',
helpCmd: 'mhelp',
admins: ['717352467280691331'],
}); });
// Begin the command loading here rather than when it's needed like in the message event. // Begin the command loading here rather than when it's needed like in the message event.
setup.init().then(() => { setup.init().then(() => {
loadCommands(); loadCommands();
loadEvents(client); loadEvents(client);
client.login(Config.token).catch(setup.again); client.login(Config.token).catch(setup.again);
}); });

View File

@ -1,64 +1,65 @@
import { existsSync as exists } from 'fs'; import {existsSync as exists} from "fs";
import inquirer from 'inquirer'; import inquirer from "inquirer";
import Storage from './core/storage'; import Storage from "./core/storage";
import { Config } from './core/structures'; import {Config} from "./core/structures";
import $, { setConsoleActivated } from './core/lib'; import $, {setConsoleActivated} from "./core/lib";
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet. // This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
// And that file won't be written until the data is successfully initialized. // And that file won't be written until the data is successfully initialized.
const prompts = [ const prompts = [
{ {
type: 'password', type: "password",
name: 'token', name: "token",
message: "What's your bot's token?", message: "What's your bot's token?",
mask: true, mask: true
}, },
{ {
type: 'input', type: "input",
name: 'prefix', name: "prefix",
message: "What do you want your bot's prefix to be?", message: "What do you want your bot's prefix to be?",
default: '$', default: "$"
}, },
{ {
type: 'input', type: "input",
name: 'owner', name: "owner",
message: "Enter the owner's user ID here.", message: "Enter the owner's user ID here."
}, },
{ {
type: 'input', type: "input",
name: 'admins', name: "admins",
message: 'Enter a list of bot admins (by their IDs) separated by spaces.', message:
}, "Enter a list of bot admins (by their IDs) separated by spaces."
{ },
type: 'input', {
name: 'support', type: "input",
message: name: "support",
'Enter a list of bot troubleshooters (by their IDs) separated by spaces.', message:
}, "Enter a list of bot troubleshooters (by their IDs) separated by spaces."
}
]; ];
export default { export default {
async init() { async init() {
while (!exists('data/config.json')) { while (!exists("data/config.json")) {
const answers = await inquirer.prompt(prompts); const answers = await inquirer.prompt(prompts);
Storage.open('data'); Storage.open("data");
Config.token = answers.token as string; Config.token = answers.token as string;
Config.prefix = answers.prefix as string; Config.prefix = answers.prefix as string;
Config.owner = answers.owner as string; Config.owner = answers.owner as string;
const admins = answers.admins as string; const admins = answers.admins as string;
Config.admins = admins !== '' ? admins.split(' ') : []; Config.admins = admins !== "" ? admins.split(" ") : [];
const support = answers.support as string; const support = answers.support as string;
Config.support = support !== '' ? support.split(' ') : []; Config.support = support !== "" ? support.split(" ") : [];
Config.save(false); Config.save(false);
}
},
/** Prompt the user to set their token again. */
async again() {
$.error("It seems that the token you provided is invalid.");
setConsoleActivated(false);
const answers = await inquirer.prompt(prompts.slice(0, 1));
Config.token = answers.token as string;
Config.save(false);
process.exit();
} }
},
/** Prompt the user to set their token again. */
async again() {
$.error('It seems that the token you provided is invalid.');
setConsoleActivated(false);
const answers = await inquirer.prompt(prompts.slice(0, 1));
Config.token = answers.token as string;
Config.save(false);
process.exit();
},
}; };

View File

@ -1,111 +1,107 @@
import { strict as assert } from 'assert'; import {strict as assert} from "assert";
import { import {NumberWrapper, StringWrapper, ArrayWrapper} from "../src/core/wrappers";
NumberWrapper,
StringWrapper,
ArrayWrapper,
} from '../src/core/wrappers';
// I can't figure out a way to run the test suite while running the bot. // I can't figure out a way to run the test suite while running the bot.
describe('Wrappers', () => { describe("Wrappers", () => {
describe('NumberWrapper', () => { describe("NumberWrapper", () => {
describe('#pluralise()', () => { describe("#pluralise()", () => {
it('should return "5 credits"', () => { it('should return "5 credits"', () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(5).pluralise('credit', 's'), new NumberWrapper(5).pluralise("credit", "s"),
'5 credits', "5 credits"
); );
}); });
it('should return "1 credit"', () => { it('should return "1 credit"', () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(1).pluralise('credit', 's'), new NumberWrapper(1).pluralise("credit", "s"),
'1 credit', "1 credit"
); );
}); });
it('should return "-1 credits"', () => { it('should return "-1 credits"', () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(-1).pluralise('credit', 's'), new NumberWrapper(-1).pluralise("credit", "s"),
'-1 credits', "-1 credits"
); );
}); });
it('should be able to work with a plural suffix', () => { it("should be able to work with a plural suffix", () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(2).pluralise('part', 'ies', 'y'), new NumberWrapper(2).pluralise("part", "ies", "y"),
'2 parties', "2 parties"
); );
}); });
it('should be able to work with a singular suffix', () => { it("should be able to work with a singular suffix", () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(1).pluralise('part', 'ies', 'y'), new NumberWrapper(1).pluralise("part", "ies", "y"),
'1 party', "1 party"
); );
}); });
it('should be able to exclude the number', () => { it("should be able to exclude the number", () => {
assert.strictEqual( assert.strictEqual(
new NumberWrapper(1).pluralise('credit', 's', '', true), new NumberWrapper(1).pluralise("credit", "s", "", true),
'credit', "credit"
); );
}); });
});
describe("#pluraliseSigned()", () => {
it('should return "-1 credits"', () => {
assert.strictEqual(
new NumberWrapper(-1).pluraliseSigned("credit", "s"),
"-1 credits"
);
});
it('should return "+0 credits"', () => {
assert.strictEqual(
new NumberWrapper(0).pluraliseSigned("credit", "s"),
"+0 credits"
);
});
it('should return "+1 credit"', () => {
assert.strictEqual(
new NumberWrapper(1).pluraliseSigned("credit", "s"),
"+1 credit"
);
});
});
}); });
describe('#pluraliseSigned()', () => { describe("StringWrapper", () => {
it('should return "-1 credits"', () => { describe("#replaceAll()", () => {
assert.strictEqual( it('should convert "test" to "zesz"', () => {
new NumberWrapper(-1).pluraliseSigned('credit', 's'), assert.strictEqual(
'-1 credits', new StringWrapper("test").replaceAll("t", "z"),
); "zesz"
}); );
});
});
it('should return "+0 credits"', () => { describe("#toTitleCase()", () => {
assert.strictEqual( it("should capitalize the first letter of each word", () => {
new NumberWrapper(0).pluraliseSigned('credit', 's'), assert.strictEqual(
'+0 credits', new StringWrapper(
); "yeetus deletus find salvation from jesus"
}); ).toTitleCase(),
"Yeetus Deletus Find Salvation From Jesus"
it('should return "+1 credit"', () => { );
assert.strictEqual( });
new NumberWrapper(1).pluraliseSigned('credit', 's'), });
'+1 credit',
);
});
});
});
describe('StringWrapper', () => {
describe('#replaceAll()', () => {
it('should convert "test" to "zesz"', () => {
assert.strictEqual(
new StringWrapper('test').replaceAll('t', 'z'),
'zesz',
);
});
}); });
describe('#toTitleCase()', () => { describe("ArrayWrapper", () => {
it('should capitalize the first letter of each word', () => { describe("#split()", () => {
assert.strictEqual( it("should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]", () => {
new StringWrapper( assert.deepStrictEqual(
'yeetus deletus find salvation from jesus', new ArrayWrapper([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).split(3),
).toTitleCase(), [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
'Yeetus Deletus Find Salvation From Jesus', );
); });
}); });
}); });
});
describe('ArrayWrapper', () => {
describe('#split()', () => {
it('should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]', () => {
assert.deepStrictEqual(
new ArrayWrapper([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).split(3),
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]],
);
});
});
});
}); });

View File

@ -1,17 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "dist", "outDir": "dist",
"target": "ES6", "target": "ES6",
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"strictNullChecks": true, "strictNullChecks": true,
"strictFunctionTypes": true, "strictFunctionTypes": true,
"strictPropertyInitialization": true, "strictPropertyInitialization": true,
"removeComments": true "removeComments": true
}, },
"exclude": ["test"] "exclude": ["test"]
} }