aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/MessageEvents.ts24
-rw-r--r--src/globals.d.ts2
-rw-r--r--src/plugins/apiMessageEvents.ts2
-rw-r--r--src/plugins/nitroBypass.ts246
-rw-r--r--src/plugins/petpet.ts8
-rw-r--r--src/utils/dependencies.ts76
6 files changed, 305 insertions, 53 deletions
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<SendListener>();
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) {
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 <https://www.gnu.org/licenses/>.
*/
-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,12 +70,27 @@ 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,
replacement: [
"canUseHighVideoUploadQuality",
@@ -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);
},
- start() {
- if (!Settings.plugins.NitroBypass.enableEmojiBypass) {
- return;
+ 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;
}
- if (this.canUseEmotes) {
- console.info("[NitroBypass] Skipping start because you have nitro");
+ gif.finish();
+ const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
+ promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
+ },
+
+ start() {
+ const settings = Settings.plugins.NitroBypass;
+ if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
return;
}
- const { getCustomEmojiById } = findByProps("getCustomEmojiById");
+ const EmojiStore = lazyWebpack(filters.byProps("getCustomEmojiById"));
+ const StickerStore = lazyWebpack(filters.byProps("getAllGuildStickers")) as {
+ getPremiumPacks(): StickerPack[];
+ getAllGuildStickers(): Map<string, Sticker[]>;
+ getStickerById(id: string): Sticker | undefined;
+ };
- function getWordBoundary(origStr, offset) {
+ function getWordBoundary(origStr: string, offset: number) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
- this.preSend = addPreSendListener((_, messageObj) => {
+ this.preSend = addPreSendListener((channelId, messageObj, extra) => {
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) => {
- 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;
- for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
- const emoji = getCustomEmojiById(emojiId);
- if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
- if (!emoji.require_colons) continue;
+ 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 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)}`;
- });
+ 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(/(?<!\\)<a?:(\w+):(\d+)>/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 <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;
+}