Merge branch 'typescript' of https://github.com/keanuplayz/TravBot-v3 into experimental-core

This commit is contained in:
WatDuhHekBro 2021-03-30 03:23:11 -05:00
commit 00addd468c
12 changed files with 2242 additions and 119 deletions

1919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,7 @@
"glob": "^7.1.6",
"inquirer": "^7.3.3",
"moment": "^2.29.1",
"ms": "^2.1.3",
"os": "^0.1.1"
"ms": "^2.1.3"
},
"devDependencies": {
"@types/glob": "^7.1.3",

View File

@ -124,14 +124,15 @@ export default new Command({
number: new Command({
description: "Amount of messages to delete.",
async run($: CommonLibrary): Promise<any> {
if ($.channel.type === "dm") {
await $.channel.send("Can't clear messages in the DMs!");
return;
}
$.message.delete();
const fetched = await $.channel.messages.fetch({
limit: $.args[0]
});
$.channel
/// @ts-ignore
.bulkDelete(fetched)
.catch((error: any) => $.channel.send(`Error: ${error}`));
await $.channel.bulkDelete(fetched);
}
})
}),
@ -157,8 +158,7 @@ export default new Command({
permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> {
const nickName = $.args.join(" ");
const trav = $.guild?.members.cache.find((member) => member.id === $.client.user?.id);
await trav?.setNickname(nickName);
await $.guild?.me?.setNickname(nickName);
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES))
$.message.delete({timeout: 5000}).catch($.handler.bind($));
$.channel.send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000}));

View File

@ -62,9 +62,9 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje
}
export function isAuthorized(guild: Guild | null, channel: TextChannel | DMChannel | NewsChannel): boolean {
if (guild?.id === "637512823676600330" || process.argv[2] === "dev") return true;
if (guild?.id === "637512823676600330" && channel?.id === "669464416420364288" || process.argv[2] === "dev") return true;
else {
channel.send("Sorry, this command can only be used in Monika's emote server.");
channel.send("Sorry, this command can only be used in Monika's emote server. (#mon-stocks)");
return false;
}
}

View File

