/*
* 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 .
*/
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findComponentByCode } from "@webpack";
import {
Button,
ChannelStore,
Constants,
FluxDispatcher,
GuildStore,
IconUtils,
MessageStore,
Parser,
PermissionsBits,
PermissionStore,
RestAPI,
Text,
TextAndImagesSettingsStores,
UserStore
} from "@webpack/common";
import { Channel, Message } from "discord-types/general";
const messageCache = new Map();
const Embed = findComponentByCode(".inlineMediaEmbed");
const AutoModEmbed = findComponentByCode(".withFooter]:", "childrenMessageContent:");
const ChannelMessage = findComponentByCode("renderSimpleAccessories)");
const SearchResultClasses = findByProps("message", "searchResult");
const EmbedClasses = findByProps("embedAuthorIcon", "embedAuthor", "embedAuthor");
const messageLinkRegex = /(?
}
});
async function fetchMessage(channelID: string, messageID: string) {
const cached = messageCache.get(messageID);
if (cached) return cached.message;
messageCache.set(messageID, { fetched: false });
const res = await RestAPI.get({
url: Constants.Endpoints.MESSAGES(channelID),
query: {
limit: 1,
around: messageID
},
retries: 2
}).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,
fetched: true
});
return message;
}
function getImages(message: Message): Attachment[] {
const attachments: Attachment[] = [];
for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
if (content_type?.startsWith("image/"))
attachments.push({
height: height!,
width: width!,
url: url,
proxyURL: proxy_url!
});
}
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
});
}
return attachments;
}
function noContent(attachments: number, embeds: number) {
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" : ""}]`;
}
function requiresRichEmbed(message: Message) {
if (message.components.length) return true;
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;
return false;
}
function computeWidthAndHeight(width: number, height: number) {
const maxWidth = 400;
const maxHeight = 300;
if (width > height) {
const adjustedWidth = Math.min(width, maxWidth);
return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
}
const adjustedHeight = Math.min(height, maxHeight);
return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
}
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);
}
});
}
function MessageEmbedAccessory({ message }: { message: Message; }) {
// @ts-ignore
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
const accessories = [] as (JSX.Element | null)[];
for (const [_, channelID, messageID] of message.content!.matchAll(messageLinkRegex)) {
if (embeddedBy.includes(messageID) || embeddedBy.length > 2) {
continue;
}
const linkedChannel = ChannelStore.getChannel(channelID);
if (!linkedChannel || (!linkedChannel.isPrivate() && !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, linkedChannel))) {
continue;
}
const { listMode, idList } = settings.store;
const isListed = [linkedChannel.guild_id, channelID, message.author.id].some(id => id && idList.includes(id));
if (listMode === "blacklist" && isListed) continue;
if (listMode === "whitelist" && !isListed) continue;
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;
delete msg.interaction;
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: msg
}))
);
continue;
}
}
const messageProps: MessageEmbedProps = {
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
channel: linkedChannel
};
const type = settings.store.automodEmbeds;
accessories.push(
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
?
:
);
}
return accessories.length ? <>{accessories}> : null;
}
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))];
}
function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps): JSX.Element | null {
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
const [channelLabel, iconUrl] = getChannelLabelAndIconUrl(channel);
return (