diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8c546..4142c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# ??? +- `vaporwave`: Transforms input into full-width text +- `eco post`: A play on `eco get` +- `admin set prefix (<@bot>)`: Allows you to target a bot when setting a prefix if two bots have conflicting prefixes +- `party`: Sets the bot's status to streaming with a certain URL +- `eco award`: Awards users with Mons, only accessible by that person +- `thonk`: A result can now be discarded if the person who called the command reacts with ❌ +- `scanemotes forcereset`: Removes the cooldown on `scanemotes`, only accessible by bot support and up +- `urban`: Bug fixes + # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-??-??) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. - Utility functions are no longer attached to the command menu. Stuff like `$.paginate()` and `$(5).pluralise()` instead need to be imported and used as regular functions. diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 022383a..c87241a 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -2,7 +2,7 @@ import {Command, NamedCommand, callMemberByUsername} from "../../core"; import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils"; import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core"; import {BuyCommand, ShopCommand} from "./modules/eco-shop"; -import {MondayCommand} from "./modules/eco-extras"; +import {MondayCommand, AwardCommand} from "./modules/eco-extras"; import {BetCommand} from "./modules/eco-bet"; export default new NamedCommand({ @@ -18,7 +18,12 @@ export default new NamedCommand({ buy: BuyCommand, shop: ShopCommand, monday: MondayCommand, - bet: BetCommand + bet: BetCommand, + award: AwardCommand, + post: new NamedCommand({ + description: "A play on `eco get`", + run: "`405 Method Not Allowed`" + }) }, id: "user", user: new Command({ diff --git a/src/commands/fun/modules/eco-extras.ts b/src/commands/fun/modules/eco-extras.ts index f2760dd..2d69f29 100644 --- a/src/commands/fun/modules/eco-extras.ts +++ b/src/commands/fun/modules/eco-extras.ts @@ -1,6 +1,8 @@ import {Command, NamedCommand} from "../../../core"; import {Storage} from "../../../structures"; import {isAuthorized, getMoneyEmbed} from "./eco-utils"; +import {User} from "discord.js"; +import {pluralise} from "../../../lib"; const WEEKDAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -32,3 +34,45 @@ export const MondayCommand = new NamedCommand({ } } }); + +export const AwardCommand = new NamedCommand({ + description: "Only usable by Mon, awards one or a specified amount of Mons to the user.", + usage: " ()", + aliases: ["give"], + run: "You need to specify a user!", + user: new Command({ + async run({message, channel, guild, author, member, client, args}) { + if (author.id === "394808963356688394" || IS_DEV_MODE) { + const target = args[0] as User; + const user = Storage.getUser(target.id); + user.money++; + Storage.save(); + channel.send(`1 Mon given to ${target.username}.`, getMoneyEmbed(target)); + } else { + channel.send("This command is restricted to the bean."); + } + }, + number: new Command({ + async run({message, channel, guild, author, member, client, args}) { + if (author.id === "394808963356688394" || IS_DEV_MODE) { + const target = args[0] as User; + const amount = Math.floor(args[1]); + + if (amount > 0) { + const user = Storage.getUser(target.id); + user.money += amount; + Storage.save(); + channel.send( + `${pluralise(amount, "Mon", "s")} given to ${target.username}.`, + getMoneyEmbed(target) + ); + } else { + channel.send("You need to enter a number greater than 0."); + } + } else { + channel.send("This command is restricted to the bean."); + } + } + }) + }) +}); diff --git a/src/commands/fun/party.ts b/src/commands/fun/party.ts new file mode 100644 index 0000000..90e62c5 --- /dev/null +++ b/src/commands/fun/party.ts @@ -0,0 +1,13 @@ +import {Command, NamedCommand} from "../../core"; + +export default new NamedCommand({ + description: "Initiates a celebratory stream from the bot.", + async run({message, channel, guild, author, member, client, args}) { + channel.send("This calls for a celebration!"); + client.user!.setActivity({ + type: "STREAMING", + url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + name: "Celebration!" + }); + } +}); diff --git a/src/commands/fun/thonk.ts b/src/commands/fun/thonk.ts index 762549e..35e4d4a 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -36,6 +36,13 @@ export default new NamedCommand({ usage: "thonk ([text])", async run({message, channel, guild, author, member, client, args}) { if (args.length > 0) phrase = args.join(" "); - channel.send(transform(phrase)); + const msg = await channel.send(transform(phrase)); + msg.createReactionCollector( + (reaction, user) => { + if (user.id === author.id && reaction.emoji.name === "❌") msg.delete(); + return false; + }, + {time: 60000} + ); } }); diff --git a/src/commands/fun/urban.ts b/src/commands/fun/urban.ts index 2901433..212a2a5 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -1,27 +1,31 @@ import {Command, NamedCommand} from "../../core"; import {MessageEmbed} from "discord.js"; -// Anycasting Alert -const urban = require("relevant-urban"); +import urban from "relevant-urban"; export default new NamedCommand({ description: "Gives you a definition of the inputted word.", - async run({message, channel, guild, author, member, client, args}) { - if (!args[0]) { - channel.send("Please input a word."); + run: "Please input a word.", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters" + urban(encodeURIComponent(args.join(" "))) + .then((res) => { + const embed = new MessageEmbed() + .setColor(0x1d2439) + .setTitle(res.word) + .setURL(res.urbanURL) + .setDescription(`**Definition:**\n*${res.definition}*\n\n**Example:**\n*${res.example}*`) + // [Bug Fix] When an embed field is empty (if the author field is missing, like the top entry for "british"): "RangeError [EMBED_FIELD_VALUE]: MessageEmbed field values may not be empty." + .addField("Author", res.author || "N/A", true) + .addField("Rating", `**\`Upvotes: ${res.thumbsUp} | Downvotes: ${res.thumbsDown}\`**`); + if (res.tags && res.tags.length > 0 && res.tags.join(" ").length < 1024) + embed.addField("Tags", res.tags.join(", "), true); + + channel.send(embed); + }) + .catch(() => { + channel.send("Sorry, that word was not found."); + }); } - const res = await urban(args.join(" ")).catch((e: Error) => { - return channel.send("Sorry, that word was not found."); - }); - const embed = new MessageEmbed() - .setColor(0x1d2439) - .setTitle(res.word) - .setURL(res.urbanURL) - .setDescription(`**Definition:**\n*${res.definition}*\n\n**Example:**\n*${res.example}*`) - .addField("Author", res.author, true) - .addField("Rating", `**\`Upvotes: ${res.thumbsUp} | Downvotes: ${res.thumbsDown}\`**`); - if (res.tags.length > 0 && res.tags.join(" ").length < 1024) { - embed.addField("Tags", res.tags.join(", "), true); - } - channel.send(embed); - } + }) }); diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts new file mode 100644 index 0000000..ad6b945 --- /dev/null +++ b/src/commands/fun/vaporwave.ts @@ -0,0 +1,34 @@ +import {Command, NamedCommand} from "../../core"; + +const vaporwave = (() => { + const map = new Map(); + const vaporwave = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!"#$%&'()*+,-./0123456789:;<=>?@[\]^_`{|}~ "; + const normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`{|}~ "; + if (vaporwave.length !== normal.length) console.error("Vaporwave text failed to load properly!"); + for (let i = 0; i < vaporwave.length; i++) map.set(normal[i], vaporwave[i]); + return map; +})(); + +function getVaporwaveText(text: string): string { + let output = ""; + + for (const c of text) { + const transformed = vaporwave.get(c); + if (transformed) output += transformed; + } + + return output; +} + +export default new NamedCommand({ + description: "Transforms your text into vaporwave.", + run: "You need to enter some text!", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + const text = getVaporwaveText(args.join(" ")); + if (text !== "") channel.send(text); + else channel.send("Make sure to enter at least one valid character."); + } + }) +}); diff --git a/src/commands/fun/weather.ts b/src/commands/fun/weather.ts index 590755f..07c44f5 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -1,36 +1,38 @@ import {Command, NamedCommand} from "../../core"; import {MessageEmbed} from "discord.js"; -// Anycasting Alert -const weather = require("weather-js"); +import {find} from "weather-js"; export default new NamedCommand({ description: "Shows weather info of specified location.", - async run({message, channel, guild, author, member, client, args}) { - if (args.length == 0) return channel.send("You need to provide a city."); - return weather.find( - { - search: args.join(" "), - degreeType: "C" - }, - function (err: any, result: any) { - if (err) channel.send(err); - var current = result[0].current; - var location = result[0].location; - const embed = new MessageEmbed() - .setDescription(`**${current.skytext}**`) - .setAuthor(`Weather for ${current.observationpoint}`) - .setThumbnail(current.imageUrl) - .setColor(0x00ae86) - .addField("Timezone", `UTC${location.timezone}`, true) - .addField("Degree Type", "C", true) - .addField("Temperature", `${current.temperature} Degrees`, true) - .addField("Feels like", `${current.feelslike} Degrees`, true) - .addField("Winds", current.winddisplay, true) - .addField("Humidity", `${current.humidity}%`, true); - channel.send({ - embed - }); - } - ); - } + run: "You need to provide a city.", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + find( + { + search: args.join(" "), + degreeType: "C" + }, + function (error, result) { + if (error) return channel.send(error.toString()); + if (result.length === 0) return channel.send("No city found by that name."); + var current = result[0].current; + var location = result[0].location; + const embed = new MessageEmbed() + .setDescription(`**${current.skytext}**`) + .setAuthor(`Weather for ${current.observationpoint}`) + .setThumbnail(current.imageUrl) + .setColor(0x00ae86) + .addField("Timezone", `UTC${location.timezone}`, true) + .addField("Degree Type", "C", true) + .addField("Temperature", `${current.temperature} Degrees`, true) + .addField("Feels like", `${current.feelslike} Degrees`, true) + .addField("Winds", current.winddisplay, true) + .addField("Humidity", `${current.humidity}%`, true); + return channel.send({ + embed + }); + } + ); + } + }) }); diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index 5e0c225..f487e07 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -1,7 +1,7 @@ import {Command, NamedCommand, botHasPermission, getPermissionLevel, getPermissionName, CHANNEL_TYPE} from "../../core"; import {clean} from "../../lib"; import {Config, Storage} from "../../structures"; -import {Permissions, TextChannel} from "discord.js"; +import {Permissions, TextChannel, User} from "discord.js"; import {logs} from "../../modules/globals"; function getLogBuffer(type: string) { @@ -34,7 +34,7 @@ export default new NamedCommand({ subcommands: { prefix: new NamedCommand({ description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", - usage: "()", + usage: "() (<@bot>)", async run({message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).prefix = null; Storage.save(); @@ -47,7 +47,17 @@ export default new NamedCommand({ Storage.getGuild(guild!.id).prefix = args[0]; Storage.save(); channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); - } + }, + user: new Command({ + description: "Specifies the bot in case of conflicting prefixes.", + async run({message, channel, guild, author, member, client, args}) { + if ((args[1] as User).id === client.user!.id) { + Storage.getGuild(guild!.id).prefix = args[0]; + Storage.save(); + channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); + } + } + }) }) }), welcome: new NamedCommand({ diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 22bc97b..1b8aa07 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -2,8 +2,9 @@ import {Command, NamedCommand} from "../../core"; import {processEmoteQueryFormatted} from "./modules/emote-utils"; export default new NamedCommand({ - description: "Send the specified emote.", - run: "Please provide a command name.", + description: + "Send the specified emote list. Enter + to move an emote list to the next line, - to add a space, and _ to add a zero-width space.", + run: "Please provide a list of emotes.", any: new Command({ description: "The emote(s) to send.", usage: "", diff --git a/src/commands/utility/modules/emote-utils.ts b/src/commands/utility/modules/emote-utils.ts index f766cba..2c6df68 100644 --- a/src/commands/utility/modules/emote-utils.ts +++ b/src/commands/utility/modules/emote-utils.ts @@ -76,6 +76,7 @@ function processEmoteQuery(query: string[], isFormatted: boolean): string[] { if (isFormatted) { if (emote == "-") return " "; if (emote == "+") return "\n"; + if (emote == "_") return "\u200b"; } // Selector number used for disambiguating multiple emotes with same name. diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index a60d861..3662c57 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -182,5 +182,15 @@ export default new NamedCommand({ } return await channel.send(lines, {split: true}); + }, + subcommands: { + forcereset: new NamedCommand({ + description: "Forces the cooldown timer to reset.", + permission: PERMISSIONS.BOT_SUPPORT, + async run({message, channel, guild, author, member, client, args}) { + lastUsedTimestamps[guild!.id] = 0; + channel.send("Reset the cooldown on `scanemotes`."); + } + }) } }); diff --git a/src/commands/utility/translate.ts b/src/commands/utility/translate.ts index b697855..e0e9250 100644 --- a/src/commands/utility/translate.ts +++ b/src/commands/utility/translate.ts @@ -1,6 +1,5 @@ import {Command, NamedCommand} from "../../core"; -// Anycasting Alert -const translate = require("translate-google"); +import translate from "translate-google"; export default new NamedCommand({ description: "Translates your input.", @@ -11,7 +10,7 @@ export default new NamedCommand({ translate(input, { to: lang }) - .then((res: any) => { + .then((res) => { channel.send({ embed: { title: "Translation", @@ -28,10 +27,10 @@ export default new NamedCommand({ } }); }) - .catch((err: any) => { - console.error(err); + .catch((error) => { + console.error(error); channel.send( - `${err}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` + `${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` ); }); } diff --git a/src/defs/translate.d.ts b/src/defs/translate.d.ts new file mode 100644 index 0000000..15e022b --- /dev/null +++ b/src/defs/translate.d.ts @@ -0,0 +1,9 @@ +interface TranslateOptions { + from?: string; + to?: string; +} + +declare module "translate-google" { + function translate(input: string, options: TranslateOptions): Promise; + export = translate; +} diff --git a/src/defs/urban.d.ts b/src/defs/urban.d.ts new file mode 100644 index 0000000..cc97806 --- /dev/null +++ b/src/defs/urban.d.ts @@ -0,0 +1,17 @@ +interface Definition { + id: number; + word: string; + thumbsUp: number; + thumbsDown: number; + author: string; + urbanURL: string; + example: string; + definition: string; + tags: string[] | null; + sounds: string[] | null; +} + +declare module "relevant-urban" { + function urban(query: string): Promise; + export = urban; +} diff --git a/src/defs/weather.d.ts b/src/defs/weather.d.ts new file mode 100644 index 0000000..972de8c --- /dev/null +++ b/src/defs/weather.d.ts @@ -0,0 +1,92 @@ +interface WeatherJSOptions { + search: string; + lang?: string; + degreeType?: string; + timeout?: number; +} + +interface WeatherJSResult { + location: { + name: string; + lat: string; + long: string; + timezone: string; + alert: string; + degreetype: string; + imagerelativeurl: string; + }; + current: { + temperature: string; + skycode: string; + skytext: string; + date: string; + observationtime: string; + observationpoint: string; + feelslike: string; + humidity: string; + winddisplay: string; + day: string; + shortday: string; + windspeed: string; + imageUrl: string; + }; + forecast: [ + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + } + ]; +} + +declare module "weather-js" { + const find: ( + options: WeatherJSOptions, + callback: (error: Error | string | null, result: WeatherJSResult[]) => any + ) => void; +}