/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2022 Vendicated, Samu and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; import { addMessagePreEditListener, addMessagePreSendListener, MessageObject, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { migratePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs, GUILD_IDS } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import definePlugin from "@utils/types"; import { DraftType, UploadHandler, UploadManager, UserAffinitiesStore, UserStore } from "@webpack/common"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { calculateAffinityScore, calculateCanvasSize, FRAMES, fromMorse, generatePoissonDiskPosition, getCuteAnimeBoys, getCuteNeko, getFavoriteGif, isMorse, loadFriendImage, loadImage, makeFreaky, mock, resolveImage, settings, toMorse, uwuify, uwuifyArray } from "./utils"; migratePluginSettings("MoreCommands", "FriendCloud", "GifRoulette", "ImgToGif", "MoreKaomoji"); export default definePlugin({ name: "MoreCommands", description: "Adds various fun and useful commands", dependencies: ["CommandsAPI"], tags: ["Commands", "Fun", "Shortcuts"], authors: [ Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, Devs.thororen, Devs.Korbo, Devs.nyx, Devs.amy, Devs.Samwich, EquicordDevs.Fafa, Devs.JacobTm, EquicordDevs.voidbbg ], settings, commands: [ { name: "systeminfo", description: "Shows system information", options: [], execute: async (opts, ctx) => { try { const { userAgent, hardwareConcurrency, onLine, languages } = navigator; const { width, height, colorDepth } = window.screen; const { deviceMemory, connection }: { deviceMemory: any, connection: any; } = navigator as any; const platform = userAgent.includes("Windows") ? "Windows" : userAgent.includes("Mac") ? "MacOS" : userAgent.includes("Linux") ? "Linux" : "Unknown"; const isMobile = /Mobile|Android|iPhone/i.test(userAgent); const deviceType = isMobile ? "Mobile" : "Desktop"; const browserInfo = userAgent.match(/(?:chrome|firefox|safari|edge|opr)\/?\s*(\d+)/i)?.[0] || "Unknown"; const networkInfo = connection ? `${connection.effectiveType || "Unknown"}` : "Unknown"; const info = [ `> **Platform**: ${platform}`, `> **Device Type**: ${deviceType}`, `> **Browser**: ${browserInfo}`, `> **CPU Cores**: ${hardwareConcurrency || "N/A"}`, `> **Memory**: ${deviceMemory ? `${deviceMemory}GB` : "N/A"}`, `> **Screen**: ${width}x${height} (${colorDepth}bit)`, `> **Languages**: ${languages?.join(", ")}`, `> **Network**: ${networkInfo} (${onLine ? "Online" : "Offline"})` ].join("\n"); return { content: info }; } catch (err) { sendBotMessage(ctx.channel.id, { content: "Failed to fetch system information" }); } }, }, { name: "getuptime", description: "Returns the system uptime", execute: async () => { const uptime = performance.now() / 1000; const uptimeInfo = `> **System Uptime**: ${Math.floor(uptime / 60)} minutes`; return { content: uptimeInfo }; }, }, { name: "gettime", description: "Returns the current server time", execute: async () => { const currentTime = new Date().toLocaleString(); return { content: `> **Current Time**: ${currentTime}` }; }, }, { name: "choose", description: "Randomly chooses from provided options", options: [ { name: "choices", description: "Comma-separated list of choices", type: ApplicationCommandOptionType.STRING, required: true } ], execute: opts => { const choices = findOption(opts, "choices", "").split(",").map(c => c.trim()); const choice = choices[Math.floor(Math.random() * choices.length)]; return { content: `I choose: ${choice}` }; } }, { name: "rolldice", description: "Roll a die with the specified number of sides", options: [RequiredMessageOption], execute: opts => { const sides = parseInt(findOption(opts, "message", "6")); const roll = Math.floor(Math.random() * sides) + 1; return { content: `You rolled a ${roll}!` }; }, }, { name: "flipcoin", description: "Flips a coin and returns heads or tails", options: [], execute: (opts, ctx) => { const flip = Math.random() < 0.5 ? "Heads" : "Tails"; return { content: `The coin landed on: ${flip}` }; }, }, { name: "ask", description: "Ask a yes/no question and get an answer", options: [RequiredMessageOption], execute: opts => { const question = findOption(opts, "message", ""); const responses = ["Yes", "No", "Maybe", "Ask again later", "Definitely not", "It is certain"]; const response = responses[Math.floor(Math.random() * responses.length)]; return { content: `${question} - ${response}` }; }, }, { name: "randomanimal", description: "Get a random cat picture", options: [ { name: "animal", description: "pick your animal", type: ApplicationCommandOptionType.STRING, required: true, choices: [ { name: "cat", value: "cat", label: "cat" }, { name: "dog", value: "dog", label: "dog" }, ] } ], execute: (opts, ctx) => { return (async () => { const animal = findOption(opts, "animal") as string; let url; if (animal === "cat") { url = "https://api.thecatapi.com/v1/images/search"; } else if (animal === "dog") { url = "https://api.thedogapi.com/v1/images/search"; } try { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to fetch ${animal} image`); const data = await response.json(); return { content: data[0].url }; } catch (err) { sendBotMessage(ctx.channel.id, { content: "Sorry, couldn't fetch a cat picture right now 😿" }); } })(); }, }, { name: "randomnumber", description: "Generates a random number between two values", options: [ { name: "min", description: "Minimum value", type: ApplicationCommandOptionType.INTEGER, required: true }, { name: "max", description: "Maximum value", type: ApplicationCommandOptionType.INTEGER, required: true } ], execute: opts => { const min = parseInt(findOption(opts, "min", "0")); const max = parseInt(findOption(opts, "max", "100")); const number = Math.floor(Math.random() * (max - min + 1)) + min; return { content: `Random number between ${min} and ${max}: ${number}` }; } }, { name: "transform", description: "Transform your text with the specified option", options: [ { name: "text", description: "TEXT TO UPPERCASE", type: ApplicationCommandOptionType.STRING, required: true }, { name: "transformation", description: "transformation to apply to your text", type: ApplicationCommandOptionType.STRING, required: true, choices: [ { name: "toLowerCase", value: "toLowerCase", label: "toLowerCase" }, { name: "toUpperCase", value: "toUpperCase", label: "toUpperCase" }, { name: "toLocaleLowerCase", value: "toLocaleLowerCase", label: "toLocaleLowerCase" }, { name: "toLocaleUpperCase", value: "toLocaleUpperCase", label: "toLocaleUpperCase" }, { name: "stay the same", value: "same", label: "stay the same" } ] }, { name: "repeat", description: "how many times to repeat", type: ApplicationCommandOptionType.INTEGER, required: false }, { name: "reverse", description: "reverse your text", type: ApplicationCommandOptionType.BOOLEAN, required: false }, { name: "normalize", description: "which normailze option to use", type: ApplicationCommandOptionType.STRING, required: false, choices: [ { name: "NFC", value: "NFC", label: "NFC" }, { name: "NFD", value: "NFD", label: "NFD" }, { name: "NFKC", value: "NFKC", label: "NFKC" }, { name: "NFKD", value: "NFKD", label: "NFKD" } ] }, ], execute: opts => { let text = findOption(opts, "text") as string; const transform = findOption(opts, "transformation") as string; const repeat = findOption(opts, "repeat") as number | undefined ?? 1; const normalize = findOption(opts, "normalize") as string | undefined; const reverse = findOption(opts, "reverse") as boolean | undefined; if (transform !== "same") { text = (text as any)[transform]?.call(text) ?? text; } if (normalize) text = text.normalize(normalize); if (reverse) text = text.split("").reverse().join(""); return { content: text.repeat(repeat) }; }, }, { name: "wordcount", description: "Counts the number of words in a message", options: [RequiredMessageOption], inputType: ApplicationCommandInputType.BOT, execute: (opts, ctx) => { const message = findOption(opts, "message", ""); const wordCount = message.trim().split(/\s+/).length; sendBotMessage(ctx.channel.id, { content: `The message contains ${wordCount} words.` }); }, }, { name: "countdown", description: "Starts a countdown from a specified number", options: [ { name: "number", description: "Number to countdown from (max 10)", type: ApplicationCommandOptionType.INTEGER, required: true } ], inputType: ApplicationCommandInputType.BOT, execute: async (opts, ctx) => { const number = Math.min(parseInt(findOption(opts, "number", "5")), 10); if (isNaN(number) || number < 1) { sendBotMessage(ctx.channel.id, { content: "Please provide a valid number between 1 and 10!" }); return; } sendBotMessage(ctx.channel.id, { content: `Starting countdown from ${number}...` }); for (let i = number; i >= 0; i--) { await new Promise(resolve => setTimeout(resolve, 1000)); sendBotMessage(ctx.channel.id, { content: i === 0 ? "🎉 Go! 🎉" : `${i}...` }); } }, }, { name: "nekos", description: "Send Neko", execute: async () => ({ content: await getCuteNeko() }) }, { name: "anime-boys", description: "Send cute anime boys", options: [ { name: "cat", description: "If set, this will send exclusively cute anime cat boys", type: ApplicationCommandOptionType.BOOLEAN, required: false, }, ], execute: async opts => { let sub = "cuteanimeboys"; const cat = findOption(opts, "cat") as boolean | undefined; if (cat) sub = "animecatboys"; return { content: await getCuteAnimeBoys(sub) }; }, }, { name: "ping", description: "Pings the bot to check if it's responding", options: [], inputType: ApplicationCommandInputType.BOT, execute: (opts, ctx) => { sendBotMessage(ctx.channel.id, { content: "Pong!" }); }, }, { name: "echo", description: "Sends a message as Clyde (locally)", options: [OptionalMessageOption], inputType: ApplicationCommandInputType.BOT, execute: (opts, ctx) => { const content = findOption(opts, "message", ""); sendBotMessage(ctx.channel.id, { content }); }, }, { name: "lenny", description: "Sends a lenny face", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" }), }, { name: "mock", description: "mOcK PeOpLe", options: [RequiredMessageOption], execute: opts => ({ content: mock(findOption(opts, "message", "")) }), }, { inputType: ApplicationCommandInputType.BUILT_IN_TEXT, name: "slap", description: "Slap someone/something.", options: [{ name: "victim", description: "Thing to slap", required: true, type: ApplicationCommandOptionType.STRING, }], execute: opts => { const victim = findOption(opts, "victim") as string; return { content: `<@${UserStore.getCurrentUser().id}> slaps ${victim} around a bit with a large trout` }; } }, { name: "freaky", description: "it's freaky.", inputType: ApplicationCommandInputType.BUILT_IN, options: [{ name: "message", description: "yoooo freaky", type: ApplicationCommandOptionType.STRING, required: true }], execute: (opts, ctx) => { sendMessage(ctx.channel.id, { content: makeFreaky(findOption(opts, "message", "")) }); } }, { inputType: ApplicationCommandInputType.BUILT_IN_TEXT, name: "morse", description: "Translate to or from Morse code", options: [ { name: "text", description: "Text to convert", type: ApplicationCommandOptionType.STRING, required: true } ], execute: opts => { const input = opts.find(o => o.name === "text")?.value as string; const output = isMorse(input) ? fromMorse(input) : toMorse(input); return { content: `${output}` }; }, }, { name: "uwuify", description: "uwuifies your messages", options: [RequiredMessageOption], execute: opts => ({ content: uwuify(findOption(opts, "message", "")), }), }, { name: "gifroulette", description: "Tempt fate and send a gif", execute: (opts, other) => { if (GUILD_IDS.includes(other?.guild?.id ?? "")) return sendBotMessage(other.channel.id, { content: "This command is restricted in this server." }); return { content: getFavoriteGif(opts, other) }; } }, { inputType: ApplicationCommandInputType.BUILT_IN, name: "friendcloud", description: "Display user you most interact with in a cloud", options: [ { name: "count", description: "Number of users to display", type: ApplicationCommandOptionType.NUMBER, required: false } ], execute: async (opts, cmdCtx) => { const count = findOption(opts, "count", 25); if (!count) return sendBotMessage(cmdCtx.channel.id, { content: "The count must be 1 or higher!" }); try { const affinities = UserAffinitiesStore.getUserAffinities(); if (!affinities?.length) { return sendBotMessage(cmdCtx.channel.id, { content: "No affinities found. Check your [privacy settings]()." }); } const users = affinities .map(e => ({ member: UserStore.getUser(e.otherUserId), affinity: calculateAffinityScore(e) })) .filter(x => x.member?.id) .sort((a, b) => b.affinity - a.affinity) .slice(0, count); if (!users.length) { return sendBotMessage(cmdCtx.channel.id, { content: "No valid users found in affinities. Check your [privacy settings]()." }); } const minAffinity = Math.min(...users.map(u => u.affinity)); const maxAffinity = Math.max(...users.map(u => u.affinity)); const minSize = 120; const maxSize = 240; const getSize = (affinity: number): number => { if (maxAffinity === minAffinity) return (minSize + maxSize) / 2; return minSize + ((affinity - minAffinity) / (maxAffinity - minAffinity)) * (maxSize - minSize); }; const avgSize = (minSize + maxSize) / 2; const { width: canvasWidth, height: canvasHeight } = calculateCanvasSize(users.length, avgSize); const canvas = document.createElement("canvas"); canvas.width = canvasWidth; canvas.height = canvasHeight; const ctx = canvas.getContext("2d")!; const positions: Array<{ x: number, y: number, size: number; }> = []; const userPositions = users.map(user => { const size = getSize(user.affinity); const pos = generatePoissonDiskPosition(positions, canvasWidth, canvasHeight, size); positions.push({ x: pos.x, y: pos.y, size }); return { ...user, x: pos.x, y: pos.y, size }; }); let loadedImages = 0; const totalImages = userPositions.length; const drawImage = async user => { try { const avatarUrl = user.member?.avatar ? `https://cdn.discordapp.com/avatars/${user.member.id}/${user.member?.avatar}.webp?size=256` : `https://cdn.discordapp.com/embed/avatars/${user.member.id as any as number % 5}.png`; const img = await loadFriendImage(avatarUrl); const centerX = user.x + user.size / 2; const centerY = user.y + user.size / 2; ctx.save(); ctx.beginPath(); ctx.arc(centerX, centerY, user.size / 2, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(img, user.x, user.y, user.size, user.size); ctx.restore(); ctx.strokeStyle = "#808080"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(centerX, centerY, user.size / 2 + 1, 0, Math.PI * 2); ctx.stroke(); } catch { // we ignore } finally { loadedImages++; if (loadedImages === totalImages) { canvas.toBlob(blob => { if (!blob) { sendBotMessage(cmdCtx.channel.id, { content: "Couldn't generate the image :c" }); return; } const file = new File([blob], "affinities-cloud.png", { type: "image/png" }); UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage); }, "image/png"); } } }; userPositions.forEach(drawImage); } catch (e: unknown) { if (e instanceof Error) sendBotMessage(cmdCtx.channel.id, { content: e.message }); } }, }, { inputType: ApplicationCommandInputType.BUILT_IN, name: "imgtogif", description: "Allows you to turn an image to a gif", options: [ { name: "image", description: "Image attachment to use", type: ApplicationCommandOptionType.ATTACHMENT }, { name: "width", description: "Width of the gif", type: ApplicationCommandOptionType.INTEGER }, { name: "height", description: "Height of the gif", type: ApplicationCommandOptionType.INTEGER } ], execute: async (opts, cmdCtx) => { try { const { image, width, height } = await resolveImage(opts, cmdCtx); if (!image) throw "No Image specified!"; const avatar = await loadImage(image); let gifWidth: number; let gifHeight: number; if (width && height) { gifWidth = width; gifHeight = height; } else if (width) { gifWidth = width; gifHeight = Math.round((avatar.height / avatar.width) * width); } else if (height) { gifHeight = height; gifWidth = Math.round((avatar.width / avatar.height) * height); } else { gifWidth = avatar.width; gifHeight = avatar.height; } const gif = GIFEncoder(); const canvas = document.createElement("canvas"); canvas.width = gifWidth; canvas.height = gifHeight; const ctx = canvas.getContext("2d")!; UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); for (let i = 0; i < FRAMES; i++) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height); const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); const palette = quantize(data, 256); const index = applyPalette(data, palette); gif.writeFrame(index, canvas.width, canvas.height, { transparent: true, palette, }); } gif.finish(); const originalName = image.name ? image.name.replace(/\.[^/.]+$/, "") : "converted"; const file = new File([new Uint8Array(gif.bytesView())], `${originalName}.gif`, { type: "image/gif" }); setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10); } catch (err) { UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); sendBotMessage(cmdCtx.channel.id, { content: String(err) }); } }, }, { name: "dissatisfaction", description: " >﹏<", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + " >﹏<", }), }, { name: "smug", description: "ಠ_ಠ", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ಠ_ಠ", }), }, { name: "happy", description: "ヽ(´▽`)/", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ヽ(´▽`)/", }), }, { name: "crying", description: "ಥ_ಥ", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ಥ_ಥ", }), }, { name: "angry", description: "ヽ(`Д´)ノ", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ヽ(`Д´)ノ", }), }, { name: "anger", description: "ヽ(o`皿′o)ノ", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ヽ(o`皿′o)ノ", }), }, { name: "joy", description: "<( ̄︶ ̄)>", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "<( ̄︶ ̄)>", }), }, { name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "૮ ˶ᵔ ᵕ ᵔ˶ ა", }), }, { name: "confused", description: "(•ิ_•ิ)?", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(•ิ_•ิ)?", }), }, { name: "sleeping", description: "(ᴗ_ᴗ)", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(ᴗ_ᴗ)", }), }, { name: "laughing", description: "o(≧▽≦)o", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "o(≧▽≦)o", }), }, /* even more kaomoji */ { name: "giving", description: "(ノ◕ヮ◕)ノ*:・゚✧", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(ノ◕ヮ◕)ノ*:・゚✧", }), }, { name: "peace", description: "✌(◕‿-)✌", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "✌(◕‿-)✌", }), }, { name: "ending1", description: "Ꮺ ָ࣪ ۰ ͙⊹", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "Ꮺ ָ࣪ ۰ ͙⊹", }), }, { name: "uwu", description: "(>⩊<)", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(>⩊<)", }), }, { name: "comfy", description: "(─‿‿─)♡", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(─‿‿─)♡", }), }, { name: "lovehappy", description: "(*≧ω≦*)", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(*≧ω≦*)", }), }, { name: "loveee", description: "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", }), }, { name: "give", description: "(ノ= ⩊ = )ノ", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "(ノ= ⩊ = )ノ", }), }, { name: "lovegive", description: "ღゝ◡╹)ノ♡", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "ღゝ◡╹)ノ♡", }), }, { name: "music", description: "( ̄▽ ̄)/♫•¨•.¸¸♪", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "( ̄▽ ̄)/♫•¨•.¸¸♪", }), }, { name: "stars", description: ".𖥔 ݁ ˖๋ ࣭ ⭑", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + ".𖥔 ݁ ˖๋ ࣭ ⭑", }), }, { name: "lovegiving", description: "⸜(。˃ ᵕ ˂ )⸝♡", options: [OptionalMessageOption], execute: opts => ({ content: findOption(opts, "message", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡", }), } ], patches: [ { find: ".isPureReactComponent=!0;", predicate: () => settings.store.uwuEverything, replacement: { match: /(\.defaultProps\).{0,80}return \i\(\i,\i,void 0,void 0,null,)(\i)\)/, replace: "$1$self.uwuifyProps($2))" } } ], uwuifyProps(props: any) { if (!props.children) return props; if (typeof props.children === "string") props.children = uwuify(props.children); else if (Array.isArray(props.children)) props.children = uwuifyArray(props.children); return props; }, onSend(msg: MessageObject) { // Only run when it's enabled if (settings.store.uwuEveryMessage) { msg.content = uwuify(msg.content); } }, start() { this.preSend = addMessagePreSendListener((_, msg) => this.onSend(msg)); this.preEdit = addMessagePreEditListener((_cid, _mid, msg) => this.onSend(msg) ); }, stop() { removeMessagePreSendListener(this.preSend); removeMessagePreEditListener(this.preEdit); }, });