Add permissions checks for FakeNitro actions (#2160)
Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
parent
27696ed62a
commit
ed5e1be7a4
4 changed files with 105 additions and 36 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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});"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
1
src/webpack/common/types/utils.d.ts
vendored
1
src/webpack/common/types/utils.d.ts
vendored
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue