This commit is contained in:
thororen 2024-04-17 14:29:47 -04:00
parent 538b87062a
commit ea7451bcdc
326 changed files with 24876 additions and 2280 deletions

View file

@ -0,0 +1,36 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { settings } from "../index";
export class LimitedMap<K, V> {
public map: Map<K, V> = new Map();
constructor() { }
set(key: K, value: V) {
if (settings.store.cacheLimit > 0 && this.map.size >= settings.store.cacheLimit) {
// delete the first entry
this.map.delete(this.map.keys().next().value);
}
this.map.set(key, value);
}
get(key: K) {
return this.map.get(key);
}
}

View file

@ -0,0 +1,116 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { MessageStore } from "@webpack/common";
import { User } from "discord-types/general";
import { LoggedMessageJSON, RefrencedMessage } from "../types";
import { getGuildIdByChannel, isGhostPinged } from "./index";
export function cleanupMessage(message: any, removeDetails: boolean = true): LoggedMessageJSON {
const ret: LoggedMessageJSON = typeof message.toJS === "function" ? JSON.parse(JSON.stringify(message.toJS())) : { ...message };
if (removeDetails) {
ret.author.phone = undefined;
ret.author.email = undefined;
}
ret.ghostPinged = ret.mentioned ?? isGhostPinged(message);
ret.guildId = ret.guild_id ?? getGuildIdByChannel(ret.channel_id);
ret.embeds = (ret.embeds ?? []).map(cleanupEmbed);
ret.deleted = ret.deleted ?? false;
ret.deletedTimestamp = ret.deleted ? (new Date()).toISOString() : undefined;
ret.editHistory = ret.editHistory ?? [];
if (ret.type === 19) {
ret.message_reference = message.message_reference || message.messageReference;
if (ret.message_reference) {
if (message.referenced_message) {
ret.referenced_message = cleanupMessage(message.referenced_message) as RefrencedMessage;
} else if (MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)) {
ret.referenced_message = cleanupMessage(MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)) as RefrencedMessage;
}
}
}
return ret;
}
export function cleanUpCachedMessage(message: any) {
const ret = cleanupMessage(message, false);
ret.ourCache = true;
return ret;
}
// stolen from mlv2
export function cleanupEmbed(embed) {
/* backported code from MLV2 rewrite */
if (!embed.id) return embed; /* already cleaned */
const retEmbed: any = {};
if (typeof embed.rawTitle === "string") retEmbed.title = embed.rawTitle;
if (typeof embed.rawDescription === "string") retEmbed.description = embed.rawDescription;
if (typeof embed.referenceId !== "undefined") retEmbed.reference_id = embed.referenceId;
// if (typeof embed.color === "string") retEmbed.color = ZeresPluginLibrary.ColorConverter.hex2int(embed.color);
if (typeof embed.type !== "undefined") retEmbed.type = embed.type;
if (typeof embed.url !== "undefined") retEmbed.url = embed.url;
if (typeof embed.provider === "object") retEmbed.provider = { name: embed.provider.name, url: embed.provider.url };
if (typeof embed.footer === "object") retEmbed.footer = { text: embed.footer.text, icon_url: embed.footer.iconURL, proxy_icon_url: embed.footer.iconProxyURL };
if (typeof embed.author === "object") retEmbed.author = { name: embed.author.name, url: embed.author.url, icon_url: embed.author.iconURL, proxy_icon_url: embed.author.iconProxyURL };
if (typeof embed.timestamp === "object" && embed.timestamp._isAMomentObject) retEmbed.timestamp = embed.timestamp.milliseconds();
if (typeof embed.thumbnail === "object") {
if (typeof embed.thumbnail.proxyURL === "string" || (typeof embed.thumbnail.url === "string" && !embed.thumbnail.url.endsWith("?format=jpeg"))) {
retEmbed.thumbnail = {
url: embed.thumbnail.url,
proxy_url: typeof embed.thumbnail.proxyURL === "string" ? embed.thumbnail.proxyURL.split("?format")[0] : undefined,
width: embed.thumbnail.width,
height: embed.thumbnail.height
};
}
}
if (typeof embed.image === "object") {
retEmbed.image = {
url: embed.image.url,
proxy_url: embed.image.proxyURL,
width: embed.image.width,
height: embed.image.height
};
}
if (typeof embed.video === "object") {
retEmbed.video = {
url: embed.video.url,
proxy_url: embed.video.proxyURL,
width: embed.video.width,
height: embed.video.height
};
}
if (Array.isArray(embed.fields) && embed.fields.length) {
retEmbed.fields = embed.fields.map(e => ({ name: e.rawName, value: e.rawValue, inline: e.inline }));
}
return retEmbed;
}
// stolen from mlv2
export function cleanupUserObject(user: User) {
/* backported from MLV2 rewrite */
return {
discriminator: user.discriminator,
username: user.username,
avatar: user.avatar,
id: user.id,
bot: user.bot,
public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : (user as any).public_flags
};
}

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
export const DEFAULT_IMAGE_CACHE_DIR = "savedImages";

