/* * Vencord, a Discord client mod * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { definePluginSettings } from "@api/Settings"; import { OptionType } from "@utils/types"; import { CommandArgument, CommandContext } from "@vencord/discord-types"; import { DraftType, UploadAttachmentStore, UploadManager, UserSettingsActionCreators } from "@webpack/common"; export const settings = definePluginSettings({ addFreakyEnding: { type: OptionType.BOOLEAN, description: "Add ๐Ÿ‘… or โค๏ธ at the end", default: false }, uwuEveryMessage: { description: "Make every single message uwuified", type: OptionType.BOOLEAN, default: false, restartNeeded: false }, uwuEverything: { description: "Makes *all* text uwuified - really bad idea", type: OptionType.BOOLEAN, default: false, restartNeeded: true } }); export function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } export async function getCuteAnimeBoys(sub: string) { const res = await fetch(`https://www.reddit.com/r/${sub}/top.json?limit=100&t=all`); const { children } = (await res.json()).data; const r = rand(0, children.length - 1); return children[r].data.url ?? ""; } export async function getCuteNeko(): Promise { const res = await fetch("https://nekos.best/api/v2/neko"); const url = (await res.json()).results[0].url as string | null; return url ?? ""; } export function mock(input: string): string { let output = ""; for (let i = 0; i < input.length; i++) { output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase(); } return output; } const charMap: Record = { q: "๐“บ", w: "๐”€", e: "๐“ฎ", r: "๐“ป", t: "๐“ฝ", y: "๐”‚", u: "๐“พ", i: "๐“ฒ", o: "๐“ธ", p: "๐“น", a: "๐“ช", s: "๐“ผ", d: "๐“ญ", f: "๐“ฏ", g: "๐“ฐ", h: "๐“ฑ", j: "๐“ณ", k: "๐“ด", l: "๐“ต", z: "๐”ƒ", x: "๐”", c: "๐“ฌ", v: "๐“ฟ", b: "๐“ซ", n: "๐“ท", m: "๐“ถ", Q: "๐“ ", W: "๐“ฆ", E: "๐“”", R: "๐“ก", T: "๐“ฃ", Y: "๐“จ", U: "๐“ค", I: "๐“˜", O: "๐“ž", P: "๐“Ÿ", A: "๐“", S: "๐“ข", D: "๐““", F: "๐“•", G: "๐“–", H: "๐“—", J: "๐“™", K: "๐“š", L: "๐“›", Z: "๐“ฉ", X: "๐“ง", C: "๐“’", V: "๐“ฅ", B: "๐“‘", N: "๐“", M: "๐“œ", }; const mapCharacters = (text: string, map: Record) => text.split("").map(char => map[char] || char).join(""); export function makeFreaky(text: string) { text = mapCharacters(text.trim() || "freaky", charMap); if (settings.store.addFreakyEnding) text += Math.random() < 0.25 ? " ๐Ÿ‘…" : " โค๏ธ"; return text; } const morseMap = { A: ".-", B: "-...", C: "-.-.", D: "-..", E: ".", F: "..-.", G: "--.", H: "....", I: "..", J: ".---", K: "-.-", L: ".-..", M: "--", N: "-.", O: "---", P: ".--.", Q: "--.-", R: ".-.", S: "...", T: "-", U: "..-", V: "...-", W: ".--", X: "-..-", Y: "-.--", Z: "--..", 0: "-----", 1: ".----", 2: "..---", 3: "...--", 4: "....-", 5: ".....", 6: "-....", 7: "--...", 8: "---..", 9: "----.", " ": "/" }; export const toMorse = (text: string) => { return text.toUpperCase().split("").map(char => morseMap[char] ?? "").join(" "); }; export const fromMorse = (text: string) => { const reversedMap = Object.fromEntries(Object.entries(morseMap).map(([k, v]) => [v, k])); const raw = text.split(" ").map(code => reversedMap[code] ?? "").join("").toLowerCase(); return raw.charAt(0).toUpperCase() + raw.slice(1); }; export const isMorse = (text: string) => /^[.\-/ ]+$/.test(text); export const endings = [ "rawr x3", "OwO", "UwU", "o.O", "-.-", ">w<", "(โ‘…ห˜๊’ณห˜)", "(๊ˆแด—๊ˆ)", "(ห˜ฯ‰ห˜)", "(U แต• Uโ)", "ฯƒฯ‰ฯƒ", "รฒฯ‰รณ", "(///หฌ///โœฟ)", "(U ๏น U)", "( อกo ฯ‰ อกo )", "ส˜wส˜", ":3", ":3", // important enough to have twice ":3", // important enough to have thrice "XD", "nyaa~~", "mya", ">_<", "๐Ÿ˜ณ", "๐Ÿฅบ", "๐Ÿ˜ณ๐Ÿ˜ณ๐Ÿ˜ณ", "rawr", "^^", "^^;;", "(ห† ๏ปŒ ห†)โ™ก", "^โ€ข๏ปŒโ€ข^", "/(^โ€ขฯ‰โ€ข^)", "(โœฟoฯ‰o)" ]; export const replacements = [ ["small", "smol"], ["cute", "kawaii"], ["fluff", "floof"], ["love", "luv"], ["stupid", "baka"], ["what", "nani"], ["meow", "nya"], ["hello", "hewwo"], ]; export function selectRandomElement(arr) { // generate a random index based on the length of the array const randomIndex = Math.floor(Math.random() * arr.length); // return the element at the randomly generated index return arr[randomIndex]; } export const isOneCharacterString = (str: string): boolean => { return str.split("").every((char: string) => char === str[0]); }; export function replaceString(inputString) { let replaced = false; for (const replacement of replacements) { const regex = new RegExp(`\\b${replacement[0]}\\b`, "gi"); if (regex.test(inputString)) { inputString = inputString.replace(regex, replacement[1]); replaced = true; } } return replaced ? inputString : false; } export function uwuify(message: string): string { const rule = /\S+|\s+/g; const words: string[] | null = message.match(rule); let answer = ""; if (words === null) return ""; for (let i = 0; i < words.length; i++) { if (isOneCharacterString(words[i]) || words[i].startsWith("https://")) { answer += words[i]; continue; } if (!replaceString(words[i])) { answer += words[i] .replace(/n(?=[aeo])/g, "ny") .replace(/l|r/g, "w"); } else answer += replaceString(words[i]); } answer += " " + selectRandomElement(endings); return answer; } export function uwuifyArray(arr) { const newArr = [...arr]; newArr.forEach((item, index) => { if (Array.isArray(item)) { newArr[index] = uwuifyArray(item); } else if (typeof item === "string") { newArr[index] = uwuify(item); } }); return newArr; } export function getFavoriteGif(opts: CommandArgument[], other: CommandContext) { const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue(); const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs); const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)]; return `${chosenGifUrl}`; } export function calculateAffinityScore(affinity): number { const weights = { friend: 0.15, dm: 0.30, vc: 0.25, serverMsg: 0.20, communication: 0.10 }; let score = 0; if (affinity.isFriend) score += weights.friend * 100; score += affinity.dmProbability * weights.dm * 100; score += affinity.vcProbability * weights.vc * 100; score += affinity.serverMessageProbability * weights.serverMsg * 100; score += affinity.communicationProbability * weights.communication * 100; return Math.round(Math.min(100, Math.max(0, score)) * 100) / 100; } // stolen from petpet thanks vee export function loadFriendImage(source: File | string): Promise { const isFile = source instanceof File; const url = isFile ? URL.createObjectURL(source) : source; return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { if (isFile) URL.revokeObjectURL(url); resolve(img); }; img.onerror = (event, _source, _lineno, _colno, err) => { if (isFile) URL.revokeObjectURL(url); reject(err || event); }; img.crossOrigin = "anonymous"; img.src = url; }); } export function generatePoissonDiskPosition( existingPositions: Array<{ x: number, y: number, size: number; }>, canvasWidth: number, canvasHeight: number, size: number ): { x: number, y: number; } { const edgePadding = 10; const minDist = size * 1.5; const textSpace = 60; const k = 30; function isValid(x: number, y: number) { if ( x < edgePadding || x + size > canvasWidth - edgePadding || y < edgePadding || y + size > canvasHeight - textSpace - edgePadding ) return false; return !existingPositions.some(pos => { const dx = pos.x - x; const dy = pos.y - y; const dist = Math.hypot(dx, dy); const minAllowed = (pos.size + size) / 2 + (minDist - size); return dist < minAllowed; }); } if (existingPositions.length === 0) { return { x: canvasWidth / 2 - size / 2, y: canvasHeight / 2 - size / 2 }; } for (let tries = 0; tries < 100; tries++) { const base = existingPositions[Math.floor(Math.random() * existingPositions.length)]; for (let i = 0; i < k; i++) { const angle = Math.random() * Math.PI * 2; const radius = minDist + Math.random() * minDist; const x = base.x + Math.cos(angle) * radius; const y = base.y + Math.sin(angle) * radius; if (isValid(x, y)) { return { x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) }; } } } for (let tries = 0; tries < 100; tries++) { const x = Math.random() * (canvasWidth - size - edgePadding * 2) + edgePadding; const y = Math.random() * (canvasHeight - size - textSpace - edgePadding * 2) + edgePadding; if (isValid(x, y)) { return { x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) }; } } return { x: edgePadding, y: edgePadding }; } export function calculateCanvasSize(userCount: number, avatarSize: number): { width: number, height: number; } { const padding = 50; const textSpace = 60; const itemWidth = avatarSize + padding; const itemHeight = avatarSize + textSpace + padding; const aspectRatio = 16 / 9; const cols = Math.ceil(Math.sqrt(userCount * aspectRatio)); const rows = Math.ceil(userCount / cols); return { width: Math.max(1000, cols * itemWidth + padding), height: Math.max(700, rows * itemHeight + padding) }; } export const FRAMES = 1; export function loadImage(source: File | string) { const isFile = source instanceof File; const url = isFile ? URL.createObjectURL(source) : source; return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { if (isFile) URL.revokeObjectURL(url); resolve(img); }; img.onerror = (event, _source, _lineno, _colno, err) => { if (isFile) URL.revokeObjectURL(url); reject(err || event); }; img.crossOrigin = "Anonymous"; img.src = url; }); } export async function resolveImage(options: CommandArgument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> { let image: File | null = null; let width: number | null = null; let height: number | null = null; for (const opt of options) { switch (opt.name) { case "image": const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand); if (upload) { if (!upload.isImage) { UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); throw "Upload is not an image"; } image = upload.item.file; } break; case "width": width = Number(opt.value); break; case "height": height = Number(opt.value); break; } } UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); return { image, width, height }; }