mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-21 20:37:02 -04:00
forked!!
This commit is contained in:
parent
538b87062a
commit
ea7451bcdc
326 changed files with 24876 additions and 2280 deletions
|
@ -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);
|
||||
}
|
||||
}
|
116
src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts
Normal file
116
src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts
Normal 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
|
||||
};
|
||||
}
|
19
src/equicordplugins/messageLoggerEnhanced/utils/constants.ts
Normal file
19
src/equicordplugins/messageLoggerEnhanced/utils/constants.ts
Normal 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";
|
|
@ -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
|
184
src/equicordplugins/messageLoggerEnhanced/utils/index.ts
Normal file
184
src/equicordplugins/messageLoggerEnhanced/utils/index.ts
Normal 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(",");
|
||||
}
|
41
src/equicordplugins/messageLoggerEnhanced/utils/memoize.ts
Normal file
41
src/equicordplugins/messageLoggerEnhanced/utils/memoize.ts
Normal 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;
|
||||
}
|
156
src/equicordplugins/messageLoggerEnhanced/utils/misc.ts
Normal file
156
src/equicordplugins/messageLoggerEnhanced/utils/misc.ts
Normal 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")>;
|
||||
|
||||
}
|
101
src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts
Normal file
101
src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts
Normal 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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
});
|
|
@ -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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue