/* * 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 . */ import { DataStore } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { Button, Forms, React, TextInput, useState } from "@webpack/common"; const STRING_RULES_KEY = "TextReplace_rulesString"; const REGEX_RULES_KEY = "TextReplace_rulesRegex"; type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>; interface TextReplaceProps { title: string; rulesArray: Rule[]; } const makeEmptyRule: () => Rule = () => ({ find: "", replace: "", onlyIfIncludes: "" }); const makeEmptyRuleArray = () => [makeEmptyRule()]; const settings = definePluginSettings({ replace: { type: OptionType.COMPONENT, component: () => { const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]); return ( <> ); } }, stringRules: { type: OptionType.CUSTOM, default: makeEmptyRuleArray(), }, regexRules: { type: OptionType.CUSTOM, default: makeEmptyRuleArray(), } }); function stringToRegex(str: string) { const match = str.match(/^(\/)?(.+?)(?:\/([gimsuyv]*))?$/); // Regex to match regex return match ? new RegExp( match[2], // Pattern match[3] ?.split("") // Remove duplicate flags .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos) .join("") ?? "g" ) : new RegExp(str); // Not a regex, return string } function renderFindError(find: string) { try { stringToRegex(find); return null; } catch (e) { return ( {String(e)} ); } } function Input({ initialValue, onChange, placeholder }: { placeholder: string; initialValue: string; onChange(value: string): void; }) { const [value, setValue] = useState(initialValue); return ( value !== initialValue && onChange(value)} /> ); } function TextReplace({ title, rulesArray }: TextReplaceProps) { const isRegexRules = title === "Using Regex"; async function onClickRemove(index: number) { if (index === rulesArray.length - 1) return; rulesArray.splice(index, 1); } async function onChange(e: string, index: number, key: string) { if (index === rulesArray.length - 1) { rulesArray.push(makeEmptyRule()); } rulesArray[index][key] = e; if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) { rulesArray.splice(index, 1); } } return ( <> {title} { rulesArray.map((rule, index) => onChange(e, index, "find")} /> onChange(e, index, "replace")} /> onChange(e, index, "onlyIfIncludes")} /> {isRegexRules && renderFindError(rule.find)} ) } ); } function TextReplaceTesting() { const [value, setValue] = useState(""); return ( <> Test Rules ); } function applyRules(content: string): string { if (content.length === 0) { return content; } for (const rule of settings.store.stringRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); } for (const rule of settings.store.regexRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; try { const regex = stringToRegex(rule.find); content = content.replace(regex, rule.replace.replaceAll("\\n", "\n")); } catch (e) { new Logger("TextReplace").error(`Invalid regex: ${rule.find}`); } } content = content.trim(); return content; } const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479"; export default definePlugin({ name: "TextReplace", description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server", authors: [Devs.AutumnVN, Devs.TheKodeToad], settings, onBeforeMessageSend(channelId, msg) { // Channel used for sharing rules, applying rules here would be messy if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return; msg.content = applyRules(msg.content); }, async start() { // TODO(OptionType.CUSTOM Related): Remove DataStore rules migrations once enough time has passed const oldStringRules = await DataStore.get(STRING_RULES_KEY); if (oldStringRules != null) { settings.store.stringRules = oldStringRules; await DataStore.del(STRING_RULES_KEY); } const oldRegexRules = await DataStore.get(REGEX_RULES_KEY); if (oldRegexRules != null) { settings.store.regexRules = oldRegexRules; await DataStore.del(REGEX_RULES_KEY); } } });