Add permissions checks for FakeNitro actions (#2160)

Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
Nuckyz 2024-02-27 09:19:05 -03:00 committed by GitHub
parent 27696ed62a
commit ed5e1be7a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 105 additions and 36 deletions

View file

@ -74,7 +74,7 @@ export interface MessageExtra {
} }
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>; export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>; export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<SendListener>(); const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>(); const editListeners = new Set<EditListener>();
@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
for (const listener of sendListeners) { for (const listener of sendListeners) {
try { try {
const result = await listener(channelId, messageObj, extra); const result = await listener(channelId, messageObj, extra);
if (result && result.cancel === true) { if (result?.cancel) {
return true; return true;
} }
} catch (e) { } catch (e) {
@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
for (const listener of editListeners) { for (const listener of editListeners) {
try { try {
await listener(channelId, messageId, messageObj); const result = await listener(channelId, messageId, messageObj);
if (result?.cancel) {
return true;
}
} catch (e) { } catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
} }
} }
return false;
} }
/** /**

View file

@ -25,10 +25,13 @@ export default definePlugin({
authors: [Devs.Arjix, Devs.hunt, Devs.Ven], authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
patches: [ patches: [
{ {
find: '"MessageActionCreators"', find: ".Messages.EDIT_TEXTAREA_HELP",
replacement: { replacement: {
match: /async editMessage\(.+?\)\{/, match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/,
replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);" replace: (match, args) => "" +
`async ${match}` +
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
"return Promise.resolve({shoudClear:true,shouldRefocus:true});"
} }
}, },
{ {

View file

@ -17,14 +17,14 @@
*/ */
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord"; import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
@ -51,8 +51,6 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsA
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n;
const enum EmojiIntentions { const enum EmojiIntentions {
REACTION = 0, REACTION = 0,
@ -162,8 +160,23 @@ const settings = definePluginSettings({
description: "Whether to use hyperlinks when sending fake emojis and stickers", description: "Whether to use hyperlinks when sending fake emojis and stickers",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true default: true
} },
}); }).withPrivateSettings<{
disableEmbedPermissionCheck: boolean;
}>();
function hasPermission(channelId: string, permission: bigint) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isPrivate()) return true;
return PermissionStore.can(permission, channel);
}
const hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS);
const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS);
const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);
const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);
export default definePlugin({ export default definePlugin({
name: "FakeNitro", name: "FakeNitro",
@ -696,22 +709,6 @@ export default definePlugin({
} }
}, },
hasPermissionToUseExternalEmojis(channelId: string): boolean {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
},
hasPermissionToUseExternalStickers(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
},
getStickerLink(stickerId: string) { getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`; return `https://media.discordapp.net/stickers/${stickerId}.png?size=${settings.store.stickerSize}`;
}, },
@ -722,7 +719,7 @@ export default definePlugin({
const { frames, width, height } = await parseURL(stickerLink); const { frames, width, height } = await parseURL(stickerLink);
const gif = GIFEncoder(); const gif = GIFEncoder();
const resolution = Settings.plugins.FakeNitro.stickerSize; const resolution = settings.store.stickerSize;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = resolution; canvas.width = resolution;
@ -783,9 +780,38 @@ export default definePlugin({
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " "; return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
} }
this.preSend = addPreSendListener((channelId, messageObj, extra) => { function cannotEmbedNotice() {
return new Promise<boolean>(resolve => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You are trying to send/edit a message that contains a FakeNitro emoji or sticker
, however you do not have permissions to embed links in the current channel.
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
You can disable this notice in the plugin settings.
</Forms.FormText>
</div>,
confirmText: "Send Anyway",
cancelText: "Cancel",
secondaryConfirmText: "Do not show again",
onConfirm: () => resolve(true),
onCloseCallback: () => setImmediate(() => resolve(false)),
onConfirmSecondary() {
settings.store.disableEmbedPermissionCheck = true;
resolve(true);
}
});
});
}
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this; const { guildId } = this;
let hasBypass = false;
stickerBypass: { stickerBypass: {
if (!s.enableStickerBypass) if (!s.enableStickerBypass)
break stickerBypass; break stickerBypass;
@ -798,7 +824,7 @@ export default definePlugin({
if ("pack_id" in sticker) if ("pack_id" in sticker)
break stickerBypass; break stickerBypass;
const canUseStickers = this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId); const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId);
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId)) if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
break stickerBypass; break stickerBypass;
@ -812,9 +838,24 @@ export default definePlugin({
} }
if (sticker.format_type === StickerType.APNG) { if (sticker.format_type === StickerType.APNG) {
if (!hasAttachmentPerms(channelId)) {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You cannot send this message because it contains an animated FakeNitro sticker,
and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.
</Forms.FormText>
</div>
});
} else {
this.sendAnimatedSticker(link, sticker.id, channelId); this.sendAnimatedSticker(link, sticker.id, channelId);
}
return { cancel: true }; return { cancel: true };
} else { } else {
hasBypass = true;
const url = new URL(link); const url = new URL(link);
url.searchParams.set("name", sticker.name); url.searchParams.set("name", sticker.name);
@ -824,13 +865,15 @@ export default definePlugin({
} }
if (s.enableEmojiBypass) { if (s.enableEmojiBypass) {
const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId); const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
for (const emoji of messageObj.validNonShortcutEmojis) { for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue; if (!emoji.require_colons) continue;
if (emoji.available !== false && canUseEmotes) continue; if (emoji.available !== false && canUseEmotes) continue;
if (emoji.guildId === guildId && !emoji.animated) continue; if (emoji.guildId === guildId && !emoji.animated) continue;
hasBypass = true;
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = new URL(emoji.url); const url = new URL(emoji.url);
@ -843,16 +886,24 @@ export default definePlugin({
} }
} }
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
return { cancel: true };
}
}
return { cancel: false }; return { cancel: false };
}); });
this.preEdit = addPreEditListener((channelId, __, messageObj) => { this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return; if (!s.enableEmojiBypass) return;
const canUseEmotes = this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId);
const { guildId } = this; const { guildId } = this;
let hasBypass = false;
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => { messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {
const emoji = EmojiStore.getCustomEmojiById(emojiId); const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null) return emojiStr; if (emoji == null) return emojiStr;
@ -860,12 +911,22 @@ export default definePlugin({
if (emoji.available !== false && canUseEmotes) return emojiStr; if (emoji.available !== false && canUseEmotes) return emojiStr;
if (emoji.guildId === guildId && !emoji.animated) return emojiStr; if (emoji.guildId === guildId && !emoji.animated) return emojiStr;
hasBypass = true;
const url = new URL(emoji.url); const url = new URL(emoji.url);
url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("size", s.emojiSize.toString());
url.searchParams.set("name", emoji.name); url.searchParams.set("name", emoji.name);
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`; return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${emoji.name}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`;
}); });
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
return { cancel: true };
}
}
return { cancel: false };
}); });
}, },

View file

@ -59,6 +59,7 @@ export interface Alerts {
onCancel?(): void; onCancel?(): void;
onConfirm?(): void; onConfirm?(): void;
onConfirmSecondary?(): void; onConfirmSecondary?(): void;
onCloseCallback?(): void;
}): void; }): void;
/** This is a noop, it does nothing. */ /** This is a noop, it does nothing. */
close(): void; close(): void;