diff --git a/src/plugins/keywordNotify/README.md b/src/plugins/keywordNotify/README.md index 0a3e6e6e1..0bc61d611 100644 --- a/src/plugins/keywordNotify/README.md +++ b/src/plugins/keywordNotify/README.md @@ -1,3 +1,3 @@ # KeywordNotify -Sends a notification when a message matches any number of regex cases. \ No newline at end of file +Allows for custom regex-defined keywords to notify the user exactly how a ping would. Adds a custom inbox for viewing keywords next to the mentions. \ No newline at end of file diff --git a/src/plugins/keywordNotify/index.tsx b/src/plugins/keywordNotify/index.tsx index eb240d006..d7aeedf9a 100644 --- a/src/plugins/keywordNotify/index.tsx +++ b/src/plugins/keywordNotify/index.tsx @@ -4,40 +4,41 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import definePlugin, { OptionType } from "@utils/types"; -import { DataStore } from "@api/index"; -import { definePluginSettings } from "@api/Settings"; -import { DeleteIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import { Flex } from "@components/Flex"; -import { TextInput, useState, Forms, Button, UserStore, UserUtils, TabBar, ChannelStore, SelectedChannelStore } from "@webpack/common"; -import { useForceUpdater } from "@utils/react"; -import { findByCodeLazy, findByPropsLazy, mapMangledModuleLazy, filters } from "@webpack"; import "./style.css"; -let regexes = []; +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 { Margins } from "@utils/margins"; +import { useForceUpdater } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { Button, ChannelStore, Forms, SearchableSelect,SelectedChannelStore, TabBar, TextInput, UserStore, UserUtils, useState } from "@webpack/common"; +import { Message, User } from "discord-types/general/index.js"; + +let keywordEntries: Array<{ regex: string, listIds: Array, listType: ListType }> = []; +let currentUser: User; +let keywordLog: Array = []; const MenuHeader = findByCodeLazy(".useInDesktopNotificationCenterExperiment)()?"); -const Popout = findByPropsLazy("ItemsPopout"); +const Popout = findByCodeLazy("let{analyticsName:"); const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout"); +const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries"; +const KEYWORD_LOG_KEY = "KeywordNotify_log"; const { createMessageRecord } = findByPropsLazy("createMessageRecord", "updateMessageRecord"); - -async function setRegexes(idx: number, reg: string) { - regexes[idx] = reg; - await DataStore.set("KeywordNotify_rules", regexes); -} - -async function removeRegex(idx: number, updater: () => void) { - regexes.splice(idx, 1); - await DataStore.set("KeywordNotify_rules", regexes); +async function addKeywordEntry(updater: () => void) { + keywordEntries.push({ regex: "", listIds: [], listType: ListType.BlackList }); + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); updater(); } -async function addRegex(updater: () => void) { - regexes.push(""); - await DataStore.set("KeywordNotify_rules", regexes); +async function removeKeywordEntry(idx: number, updater: () => void) { + keywordEntries.splice(idx, 1); + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); updater(); } @@ -49,86 +50,201 @@ function safeMatchesRegex(s: string, r: string) { } } + +enum ListType { + BlackList = "BlackList", + Whitelist = "Whitelist" +} + function highlightKeywords(s: string, r: Array) { - let reg; + let regex: RegExp; try { - reg = new RegExp(r.join("|"), "g"); + regex = new RegExp(r.join("|"), "g"); } catch { return [s]; } - let matches = s.match(reg); + const matches = s.match(regex); if (!matches) return [s]; - let parts = [...matches.map((e) => { - let idx = s.indexOf(e); - let before = s.substring(0, idx); + const parts = [...matches.map(e => { + const idx = s.indexOf(e); + const before = s.substring(0, idx); s = s.substring(idx + e.length); return before; }, s), s]; return parts.map(e => [ ({e}), - matches.length ? ({matches.splice(0, 1)[0]}) : [] + matches!.length ? ({matches!.splice(0, 1)[0]}) : [] ]); } +function Collapsible({ title, children }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && children} +
+ ); +} + +function ListedIds({ listIds, setListIds }) { + const update = useForceUpdater(); + const [values] = useState(listIds); + + async function onChange(e: string, index: number) { + values[index] = e; + setListIds(values); + update(); + } + + const elements = values.map((currentValue: string, index: number) => { + return ( + +
+ onChange(e, index)} + /> +
+ +
+ ); + }); + + return ( + <> + {elements} + + ); +} + +function ListTypeSelector({ listType, setListType }) { + return ( + + ); +} + + +function KeywordEntries() { + const update = useForceUpdater(); + const [values] = useState(keywordEntries); + + async function setRegex(index: number, value: string) { + keywordEntries[index].regex = value; + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); + update(); + } + + async function setListType(index: number, value: ListType) { + keywordEntries[index].listType = value; + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); + update(); + } + + async function setListIds(index: number, value: Array) { + keywordEntries[index].listIds = value ?? []; + await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries); + update(); + } + + const elements = keywordEntries.map((entry, i) => { + return ( + <> + + +
+ setRegex(i, e)} + /> +
+ +
+ + Whitelist/Blacklist + +
+ setListIds(i, e)}/> +
+
+
+ + +
+ setListType(i, e)}/> +
+
+ + + ); + }); + + return ( + <> + {elements} +
+ + ); +} + const settings = definePluginSettings({ ignoreBots: { type: OptionType.BOOLEAN, description: "Ignore messages from bots", default: true }, - keywords: { type: OptionType.COMPONENT, description: "", - component: () => { - const update = useForceUpdater(); - const [values, setValues] = useState(regexes); - - const elements = regexes.map((a, i) => { - const setValue = (v: string) => { - let valuesCopy = [...values]; - valuesCopy[i] = v; - setValues(valuesCopy); - } - - return ( - <> - Keyword Regex {i + 1} - - -
- setRegexes(i, values[i])} - /> -
- -
- - ) - }); - - return ( - <> - {elements} -
- - ); - } - }, + component: () => + } }); export default definePlugin({ @@ -138,12 +254,12 @@ export default definePlugin({ settings, patches: [ { - find: "}_dispatch(", - replacement: { + find: "}_dispatch(", + replacement: { match: /}_dispatch\((\i),\i\){/, replace: "$&$1=$self.modify($1);" - } - }, + } + }, { find: "Messages.UNREADS_TAB_LABEL}", replacement: { @@ -168,49 +284,81 @@ export default definePlugin({ ], async start() { - regexes = await DataStore.get("KeywordNotify_rules") ?? []; - this.me = await UserUtils.getUser(UserStore.getCurrentUser().id); - this.onUpdate = ()=>null; - this.keywordLog = []; + keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? []; + currentUser = await UserUtils.getUser(UserStore.getCurrentUser().id); + this.onUpdate = () => null; - (await DataStore.get("KeywordNotify_log") ?? []).map((e) => JSON.parse(e)).forEach((e) => { + (await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => { this.addToLog(e); }); }, - applyRegexes(m) { - if (settings.store.ignoreBots && m.author.bot) - return; - + applyKeywordEntries(m: Message) { let matches = false; - if (regexes.some(r => r != "" && (safeMatchesRegex(m.content, r)))) { - matches = true; - } - for (let embed of m.embeds) { - if (regexes.some(r => r != "" && (safeMatchesRegex(embed.description, r) || safeMatchesRegex(embed.title, r)))) { + keywordEntries.forEach(entry => { + if (entry.regex === "") { + return; + } + + let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id); + if (!listed) { + const channel = ChannelStore.getChannel(m.channel_id); + if (channel != null) { + listed = entry.listIds.some(id => id === channel.guild_id); + } + } + + const whitelistMode = entry.listType === ListType.Whitelist; + if (!whitelistMode && listed) { + return; + } + if (whitelistMode && !listed) { + return; + } + + if (settings.store.ignoreBots && m.author.bot) { + if (!whitelistMode || !entry.listIds.includes(m.author.id)) { + return; + } + } + + if (safeMatchesRegex(m.content, entry.regex)) { matches = true; } - } + + for (const embed of m.embeds as any) { + if (safeMatchesRegex(embed.description, entry.regex) || safeMatchesRegex(embed.title, entry.regex)) { + matches = true; + } else if (embed.fields != null) { + for (const field of embed.fields as Array<{ name: string, value: string }>) { + if (safeMatchesRegex(field.value, entry.regex) || safeMatchesRegex(field.name, entry.regex)) { + matches = true; + } + } + } + } + }); if (matches) { - m.mentions.push(this.me); + // @ts-ignore + m.mentions.push(currentUser); - if (m.author.id != this.me.id) + if (m.author.id !== currentUser.id) this.addToLog(m); } }, - addToLog(m) { - if (m == null || this.keywordLog.some((e) => e.id == m.id)) + addToLog(m: Message) { + if (m == null || keywordLog.some(e => e.id === m.id)) return; - let thing = createMessageRecord(m); - this.keywordLog.push(thing); - this.keywordLog.sort((a, b) => b.timestamp - a.timestamp); + const thing = createMessageRecord(m); + keywordLog.push(thing); + keywordLog.sort((a, b) => b.timestamp - a.timestamp); - if (this.keywordLog.length > 50) - this.keywordLog.pop(); + if (keywordLog.length > 50) + keywordLog.pop(); this.onUpdate(); }, @@ -225,27 +373,28 @@ export default definePlugin({ }, tryKeywordMenu(setTab, onJump, closePopout) { - let header = ( - + const header = ( + ); - let channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); + const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); - let [keywordLog, setKeywordLog] = useState(this.keywordLog); + const [tempLogs, setKeywordLog] = useState(keywordLog); this.onUpdate = () => { - let newLog = [...this.keywordLog]; + const newLog = [...keywordLog]; setKeywordLog(newLog); - DataStore.set("KeywordNotify_log", newLog.map((e) => JSON.stringify(e))); + DataStore.set(KEYWORD_LOG_KEY, newLog.map(e => JSON.stringify(e))); }; - let onDelete = (m) => { - this.keywordLog = this.keywordLog.filter((e) => e.id != m.id); + const onDelete = m => { + keywordLog = keywordLog.filter(e => e.id !== m.id); this.onUpdate(); }; - let messageRender = (e, t) => { - let msg = this.renderMsg({ + const messageRender = (e, t) => { + console.log(this); + const msg = this.renderMsg({ message: e, gotoMessage: t, dismissible: true @@ -256,36 +405,42 @@ export default definePlugin({ msg.props.children[0].props.children.props.onClick = () => onDelete(e); msg.props.children[1].props.children[1].props.message.customRenderedContent = { - content: highlightKeywords(e.content, regexes) + content: highlightKeywords(e.content, keywordEntries.map(e => e.regex)) }; return [msg]; }; + /*return ( + <> +

hi uwu

+ + );*/ + return ( <> - header} renderMessage={messageRender} channel={channel} onJump={onJump} - onFetch={()=>null} + onFetch={() => null} onCloseMessage={onDelete} - loadMore={()=>null} - messages={keywordLog} - renderEmptyState={()=>null} + loadMore={() => null} + messages={tempLogs} + renderEmptyState={() => null} /> ); }, modify(e) { - if (e.type == "MESSAGE_CREATE") { - this.applyRegexes(e.message); - } else if (e.type == "LOAD_MESSAGES_SUCCESS") { + if (e.type === "MESSAGE_CREATE") { + this.applyKeywordEntries(e.message); + } else if (e.type === "LOAD_MESSAGES_SUCCESS") { for (let msg = 0; msg < e.messages.length; ++msg) { - this.applyRegexes(e.messages[msg]); + this.applyKeywordEntries(e.messages[msg]); } } return e; diff --git a/src/plugins/keywordNotify/style.css b/src/plugins/keywordNotify/style.css index b6829225d..d549b694d 100644 --- a/src/plugins/keywordNotify/style.css +++ b/src/plugins/keywordNotify/style.css @@ -3,7 +3,14 @@ } .keywordnotify-delete { - padding: 0px, + padding: 0; color: var(--primary-400); transition: color 0.2s ease-in-out; +} + +.keywordnotify-collapsible { + display: flex; + align-items: center; + padding: 8px; + cursor: pointer; } \ No newline at end of file