/* * 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 "./messageLogger.css"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { FluxDispatcher, i18n, Menu, moment, Parser, Timestamp, UserStore } from "@webpack/common"; import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage"); function addDeleteStyle() { if (Settings.plugins.MessageLogger.deleteStyle === "text") { enableStyle(textStyle); disableStyle(overlayStyle); } else { disableStyle(textStyle); enableStyle(overlayStyle); } } const REMOVE_HISTORY_ID = "ml-remove-history"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => { const { message } = props; const { deleted, editHistory, id, channel_id } = message; if (!deleted && !editHistory?.length) return; toggle: { if (!deleted) break toggle; const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`); if (!domElement) break toggle; children.push(( domElement.classList.toggle("messagelogger-deleted")} /> )); } children.push(( { if (deleted) { FluxDispatcher.dispatch({ type: "MESSAGE_DELETE", channelId: channel_id, id, mlDeleted: true }); } else { message.editHistory = []; } }} /> )); }; export default definePlugin({ name: "MessageLogger", description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven], start() { addDeleteStyle(); addContextMenuPatch("message", patchMessageContextMenu); }, stop() { removeContextMenuPatch("message", patchMessageContextMenu); }, renderEdit(edit: { timestamp: any, content: string; }) { return (
{Parser.parse(edit.content)} {" "}({i18n.Messages.MESSAGE_EDITED})
); }, makeEdit(newMessage: any, oldMessage: any): any { return { timestamp: moment?.call(newMessage.edited_timestamp), content: oldMessage.content }; }, options: { deleteStyle: { type: OptionType.SELECT, description: "The style of deleted messages", default: "text", options: [ { label: "Red text", value: "text", default: true }, { label: "Red overlay", value: "overlay" } ], onChange: () => addDeleteStyle() }, ignoreBots: { type: OptionType.BOOLEAN, description: "Whether to ignore messages by bots", default: false }, ignoreSelf: { type: OptionType.BOOLEAN, description: "Whether to ignore messages by yourself", default: false } }, handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) { try { if (cache == null || (!isBulk && !cache.has(data.id))) return cache; const { ignoreBots, ignoreSelf } = Settings.plugins.MessageLogger; const myId = UserStore.getCurrentUser().id; function mutate(id: string) { const msg = cache.get(id); if (!msg) return; const EPHEMERAL = 64; const shouldIgnore = data.mlDeleted || (msg.flags & EPHEMERAL) === EPHEMERAL || ignoreBots && msg.author?.bot || ignoreSelf && msg.author?.id === myId; if (shouldIgnore) { cache = cache.remove(id); } else { cache = cache.update(id, m => m .set("deleted", true) .set("attachments", m.attachments.map(a => (a.deleted = true, a)))); } } if (isBulk) { data.ids.forEach(mutate); } else { mutate(data.id); } } catch (e) { new Logger("MessageLogger").error("Error during handleDelete", e); } return cache; }, // Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996 patches: [ { // MessageStore // Module 171447 find: "displayName=\"MessageStore\"", replacement: [ { // Add deleted=true to all target messages in the MESSAGE_DELETE event match: /MESSAGE_DELETE:function\((\w)\){var .+?((?:\w{1,2}\.){2})getOrCreate.+?},/, replace: "MESSAGE_DELETE:function($1){" + " var cache = $2getOrCreate($1.channelId);" + " cache = $self.handleDelete(cache, $1, false);" + " $2commit(cache);" + "}," }, { // Add deleted=true to all target messages in the MESSAGE_DELETE_BULK event match: /MESSAGE_DELETE_BULK:function\((\w)\){var .+?((?:\w{1,2}\.){2})getOrCreate.+?},/, replace: "MESSAGE_DELETE_BULK:function($1){" + " var cache = $2getOrCreate($1.channelId);" + " cache = $self.handleDelete(cache, $1, true);" + " $2commit(cache);" + "}," }, { // Add current cached content + new edit time to cached message's editHistory match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/, replace: "$1" + ".update($3,m =>" + " (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m" + ")" + ".update($3" }, { // fix up key (edit last message) attempting to edit a deleted message match: /(?<=getLastEditableMessage=.{0,200}\.find\(\(function\((\i)\)\{)return/, replace: "return !$1.deleted &&" } ] }, { // Message domain model // Module 451 find: "isFirstMessageInForumPost=function", replacement: [ { match: /(\w)\.customRenderedContent=(\w)\.customRenderedContent;/, replace: "$1.customRenderedContent = $2.customRenderedContent;" + "$1.deleted = $2.deleted || false;" + "$1.editHistory = $2.editHistory || [];" } ] }, { // Updated message transformer(?) // Module 819525 find: "THREAD_STARTER_MESSAGE?null===", replacement: [ // { // // DEBUG: Log the params of the target function to the patch below // match: /function N\(e,t\){/, // replace: "function L(e,t){console.log('pre-transform', e, t);" // }, { // Pass through editHistory & deleted & original attachments to the "edited message" transformer match: /interactionData:(\w)\.interactionData/, replace: "interactionData:$1.interactionData," + "deleted:$1.deleted," + "editHistory:$1.editHistory," + "attachments:$1.attachments" }, // { // // DEBUG: Log the params of the target function to the patch below // match: /function R\(e\){/, // replace: "function R(e){console.log('after-edit-transform', arguments);" // }, { // Construct new edited message and add editHistory & deleted (ref above) // Pass in custom data to attachment parser to mark attachments deleted as well match: /attachments:(\w{1,2})\((\w)\)/, replace: "attachments: $1((() => {" + " let old = arguments[1]?.attachments;" + " if (!old) return $2;" + " let new_ = $2.attachments?.map(a => a.id) ?? [];" + " let diff = old.filter(a => !new_.includes(a.id));" + " old.forEach(a => a.deleted = true);" + " $2.attachments = [...diff, ...$2.attachments];" + " return $2;" + "})())," + "deleted: arguments[1]?.deleted," + "editHistory: arguments[1]?.editHistory" }, { // Preserve deleted attribute on attachments match: /(\((\w)\){return null==\2\.attachments.+?)spoiler:/, replace: "$1deleted: arguments[0]?.deleted," + "spoiler:" } ] }, { // Attachment renderer // Module 96063 find: "[\"className\",\"attachment\",\"inlineMedia\"", replacement: [ { match: /((\w)\.className,\w=\2\.attachment),/, replace: "$1,deleted=$2.attachment?.deleted," }, { match: /\["className","attachment","inlineMedia".+?className:/, replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +" } ] }, { // Base message component renderer // Module 748241 find: "Message must not be a thread starter message", replacement: [ { // Append messagelogger-deleted to classNames if deleted match: /\)\("li",\{(.+?),className:/, replace: ")(\"li\",{$1,className:(arguments[0].message.deleted ? \"messagelogger-deleted \" : \"\")+" } ] }, { // Message content renderer // Module 43016 find: "Messages.MESSAGE_EDITED,\")\"", replacement: [ { // Render editHistory in the deepest div for message content match: /(\)\("div",\{id:.+?children:\[)/, replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), " } ] }, { // ReferencedMessageStore // Module 778667 find: "displayName=\"ReferencedMessageStore\"", replacement: [ { match: /MESSAGE_DELETE:function\((\w)\).+?},/, replace: "MESSAGE_DELETE:function($1){}," }, { match: /MESSAGE_DELETE_BULK:function\((\w)\).+?},/, replace: "MESSAGE_DELETE_BULK:function($1){}," } ] }, { // Message context base menu // Module 600300 find: "id:\"remove-reactions\"", replacement: [ { // Remove the first section if message is deleted match: /children:(\[""===.+?\])/, replace: "children:arguments[0].message.deleted?[]:$1" } ] } // { // // MessageStore caching internals // // Module 819525 // find: "e.getOrCreate=function(t)", // replacement: [ // // { // // // DEBUG: log getOrCreate return values from MessageStore caching internals // // match: /getOrCreate=function(.+?)return/, // // replace: "getOrCreate=function$1console.log('getOrCreate',n);return" // // } // ] // } ] });