diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index 90e8c73..8411c9a 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -37,25 +37,33 @@ export interface MessageObject { validNonShortcutEmojis: Emoji[]; } -export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void; +export interface MessageExtra { + stickerIds?: string[]; +} + +export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean }; export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void; const sendListeners = new Set(); const editListeners = new Set(); -export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) { +export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) { for (const listener of sendListeners) { try { - listener(channelId, messageObj, extra); - } catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); } + const result = listener(channelId, messageObj, extra); + if (result && result.cancel === true) { + return true; + } + } catch (e) { MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e); } } + return false; } -export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) { +export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { for (const listener of editListeners) { try { - listener(channeld, messageId, messageObj); - } catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); } + listener(channelId, messageId, messageObj); + } catch (e) { MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); } } } @@ -90,7 +98,7 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve for (const listener of listeners) { try { listener(message, channel, event); - } catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); } + } catch (e) { MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e); } } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 2872f62..069cbcb 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -21,7 +21,7 @@ declare global { /** * This exists only at build time, so references to it in patches should insert it * via String interpolation OR use different replacement code based on this - * but NEVER refrence it inside the patched code + * but NEVER reference it inside the patched code * * @example * // BAD diff --git a/src/plugins/apiMessageEvents.ts b/src/plugins/apiMessageEvents.ts index 541c12d..bf7c7f5 100644 --- a/src/plugins/apiMessageEvents.ts +++ b/src/plugins/apiMessageEvents.ts @@ -28,7 +28,7 @@ export default definePlugin({ find: "sendMessage:function", replacement: [{ match: /(?<=_sendMessage:function\([^)]+\)){/, - replace: "{Vencord.Api.MessageEvents._handlePreSend(...arguments);" + replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};" }, { match: /(?<=\beditMessage:function\([^)]+\)){/, replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);" diff --git a/src/plugins/nitroBypass.ts b/src/plugins/nitroBypass.ts index 7fed9cd..f4bdc84 100644 --- a/src/plugins/nitroBypass.ts +++ b/src/plugins/nitroBypass.ts @@ -16,18 +16,50 @@ * along with this program. If not, see . */ -import { addPreEditListener, addPreSendListener, removePreEditListener,removePreSendListener } from "../api/MessageEvents"; +import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "../api/MessageEvents"; +import { lazyWebpack } from "../utils"; import { Devs } from "../utils/constants"; +import { ApngDisposeOp, getGifEncoder, importApngJs } from "../utils/dependencies"; import definePlugin, { OptionType } from "../utils/types"; import { Settings } from "../Vencord"; -import { findByProps } from "../webpack"; -import { UserStore } from "../webpack/common"; +import { filters } from "../webpack"; +import { ChannelStore, UserStore } from "../webpack/common"; + +const DRAFT_TYPE = 0; +const promptToUpload = lazyWebpack(filters.byCode("UPLOAD_FILE_LIMIT_ERROR")); + +interface Sticker { + available: boolean; + description: string; + format_type: number; + guild_id: string; + id: string; + name: string; + tags: string; + type: number; + _notAvailable?: boolean; +} + +interface StickerPack { + id: string; + name: string; + sku_id: string; + description: string; + cover_sticker_id: string; + banner_asset_id: string; + stickers: Sticker[]; +} export default definePlugin({ name: "NitroBypass", - authors: [Devs.Arjix], - description: "Allows you to stream in nitro quality and send fake emojis.", + authors: [ + Devs.Arjix, + Devs.D3SOX, + Devs.Ven + ], + description: "Allows you to stream in nitro quality and send fake emojis/stickers.", dependencies: ["MessageEventsAPI"], + patches: [ { find: "canUseAnimatedEmojis:function", @@ -38,10 +70,25 @@ export default definePlugin({ ].map(func => { return { match: new RegExp(`${func}:function\\(.+?}`), - replace: `${func}:function (e) { return true; }` + replace: `${func}:function(e){return true;}` }; }) }, + { + find: "canUseAnimatedEmojis:function", + predicate: () => Settings.plugins.NitroBypass.enableStickerBypass === true, + replacement: { + match: /canUseStickersEverywhere:function\(.+?}/, + replace: "canUseStickersEverywhere:function(e){return true;}" + }, + }, + { + find: "\"SENDABLE\"", + replacement: { + match: /(\w+)\.available\?/, + replace: "true?" + } + }, { find: "canUseAnimatedEmojis:function", predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true, @@ -52,7 +99,7 @@ export default definePlugin({ ].map(func => { return { match: new RegExp(`${func}:function\\(.+?}`), - replace: `${func}:function (e) { return true; }` + replace: `${func}:function(e){return true;}` }; }) }, @@ -63,8 +110,9 @@ export default definePlugin({ match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g, replace: "" } - } + }, ], + options: { enableEmojiBypass: { description: "Allow sending fake emojis", @@ -72,6 +120,18 @@ export default definePlugin({ default: true, restartNeeded: true, }, + enableStickerBypass: { + description: "Allow sending fake stickers", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true, + }, + stickerSize: { + description: "Size of the stickers when sending", + type: OptionType.SLIDER, + default: 160, + markers: [32, 64, 128, 160, 256, 512], + }, enableStreamQualityBypass: { description: "Allow streaming in nitro quality", type: OptionType.BOOLEAN, @@ -88,50 +148,162 @@ export default definePlugin({ return Boolean(UserStore.getCurrentUser().premiumType); }, + getStickerLink(stickerId: string) { + return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.NitroBypass.stickerSize}`; + }, + + async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) { + const [{ parseURL }, { + GIFEncoder, + quantize, + applyPalette + }] = await Promise.all([importApngJs(), getGifEncoder()]); + + const { frames, width, height } = await parseURL(stickerLink); + + const gif = new GIFEncoder(); + const resolution = Settings.plugins.NitroBypass.stickerSize; + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d", { + willReadFrequently: true + })!; + + const scale = resolution / width; + ctx.scale(scale, scale); + + let lastImg: HTMLImageElement | null = null; + for (const { left, top, width, height, disposeOp, img, delay } of frames) { + if (disposeOp === ApngDisposeOp.BACKGROUND) { + ctx.clearRect(left, top, width, height); + } + ctx.drawImage(img, left, top, width, height); + + const { data } = ctx.getImageData(0, 0, resolution, resolution); + + const palette = quantize(data, 256); + const index = applyPalette(data, palette); + + gif.writeFrame(index, resolution, resolution, { + transparent: true, + palette, + delay, + }); + + if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) { + ctx.drawImage(lastImg, left, top, width, height); + } + lastImg = img; + } + + gif.finish(); + const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" }); + promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); + }, + start() { - if (!Settings.plugins.NitroBypass.enableEmojiBypass) { + const settings = Settings.plugins.NitroBypass; + if (!settings.enableEmojiBypass && !settings.enableStickerBypass) { return; } - if (this.canUseEmotes) { - console.info("[NitroBypass] Skipping start because you have nitro"); - return; - } + const EmojiStore = lazyWebpack(filters.byProps("getCustomEmojiById")); + const StickerStore = lazyWebpack(filters.byProps("getAllGuildStickers")) as { + getPremiumPacks(): StickerPack[]; + getAllGuildStickers(): Map; + getStickerById(id: string): Sticker | undefined; + }; - const { getCustomEmojiById } = findByProps("getCustomEmojiById"); - - function getWordBoundary(origStr, offset) { + function getWordBoundary(origStr: string, offset: number) { return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; } - this.preSend = addPreSendListener((_, messageObj) => { - const { guildId } = this; - for (const emoji of messageObj.validNonShortcutEmojis) { - if (!emoji.require_colons) continue; - if (emoji.guildId === guildId && !emoji.animated) continue; - - const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; - const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48"); - messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; - }); - } - }); - - this.preEdit = addPreEditListener((_, __, messageObj) => { + this.preSend = addPreSendListener((channelId, messageObj, extra) => { const { guildId } = this; - for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?/ig)) { - const emoji = getCustomEmojiById(emojiId); - if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue; - if (!emoji.require_colons) continue; + if (settings.enableStickerBypass) { + const stickerId = extra?.stickerIds?.[0]; - const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48"); - messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { - return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; - }); + if (stickerId) { + let stickerLink = this.getStickerLink(stickerId); + let sticker: Sticker | undefined; + + const discordStickerPack = StickerStore.getPremiumPacks().find(pack => { + const discordSticker = pack.stickers.find(sticker => sticker.id === stickerId); + if (discordSticker) { + sticker = discordSticker; + } + return discordSticker; + }); + + if (discordStickerPack) { + // discord stickers provided by the Distok project + stickerLink = `https://distok.top/stickers/${discordStickerPack.id}/${stickerId}.gif`; + } else { + // guild stickers + sticker = StickerStore.getStickerById(stickerId); + } + + if (sticker) { + // when the user has Nitro and the sticker is available, send the sticker normally + if (this.canUseEmotes && sticker.available) { + return { cancel: false }; + } + + // only modify if sticker is not from current guild + if (sticker.guild_id !== guildId) { + // if it's an animated guild sticker, download it, convert to gif and send it + const isAnimated = sticker.format_type === 2; + if (!discordStickerPack && isAnimated) { + this.sendAnimatedSticker(stickerLink, stickerId, channelId); + return { cancel: true }; + } + + if (messageObj.content) + messageObj.content += " "; + messageObj.content += stickerLink; + + delete extra.stickerIds; + } + } + } } + + if (!this.canUseEmotes && settings.enableEmojiBypass) { + for (const emoji of messageObj.validNonShortcutEmojis) { + if (!emoji.require_colons) continue; + if (emoji.guildId === guildId && !emoji.animated) continue; + + const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; + const url = emoji.url.replace(/\?size=\d+/, "?size=48"); + messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { + return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; + }); + } + } + + return { cancel: false }; }); + + if (!this.canUseEmotes && settings.enableEmojiBypass) { + this.preEdit = addPreEditListener((_, __, messageObj) => { + const { guildId } = this; + + for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?/ig)) { + const emoji = EmojiStore.getCustomEmojiById(emojiId); + if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue; + if (!emoji.require_colons) continue; + + const url = emoji.url.replace(/\?size=\d+/, "?size=48"); + messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { + return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; + }); + } + }); + } }, stop() { diff --git a/src/plugins/petpet.ts b/src/plugins/petpet.ts index 093acba..961d47a 100644 --- a/src/plugins/petpet.ts +++ b/src/plugins/petpet.ts @@ -17,8 +17,9 @@ */ import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption } from "../api/Commands"; +import { lazyWebpack, makeLazy } from "../utils"; import { Devs } from "../utils/constants"; -import { lazyWebpack, makeLazy } from "../utils/misc"; +import { getGifEncoder } from "../utils/dependencies"; import definePlugin from "../utils/types"; import { filters } from "../webpack"; @@ -27,11 +28,6 @@ const DEFAULT_DELAY = 20; const DEFAULT_RESOLUTION = 128; const FRAMES = 10; -// https://github.com/mattdesl/gifenc -// this lib is way better than gif.js and all other libs, they're all so terrible but this one is nice -// @ts-ignore ts mad -const getGifEncoder = makeLazy(() => import("https://unpkg.com/gifenc@1.0.3/dist/gifenc.esm.js")); - const getFrames = makeLazy(() => Promise.all( Array.from( { length: FRAMES }, diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts new file mode 100644 index 0000000..a7766de --- /dev/null +++ b/src/utils/dependencies.ts @@ -0,0 +1,76 @@ +/* + * 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 { makeLazy } from "./misc"; + +/* + Add dynamically loaded dependencies for plugins here. + */ + +// https://github.com/mattdesl/gifenc +// this lib is way better than gif.js and all other libs, they're all so terrible but this one is nice +// @ts-ignore ts mad +export const getGifEncoder = makeLazy(() => import("https://unpkg.com/gifenc@1.0.3/dist/gifenc.esm.js")); + +// needed to parse APNGs in the nitroBypass plugin +export const importApngJs = makeLazy(async () => { + const exports = {}; + const winProxy = new Proxy(window, { set: (_, k, v) => exports[k] = v }); + Function("self", await fetch("https://cdnjs.cloudflare.com/ajax/libs/apng-canvas/2.1.1/apng-canvas.min.js").then(r => r.text()))(winProxy); + // @ts-ignore + return exports.APNG as { parseURL(url: string): Promise; }; +}); + +// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk +export enum ApngDisposeOp { + /** + * no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. + */ + NONE, + /** + * the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + */ + BACKGROUND, + /** + * the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. + */ + PREVIOUS +} + +// TODO: Might need to somehow implement this +export enum ApngBlendOp { + SOURCE, + OVER +} +export interface ApngFrame { + left: number; + top: number; + width: number; + height: number; + img: HTMLImageElement; + delay: number; + blendOp: ApngBlendOp; + disposeOp: ApngDisposeOp; +} + +export interface ApngFrameData { + width: number; + height: number; + frames: ApngFrame[]; + playTime: number; +}