2022-12-14 22:44:58 +00:00
|
|
|
/*
|
|
|
|
* 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/>.
|
|
|
|
*/
|
|
|
|
|
2024-04-03 12:51:28 +00:00
|
|
|
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
2023-05-05 23:36:00 +00:00
|
|
|
import { definePluginSettings } from "@api/Settings";
|
2023-03-02 20:01:31 +00:00
|
|
|
import ErrorBoundary from "@components/ErrorBoundary";
|
2022-12-14 22:44:58 +00:00
|
|
|
import { Devs } from "@utils/constants.js";
|
2023-05-05 23:36:00 +00:00
|
|
|
import { classes } from "@utils/misc";
|
2022-12-14 22:44:58 +00:00
|
|
|
import { Queue } from "@utils/Queue";
|
|
|
|
import definePlugin, { OptionType } from "@utils/types";
|
2024-05-03 07:49:14 +00:00
|
|
|
import { findByProps, findComponentByCode } from "@webpack";
|
2022-12-14 22:44:58 +00:00
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
ChannelStore,
|
|
|
|
FluxDispatcher,
|
|
|
|
GuildStore,
|
2024-02-15 09:36:01 +00:00
|
|
|
IconUtils,
|
2022-12-14 22:44:58 +00:00
|
|
|
MessageStore,
|
|
|
|
Parser,
|
2024-02-15 09:12:33 +00:00
|
|
|
PermissionsBits,
|
2022-12-14 22:44:58 +00:00
|
|
|
PermissionStore,
|
|
|
|
RestAPI,
|
|
|
|
Text,
|
2023-10-25 15:33:22 +00:00
|
|
|
TextAndImagesSettingsStores,
|
2022-12-14 22:44:58 +00:00
|
|
|
UserStore
|
|
|
|
} from "@webpack/common";
|
2024-02-15 09:12:33 +00:00
|
|
|
import { Channel, Message } from "discord-types/general";
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
const messageCache = new Map<string, {
|
|
|
|
message?: Message;
|
|
|
|
fetched: boolean;
|
|
|
|
}>();
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2024-05-03 02:18:12 +00:00
|
|
|
const Embed = findComponentByCode(".inlineMediaEmbed");
|
|
|
|
const AutoModEmbed = findComponentByCode(".withFooter]:", "childrenMessageContent:");
|
|
|
|
const ChannelMessage = findComponentByCode("renderSimpleAccessories)");
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2024-05-03 02:18:12 +00:00
|
|
|
const SearchResultClasses = findByProps("message", "searchResult");
|
|
|
|
const EmbedClasses = findByProps("embedAuthorIcon", "embedAuthor", "embedAuthor");
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2024-02-15 09:12:33 +00:00
|
|
|
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
|
2023-04-29 02:16:28 +00:00
|
|
|
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
|
2023-03-02 20:01:31 +00:00
|
|
|
|
|
|
|
interface Attachment {
|
|
|
|
height: number;
|
|
|
|
width: number;
|
|
|
|
url: string;
|
|
|
|
proxyURL?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MessageEmbedProps {
|
|
|
|
message: Message;
|
|
|
|
channel: Channel;
|
|
|
|
}
|
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
const messageFetchQueue = new Queue();
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
const settings = definePluginSettings({
|
|
|
|
messageBackgroundColor: {
|
|
|
|
description: "Background color for messages in rich embeds",
|
|
|
|
type: OptionType.BOOLEAN
|
|
|
|
},
|
|
|
|
automodEmbeds: {
|
|
|
|
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
|
|
|
type: OptionType.SELECT,
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
label: "Always use automod embeds",
|
|
|
|
value: "always"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
|
|
|
value: "prefer"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: "Never use automod embeds",
|
|
|
|
value: "never",
|
|
|
|
default: true
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
2023-07-25 23:41:41 +00:00
|
|
|
listMode: {
|
|
|
|
description: "Whether to use ID list as blacklist or whitelist",
|
|
|
|
type: OptionType.SELECT,
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
label: "Blacklist",
|
|
|
|
value: "blacklist",
|
|
|
|
default: true
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: "Whitelist",
|
|
|
|
value: "whitelist"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
idList: {
|
|
|
|
description: "Guild/channel/user IDs to blacklist or whitelist (separate with comma)",
|
|
|
|
type: OptionType.STRING,
|
|
|
|
default: ""
|
|
|
|
},
|
2023-03-02 20:01:31 +00:00
|
|
|
clearMessageCache: {
|
|
|
|
type: OptionType.COMPONENT,
|
|
|
|
description: "Clear the linked message cache",
|
|
|
|
component: () =>
|
|
|
|
<Button onClick={() => messageCache.clear()}>
|
|
|
|
Clear the linked message cache
|
|
|
|
</Button>
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchMessage(channelID: string, messageID: string) {
|
|
|
|
const cached = messageCache.get(messageID);
|
|
|
|
if (cached) return cached.message;
|
|
|
|
|
|
|
|
messageCache.set(messageID, { fetched: false });
|
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
const res = await RestAPI.get({
|
2023-03-02 20:01:31 +00:00
|
|
|
url: `/channels/${channelID}/messages`,
|
2022-12-14 22:44:58 +00:00
|
|
|
query: {
|
|
|
|
limit: 1,
|
|
|
|
around: messageID
|
|
|
|
},
|
|
|
|
retries: 2
|
2023-03-02 20:01:31 +00:00
|
|
|
}).catch(() => null);
|
|
|
|
|
|
|
|
const msg = res?.body?.[0];
|
|
|
|
if (!msg) return;
|
|
|
|
|
|
|
|
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
|
|
|
|
|
|
|
|
messageCache.set(message.id, {
|
|
|
|
message,
|
2022-12-14 22:44:58 +00:00
|
|
|
fetched: true
|
2023-03-02 20:01:31 +00:00
|
|
|
});
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
return message;
|
2022-12-14 22:44:58 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
function getImages(message: Message): Attachment[] {
|
|
|
|
const attachments: Attachment[] = [];
|
2023-03-02 20:01:31 +00:00
|
|
|
|
|
|
|
for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
|
|
|
|
if (content_type?.startsWith("image/"))
|
2022-12-14 22:44:58 +00:00
|
|
|
attachments.push({
|
2023-03-02 20:01:31 +00:00
|
|
|
height: height!,
|
|
|
|
width: width!,
|
|
|
|
url: url,
|
|
|
|
proxyURL: proxy_url!
|
2022-12-14 22:44:58 +00:00
|
|
|
});
|
2023-03-02 20:01:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const { type, image, thumbnail, url } of message.embeds ?? []) {
|
|
|
|
if (type === "image")
|
|
|
|
attachments.push({ ...(image ?? thumbnail!) });
|
|
|
|
else if (url && type === "gifv" && !tenorRegex.test(url))
|
|
|
|
attachments.push({
|
|
|
|
height: thumbnail!.height,
|
|
|
|
width: thumbnail!.width,
|
|
|
|
url
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
return attachments;
|
|
|
|
}
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
function noContent(attachments: number, embeds: number) {
|
2022-12-14 22:44:58 +00:00
|
|
|
if (!attachments && !embeds) return "";
|
|
|
|
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
|
|
|
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
|
|
|
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
2023-03-02 20:01:31 +00:00
|
|
|
}
|
2022-12-14 22:44:58 +00:00
|
|
|
|
|
|
|
function requiresRichEmbed(message: Message) {
|
2023-03-02 20:01:31 +00:00
|
|
|
if (message.components.length) return true;
|
2023-03-02 20:05:09 +00:00
|
|
|
if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true;
|
|
|
|
if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true;
|
2023-03-02 20:01:31 +00:00
|
|
|
|
|
|
|
return false;
|
2022-12-14 22:44:58 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
function computeWidthAndHeight(width: number, height: number) {
|
|
|
|
const maxWidth = 400;
|
|
|
|
const maxHeight = 300;
|
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
if (width > height) {
|
2023-03-02 20:01:31 +00:00
|
|
|
const adjustedWidth = Math.min(width, maxWidth);
|
|
|
|
return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
|
2022-12-14 22:44:58 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
const adjustedHeight = Math.min(height, maxHeight);
|
|
|
|
return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
|
2022-12-14 22:44:58 +00:00
|
|
|
}
|
|
|
|
|
2023-02-12 18:43:57 +00:00
|
|
|
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
|
|
|
return new Proxy(message, {
|
|
|
|
get(_, prop) {
|
|
|
|
if (prop === "vencordEmbeddedBy") return embeddedBy;
|
|
|
|
// @ts-ignore ts so bad
|
|
|
|
return Reflect.get(...arguments);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
|
|
|
|
function MessageEmbedAccessory({ message }: { message: Message; }) {
|
|
|
|
// @ts-ignore
|
|
|
|
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
|
|
|
|
|
|
|
const accessories = [] as (JSX.Element | null)[];
|
|
|
|
|
|
|
|
let match = null as RegExpMatchArray | null;
|
|
|
|
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
|
2024-02-15 09:12:33 +00:00
|
|
|
const [_, channelID, messageID] = match;
|
2023-03-02 20:01:31 +00:00
|
|
|
if (embeddedBy.includes(messageID)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const linkedChannel = ChannelStore.getChannel(channelID);
|
2024-02-15 09:36:01 +00:00
|
|
|
if (!linkedChannel || (!linkedChannel.isPrivate() && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, linkedChannel))) {
|
2023-03-02 20:01:31 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-07-25 23:41:41 +00:00
|
|
|
const { listMode, idList } = settings.store;
|
|
|
|
|
2024-02-15 09:12:33 +00:00
|
|
|
const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id));
|
2023-07-25 23:41:41 +00:00
|
|
|
|
|
|
|
if (listMode === "blacklist" && isListed) continue;
|
|
|
|
if (listMode === "whitelist" && !isListed) continue;
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
let linkedMessage = messageCache.get(messageID)?.message;
|
|
|
|
if (!linkedMessage) {
|
|
|
|
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
|
|
|
if (linkedMessage) {
|
|
|
|
messageCache.set(messageID, { message: linkedMessage, fetched: true });
|
|
|
|
} else {
|
|
|
|
const msg = { ...message } as any;
|
|
|
|
delete msg.embeds;
|
2023-05-28 19:59:22 +00:00
|
|
|
delete msg.interaction;
|
|
|
|
|
2024-03-22 01:27:09 +00:00
|
|
|
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
|
2023-03-02 20:01:31 +00:00
|
|
|
.then(m => m && FluxDispatcher.dispatch({
|
|
|
|
type: "MESSAGE_UPDATE",
|
|
|
|
message: msg
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const messageProps: MessageEmbedProps = {
|
|
|
|
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
2024-02-15 09:12:33 +00:00
|
|
|
channel: linkedChannel
|
2023-03-02 20:01:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const type = settings.store.automodEmbeds;
|
|
|
|
accessories.push(
|
|
|
|
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
|
|
|
? <AutomodEmbedAccessory {...messageProps} />
|
|
|
|
: <ChannelMessageEmbedAccessory {...messageProps} />
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return accessories.length ? <>{accessories}</> : null;
|
|
|
|
}
|
|
|
|
|
2024-02-15 09:36:01 +00:00
|
|
|
function getChannelLabelAndIconUrl(channel: Channel) {
|
|
|
|
if (channel.isDM()) return ["Direct Message", IconUtils.getUserAvatarURL(UserStore.getUser(channel.recipients[0]))];
|
|
|
|
if (channel.isGroupDM()) return ["Group DM", IconUtils.getChannelIconURL(channel)];
|
|
|
|
return ["Server", IconUtils.getGuildIconURL(GuildStore.getGuild(channel.guild_id))];
|
|
|
|
}
|
|
|
|
|
2024-02-15 09:12:33 +00:00
|
|
|
function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null {
|
2023-03-02 20:01:31 +00:00
|
|
|
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
|
|
|
|
2024-02-15 09:36:01 +00:00
|
|
|
const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Embed
|
|
|
|
embed={{
|
|
|
|
rawDescription: "",
|
|
|
|
color: "var(--background-secondary)",
|
|
|
|
author: {
|
|
|
|
name: <Text variant="text-xs/medium" tag="span">
|
|
|
|
<span>{channelLabel} - </span>
|
|
|
|
{Parser.parse(channel.isDM() ? `<@${dmReceiver.id}>` : `<#${channel.id}>`)}
|
|
|
|
</Text>,
|
|
|
|
iconProxyURL: iconUrl
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
renderDescription={() => (
|
|
|
|
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
|
|
|
|
<ChannelMessage
|
|
|
|
id={`message-link-embeds-${message.id}`}
|
|
|
|
message={message}
|
|
|
|
channel={channel}
|
|
|
|
subscribeToComponentDispatch={false}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
2023-03-02 20:01:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
2024-02-15 09:12:33 +00:00
|
|
|
const { message, channel } = props;
|
2023-10-25 15:33:22 +00:00
|
|
|
const compact = TextAndImagesSettingsStores.MessageDisplayCompact.useSetting();
|
2023-03-02 20:01:31 +00:00
|
|
|
const images = getImages(message);
|
|
|
|
const { parse } = Parser;
|
|
|
|
|
2024-02-15 09:36:01 +00:00
|
|
|
const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);
|
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
return <AutoModEmbed
|
|
|
|
channel={channel}
|
|
|
|
childrenAccessories={
|
2024-02-15 09:36:01 +00:00
|
|
|
<Text color="text-muted" variant="text-xs/medium" tag="span" className={`${EmbedClasses.embedAuthor} ${EmbedClasses.embedMargin}`}>
|
|
|
|
{iconUrl && <img src={iconUrl} className={EmbedClasses.embedAuthorIcon} alt="" />}
|
|
|
|
<span>
|
|
|
|
<span>{channelLabel} - </span>
|
|
|
|
{channel.isDM()
|
|
|
|
? Parser.parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
|
|
|
|
: Parser.parse(`<#${channel.id}>`)
|
|
|
|
}
|
|
|
|
</span>
|
2023-03-02 20:01:31 +00:00
|
|
|
</Text>
|
|
|
|
}
|
2023-10-25 15:33:22 +00:00
|
|
|
compact={compact}
|
2023-03-02 20:01:31 +00:00
|
|
|
content={
|
|
|
|
<>
|
|
|
|
{message.content || message.attachments.length <= images.length
|
|
|
|
? parse(message.content)
|
|
|
|
: [noContent(message.attachments.length, message.embeds.length)]
|
|
|
|
}
|
2023-10-31 22:56:13 +00:00
|
|
|
{images.map(a => {
|
2023-03-02 20:01:31 +00:00
|
|
|
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
|
|
|
return (
|
2023-10-31 22:56:13 +00:00
|
|
|
<div>
|
2023-03-02 20:01:31 +00:00
|
|
|
<img src={a.url} width={width} height={height} />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
hideTimestamp={false}
|
|
|
|
message={message}
|
|
|
|
_messageEmbed="automod"
|
|
|
|
/>;
|
|
|
|
}
|
|
|
|
|
2022-12-14 22:44:58 +00:00
|
|
|
export default definePlugin({
|
|
|
|
name: "MessageLinkEmbeds",
|
|
|
|
description: "Adds a preview to messages that link another message",
|
2023-07-25 23:41:41 +00:00
|
|
|
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
2023-10-25 15:33:22 +00:00
|
|
|
dependencies: ["MessageAccessoriesAPI"],
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
settings,
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
start() {
|
|
|
|
addAccessory("messageLinkEmbed", props => {
|
|
|
|
if (!messageLinkRegex.test(props.message.content))
|
|
|
|
return null;
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
// need to reset the regex because it's global
|
|
|
|
messageLinkRegex.lastIndex = 0;
|
2022-12-14 22:44:58 +00:00
|
|
|
|
2023-03-02 20:01:31 +00:00
|
|
|
return (
|
|
|
|
<ErrorBoundary>
|
2023-07-25 23:41:41 +00:00
|
|
|
<MessageEmbedAccessory
|
|
|
|
message={props.message}
|
|
|
|
/>
|
2023-03-02 20:01:31 +00:00
|
|
|
</ErrorBoundary>
|
2022-12-14 22:44:58 +00:00
|
|
|
);
|
2023-03-02 20:01:31 +00:00
|
|
|
}, 4 /* just above rich embeds */);
|
2022-12-14 22:44:58 +00:00
|
|
|
},
|
2024-04-03 12:51:28 +00:00
|
|
|
|
|
|
|
stop() {
|
|
|
|
removeAccessory("messageLinkEmbed");
|
|
|
|
}
|
2022-12-14 22:44:58 +00:00
|
|
|
});
|