@ -6,6 +6,9 @@ import {CommonLibrary, formatBytes, trimArray} from "../core/lib";
import {verificationLevels, filterLevels, regions, flags} from "../defs/info";
import moment from "moment";
import utc from "moment";
import {Guild} from "discord.js";
const {version} = require("../../package.json");
export default new Command({
description: "Command to provide all sorts of info about the current server, a user, etc.",
@ -34,13 +37,6 @@ export default new Command({
async run($: CommonLibrary): Promise<any> {
const core = os.cpus()[0];
const embed = new MessageEmbed()
.setThumbnail(
/// @ts-ignore
$.client.user?.displayAvatarURL({
dynamic: true,
size: 2048
})
)
.setColor($.guild?.me?.displayHexColor || "BLUE")
.addField("General", [
`** Client:** ${$.client.user?.tag} (${$.client.user?.id})`,
@ -66,84 +62,59 @@ export default new Command({
`\u3000 • Speed: ${core.speed}MHz`,
`** Memory:**`,
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`,
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapTotal)}`
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}`
])
.setTimestamp();
const avatarURL = $.client.user?.displayAvatarURL({
dynamic: true,
size: 2048
});
if (avatarURL) embed.setThumbnail(avatarURL);
$.channel.send(embed);
}
}),
guild: new Command({
description: "Displays info about the current guild.",
description: "Displays info about the current guild or another guild.",
usage: "(<guild name>/<guild ID>)",
async run($: CommonLibrary): Promise<any> {
if ($.guild) {
const roles = $.guild.roles.cache
.sort((a, b) => b.position - a.position)
.map((role) => role.toString());
const members = $.guild.members.cache;
const channels = $.guild.channels.cache;
const emojis = $.guild.emojis.cache;
const iconURL = $.guild.iconURL({dynamic: true});
const embed = new MessageEmbed()
.setDescription(`**Guild information for __${$.guild.name}__**`)
.setColor("BLUE");
if (iconURL)
embed
.setThumbnail(iconURL)
.addField("General", [
`** Name:** ${$.guild.name}`,
`** ID:** ${$.guild.id}`,
`** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`,
`** Region:** ${regions[$.guild.region]}`,
`** Boost Tier:** ${$.guild.premiumTier ? `Tier ${$.guild.premiumTier}` : "None"}`,
`** Explicit Filter:** ${filterLevels[$.guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[$.guild.verificationLevel]}`,
`** Time Created:** ${moment($.guild.createdTimestamp).format("LT")} ${moment(
$.guild.createdTimestamp
).format("LL")} ${moment($.guild.createdTimestamp).fromNow()})`,
"\u200b"
])
.addField("Statistics", [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter((emoji) => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter((emoji) => emoji.animated).size}`,
`** Member Count:** ${$.guild.memberCount}`,
`** Humans:** ${members.filter((member) => !member.user.bot).size}`,
`** Bots:** ${members.filter((member) => member.user.bot).size}`,
`** Text Channels:** ${channels.filter((channel) => channel.type === "text").size}`,
`** Voice Channels:** ${channels.filter((channel) => channel.type === "voice").size}`,
`** Boost Count:** ${$.guild.premiumSubscriptionCount || "0"}`,
`\u200b`
])
.addField("Presence", [
`** Online:** ${members.filter((member) => member.presence.status === "online").size}`,
`** Idle:** ${members.filter((member) => member.presence.status === "idle").size}`,
`** Do Not Disturb:** ${
members.filter((member) => member.presence.status === "dnd").size
}`,
`** Offline:** ${
members.filter((member) => member.presence.status === "offline").size
}`,
"\u200b"
])
.addField(
`Roles [${roles.length - 1}]`,
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles) : "None"
)
.setTimestamp();
$.channel.send(embed);
$.channel.send(await getGuildInfo($.guild, $.guild));
} else {
$.channel.send("Please execute this command in a guild.");
}
}
},
any: new Command({
description: "Display info about a guild by finding its name or ID.",
async run($: CommonLibrary): Promise<any> {
// If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild
if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) {
const id = $.args[0];
const guild = $.client.guilds.cache.get(id);
if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild));
} else {
$.channel.send(`None of the servers I'm in matches the guild ID \`${id}\`!`);
}
} else {
const query: string = $.args.join(" ").toLowerCase();
const guild = $.client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild));
} else {
$.channel.send(`None of the servers I'm in matches the query \`${query}\`!`);
}
}
}
})
})
},
user: new Command({
description: "Displays info about mentioned user.",
async run($: CommonLibrary): Promise<any> {
// Transforms the User object into a GuildMember object of the current guild.
const member = $.guild?.members.resolve($.args[0]) ?? (await $.guild?.members.fetch($.args[0]));
const member = await $.guild?.members.fetch($.args[0]);
if (!member)
return $.channel.send(
@ -154,8 +125,7 @@ export default new Command({
.sort((a: {position: number}, b: {position: number}) => b.position - a.position)
.map((role: {toString: () => any}) => role.toString())
.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.
const userFlags = ((await member.user.fetchFlags()) as UserFlags).toArray();
const userFlags = (await member.user.fetchFlags()).toArray();
const embed = new MessageEmbed()
.setThumbnail(member.user.displayAvatarURL({dynamic: true, size: 512}))
@ -188,3 +158,64 @@ export default new Command({
}
})
});
async function getGuildInfo(guild: Guild, currentGuild: Guild | null) {
const members = await guild.members.fetch({
withPresences: true,
force: true
});
const roles = guild.roles.cache.sort((a, b) => b.position - a.position).map((role) => role.toString());
const channels = guild.channels.cache;
const emojis = guild.emojis.cache;
const iconURL = guild.iconURL({dynamic: true});
const embed = new MessageEmbed().setDescription(`**Guild information for __${guild.name}__**`).setColor("BLUE");
const displayRoles = !!(currentGuild && guild.id === currentGuild.id);
if (iconURL) {
embed
.setThumbnail(iconURL)
.addField("General", [
`** Name:** ${guild.name}`,
`** ID:** ${guild.id}`,
`** Owner:** ${guild.owner?.user.tag} (${guild.ownerID})`,
`** Region:** ${regions[guild.region]}`,
`** Boost Tier:** ${guild.premiumTier ? `Tier ${guild.premiumTier}` : "None"}`,
`** Explicit Filter:** ${filterLevels[guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[guild.verificationLevel]}`,
`** Time Created:** ${moment(guild.createdTimestamp).format("LT")} ${moment(
guild.createdTimestamp
).format("LL")} ${moment(guild.createdTimestamp).fromNow()}`,
"\u200b"
])
.addField("Statistics", [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter((emoji) => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter((emoji) => emoji.animated).size}`,
`** Member Count:** ${guild.memberCount}`,
`** Humans:** ${members.filter((member) => !member.user.bot).size}`,
`** Bots:** ${members.filter((member) => member.user.bot).size}`,
`** Text Channels:** ${channels.filter((channel) => channel.type === "text").size}`,
`** Voice Channels:** ${channels.filter((channel) => channel.type === "voice").size}`,
`** Boost Count:** ${guild.premiumSubscriptionCount || "0"}`,
`\u200b`
])
.addField("Presence", [
`** Online:** ${members.filter((member) => member.presence.status === "online").size}`,
`** Idle:** ${members.filter((member) => member.presence.status === "idle").size}`,
`** Do Not Disturb:** ${members.filter((member) => member.presence.status === "dnd").size}`,
`** Offline:** ${members.filter((member) => member.presence.status === "offline").size}`,
displayRoles ? "\u200b" : ""
])
.setTimestamp();
// Only add the roles if the guild the bot is sending the message to is the same one that's being requested.
if (displayRoles) {
embed.addField(
`Roles [${roles.length - 1}]`,
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles) : "None"
);
}
}
return embed;
}

View File

@ -9,15 +9,14 @@ export default new Command({
if (!voiceChannel) return $.channel.send("You are not in a voice channel.");
if (!$.guild?.me?.hasPermission("MANAGE_CHANNELS"))
if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS"))
return $.channel.send("I am lacking the required permissions to perform this action.");
if ($.args.length === 0) return $.channel.send("Please provide a new voice channel name.");
const changeVC = $.guild.channels.resolve(voiceChannel.id);
$.channel
.send(`Changed channel name from "${voiceChannel}" to "${$.args.join(" ")}".`)
/// @ts-ignore
.then(changeVC?.setName($.args.join(" ")));
const prevName = voiceChannel.name;
const newName = $.args.join(" ");
await voiceChannel.setName(newName);
await $.channel.send(`Changed channel name from "${prevName}" to "${newName}".`);
}
});

View File

@ -12,7 +12,6 @@ export default new Command({
async run({guild, channel, message, args}) {
let output = "";
for (const query of args) output += queryClosestEmoteByName(query).toString();
if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete();
channel.send(output);
}
})

View File

@ -1,32 +1,117 @@
import {GuildEmoji} from "discord.js";
import {MessageEmbed} from "discord.js";
import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
import vm from "vm";
const REGEX_TIMEOUT_MS = 1000;
export default new Command({
description: "Lists all emotes the bot has in it's registry,",
endpoint: true,
usage: "<regex pattern> (-flags)",
async run($: CommonLibrary): Promise<any> {
const nsfw: string | string[] = [];
const pages = $.client.emojis.cache.filter((x) => !nsfw.includes(x.guild.id), this).array();
const pagesSplit = $(pages).split(20);
$.log(pagesSplit);
var embed = new MessageEmbed().setTitle("**Emoji list!**").setColor("AQUA");
let desc = "";
displayEmoteList($, $.client.emojis.cache.array());
},
any: new Command({
description:
"Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i",
async run($: CommonLibrary): Promise<any> {
// If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward)
if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) {
const guildID: string = $.args[0];
for (const emote of pagesSplit[0]) {
desc += `${emote} | ${emote.name}\n`;
displayEmoteList($, $.client.emojis.cache.filter((emote) => emote.guild.id === guildID).array());
} else {
// Otherwise, search via a regex pattern
let flags: string | undefined = undefined;
if (/^-[dgimsuy]{1,7}$/.test($.args[$.args.length - 1])) {
flags = $.args.pop().substring(1);
}
let emoteCollection = $.client.emojis.cache.array();
// Creates a sandbox to stop a regular expression if it takes too much time to search.
// To avoid passing in a giant data structure, I'll just pass in the structure {[id: string]: [name: string]}.
//let emotes: {[id: string]: string} = {};
let emotes = new Map<string, string>();
for (const emote of emoteCollection) {
emotes.set(emote.id, emote.name);
}
// The result will be sandbox.emotes because it'll be modified in-place.
const sandbox = {
regex: new RegExp($.args.join(" "), flags),
emotes
};
const context = vm.createContext(sandbox);
if (vm.isContext(sandbox)) {
// Restrict an entire query to the timeout specified.
try {
const script = new vm.Script(
"for(const [id, name] of emotes.entries()) if(!regex.test(name)) emotes.delete(id);"
);
script.runInContext(context, {timeout: REGEX_TIMEOUT_MS});
emotes = sandbox.emotes;
emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted.
displayEmoteList($, emoteCollection);
} catch (error) {
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
$.channel.send(
`The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.`
);
} else {
throw new Error(error);
}
}
} else {
$.channel.send("Failed to initialize sandbox.");
}
}
}
})
});
async function displayEmoteList($: CommonLibrary, emotes: GuildEmoji[]) {
emotes.sort((a, b) => {
const first = a.name.toLowerCase();
const second = b.name.toLowerCase();
if (first > second) return 1;
else if (first < second) return -1;
else return 0;
});
const sections = $(emotes).split(20);
const pages = sections.length;
const embed = new MessageEmbed().setTitle("**Emotes**").setColor("AQUA");
let desc = "";
// Gather the first page (if it even exists, which it might not if there no valid emotes appear)
if (pages > 0) {
for (const emote of sections[0]) {
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
}
embed.setDescription(desc);
const msg = await $.channel.send({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);
});
if (pages > 1) {
embed.setTitle(`**Emotes** (Page 1 of ${pages})`);
const msg = await $.channel.send({embed});
$.paginate(msg, $.author.id, pages, (page) => {
let desc = "";
for (const emote of sections[page]) {
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
}
embed.setTitle(`**Emotes** (Page ${page + 1} of ${pages})`);
embed.setDescription(desc);
msg.edit(embed);
});
} else {
await $.channel.send({embed});
}
} else {
$.channel.send("No valid emotes found by that query.");
}
});
}

View File

@ -168,7 +168,7 @@ export function formatUTCTimestamp(now = new Date()) {
}
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!(client.user && guild?.members.resolve(client.user)?.hasPermission(permission));
return !!guild?.me?.hasPermission(permission);
}
export function updateGlobalEmoteRegistry(): void {
@ -212,20 +212,22 @@ $.paginate = async (
callback(page);
};
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
const handle = (emote: string, reacterID: string) => {
switch (emote) {
case "⬅️":
case BACKWARDS_EMOJI:
turn(-1);
break;
case "➡️":
case FORWARDS_EMOJI:
turn(1);
break;
}
};
// Listen for reactions and call the handler.
await message.react("⬅️");
await message.react("➡️");
let backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_EMOJI);
eventListeners.set(message.id, handle);
await message.awaitReactions(
(reaction, user) => {
@ -244,8 +246,8 @@ $.paginate = async (
);
// When time's up, remove the bot's own reactions.
eventListeners.delete(message.id);
message.reactions.cache.get("⬅️")?.users.remove(message.author);
message.reactions.cache.get("➡️")?.users.remove(message.author);
backwardsReaction.users.remove(message.author);
forwardsReaction.users.remove(message.author);
};
// Waits for the sender to either confirm an action or let it pass (and delete the message).

View File

@ -4,11 +4,16 @@ import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permis
import {Permissions} from "discord.js";
import {getPrefix} from "../core/structures";
import $, {replyEventListeners} from "../core/lib";
import quote from "../modules/message_embed";
export default new Event<"message">({
async on(message) {
const commands = await loadableCommands;
if (message.content.toLowerCase().includes("remember to drink water")) {
message.react("🚱");
}
// Message Setup //
if (message.author.bot) return;
@ -24,6 +29,10 @@ export default new Event<"message">({
const clientUser = message.client.user;
let usesBotSpecificPrefix = false;
if (!message.content.startsWith(prefix)) {
return quote(message);
}
// If the client user exists, check if it starts with the bot-specific prefix.
if (clientUser) {
// If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other).

View File

@ -1,13 +1,41 @@
import {Client} from "discord.js";
import * as discord from "discord.js";
import setup from "./setup";
import {Config} from "./core/structures";
import {loadEvents} from "./core/event";
import "discord.js-lavalink-lib";
import LavalinkMusic from "discord.js-lavalink-lib";
declare module "discord.js" {
interface Presence {
patch(data: any): void;
}
}
// The terrible hacks were written by none other than The Noble Programmer On The White PC.
// NOTE: Terrible hack ahead!!! In order to reduce the memory usage of the bot
// we only store the information from presences that we actually end up using,
// which currently is only the (online/idle/dnd/offline/...) status (see
// `src/commands/info.ts`). What data is retrieved from the `data` object
// (which contains the data received from the Gateway) and how can be seen
// here:
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/structures/Presence.js#L81-L110>.
const oldPresencePatch = discord.Presence.prototype.patch;
discord.Presence.prototype.patch = function patch(data: any) {
oldPresencePatch.call(this, {status: data.status});
};
// This is here in order to make it much less of a headache to access the client from other files.
// This 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 discord.Client();
// NOTE: Terrible hack continued!!! Unfortunately we can't receive the presence
// data at all when the GUILD_PRESENCES intent is disabled, so while we do
// waste network bandwidth and the CPU time for decoding the incoming packets,
// the function which handles those packets is NOP-ed out, which, among other
// things, skips the code which caches the referenced users in the packet. See
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/client/actions/PresenceUpdate.js#L7-L41>.
(client["actions"] as any)["PresenceUpdate"].handle = () => {};
(client as any).music = LavalinkMusic(client, {
lavalink: {

View File

@ -0,0 +1,60 @@
import { client } from '..'
import { Message, TextChannel, APIMessage, MessageEmbed } from 'discord.js'
import { getPrefix } from '../core/structures'
import { DiscordAPIError } from 'discord.js'
export default async function quote(message: Message) {
if (message.author.bot) return
// const message_link_regex = message.content.match(/(!)?https?:\/\/\w+\.com\/channels\/(\d+)\/(\d+)\/(\d+)/)
const message_link_regex = message.content.match(/([<!]?)https?:\/\/(?:ptb\.|canary\.|)discord(?:app)?\.com\/channels\/(\d+)\/(\d+)\/(\d+)(>?)/)
if (message_link_regex == null) return
const [, char, guildID, channelID, messageID] = message_link_regex
if (char || message.content.startsWith(getPrefix(message.guild))) return
try {
const channel = client.guilds.cache.get(guildID)?.channels.cache.get(channelID) as TextChannel
const link_message = await channel.messages.fetch(messageID)
let rtmsg: string | APIMessage = ''
if (link_message.cleanContent) {
rtmsg = new APIMessage(message.channel as TextChannel, {
content: link_message.cleanContent,
disableMentions: 'all',
files: link_message.attachments.array()
})
}
const embeds = [
...link_message.embeds.filter(v => v.type == 'rich'),
...link_message.attachments.values()
]
/// @ts-ignore
if (!link_message.cleanContent && embeds.empty) {
const Embed = new MessageEmbed()
.setDescription('🚫 The message is empty.')
return message.channel.send(Embed)
}
const infoEmbed = new MessageEmbed()
.setAuthor(
link_message.author.username,
link_message.author.displayAvatarURL({format: 'png', dynamic: true, size: 4096}))
.setTimestamp(link_message.createdTimestamp)
.setDescription(`${link_message.cleanContent}\n\nSent in **${link_message.guild?.name}** | <#${link_message.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))`);
if (link_message.attachments.size !== 0) {
const image = link_message.attachments.first();
/// @ts-ignore
infoEmbed.setImage(image.url);
}
await message.channel.send(infoEmbed)
} catch (error) {
if (error instanceof DiscordAPIError) {
message.channel.send("I don't have access to this channel, or something else went wrong.")
}
return console.error(error)
}
}