View file

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
//! hi this file is now usless. but ill keep it here just in case some people forgot to remove it from the preload.ts

View file

@ -0,0 +1,184 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { Settings } from "@api/Settings";
import { findStoreLazy } from "@webpack";
import { ChannelStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { settings } from "../index";
import { loggedMessages } from "../LoggedMessageManager";
import { LoggedMessageJSON } from "../types";
import { findLastIndex, getGuildIdByChannel } from "./misc";
export * from "./cleanUp";
export * from "./misc";
// stolen from mlv2
// https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js#L2367
interface Id { id: string, time: number; }
export const DISCORD_EPOCH = 14200704e5;
export function reAddDeletedMessages(messages: LoggedMessageJSON[], deletedMessages: string[], channelStart: boolean, channelEnd: boolean) {
if (!messages.length || !deletedMessages?.length) return;
const IDs: Id[] = [];
const savedIDs: Id[] = [];
for (let i = 0, len = messages.length; i < len; i++) {
const { id } = messages[i];
IDs.push({ id: id, time: (parseInt(id) / 4194304) + DISCORD_EPOCH });
}
for (let i = 0, len = deletedMessages.length; i < len; i++) {
const id = deletedMessages[i];
const record = loggedMessages[id];
if (!record) continue;
savedIDs.push({ id: id, time: (parseInt(id) / 4194304) + DISCORD_EPOCH });
}
savedIDs.sort((a, b) => a.time - b.time);
if (!savedIDs.length) return;
const { time: lowestTime } = IDs[IDs.length - 1];
const [{ time: highestTime }] = IDs;
const lowestIDX = channelEnd ? 0 : savedIDs.findIndex(e => e.time > lowestTime);
if (lowestIDX === -1) return;
const highestIDX = channelStart ? savedIDs.length - 1 : findLastIndex(savedIDs, e => e.time < highestTime);
if (highestIDX === -1) return;
const reAddIDs = savedIDs.slice(lowestIDX, highestIDX + 1);
reAddIDs.push(...IDs);
reAddIDs.sort((a, b) => b.time - a.time);
for (let i = 0, len = reAddIDs.length; i < len; i++) {
const { id } = reAddIDs[i];
if (messages.findIndex(e => e.id === id) !== -1) continue;
const record = loggedMessages[id];
if (!record.message) continue;
messages.splice(i, 0, record.message);
}
}
interface ShouldIgnoreArguments {
channelId?: string,
authorId?: string,
guildId?: string;
flags?: number,
bot?: boolean;
ghostPinged?: boolean;
isCachedByUs?: boolean;
}
const EPHEMERAL = 64;
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
/**
* the function `shouldIgnore` evaluates whether a message should be ignored or kept, following a priority hierarchy: User > Channel > Server.
* In this hierarchy, whitelisting takes priority; if any element (User, Channel, or Server) is whitelisted, the message is kept.
* However, if a higher-priority element, like a User, is blacklisted, it will override the whitelisting status of a lower-priority element, such as a Server, causing the message to be ignored.
* @param {ShouldIgnoreArguments} args - An object containing the message details.
* @returns {boolean} - True if the message should be ignored, false if it should be kept.
*/
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs }: ShouldIgnoreArguments): boolean {
const isEphemeral = ((flags ?? 0) & EPHEMERAL) === EPHEMERAL;
if (isEphemeral) return true; // ignore
if (channelId && guildId == null)
guildId = getGuildIdByChannel(channelId);
const myId = UserStore.getCurrentUser().id;
const { ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const { ignoreBots, ignoreSelf } = settings.store;
if (ignoreSelf && authorId === myId)
return true; // ignore
if (settings.store.alwaysLogDirectMessages && ChannelStore.getChannel(channelId ?? "-1")?.isDM?.())
return false; // keep
const shouldLogCurrentChannel = settings.store.alwaysLogCurrentChannel && SelectedChannelStore.getChannelId() === channelId;
const ids = [authorId, channelId, guildId];
const whitelistedIds = settings.store.whitelistedIds.split(",");
const isWhitelisted = settings.store.whitelistedIds.split(",").some(e => ids.includes(e));
const isAuthorWhitelisted = whitelistedIds.includes(authorId!);
const isChannelWhitelisted = whitelistedIds.includes(channelId!);
const isGuildWhitelisted = whitelistedIds.includes(guildId!);
const blacklistedIds = [
...settings.store.blacklistedIds.split(","),
...(ignoreUsers ?? []).split(","),
...(ignoreChannels ?? []).split(","),
...(ignoreGuilds ?? []).split(",")
];
const isBlacklisted = blacklistedIds.some(e => ids.includes(e));
const isAuthorBlacklisted = blacklistedIds.includes(authorId);
const isChannelBlacklisted = blacklistedIds.includes(channelId);
const shouldIgnoreMutedGuilds = settings.store.ignoreMutedGuilds;
const shouldIgnoreMutedCategories = settings.store.ignoreMutedCategories;
const shouldIgnoreMutedChannels = settings.store.ignoreMutedChannels;
if ((ignoreBots && bot) && !isAuthorWhitelisted) return true; // ignore
if (ghostPinged) return false; // keep
// author has highest priority
if (isAuthorWhitelisted) return false; // keep
if (isAuthorBlacklisted) return true; // ignore
if (isChannelWhitelisted) return false; // keep
if (isChannelBlacklisted) return true; // ignore
if (shouldLogCurrentChannel) return false; // keep
if (isWhitelisted) return false; // keep
if (isCachedByUs && (!settings.store.cacheMessagesFromServers && guildId != null && !isGuildWhitelisted)) return true; // ignore
if (isBlacklisted && (!isAuthorWhitelisted || !isChannelWhitelisted)) return true; // ignore
if (guildId != null && shouldIgnoreMutedGuilds && UserGuildSettingsStore.isMuted(guildId)) return true; // ignore
if (channelId != null && shouldIgnoreMutedCategories && UserGuildSettingsStore.isCategoryMuted(guildId, channelId)) return true; // ignore
if (channelId != null && shouldIgnoreMutedChannels && UserGuildSettingsStore.isChannelMuted(guildId, channelId)) return true; // ignore
return false; // keep;
}
export type ListType = "blacklistedIds" | "whitelistedIds";
export function addToXAndRemoveFromOpposite(list: ListType, id: string) {
const oppositeListType = list === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds";
removeFromX(oppositeListType, id);
addToX(list, id);
}
export function addToX(list: ListType, id: string) {
const items = settings.store[list] ? settings.store[list].split(",") : [];
items.push(id);
settings.store[list] = items.join(",");
}
export function removeFromX(list: ListType, id: string) {
const items = settings.store[list] ? settings.store[list].split(",") : [];
const index = items.indexOf(id);
if (index !== -1) {
items.splice(index, 1);
}
settings.store[list] = items.join(",");
}

View file

@ -0,0 +1,41 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
type MemoizedFunction<T extends (...args: any[]) => any> = {
(...args: Parameters<T>): ReturnType<T>;
clear(): void;
};
export function memoize<T extends (...args: any[]) => any>(func: T): MemoizedFunction<T> {
const cache = new Map<string, ReturnType<T>>();
const memoizedFunc = (...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = func(...args);
cache.set(key, result);
return result;
};
memoizedFunc.clear = () => cache.clear();
return memoizedFunc;
}

View file

@ -0,0 +1,156 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { get, set } from "@api/DataStore";
import { PluginNative } from "@utils/types";
import { findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore, moment, UserStore } from "@webpack/common";
import { LOGGED_MESSAGES_KEY, MessageLoggerStore } from "../LoggedMessageManager";
import { LoggedMessageJSON } from "../types";
import { DEFAULT_IMAGE_CACHE_DIR } from "./constants";
import { DISCORD_EPOCH } from "./index";
import { memoize } from "./memoize";
const MessageClass: any = findLazy(m => m?.prototype?.isEdited);
const AuthorClass = findLazy(m => m?.prototype?.getAvatarURL);
const embedModule = findByPropsLazy("sanitizeEmbed");
export function getGuildIdByChannel(channel_id: string) {
return ChannelStore.getChannel(channel_id)?.guild_id;
}
export const isGhostPinged = (message?: LoggedMessageJSON) => {
return message?.ghostPinged || message?.deleted && hasPingged(message);
};
export const hasPingged = (message?: LoggedMessageJSON | { mention_everyone: boolean, mentions: any[]; }) => {
return message && !!(
message.mention_everyone ||
message.mentions?.find(m => (typeof m === "string" ? m : m.id) === UserStore.getCurrentUser().id)
);
};
export const discordIdToDate = (id: string) => new Date((parseInt(id) / 4194304) + DISCORD_EPOCH);
export const sortMessagesByDate = (timestampA: string, timestampB: string) => {
// very expensive
// const timestampA = discordIdToDate(a).getTime();
// const timestampB = discordIdToDate(b).getTime();
// return timestampB - timestampA;
// newest first
if (timestampA < timestampB) {
return 1;
} else if (timestampA > timestampB) {
return -1;
} else {
return 0;
}
};
// stolen from mlv2
export function findLastIndex<T>(array: T[], predicate: (e: T, t: number, n: T[]) => boolean) {
let l = array.length;
while (l--) {
if (predicate(array[l], l, array))
return l;
}
return -1;
}
const getTimestamp = (timestamp: any): Date => {
return new Date(timestamp);
};
export const mapEditHistory = (m: any) => {
m.timestamp = getTimestamp(m.timestamp);
return m;
};
export const messageJsonToMessageClass = memoize((log: { message: LoggedMessageJSON; }) => {
// console.time("message populate");
if (!log?.message) return null;
const message: any = new MessageClass(log.message);
// @ts-ignore
message.timestamp = getTimestamp(message.timestamp);
const editHistory = message.editHistory?.map(mapEditHistory);
if (editHistory && editHistory.length > 0) {
message.editHistory = editHistory;
}
if (message.editedTimestamp)
message.editedTimestamp = getTimestamp(message.editedTimestamp);
message.author = new AuthorClass(message.author);
message.author.nick = message.author.globalName ?? message.author.username;
message.embeds = message.embeds.map(e => embedModule.sanitizeEmbed(message.channel_id, message.id, e));
if (message.poll)
message.poll.expiry = moment(message.poll.expiry);
// console.timeEnd("message populate");
return message;
});
export function parseJSON(json?: string | null) {
try {
return JSON.parse(json!);
} finally {
return null;
}
}
export async function doesBlobUrlExist(url: string) {
const res = await fetch(url);
return res.ok;
}
export function getNative(): PluginNative<typeof import("../native")> {
if (IS_WEB) {
const Native = {
getLogsFromFs: async () => get(LOGGED_MESSAGES_KEY, MessageLoggerStore),
writeLogs: async (logs: string) => set(LOGGED_MESSAGES_KEY, JSON.parse(logs), MessageLoggerStore),
getDefaultNativeImageDir: async () => DEFAULT_IMAGE_CACHE_DIR,
getDefaultNativeDataDir: async () => "",
deleteFileNative: async () => { },
chooseDir: async (x: string) => "",
getSettings: async () => ({ imageCacheDir: DEFAULT_IMAGE_CACHE_DIR, logsDir: "" }),
init: async () => { },
initDirs: async () => { },
getImageNative: async (x: string) => new Uint8Array(0),
getNativeSavedImages: async () => new Map(),
messageLoggerEnhancedUniqueIdThingyIdkMan: async () => { },
showItemInFolder: async () => { },
writeImageNative: async () => { },
} satisfies PluginNative<typeof import("../native")>;
return Native;
}
return Object.values(VencordNative.pluginHelpers)
.find(m => m.messageLoggerEnhancedUniqueIdThingyIdkMan) as PluginNative<typeof import("../native")>;
}

View file

@ -0,0 +1,101 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { ChannelStore, GuildStore } from "@webpack/common";
import { LoggedMessageJSON } from "../types";
import { getGuildIdByChannel } from "./index";
import { memoize } from "./memoize";
const validIdSearchTypes = ["server", "guild", "channel", "in", "user", "from", "message"] as const;
type ValidIdSearchTypesUnion = typeof validIdSearchTypes[number];
interface QueryResult {
success: boolean;
query: string;
type?: ValidIdSearchTypesUnion;
id?: string;
negate?: boolean;
}
export const parseQuery = memoize((query: string = ""): QueryResult => {
let trimmedQuery = query.trim();
if (!trimmedQuery) {
return { success: false, query };
}
let negate = false;
if (trimmedQuery.startsWith("!")) {
negate = true;
trimmedQuery = trimmedQuery.substring(trimmedQuery.length, 1);
}
const [filter, rest] = trimmedQuery.split(" ", 2);
if (!filter) {
return { success: false, query };
}
const [type, id] = filter.split(":") as [ValidIdSearchTypesUnion, string];
if (!type || !id || !validIdSearchTypes.includes(type)) {
return { success: false, query };
}
return {
success: true,
type,
id,
negate,
query: rest ?? ""
};
});
export const doesMatch = (type: typeof validIdSearchTypes[number], value: string, message: LoggedMessageJSON) => {
switch (type) {
case "in":
case "channel":
const channel = ChannelStore.getChannel(message.channel_id);
if (!channel)
return message.channel_id === value;
const { name, id } = channel;
return id === value
|| name.toLowerCase().includes(value.toLowerCase());
case "message":
return message.id === value;
case "from":
case "user":
return message.author.id === value
|| message.author?.username?.toLowerCase().includes(value.toLowerCase())
|| (message.author as any)?.globalName?.toLowerCase()?.includes(value.toLowerCase());
case "guild":
case "server": {
const guildId = message.guildId ?? getGuildIdByChannel(message.channel_id);
if (!guildId) return false;
const guild = GuildStore.getGuild(guildId);
if (!guild)
return guildId === value;
return guild.id === value
|| guild.name.toLowerCase().includes(value.toLowerCase());
}
default:
return false;
}
};

View file

@ -0,0 +1,80 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 {
createStore,
del,
get,
keys,
set,
} from "@api/DataStore";
import { Flogger, Native } from "../..";
import { DEFAULT_IMAGE_CACHE_DIR } from "../constants";
const ImageStore = createStore("MessageLoggerImageData", "MessageLoggerImageStore");
interface IDBSavedImages { attachmentId: string, path: string; }
let idbSavedImages: IDBSavedImages[] = [];
(async () => {
try {
idbSavedImages = (await keys(ImageStore))
.map(m => {
const str = m.toString();
if (!str.startsWith(DEFAULT_IMAGE_CACHE_DIR)) return null;
return { attachmentId: str.split("/")?.[1]?.split(".")?.[0], path: str };
})
.filter(Boolean) as IDBSavedImages[];
} catch (err) {
Flogger.error("Failed to get idb images", err);
}
})();
export async function getImage(attachmentId: string, fileExt?: string | null): Promise<any> {
// for people who have access to native api but some images are still in idb
// also for people who dont have native api
const idbPath = idbSavedImages.find(m => m.attachmentId === attachmentId)?.path;
if (idbPath)
return get(idbPath, ImageStore);
if (IS_WEB) return null;
return await Native.getImageNative(attachmentId);
}
// file name shouldnt have any query param shinanigans
export async function writeImage(imageCacheDir: string, filename: string, content: Uint8Array): Promise<void> {
if (IS_WEB) {
const path = `${imageCacheDir}/${filename}`;
idbSavedImages.push({ attachmentId: filename.split(".")?.[0], path });
return set(path, content, ImageStore);
}
Native.writeImageNative(filename, content);
}
export async function deleteImage(attachmentId: string): Promise<void> {
const idbPath = idbSavedImages.find(m => m.attachmentId === attachmentId)?.path;
if (idbPath)
return await del(idbPath, ImageStore);
if (IS_WEB) return;
await Native.deleteFileNative(attachmentId);
}

View file

@ -0,0 +1,115 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { sleep } from "@utils/misc";
import { MessageAttachment } from "discord-types/general";
import { Flogger, Native, settings } from "../..";
import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "../../types";
import { memoize } from "../memoize";
import { deleteImage, getImage, writeImage, } from "./ImageManager";
export function getFileExtension(str: string) {
const matches = str.match(/(\.[a-zA-Z0-9]+)(?:\?.*)?$/);
if (!matches) return null;
return matches[1];
}
export function isImage(url: string) {
return /\.(jpe?g|png|gif|bmp)(\?.*)?$/i.test(url);
}
export function isAttachmentImage(attachment: MessageAttachment) {
return isImage(attachment.filename ?? attachment.url) || (attachment.content_type?.split("/")[0] === "image");
}
function transformAttachmentUrl(messageId: string, attachmentUrl: string) {
const url = new URL(attachmentUrl);
url.searchParams.set("messageId", messageId);
return url.toString();
}
export async function cacheImage(url: string, attachmentIdx: number, attachmentId: string, messageId: string, channelId: string, fileExtension: string | null, attempts = 0) {
const res = await fetch(url);
if (res.status !== 200) {
if (res.status === 404 || res.status === 403) return;
attempts++;
if (attempts > 3) {
Flogger.warn(`Failed to get image ${attachmentId} for caching, error code ${res.status}`);
return;
}
await sleep(1000);
return cacheImage(url, attachmentIdx, attachmentId, messageId, channelId, fileExtension, attempts);
}
const ab = await res.arrayBuffer();
const imageCacheDir = settings.store.imageCacheDir ?? await Native.getDefaultNativeImageDir();
const path = `${imageCacheDir}/${attachmentId}${fileExtension}`;
await writeImage(imageCacheDir, `${attachmentId}${fileExtension}`, new Uint8Array(ab));
return path;
}
export async function cacheMessageImages(message: LoggedMessage | LoggedMessageJSON) {
try {
for (let i = 0; i < message.attachments.length; i++) {
const attachment = message.attachments[i];
if (!isAttachmentImage(attachment)) {
Flogger.log("skipping", attachment.filename);
continue;
}
// apparently proxy urls last longer
attachment.url = transformAttachmentUrl(message.id, attachment.proxy_url);
const fileExtension = getFileExtension(attachment.filename ?? attachment.url) ?? attachment.content_type?.split("/")?.[1] ?? ".png";
const path = await cacheImage(attachment.url, i, attachment.id, message.id, message.channel_id, fileExtension);
if (path == null) {
Flogger.error("Failed to save image from attachment. id: ", attachment.id);
continue;
}
attachment.fileExtension = fileExtension;
attachment.path = path;
}
} catch (error) {
Flogger.error("Error caching message images:", error);
}
}
export async function deleteMessageImages(message: LoggedMessage | LoggedMessageJSON) {
for (let i = 0; i < message.attachments.length; i++) {
const attachment = message.attachments[i];
await deleteImage(attachment.id);
}
}
export const getAttachmentBlobUrl = memoize(async (attachment: LoggedAttachment) => {
const imageData = await getImage(attachment.id, attachment.fileExtension);
if (!imageData) return null;
const blob = new Blob([imageData]);
const resUrl = URL.createObjectURL(blob);
return resUrl;
});

View file

@ -0,0 +1,51 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { DataStore } from "@api/index";
import { LOGGED_MESSAGES_KEY, MessageLoggerStore } from "../LoggedMessageManager";
// 99% of this is coppied from src\utils\settingsSync.ts
export async function downloadLoggedMessages() {
const filename = "message-logger-logs.json";
const exportData = await exportLogs();
const data = new TextEncoder().encode(exportData);
if (IS_WEB || IS_VESKTOP) {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = filename;
document.body.appendChild(a);
a.click();
setImmediate(() => {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
export async function exportLogs() {
const logger_data = await DataStore.get(LOGGED_MESSAGES_KEY, MessageLoggerStore);
return JSON.stringify(logger_data, null, 4);
}