From 666b1b2806e50be2beb92a5b1f040dce3e13fe07 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:00:51 -0500 Subject: [PATCH] merge stuff to more cmds --- src/equicordplugins/friendCloud/index.ts | 294 -------------- src/equicordplugins/gifRoulette/index.tsx | 52 --- src/equicordplugins/imgToGif/index.tsx | 157 -------- src/equicordplugins/moreCommands/index.ts | 431 ++++++++++++++++++++- src/equicordplugins/moreCommands/utils.ts | 179 +++++++++ src/equicordplugins/moreKaomoji/index.ts | 216 ----------- src/plugins/implicitRelationships/index.ts | 5 +- src/webpack/common/stores.ts | 2 + 8 files changed, 610 insertions(+), 726 deletions(-) delete mode 100644 src/equicordplugins/friendCloud/index.ts delete mode 100644 src/equicordplugins/gifRoulette/index.tsx delete mode 100644 src/equicordplugins/imgToGif/index.tsx delete mode 100644 src/equicordplugins/moreKaomoji/index.ts diff --git a/src/equicordplugins/friendCloud/index.ts b/src/equicordplugins/friendCloud/index.ts deleted file mode 100644 index 759b9f12f..000000000 --- a/src/equicordplugins/friendCloud/index.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2025 Vendicated 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, sendBotMessage } from "@api/Commands"; -import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin from "@utils/types"; -import { User } from "@vencord/discord-types"; -import { findStore } from "@webpack"; -import { DraftType, UploadHandler, UserStore } from "@webpack/common"; - -const logger = new Logger("FriendCloud"); - -interface AffinitiesV2 { - otherUserId: User["id"]; - userSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU"; - otherUserSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU"; - isFriend: boolean; - dmProbability: number; - dmRank: number; - vcProbability: number; - vcRank: number; - serverMessageProbability: number; - serverMessageRank: number; - communicationProbability: number; - communicationRank: number; -} - -interface UserPosition { - member: User; - affinity: number; - x: number; - y: number; - size: number; -} - -export default definePlugin({ - name: "FriendCloud", - description: "Adds a /friendcloud command to visualize the users you most interact with", - authors: [EquicordDevs.Fafa], - commands: [ - { - 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: AffinitiesV2[] = findStore("UserAffinitiesV2Store").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 as AffinitiesV2) - })) - .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: UserPosition) => { - 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 loadImage(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 }); - else logger.error(e); - } - }, - }, - ] -}); - -function calculateAffinityScore(affinity: AffinitiesV2): 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 -function loadImage(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) => reject(err || event); - img.crossOrigin = "anonymous"; - img.src = url; - }); -} - -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 - }; -} - -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) - }; -} diff --git a/src/equicordplugins/gifRoulette/index.tsx b/src/equicordplugins/gifRoulette/index.tsx deleted file mode 100644 index fdb527f04..000000000 --- a/src/equicordplugins/gifRoulette/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { UserSettingsActionCreators } from "@webpack/common"; - -function getMessage(opts, other) { - const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue(); - - const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs); - - const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)]; - - let ownerPing = ""; - - if (other.guild != null) { - if (other.guild.ownerId != null && settings.store.pingOwnerChance && Math.random() <= 0.1) { - ownerPing = `<@${other.guild.ownerId}>`; - } - } - - return `${chosenGifUrl} ${ownerPing}`; -} - -const settings = definePluginSettings({ - pingOwnerChance: { - type: OptionType.BOOLEAN, - description: "If there should be a 1 in 10 change to ping the owner of the guild (oh no)", - default: true - } -}); - -export default definePlugin({ - name: "GifRoulette", - description: "Adds a command to send a random gif from your favourites, and a one in ten chance to ping the owner of the server", - authors: [Devs.Samwich], - settings, - commands: [ - { - name: "gifroulette", - description: "Tempt fate and send a gif", - execute: (opts, other) => ({ - content: getMessage(opts, other) - }), - } - ] -}); diff --git a/src/equicordplugins/imgToGif/index.tsx b/src/equicordplugins/imgToGif/index.tsx deleted file mode 100644 index fc4d81d52..000000000 --- a/src/equicordplugins/imgToGif/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated 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, sendBotMessage } from "@api/Commands"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { CommandArgument, CommandContext } from "@vencord/discord-types"; -import { DraftType, UploadAttachmentStore, UploadHandler, UploadManager } from "@webpack/common"; -import { applyPalette, GIFEncoder, quantize } from "gifenc"; - -const FRAMES = 1; - -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) => reject(err || event); - img.crossOrigin = "Anonymous"; - img.src = url; - }); -} - -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 }; -} - -export default definePlugin({ - name: "ImgToGif", - description: "Adds a /imgtogif slash command to create a gif from any image", - authors: [EquicordDevs.zyqunix], - commands: [ - { - 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) }); - } - }, - }, - ] -}); diff --git a/src/equicordplugins/moreCommands/index.ts b/src/equicordplugins/moreCommands/index.ts index 8e367cd0d..b174a4576 100644 --- a/src/equicordplugins/moreCommands/index.ts +++ b/src/equicordplugins/moreCommands/index.ts @@ -18,17 +18,55 @@ 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 } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import definePlugin from "@utils/types"; -import { UserStore } from "@webpack/common"; +import { DraftType, UploadHandler, UploadManager, UserAffinitiesStore, UserStore } from "@webpack/common"; +import { applyPalette, GIFEncoder, quantize } from "gifenc"; -import { fromMorse, getCuteAnimeBoys, getCuteNeko, getCutePats, isMorse, makeFreaky, mock, settings, toMorse, uwuify, uwuifyArray } from "./utils"; +import { + calculateAffinityScore, + calculateCanvasSize, + FRAMES, + fromMorse, + generatePoissonDiskPosition, + getCuteAnimeBoys, + getCuteNeko, + getCutePats, + getMessage, + 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", - authors: [Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, Devs.thororen, Devs.Korbo, Devs.nyx, Devs.amy], + 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: [ { @@ -435,7 +473,394 @@ export default definePlugin({ content: uwuify(findOption(opts, "message", "")), }), }, + { + name: "gifroulette", + description: "Tempt fate and send a gif", + execute: (opts, other) => ({ + content: getMessage(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;", diff --git a/src/equicordplugins/moreCommands/utils.ts b/src/equicordplugins/moreCommands/utils.ts index a5a0f7815..0dd7ae3a1 100644 --- a/src/equicordplugins/moreCommands/utils.ts +++ b/src/equicordplugins/moreCommands/utils.ts @@ -6,6 +6,8 @@ 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: { @@ -207,3 +209,180 @@ export function uwuifyArray(arr) { return newArr; } + +export function getMessage(opts, other) { + 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) => 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) => 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 }; +} diff --git a/src/equicordplugins/moreKaomoji/index.ts b/src/equicordplugins/moreKaomoji/index.ts deleted file mode 100644 index 986b1f332..000000000 --- a/src/equicordplugins/moreKaomoji/index.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated 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 { findOption, OptionalMessageOption } from "@api/Commands"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "MoreKaomoji", - description: "Adds more Kaomoji to discord. ヽ(´▽`)/", - authors: [Devs.JacobTm, EquicordDevs.voidbbg], - commands: [ - { - 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", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡", - }), - } - ] -}); diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts index 9c681386c..e5a547dbc 100644 --- a/src/plugins/implicitRelationships/index.ts +++ b/src/plugins/implicitRelationships/index.ts @@ -20,12 +20,9 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common"; +import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserAffinitiesStore, UserStore } from "@webpack/common"; import { Settings } from "Vencord"; -const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store"); - export default definePlugin({ name: "ImplicitRelationships", description: "Shows your implicit relationships in the Friends tab.", diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 175ab75de..1217eedbd 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -84,6 +84,7 @@ export let EditMessageStore: t.EditMessageStore; export let QuestStore: t.QuestStore; export let PendingReplyStore: t.PendingReplyStore; export let ExperimentStore: GenericStore; +export let UserAffinitiesStore: GenericStore; /** * @see jsdoc of {@link t.useStateFromStores} @@ -140,6 +141,7 @@ waitForStore("EditMessageStore", m => EditMessageStore = m); waitForStore("PendingReplyStore", m => PendingReplyStore = m); waitForStore("ExperimentStore", m => ExperimentStore = m); waitForStore("QuestStore", m => QuestStore = m); +waitForStore("UserAffinitiesV2Store", m => UserAffinitiesStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.