feat(nitroBypass): add sticker bypass (#184)

Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
Nico 2022-11-07 22:23:34 +01:00 committed by GitHub
parent d69dfd6205
commit 7d5ade21fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 307 additions and 55 deletions

View file

@ -37,25 +37,33 @@ export interface MessageObject {
validNonShortcutEmojis: Emoji[]; 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; export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
const sendListeners = new Set<SendListener>(); const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>(); const editListeners = new Set<EditListener>();
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) { export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
for (const listener of sendListeners) { for (const listener of sendListeners) {
try { try {
listener(channelId, messageObj, extra); const result = listener(channelId, messageObj, extra);
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); } 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) { for (const listener of editListeners) {
try { try {
listener(channeld, messageId, messageObj); listener(channelId, messageId, messageObj);
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); } } 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) { for (const listener of listeners) {
try { try {
listener(message, channel, event); 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); }
} }
} }

2
src/globals.d.ts vendored
View file

@ -21,7 +21,7 @@ declare global {
/** /**
* This exists only at build time, so references to it in patches should insert it * 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 * 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 * @example
* // BAD * // BAD

View file

@ -28,7 +28,7 @@ export default definePlugin({
find: "sendMessage:function", find: "sendMessage:function",
replacement: [{ replacement: [{
match: /(?<=_sendMessage:function\([^)]+\)){/, match: /(?<=_sendMessage:function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreSend(...arguments);" replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
}, { }, {
match: /(?<=\beditMessage:function\([^)]+\)){/, match: /(?<=\beditMessage:function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);" replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"

View file

@ -17,17 +17,49 @@
*/ */
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 { Devs } from "../utils/constants";
import { ApngDisposeOp, getGifEncoder, importApngJs } from "../utils/dependencies";
import definePlugin, { OptionType } from "../utils/types"; import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord"; import { Settings } from "../Vencord";
import { findByProps } from "../webpack"; import { filters } from "../webpack";
import { UserStore } from "../webpack/common"; 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({ export default definePlugin({
name: "NitroBypass", name: "NitroBypass",
authors: [Devs.Arjix], authors: [
description: "Allows you to stream in nitro quality and send fake emojis.", Devs.Arjix,
Devs.D3SOX,
Devs.Ven
],
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
patches: [ patches: [
{ {
find: "canUseAnimatedEmojis:function", find: "canUseAnimatedEmojis:function",
@ -42,6 +74,21 @@ export default definePlugin({
}; };
}) })
}, },
{
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", find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true, predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true,
@ -63,8 +110,9 @@ export default definePlugin({
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g, match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
replace: "" replace: ""
} }
} },
], ],
options: { options: {
enableEmojiBypass: { enableEmojiBypass: {
description: "Allow sending fake emojis", description: "Allow sending fake emojis",
@ -72,6 +120,18 @@ export default definePlugin({
default: true, default: true,
restartNeeded: 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: { enableStreamQualityBypass: {
description: "Allow streaming in nitro quality", description: "Allow streaming in nitro quality",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -88,50 +148,162 @@ export default definePlugin({
return Boolean(UserStore.getCurrentUser().premiumType); 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() { start() {
if (!Settings.plugins.NitroBypass.enableEmojiBypass) { const settings = Settings.plugins.NitroBypass;
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
return; return;
} }
if (this.canUseEmotes) { const EmojiStore = lazyWebpack(filters.byProps("getCustomEmojiById"));
console.info("[NitroBypass] Skipping start because you have nitro"); const StickerStore = lazyWebpack(filters.byProps("getAllGuildStickers")) as {
return; getPremiumPacks(): StickerPack[];
} getAllGuildStickers(): Map<string, Sticker[]>;
getStickerById(id: string): Sticker | undefined;
};
const { getCustomEmojiById } = findByProps("getCustomEmojiById"); function getWordBoundary(origStr: string, offset: number) {
function getWordBoundary(origStr, offset) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
} }
this.preSend = addPreSendListener((_, messageObj) => { this.preSend = addPreSendListener((channelId, messageObj, extra) => {
const { guildId } = this; const { guildId } = this;
if (settings.enableStickerBypass) {
const stickerId = extra?.stickerIds?.[0];
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) { for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue; if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue; if (emoji.guildId === guildId && !emoji.animated) continue;
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48"); const url = emoji.url.replace(/\?size=\d+/, "?size=48");
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
}); });
} }
}
return { cancel: false };
}); });
if (!this.canUseEmotes && settings.enableEmojiBypass) {
this.preEdit = addPreEditListener((_, __, messageObj) => { this.preEdit = addPreEditListener((_, __, messageObj) => {
const { guildId } = this; const { guildId } = this;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) { for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
const emoji = getCustomEmojiById(emojiId); const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue; if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
if (!emoji.require_colons) continue; if (!emoji.require_colons) continue;
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48"); const url = emoji.url.replace(/\?size=\d+/, "?size=48");
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
}); });
} }
}); });
}
}, },
stop() { stop() {

View file

@ -17,8 +17,9 @@
*/ */
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption } from "../api/Commands"; import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption } from "../api/Commands";
import { lazyWebpack, makeLazy } from "../utils";
import { Devs } from "../utils/constants"; import { Devs } from "../utils/constants";
import { lazyWebpack, makeLazy } from "../utils/misc"; import { getGifEncoder } from "../utils/dependencies";
import definePlugin from "../utils/types"; import definePlugin from "../utils/types";
import { filters } from "../webpack"; import { filters } from "../webpack";
@ -27,11 +28,6 @@ const DEFAULT_DELAY = 20;
const DEFAULT_RESOLUTION = 128; const DEFAULT_RESOLUTION = 128;
const FRAMES = 10; 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( const getFrames = makeLazy(() => Promise.all(
Array.from( Array.from(
{ length: FRAMES }, { length: FRAMES },

76
src/utils/dependencies.ts Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ApngFrameData>; };
});
// 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;
}