From b9c645a4a719b07308a3b177ead30e0113b3c77f Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Fri, 10 Jan 2025 16:58:11 -0500 Subject: [PATCH] chore: refactor chat moderation service --- grunt/pack.js | 1 - .../app-management/electron/app-helpers.js | 8 - .../electron/events/when-ready.js | 4 +- .../electron/events/windows-all-closed.js | 4 - .../chat-listeners/twitch-chat-listeners.js | 4 +- .../moderation/chat-moderation-manager.js | 476 ------------- .../moderation/chat-moderation-manager.ts | 625 ++++++++++++++++++ .../chat/moderation/moderation-service.js | 73 -- src/backend/common/common-listeners.js | 19 +- src/backend/common/profile-manager.js | 2 - ...rdsModal.js => edit-banned-words-modal.js} | 43 +- ...stModal.js => edit-url-allowlist-modal.js} | 37 +- src/gui/app/index.html | 4 +- .../app/services/chat-moderation.service.js | 154 ++--- 14 files changed, 699 insertions(+), 755 deletions(-) delete mode 100644 src/backend/chat/moderation/chat-moderation-manager.js create mode 100644 src/backend/chat/moderation/chat-moderation-manager.ts delete mode 100644 src/backend/chat/moderation/moderation-service.js rename src/gui/app/directives/modals/misc/{editBannedWordsModal.js => edit-banned-words-modal.js} (89%) rename src/gui/app/directives/modals/misc/{editUrlAllowlistModal.js => edit-url-allowlist-modal.js} (85%) diff --git a/grunt/pack.js b/grunt/pack.js index ccb0920ba..8cd0adddd 100644 --- a/grunt/pack.js +++ b/grunt/pack.js @@ -55,7 +55,6 @@ module.exports = function (grunt) { '--out="./dist/pack"', '--arch=x64', `--electronVersion=${version}`, - '--asar.unpack="moderation-service.js"', '--prune', '--overwrite', '--version-string.ProductName="Firebot v5"', diff --git a/src/backend/app-management/electron/app-helpers.js b/src/backend/app-management/electron/app-helpers.js index 0fe61ef8d..ab3198e36 100644 --- a/src/backend/app-management/electron/app-helpers.js +++ b/src/backend/app-management/electron/app-helpers.js @@ -1,16 +1,8 @@ "use strict"; exports.restartApp = () => { - const { app } = require("electron"); - try { - const chatModerationManager = require("../../chat/moderation/chat-moderation-manager"); - chatModerationManager.stopService(); - } catch (error) { - //silently fail - } - setTimeout(() => { app.relaunch({ args: process.argv.slice(1).concat(["--relaunch"]) }); app.exit(0); diff --git a/src/backend/app-management/electron/events/when-ready.js b/src/backend/app-management/electron/events/when-ready.js index ec244b169..e306acb34 100644 --- a/src/backend/app-management/electron/events/when-ready.js +++ b/src/backend/app-management/electron/events/when-ready.js @@ -159,8 +159,8 @@ exports.whenReady = async () => { startupScriptsManager.loadStartupConfig(); windowManagement.updateSplashScreenStatus("Starting chat moderation manager..."); - const chatModerationManager = require("../../../chat/moderation/chat-moderation-manager"); - chatModerationManager.load(); + const { ChatModerationManager } = require("../../../chat/moderation/chat-moderation-manager"); + ChatModerationManager.load(); windowManagement.updateSplashScreenStatus("Loading counters..."); const { CounterManager } = require("../../../counters/counter-manager"); diff --git a/src/backend/app-management/electron/events/windows-all-closed.js b/src/backend/app-management/electron/events/windows-all-closed.js index 9ab384241..598ed14ea 100644 --- a/src/backend/app-management/electron/events/windows-all-closed.js +++ b/src/backend/app-management/electron/events/windows-all-closed.js @@ -22,10 +22,6 @@ exports.windowsAllClosed = async () => { const { HotkeyManager } = require("../../../hotkeys/hotkey-manager"); HotkeyManager.unregisterAllHotkeys(); - // Stop the chat moderation service - const chatModerationManager = require("../../../chat/moderation/chat-moderation-manager"); - chatModerationManager.stopService(); - // Persist custom variables if (SettingsManager.getSetting("PersistCustomVariables")) { const customVariableManager = require("../../../common/custom-variable-manager"); diff --git a/src/backend/chat/chat-listeners/twitch-chat-listeners.js b/src/backend/chat/chat-listeners/twitch-chat-listeners.js index 99cb45bac..84d1ce90d 100644 --- a/src/backend/chat/chat-listeners/twitch-chat-listeners.js +++ b/src/backend/chat/chat-listeners/twitch-chat-listeners.js @@ -5,7 +5,7 @@ const chatCommandHandler = require("../commands/chat-command-handler"); const chatHelpers = require("../chat-helpers"); const activeUserHandler = require("./active-user-handler"); const accountAccess = require("../../common/account-access"); -const chatModerationManager = require("../moderation/chat-moderation-manager"); +const { ChatModerationManager } = require("../moderation/chat-moderation-manager"); const chatRolesManager = require("../../roles/chat-roles-manager"); const twitchEventsHandler = require("../../events/twitch-events"); const raidMessageChecker = require(".././moderation/raid-message-checker"); @@ -53,7 +53,7 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { streamerChatClient.onMessage(async (_channel, user, messageText, msg) => { const firebotChatMessage = await chatHelpers.buildFirebotChatMessage(msg, messageText); - await chatModerationManager.moderateMessage(firebotChatMessage); + await ChatModerationManager.moderateMessage(firebotChatMessage); if (firebotChatMessage.isVip === true) { chatRolesManager.addVipToVipList({ diff --git a/src/backend/chat/moderation/chat-moderation-manager.js b/src/backend/chat/moderation/chat-moderation-manager.js deleted file mode 100644 index f62850132..000000000 --- a/src/backend/chat/moderation/chat-moderation-manager.js +++ /dev/null @@ -1,476 +0,0 @@ -"use strict"; -const logger = require("../../logwrapper"); -const profileManager = require("../../common/profile-manager"); -const { Worker } = require("worker_threads"); -const frontendCommunicator = require("../../common/frontend-communicator"); -const rolesManager = require("../../roles/custom-roles-manager"); -const permitCommand = require("./url-permit-command"); -const utils = require("../../utility"); - -const getChatModerationSettingsDb = () => profileManager.getJsonDbInProfile("/chat/moderation/chat-moderation-settings"); -const getBannedWordsDb = () => profileManager.getJsonDbInProfile("/chat/moderation/banned-words", false); -const getBannedRegularExpressionsDb = () => profileManager.getJsonDbInProfile("/chat/moderation/banned-regular-expressions", false); -const getUrlAllowlistDb = () => profileManager.getJsonDbInProfile("/chat/moderation/url-allowlist", false); - -// default settings -let chatModerationSettings = { - bannedWordList: { - enabled: false, - exemptRoles: [], - outputMessage: "" - }, - emoteLimit: { - enabled: false, - exemptRoles: [], - max: 10, - outputMessage: "" - }, - urlModeration: { - enabled: false, - exemptRoles: [], - viewTime: { - enabled: false, - viewTimeInHours: 0 - }, - outputMessage: "" - }, - exemptRoles: [] -}; - -let bannedWords = { - words: [] -}; - -let bannedRegularExpressions = { - regularExpressions: [] -}; - -let urlAllowlist = { - urls: [] -}; - -function getBannedWordsList() { - if (!bannedWords || !bannedWords.words) { - return []; - } - return bannedWords.words.map(w => w.text); -} - -function getBannedRegularExpressionsList() { - if (!bannedRegularExpressions || !bannedRegularExpressions.regularExpressions) { - return []; - } - return bannedRegularExpressions.regularExpressions.map(r => r.text); -} - -function getUrlAllowlist() { - if (!urlAllowlist || !urlAllowlist.urls) { - return []; - } - return urlAllowlist.urls.map(u => u.text); -} - -/** - * @type Worker - */ -let moderationService = null; - -function startModerationService() { - if (moderationService != null) { - return; - } - - const twitchApi = require("../../twitch-api/api"); - const chat = require("../twitch-chat"); - - let servicePath = require("path").resolve(__dirname, "./moderation-service.js"); - - if (servicePath.includes("app.asar")) { - servicePath = servicePath.replace('app.asar', 'app.asar.unpacked'); - } - - moderationService = new Worker(servicePath); - - moderationService.on("message", async (event) => { - if (event == null) { - return; - } - switch (event.type) { - case "deleteMessage": { - if (event.messageId) { - logger.debug(event.logMessage); - await twitchApi.chat.deleteChatMessage(event.messageId); - - let outputMessage = chatModerationSettings.bannedWordList.outputMessage || ""; - if (outputMessage) { - outputMessage = outputMessage.replace("{userName}", event.username); - await chat.sendChatMessage(outputMessage); - } - } - break; - } - case "logWarn": { - logger.warn(event.logMessage, event.meta); - break; - } - } - }); - - moderationService.on("error", (code) => { - logger.warn(`Moderation worker failed with code: ${code}.`); - moderationService.unref(); - moderationService = null; - //startModerationService(); - }); - - moderationService.on("exit", (code) => { - logger.debug(`Moderation service stopped with code: ${code}.`); - }); - - moderationService.postMessage( - { - type: "bannedWordsUpdate", - words: getBannedWordsList() - } - ); - - moderationService.postMessage( - { - type: "bannedRegexUpdate", - regularExpressions: getBannedRegularExpressionsList() - } - ); - - logger.info("Finished setting up chat moderation worker."); -} - -function stopService() { - if (moderationService != null) { - moderationService.terminate(); - moderationService.unref(); - moderationService = null; - } -} - -const countEmojis = (str) => { - const re = /\p{Extended_Pictographic}/ug; - return ((str || '').match(re) || []).length; -}; - -/** - * - * @param {import("../../../types/chat").FirebotChatMessage} chatMessage - */ -async function moderateMessage(chatMessage) { - if (chatMessage == null) { - return; - } - - if ( - !chatModerationSettings.bannedWordList.enabled - && !chatModerationSettings.emoteLimit.enabled - && !chatModerationSettings.urlModeration.enabled - ) { - return; - } - - const userExemptGlobally = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, - chatModerationSettings.exemptRoles); - - if (userExemptGlobally) { - return; - } - - const twitchApi = require("../../twitch-api/api"); - const chat = require("../twitch-chat"); - - const userExemptForEmoteLimit = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.emoteLimit.exemptRoles); - if (chatModerationSettings.emoteLimit.enabled && !!chatModerationSettings.emoteLimit.max && !userExemptForEmoteLimit) { - const emoteCount = chatMessage.parts.filter(p => p.type === "emote").length; - const emojiCount = chatMessage.parts - .filter(p => p.type === "text") - .reduce((acc, part) => acc + countEmojis(part.text), 0); - if ((emoteCount + emojiCount) > chatModerationSettings.emoteLimit.max) { - await twitchApi.chat.deleteChatMessage(chatMessage.id); - - let outputMessage = chatModerationSettings.emoteLimit.outputMessage || ""; - if (outputMessage) { - outputMessage = outputMessage.replace("{userName}", chatMessage.username); - await chat.sendChatMessage(outputMessage); - } - - return; - } - } - - const userExemptForUrlModeration = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.urlModeration.exemptRoles); - if ( - chatModerationSettings.urlModeration.enabled && - !userExemptForUrlModeration && - !permitCommand.hasTemporaryPermission(chatMessage.username) && - !permitCommand.hasTemporaryPermission(chatMessage.userDisplayName.toLowerCase()) - ) { - let shouldDeleteMessage = false; - const message = chatMessage.rawText; - const regex = utils.getUrlRegex(); - - if (regex.test(message)) { - logger.debug("URL moderation: Found URL in message"); - - const settings = chatModerationSettings.urlModeration; - let outputMessage = settings.outputMessage || ""; - - let disallowedUrlFound = false; - - // If the urlAllowlist is empty, ANY URL is disallowed - if (urlAllowlist.length === 0) { - disallowedUrlFound = true; - } else { - const urlsFound = message.match(regex); - - // Go through the list of URLs found in the message... - for (let url of urlsFound) { - url = url.toLowerCase(); - - // And see if there's a matching rule in the allow list - const foundAllowlistRule = getUrlAllowlist().find(allowedUrl => url.includes(allowedUrl.toLowerCase())); - - // If there isn't, we have at least one bad URL, so we flag the message and dip out - if (!foundAllowlistRule) { - disallowedUrlFound = true; - break; - } - } - } - - if (disallowedUrlFound) { - if (settings.viewTime && settings.viewTime.enabled) { - const viewerDatabase = require('../../viewers/viewer-database'); - const viewer = await viewerDatabase.getViewerByUsername(chatMessage.username); - - const viewerViewTime = viewer?.minutesInChannel ? viewer?.minutesInChannel / 60 : 0; - const minimumViewTime = settings.viewTime.viewTimeInHours; - - if (viewerViewTime <= minimumViewTime) { - outputMessage = outputMessage.replace("{viewTime}", minimumViewTime.toString()); - - logger.debug("URL moderation: Not enough view time."); - shouldDeleteMessage = true; - } - } else { - shouldDeleteMessage = true; - } - - if (shouldDeleteMessage) { - await twitchApi.chat.deleteChatMessage(chatMessage.id); - - if (outputMessage) { - outputMessage = outputMessage.replace("{userName}", chatMessage.username); - await chat.sendChatMessage(outputMessage); - } - - return; - } - } - } - } - - const message = chatMessage.rawText; - const messageId = chatMessage.id; - const username = chatMessage.username; - moderationService.postMessage( - { - type: "moderateMessage", - message: message, - messageId: messageId, - username: username, - scanForBannedWords: chatModerationSettings.bannedWordList.enabled, - isExempt: rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.bannedWordList.exemptRoles), - maxEmotes: null - } - ); -} - -frontendCommunicator.on("chatMessageSettingsUpdate", (settings) => { - chatModerationSettings = settings; - try { - getChatModerationSettingsDb().push("/", settings); - } catch (error) { - if (error.name === 'DatabaseError') { - logger.error("Error saving chat moderation settings", error); - } - } -}); - -function saveBannedWordList() { - try { - getBannedWordsDb().push("/", bannedWords); - } catch (error) { - if (error.name === 'DatabaseError') { - logger.error("Error saving banned words data", error); - } - } - if (moderationService != null) { - moderationService.postMessage( - { - type: "bannedWordsUpdate", - words: getBannedWordsList() - } - ); - } -} - -function saveBannedRegularExpressionsList() { - try { - getBannedRegularExpressionsDb().push("/", bannedRegularExpressions); - - } catch (error) { - if (error.name === 'DatabaseError') { - logger.error("Error saving banned regular expressions data", error); - } - } - if (moderationService != null) { - moderationService.postMessage( - { - type: "bannedRegexUpdate", - regularExpressions: getBannedRegularExpressionsList() - } - ); - } -} - -function saveUrlAllowlist() { - try { - getUrlAllowlistDb().push("/", urlAllowlist); - } catch (error) { - if (error.name === 'DatabaseError') { - logger.error("Error saving URL allowlist data", error); - } - } -} - -frontendCommunicator.on("addBannedWords", (words) => { - bannedWords.words = bannedWords.words.concat(words); - saveBannedWordList(); -}); - -frontendCommunicator.on("removeBannedWord", (wordText) => { - bannedWords.words = bannedWords.words.filter(w => w.text.toLowerCase() !== wordText); - saveBannedWordList(); -}); - -frontendCommunicator.on("removeAllBannedWords", () => { - bannedWords.words = []; - saveBannedWordList(); -}); - -frontendCommunicator.on("addBannedRegularExpression", (expression) => { - bannedRegularExpressions.regularExpressions.push(expression); - saveBannedRegularExpressionsList(); -}); - -frontendCommunicator.on("removeBannedRegularExpression", (expression) => { - bannedRegularExpressions.regularExpressions = bannedRegularExpressions.regularExpressions.filter(r => r.text !== expression.text); - saveBannedRegularExpressionsList(); -}); - -frontendCommunicator.on("removeAllBannedRegularExpressions", () => { - bannedRegularExpressions.regularExpressions = []; - saveBannedRegularExpressionsList(); -}); - -frontendCommunicator.on("addAllowedUrls", (words) => { - urlAllowlist.urls = urlAllowlist.urls.concat(words); - saveUrlAllowlist(); -}); - -frontendCommunicator.on("removeAllowedUrl", (wordText) => { - urlAllowlist.urls = urlAllowlist.urls.filter(u => u.text.toLowerCase() !== wordText); - saveUrlAllowlist(); -}); - -frontendCommunicator.on("removeAllAllowedUrls", () => { - urlAllowlist.urls = []; - saveUrlAllowlist(); -}); - -frontendCommunicator.on("getChatModerationData", () => { - return { - settings: chatModerationSettings, - bannedWords: bannedWords.words, - bannedRegularExpressions: bannedRegularExpressions.regularExpressions, - urlAllowlist: urlAllowlist.urls - }; -}); - -function load() { - try { - const settings = getChatModerationSettingsDb().getData("/"); - if (settings && Object.keys(settings).length > 0) { - chatModerationSettings = settings; - if (settings.exemptRoles == null) { - settings.exemptRoles = []; - } - - if (settings.bannedWordList.exemptRoles == null) { - settings.bannedWordList.exemptRoles = []; - } - - if (settings.emoteLimit == null) { - settings.emoteLimit = { - enabled: false, - exemptRoles: [], - max: 10 - }; - } - - if (settings.emoteLimit.exemptRoles == null) { - settings.emoteLimit.exemptRoles = []; - } - - if (settings.urlModeration == null) { - settings.urlModeration = { - enabled: false, - exemptRoles: [], - viewTime: { - enabled: false, - viewTimeInHours: 0 - }, - outputMessage: "" - }; - } - - if (settings.urlModeration.exemptRoles == null) { - settings.urlModeration.exemptRoles = []; - } - - if (settings.urlModeration.enabled) { - permitCommand.registerPermitCommand(); - } - } - - const words = getBannedWordsDb().getData("/"); - if (words && Object.keys(words).length > 0) { - bannedWords = words; - } - - const regularExpressions = getBannedRegularExpressionsDb().getData("/"); - if (regularExpressions && Object.keys(regularExpressions).length > 0) { - bannedRegularExpressions = regularExpressions; - } - - const allowlist = getUrlAllowlistDb().getData("/"); - if (allowlist && Object.keys(allowlist).length > 0) { - urlAllowlist = allowlist; - } - } catch (error) { - if (error.name === 'DatabaseError') { - logger.error("Error loading chat moderation data", error); - } - } - logger.info("Attempting to setup chat moderation worker..."); - startModerationService(); -} -exports.load = load; -exports.stopService = stopService; -exports.moderateMessage = moderateMessage; \ No newline at end of file diff --git a/src/backend/chat/moderation/chat-moderation-manager.ts b/src/backend/chat/moderation/chat-moderation-manager.ts new file mode 100644 index 000000000..91e81eef5 --- /dev/null +++ b/src/backend/chat/moderation/chat-moderation-manager.ts @@ -0,0 +1,625 @@ +import { JsonDB } from "node-json-db"; +import fsp from "fs/promises"; +import logger from "../../logwrapper"; +import profileManager from "../../common/profile-manager"; +import frontendCommunicator from "../../common/frontend-communicator"; +import rolesManager from "../../roles/custom-roles-manager"; +import permitCommand from "./url-permit-command"; +import utils from "../../utility"; +import { FirebotChatMessage } from "../../../types/chat"; +import viewerDatabase from '../../viewers/viewer-database'; +import twitchApi from "../../twitch-api/api"; + +export interface ModerationTerm { + text: string; + createdAt: number; +} + +export interface BannedWords { + words: ModerationTerm[]; +} + +export interface BannedRegularExpressions { + regularExpressions: ModerationTerm[]; +} + +export interface UrlAllowList { + urls: ModerationTerm[] +} + +export interface ModerationImportRequest { + filePath: string; + delimiter: "newline" | "comma" | "space"; +} + +export interface ChatModerationSettings { + bannedWordList: { + enabled: boolean; + exemptRoles: string[]; + outputMessage?: string; + }; + emoteLimit: { + enabled: boolean; + exemptRoles: string[]; + max: number; + outputMessage?: string; + }; + urlModeration: { + enabled: boolean; + exemptRoles: string[]; + viewTime: { + enabled: boolean; + viewTimeInHours: number; + }; + outputMessage?: string; + }; + exemptRoles: string[]; +} + +class ChatModerationManager { + bannedWords: BannedWords = { words: [] }; + bannedRegularExpressions: BannedRegularExpressions = { regularExpressions: [] }; + urlAllowlist: UrlAllowList = { urls: [] }; + chatModerationSettings: ChatModerationSettings = { + bannedWordList: { + enabled: false, + exemptRoles: [], + outputMessage: "" + }, + emoteLimit: { + enabled: false, + exemptRoles: [], + max: 10, + outputMessage: "" + }, + urlModeration: { + enabled: false, + exemptRoles: [], + viewTime: { + enabled: false, + viewTimeInHours: 0 + }, + outputMessage: "" + }, + exemptRoles: [] + }; + + constructor() { + frontendCommunicator.on("chat-moderation:add-banned-words", (words: string[]): boolean => { + return this.addBannedWords(words); + }); + + frontendCommunicator.on("chat-moderation:remove-banned-word", (wordText: string): boolean => { + return this.removeBannedWord(wordText); + }); + + frontendCommunicator.on("chat-moderation:remove-all-banned-words", (): boolean => { + return this.removeAllBannedWords(); + }); + + frontendCommunicator.onAsync("chat-moderation:import-banned-words", async (request: ModerationImportRequest) => { + return await this.importBannedWordList(request); + }); + + frontendCommunicator.on("chat-moderation:add-banned-regular-expression", (expression: string): boolean => { + return this.addBannedRegularExpression(expression); + }); + + frontendCommunicator.on("chat-moderation:remove-banned-regular-expression", (expressionText: string): boolean => { + return this.removeBannedRegularExpression(expressionText); + }); + + frontendCommunicator.on("chat-moderation:remove-all-banned-regular-expressions", (): boolean => { + return this.removeAllBannedRegularExpressions(); + }); + + frontendCommunicator.on("chat-moderation:add-allowed-urls", (urls: string[]): boolean => { + return this.addAllowedUrls(urls); + }); + + frontendCommunicator.on("chat-moderation:remove-allowed-url", (urlText: string): boolean => { + return this.removeAllowedUrl(urlText); + }); + + frontendCommunicator.on("chat-moderation:remove-all-allowed-urls", (): boolean => { + return this.removeAllAllowedUrls(); + }); + + frontendCommunicator.onAsync("chat-moderation:import-url-allowlist", async (request: ModerationImportRequest) => { + return await this.importUrlAllowlist(request); + }); + + frontendCommunicator.on("chat-moderation:update-chat-moderation-settings", (settings: ChatModerationSettings): boolean => { + return this.saveChatModerationSettings(settings); + }); + + frontendCommunicator.on("chat-moderation:get-chat-moderation-data", () => { + return { + settings: this.chatModerationSettings, + bannedWords: this.bannedWords.words, + bannedRegularExpressions: this.bannedRegularExpressions.regularExpressions, + urlAllowlist: this.urlAllowlist.urls + }; + }); + } + + // Banned words + + private getBannedWordsDb(): JsonDB { + return profileManager.getJsonDbInProfile("/chat/moderation/banned-words", false); + } + + private getBannedWordsList(): string[] { + if (!this.bannedWords || !this.bannedWords.words) { + return []; + } + return this.bannedWords.words.map(w => w.text); + } + + private addBannedWords(words: string[]): boolean { + this.bannedWords.words = this.bannedWords.words.concat(words.map((w) => { + return { + text: w, + createdAt: new Date().valueOf() + }; + })); + return this.saveBannedWordList(); + } + + private removeBannedWord(wordText: string): boolean { + this.bannedWords.words = this.bannedWords.words.filter(w => w.text.toLowerCase() !== wordText); + return this.saveBannedWordList(); + } + + private removeAllBannedWords(): boolean { + this.bannedWords.words = []; + return this.saveBannedWordList(); + } + + private async importBannedWordList(request: ModerationImportRequest): Promise { + const { filePath, delimiter } = request; + let contents: string; + + try { + contents = await fsp.readFile(filePath, { encoding: "utf8" }); + } catch (err) { + logger.error("Error reading file for banned words", err); + return false; + } + + let words: string[] = []; + if (delimiter === "newline") { + words = contents.replace(/\r\n/g, "\n").split("\n"); + } else if (delimiter === "comma") { + words = contents.split(","); + } else if (delimiter === "space") { + words = contents.split(" "); + } + + if (words?.length) { + this.addBannedWords(words); + } + + return true; + } + + private saveBannedWordList(): boolean { + let success = false; + + try { + this.getBannedWordsDb().push("/", this.bannedWords); + success = true; + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error saving banned words data", error); + } + } + + frontendCommunicator.send("chat-moderation:banned-word-list-updated", this.bannedWords.words); + + return success; + } + + + // Regular Expressions + + private getBannedRegularExpressionsDb(): JsonDB { + return profileManager.getJsonDbInProfile("/chat/moderation/banned-regular-expressions", false); + } + + private getBannedRegularExpressionsList(): string[] { + if (!this.bannedRegularExpressions || !this.bannedRegularExpressions.regularExpressions) { + return []; + } + return this.bannedRegularExpressions.regularExpressions.map(r => r.text); + } + + private addBannedRegularExpression(expression: string): boolean { + this.bannedRegularExpressions.regularExpressions.push({ + text: expression, + createdAt: new Date().valueOf() + }); + return this.saveBannedRegularExpressionsList(); + } + + private removeBannedRegularExpression(expressionText: string): boolean { + this.bannedRegularExpressions.regularExpressions = this.bannedRegularExpressions.regularExpressions.filter(r => r.text !== expressionText); + return this.saveBannedRegularExpressionsList(); + } + + private removeAllBannedRegularExpressions(): boolean { + this.bannedRegularExpressions.regularExpressions = []; + return this.saveBannedRegularExpressionsList(); + } + + private saveBannedRegularExpressionsList(): boolean { + let success = false; + + try { + this.getBannedRegularExpressionsDb().push("/", this.bannedRegularExpressions); + success = true; + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error saving banned regular expressions data", error); + } + } + + frontendCommunicator.send("chat-moderation:banned-regex-list-updated", this.bannedRegularExpressions.regularExpressions); + + return success; + } + + + // URL Allow List + + private getUrlAllowlistDb(): JsonDB { + return profileManager.getJsonDbInProfile("/chat/moderation/url-allowlist", false); + } + + private getUrlAllowlist(): string[] { + if (!this.urlAllowlist || !this.urlAllowlist.urls) { + return []; + } + return this.urlAllowlist.urls.map(u => u.text); + } + + private addAllowedUrls(urls: string[]): boolean { + this.urlAllowlist.urls = this.urlAllowlist.urls.concat(urls.map((u) => { + return { + text: u, + createdAt: new Date().valueOf() + }; + })); + return this.saveUrlAllowlist(); + } + + private removeAllowedUrl(urlText: string): boolean { + this.urlAllowlist.urls = this.urlAllowlist.urls.filter(u => u.text.toLowerCase() !== urlText); + return this.saveUrlAllowlist(); + } + + private removeAllAllowedUrls(): boolean { + this.urlAllowlist.urls = []; + return this.saveUrlAllowlist(); + } + + private async importUrlAllowlist(request: ModerationImportRequest): Promise { + const { filePath, delimiter } = request; + + let contents: string; + try { + contents = await fsp.readFile(filePath, { encoding: "utf8" }); + } catch (err) { + logger.error("Error reading file for allowed URLs", err); + return false; + } + + let urls: string[] = []; + if (delimiter === 'newline') { + urls = contents.replace(/\r\n/g, "\n").split("\n"); + } else if (delimiter === "comma") { + urls = contents.split(","); + } else if (delimiter === "space") { + urls = contents.split(" "); + } + + if (urls?.length) { + this.addAllowedUrls(urls); + } + + return true; + } + + private saveUrlAllowlist(): boolean { + let success = false; + + try { + this.getUrlAllowlistDb().push("/", this.urlAllowlist); + success = true; + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error saving URL allowlist data", error); + } + } + + frontendCommunicator.send("chat-moderation:url-allowlist-updated", this.urlAllowlist.urls); + + return success; + } + + + // Moderation Settings + + private getChatModerationSettingsDb(): JsonDB { + return profileManager.getJsonDbInProfile("/chat/moderation/chat-moderation-settings"); + } + + private saveChatModerationSettings(settings: ChatModerationSettings): boolean { + let success = false; + + try { + this.chatModerationSettings = settings; + this.getChatModerationSettingsDb().push("/", this.chatModerationSettings); + success = true; + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error saving chat moderation settings", error); + } + } + + frontendCommunicator.send("chat-moderation:chat-moderation-settings-updated", this.chatModerationSettings); + + return success; + } + + + // Helper functions + + private countEmojis(str: string): number { + const re = /\p{Extended_Pictographic}/ug; + return ((str || '').match(re) || []).length; + } + + private hasBannedWord(input: string): boolean { + input = input.toLowerCase(); + return this.getBannedWordsList() + .some((word) => { + return input.split(" ").includes(word); + }); + } + + private matchesBannedRegex(input: string) { + const expressions = this.getBannedRegularExpressionsList().reduce(function(newArray, regex) { + try { + newArray.push(new RegExp(regex, "gi")); + } catch (error) { + logger.warn(`Unable to parse banned RegEx: ${regex}`, error); + } + + return newArray; + }, []); + const inputWords = input.split(" "); + + for (const exp of expressions) { + for (const word of inputWords) { + if (exp.test(word)) { + return true; + } + } + } + + return false; + } + + private async deleteMessage(messageId: string, outputMessage?: string, username?: string) { + await twitchApi.chat.deleteChatMessage(messageId); + + if (outputMessage?.length) { + outputMessage = outputMessage.replace("{userName}", username); + await twitchApi.chat.sendChatMessage(outputMessage); + } + } + + + // Public functions + + load() { + try { + const settings: ChatModerationSettings = this.getChatModerationSettingsDb().getData("/"); + if (settings && Object.keys(settings).length > 0) { + this.chatModerationSettings = settings; + if (settings.exemptRoles == null) { + settings.exemptRoles = []; + } + + if (settings.bannedWordList == null) { + settings.bannedWordList = { + enabled: false, + exemptRoles: [], + outputMessage: "" + }; + } + + if (settings.bannedWordList.exemptRoles == null) { + settings.bannedWordList.exemptRoles = []; + } + + if (settings.bannedWordList.outputMessage == null) { + settings.bannedWordList.outputMessage = ""; + } + + if (settings.emoteLimit == null) { + settings.emoteLimit = { + enabled: false, + exemptRoles: [], + max: 10, + outputMessage: "" + }; + } + + if (settings.emoteLimit.exemptRoles == null) { + settings.emoteLimit.exemptRoles = []; + } + + if (settings.emoteLimit.outputMessage == null) { + settings.emoteLimit.outputMessage = ""; + } + + if (settings.urlModeration == null) { + settings.urlModeration = { + enabled: false, + exemptRoles: [], + viewTime: { + enabled: false, + viewTimeInHours: 0 + }, + outputMessage: "" + }; + } + + if (settings.urlModeration.exemptRoles == null) { + settings.urlModeration.exemptRoles = []; + } + + if (settings.urlModeration.enabled) { + permitCommand.registerPermitCommand(); + } + } + + const words: BannedWords = this.getBannedWordsDb().getData("/"); + if (words && Object.keys(words).length > 0) { + this.bannedWords = words; + } + + const regularExpressions: BannedRegularExpressions = this.getBannedRegularExpressionsDb().getData("/"); + if (regularExpressions && Object.keys(regularExpressions).length > 0) { + this.bannedRegularExpressions = regularExpressions; + } + + const allowlist: UrlAllowList = this.getUrlAllowlistDb().getData("/"); + if (allowlist && Object.keys(allowlist).length > 0) { + this.urlAllowlist = allowlist; + } + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error loading chat moderation data", error); + } + } + } + + async moderateMessage(chatMessage: FirebotChatMessage) { + if (chatMessage == null) { + return; + } + + if (!this.chatModerationSettings.bannedWordList.enabled + && !this.chatModerationSettings.emoteLimit.enabled + && !this.chatModerationSettings.urlModeration.enabled + ) { + return; + } + + const userExemptGlobally = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, + this.chatModerationSettings.exemptRoles); + + if (userExemptGlobally) { + return; + } + + const userExemptForBannedWords = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, this.chatModerationSettings.bannedWordList.exemptRoles); + if (this.chatModerationSettings.bannedWordList.enabled + && !userExemptForBannedWords) { + const bannedWordFound = this.hasBannedWord(chatMessage.rawText); + if (bannedWordFound) { + await this.deleteMessage(chatMessage.id, this.chatModerationSettings.bannedWordList.outputMessage, chatMessage.userDisplayName); + return; + } + + const bannedRegexMatched = this.matchesBannedRegex(chatMessage.rawText); + if (bannedRegexMatched) { + await this.deleteMessage(chatMessage.id, this.chatModerationSettings.bannedWordList.outputMessage, chatMessage.userDisplayName); + return; + } + } + + const userExemptForEmoteLimit = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, this.chatModerationSettings.emoteLimit.exemptRoles); + if (this.chatModerationSettings.emoteLimit.enabled && !!this.chatModerationSettings.emoteLimit.max && !userExemptForEmoteLimit) { + const emoteCount = chatMessage.parts.filter(p => p.type === "emote").length; + const emojiCount = chatMessage.parts + .filter(p => p.type === "text") + .reduce((acc, part) => acc + this.countEmojis(part.text), 0); + if ((emoteCount + emojiCount) > this.chatModerationSettings.emoteLimit.max) { + await this.deleteMessage(chatMessage.id, this.chatModerationSettings.emoteLimit.outputMessage, chatMessage.userDisplayName); + return; + } + } + + const userExemptForUrlModeration = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, this.chatModerationSettings.urlModeration.exemptRoles); + if (this.chatModerationSettings.urlModeration.enabled + && !userExemptForUrlModeration + && !permitCommand.hasTemporaryPermission(chatMessage.username) + && !permitCommand.hasTemporaryPermission(chatMessage.userDisplayName.toLowerCase()) + ) { + let shouldDeleteMessage = false; + const message = chatMessage.rawText; + const regex = utils.getUrlRegex(); + + if (regex.test(message)) { + logger.debug("URL moderation: Found URL in message"); + + const settings = this.chatModerationSettings.urlModeration; + let outputMessage = settings.outputMessage || ""; + + let disallowedUrlFound = false; + + // If the urlAllowlist is empty, ANY URL is disallowed + if (this.urlAllowlist.urls.length === 0) { + disallowedUrlFound = true; + } else { + const urlsFound = message.match(regex); + + // Go through the list of URLs found in the message... + for (let url of urlsFound) { + url = url.toLowerCase(); + + // And see if there's a matching rule in the allow list + const foundAllowlistRule = this.getUrlAllowlist().find(allowedUrl => url.includes(allowedUrl.toLowerCase())); + + // If there isn't, we have at least one bad URL, so we flag the message and dip out + if (!foundAllowlistRule) { + disallowedUrlFound = true; + break; + } + } + } + + if (disallowedUrlFound) { + if (settings.viewTime && settings.viewTime.enabled) { + const viewer = await viewerDatabase.getViewerByUsername(chatMessage.username); + + const viewerViewTime = viewer?.minutesInChannel ? viewer?.minutesInChannel / 60 : 0; + const minimumViewTime = settings.viewTime.viewTimeInHours; + + if (viewerViewTime <= minimumViewTime) { + outputMessage = outputMessage.replace("{viewTime}", minimumViewTime.toString()); + + logger.debug("URL moderation: Not enough view time."); + shouldDeleteMessage = true; + } + } else { + shouldDeleteMessage = true; + } + + if (shouldDeleteMessage) { + await this.deleteMessage(chatMessage.id, outputMessage, chatMessage.userDisplayName); + return; + } + } + } + } + } +} + +const chatModerationManager = new ChatModerationManager(); + +export { chatModerationManager as ChatModerationManager }; \ No newline at end of file diff --git a/src/backend/chat/moderation/moderation-service.js b/src/backend/chat/moderation/moderation-service.js deleted file mode 100644 index a59a935a8..000000000 --- a/src/backend/chat/moderation/moderation-service.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; - -const { parentPort } = require("worker_threads"); - -let bannedWords = []; -let regularExpressions = []; - -function hasBannedWord(input) { - input = input.toLowerCase(); - return bannedWords - .some(word => { - return input.split(" ").includes(word); - }); -} - -function matchesBannedRegex(input) { - const expressions = regularExpressions.reduce(function(newArray, regex) { - try { - newArray.push(new RegExp(regex, "gi")); - } catch (error) { - parentPort.postMessage({ type: "logWarn", logMessage: `Unable to parse banned RegEx: ${regex}`, meta: error }); - } - - return newArray; - }, []); - const inputWords = input.split(" "); - - for (const exp of expressions) { - for (const word of inputWords) { - if (exp.test(word)) { - return true; - } - } - } - - return false; -} - -parentPort.on("message", event => { - if (event == null) { - return; - } - - switch (event.type) { - case "exit": - parentPort.close(); - break; - case "bannedWordsUpdate": - bannedWords = event.words; - break; - case "bannedRegexUpdate": - regularExpressions = event.regularExpressions; - break; - case "moderateMessage": { - // check for banned word - if (event.isExempt || event.message == null || event.messageId == null) { - return; - } - if (event.scanForBannedWords) { - const bannedWordFound = hasBannedWord(event.message); - if (bannedWordFound) { - parentPort.postMessage({ type: "deleteMessage", messageId: event.messageId, username: event.username }); - } else { - const bannedRegexMatched = matchesBannedRegex(event.message); - if (bannedRegexMatched) { - parentPort.postMessage({ type: "deleteMessage", messageId: event.messageId, username: event.username }); - } - } - } - break; - } - } -}); diff --git a/src/backend/common/common-listeners.js b/src/backend/common/common-listeners.js index 1a29fed3b..d2e91010c 100644 --- a/src/backend/common/common-listeners.js +++ b/src/backend/common/common-listeners.js @@ -2,9 +2,9 @@ const electron = require("electron"); const { app, ipcMain, dialog, shell } = electron; const logger = require("../logwrapper"); +const { restartApp } = require("../app-management/electron/app-helpers"); exports.setupCommonListeners = () => { - const frontendCommunicator = require("./frontend-communicator"); const profileManager = require("./profile-manager"); const { SettingsManager } = require("./settings-manager"); @@ -70,23 +70,14 @@ exports.setupCommonListeners = () => { eventsManager.triggerEvent("firebot", "category-changed", {category: category}); }); - // Front old main - - // restarts the app - ipcMain.on("restartApp", () => { - const chatModerationManager = require("../chat/moderation/chat-moderation-manager"); - chatModerationManager.stopService(); - setTimeout(() => { - app.relaunch({ args: process.argv.slice(1).concat(["--relaunch"]) }); - app.exit(0); - }, 100); - }); + frontendCommunicator.on("restartApp", () => restartApp()); - // Opens the firebot backup folder - ipcMain.on("open-backup-folder", () => { + frontendCommunicator.on("open-backup-folder", () => { shell.openPath(BackupManager.backupFolderPath); }); + // Front old main + // When we get an event from the renderer to create a new profile. ipcMain.on("createProfile", (_, profileName) => { profileManager.createNewProfile(profileName); diff --git a/src/backend/common/profile-manager.js b/src/backend/common/profile-manager.js index e64cf59bd..694e68e50 100644 --- a/src/backend/common/profile-manager.js +++ b/src/backend/common/profile-manager.js @@ -16,8 +16,6 @@ let profileToRename = null; function restartApp() { - const chatModerationManager = require("../chat/moderation/chat-moderation-manager"); - chatModerationManager.stopService(); setTimeout(() => { app.relaunch({ args: process.argv.slice(1).concat(["--relaunch"]) }); app.quit(); diff --git a/src/gui/app/directives/modals/misc/editBannedWordsModal.js b/src/gui/app/directives/modals/misc/edit-banned-words-modal.js similarity index 89% rename from src/gui/app/directives/modals/misc/editBannedWordsModal.js rename to src/gui/app/directives/modals/misc/edit-banned-words-modal.js index ed77076ab..8f901146a 100644 --- a/src/gui/app/directives/modals/misc/editBannedWordsModal.js +++ b/src/gui/app/directives/modals/misc/edit-banned-words-modal.js @@ -1,11 +1,6 @@ "use strict"; -// Basic template for a modal component, copy this and rename to build a modal. - (function() { - - const fs = require("fs"); - angular.module("firebotApp") .component("editBannedWordsModal", { template: ` @@ -88,7 +83,7 @@ $ctrl.regexHeaders = [ { name: "REGEX", - icon: "fa-quote-right", + icon: "fa-code", dataField: "text", headerStyles: { 'width': '375px' @@ -167,7 +162,7 @@ saveText: "Add", inputPlaceholder: "Enter regex", validationFn: (value) => { - return new Promise(resolve => { + return new Promise((resolve) => { if (value == null || value.trim().length < 1) { return resolve({ success: false, @@ -207,7 +202,7 @@ saveText: "Add", inputPlaceholder: "Enter banned word or phrase", validationFn: (value) => { - return new Promise(resolve => { + return new Promise((resolve) => { if (value == null || value.trim().length < 1 || value.trim().length > 359) { resolve(false); } else if (chatModerationService.chatModerationData.bannedWords @@ -231,30 +226,14 @@ component: "txtFileWordImportModal", size: 'sm', resolveObj: {}, - closeCallback: data => { - const filePath = data.filePath, - delimiter = data.delimiter; + closeCallback: async (data) => { + const success = await chatModerationService.importBannedWords(data); - let contents; - try { - contents = fs.readFileSync(filePath, { encoding: "utf8" }); - } catch (err) { - logger.error("error reading file for banned words", err); - return; + if (!success) { + utilityService.showErrorModal("There was an error importing the banned word list. Please check the log for more info."); } - let words = []; - if (delimiter === 'newline') { - words = contents.replace(/\r\n/g, "\n").split("\n"); - } else if (delimiter === "comma") { - words = contents.split(","); - } else if (delimiter === "space") { - words = contents.split(" "); - } - - if (words != null) { - chatModerationService.addBannedWords(words); - } + return success; } }); }; @@ -265,7 +244,7 @@ question: `Are you sure you want to delete all banned words and phrases?`, confirmLabel: "Delete", confirmBtnType: "btn-danger" - }).then(confirmed => { + }).then((confirmed) => { if (confirmed) { chatModerationService.removeAllBannedWords(); } @@ -278,7 +257,7 @@ question: `Are you sure you want to delete all regular expressions?`, confirmLabel: "Delete", confirmBtnType: "btn-danger" - }).then(confirmed => { + }).then((confirmed) => { if (confirmed) { chatModerationService.removeAllBannedRegularExpressions(); } @@ -286,4 +265,4 @@ }; } }); -}()); +}()); \ No newline at end of file diff --git a/src/gui/app/directives/modals/misc/editUrlAllowlistModal.js b/src/gui/app/directives/modals/misc/edit-url-allowlist-modal.js similarity index 85% rename from src/gui/app/directives/modals/misc/editUrlAllowlistModal.js rename to src/gui/app/directives/modals/misc/edit-url-allowlist-modal.js index 1c130b78c..b7dedadb4 100644 --- a/src/gui/app/directives/modals/misc/editUrlAllowlistModal.js +++ b/src/gui/app/directives/modals/misc/edit-url-allowlist-modal.js @@ -1,9 +1,6 @@ "use strict"; (function() { - - const fs = require("fs"); - angular.module("firebotApp") .component("editUrlAllowlistModal", { template: ` @@ -53,7 +50,7 @@ close: "&", dismiss: "&" }, - controller: function(chatModerationService, utilityService, logger) { + controller: function(chatModerationService, utilityService) { const $ctrl = this; $ctrl.search = ""; @@ -111,7 +108,7 @@ saveText: "Add", inputPlaceholder: "Enter allowed URL", validationFn: (value) => { - return new Promise(resolve => { + return new Promise((resolve) => { if (value == null || value.trim().length < 1 || value.trim().length > 359) { resolve(false); } else if (chatModerationService.chatModerationData.urlAllowlist @@ -135,30 +132,14 @@ component: "txtFileWordImportModal", size: 'sm', resolveObj: {}, - closeCallback: data => { - const filePath = data.filePath, - delimiter = data.delimiter; + closeCallback: async (data) => { + const success = await chatModerationService.importUrlAllowlist(data); - let contents; - try { - contents = fs.readFileSync(filePath, { encoding: "utf8" }); - } catch (err) { - logger.error("error reading file for allowed URLs", err); - return; + if (!success) { + utilityService.showErrorModal("There was an error importing the URL allowlist. Please check the log for more info."); } - let urls = []; - if (delimiter === 'newline') { - urls = contents.replace(/\r\n/g, "\n").split("\n"); - } else if (delimiter === "comma") { - urls = contents.split(","); - } else if (delimiter === "space") { - urls = contents.split(" "); - } - - if (urls != null) { - chatModerationService.addAllowedUrls(urls); - } + return success; } }); }; @@ -169,7 +150,7 @@ question: `Are you sure you want to delete all allowed URLs?`, confirmLabel: "Delete", confirmBtnType: "btn-danger" - }).then(confirmed => { + }).then((confirmed) => { if (confirmed) { chatModerationService.removeAllAllowedUrls(); } @@ -177,4 +158,4 @@ }; } }); -}()); +}()); \ No newline at end of file diff --git a/src/gui/app/index.html b/src/gui/app/index.html index 6c076dbcf..ae81d8137 100644 --- a/src/gui/app/index.html +++ b/src/gui/app/index.html @@ -282,8 +282,8 @@ - - + + diff --git a/src/gui/app/services/chat-moderation.service.js b/src/gui/app/services/chat-moderation.service.js index dc65118f3..da7fa0b5c 100644 --- a/src/gui/app/services/chat-moderation.service.js +++ b/src/gui/app/services/chat-moderation.service.js @@ -1,15 +1,13 @@ "use strict"; (function() { - - const moment = require("moment"); - angular .module("firebotApp") .factory("chatModerationService", function(backendCommunicator) { const service = {}; service.chatModerationData = { + /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ChatModerationSettings} */ settings: { bannedWordList: { enabled: false, @@ -33,124 +31,59 @@ }, exemptRoles: [] }, + + /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ModerationTerm[]} */ bannedWords: [], + + /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ModerationTerm[]} */ bannedRegularExpressions: [], + + /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ModerationTerm[]} */ urlAllowlist: [] }; service.loadChatModerationData = () => { - const data = backendCommunicator.fireEventSync("getChatModerationData"); + const data = backendCommunicator.fireEventSync("chat-moderation:get-chat-moderation-data"); if (data != null) { service.chatModerationData = data; - if (service.chatModerationData.settings.exemptRoles == null) { - service.chatModerationData.settings.exemptRoles = []; - } - - if (service.chatModerationData.settings.bannedWordList.exemptRoles == null) { - service.chatModerationData.settings.bannedWordList.exemptRoles = []; - } - - if (service.chatModerationData.settings.bannedWordList.outputMessage == null) { - service.chatModerationData.settings.bannedWordList.outputMessage = ""; - } - - if (service.chatModerationData.settings.emoteLimit == null) { - service.chatModerationData.settings.emoteLimit = { - enabled: false, - exemptRoles: [], - max: 10, - outputMessage: "" - }; - } - if (service.chatModerationData.settings.emoteLimit.exemptRoles == null) { - service.chatModerationData.settings.emoteLimit.exemptRoles = []; - } - - if (service.chatModerationData.settings.emoteLimit.outputMessage == null) { - service.chatModerationData.settings.emoteLimit.outputMessage = ""; - } - - if (service.chatModerationData.settings.urlModeration.exemptRoles == null) { - service.chatModerationData.settings.urlModeration.exemptRoles = []; - } - - if (service.chatModerationData.urlAllowlist == null) { - service.chatModerationData.urlAllowlist = []; - } } }; service.saveChatModerationSettings = () => { - backendCommunicator.fireEvent("chatMessageSettingsUpdate", service.chatModerationData.settings); + backendCommunicator.fireEvent("chat-moderation:update-chat-moderation-settings", service.chatModerationData.settings); }; service.addBannedWords = (words) => { - const normalizedWords = words .filter(w => w != null && w.trim().length > 0 && w.trim().length < 360) .map(w => w.trim().toLowerCase()); - const mapped = [...new Set(normalizedWords)].map(w => { - return { - text: w, - createdAt: moment().valueOf() - }; - }); - - service.chatModerationData.bannedWords = service.chatModerationData.bannedWords.concat(mapped); - - backendCommunicator.fireEvent("addBannedWords", mapped); + backendCommunicator.send("chat-moderation:add-banned-words", normalizedWords); }; service.addBannedRegex = (text) => { - const mapped = { - text, - createdAt: moment().valueOf() - }; - - service.chatModerationData.bannedRegularExpressions.push(mapped); - - backendCommunicator.fireEvent("addBannedRegularExpression", mapped); - }; - - service.removeBannedWordAtIndex = (index) => { - const word = service.chatModerationData.bannedWords[index]; - if (word) { - backendCommunicator.fireEvent("removeBannedWord", word.text); - service.chatModerationData.bannedWords.splice(index, 1); - } + backendCommunicator.fireEvent("chat-moderation:add-banned-regular-expression", text); }; service.removeBannedWordByText = (text) => { - const index = service.chatModerationData.bannedWords.findIndex(w => w.text === text); - if (index > -1) { - service.removeBannedWordAtIndex(index); - } + backendCommunicator.send("chat-moderation:remove-banned-word", text); }; service.removeAllBannedWords = () => { - service.chatModerationData.bannedWords = []; - backendCommunicator.fireEvent("removeAllBannedWords"); + backendCommunicator.send("chat-moderation:remove-all-banned-words"); }; - service.removeRegexAtIndex = (index) => { - const regex = service.chatModerationData.bannedRegularExpressions[index]; - if (regex) { - backendCommunicator.fireEvent("removeBannedRegularExpression", regex); - service.chatModerationData.bannedRegularExpressions.splice(index, 1); - } + /** @param {import("../../../backend/chat/moderation/chat-moderation-manager").BannedWordImportRequest} request */ + service.importBannedWords = async (request) => { + return await backendCommunicator.fireEventAsync("chat-moderation:import-banned-words", request); }; service.removeRegex = (text) => { - const index = service.chatModerationData.bannedRegularExpressions.findIndex(r => r.text === text); - if (index > -1) { - service.removeRegexAtIndex(index); - } + backendCommunicator.fireEvent("chat-moderation:remove-banned-regular-expression", text); }; service.removeAllBannedRegularExpressions = () => { - service.chatModerationData.bannedRegularExpressions = []; - backendCommunicator.fireEvent("removeAllBannedRegularExpressions"); + backendCommunicator.fireEvent("chat-moderation:remove-all-banned-regular-expressions"); }; service.addAllowedUrls = (urls) => { @@ -158,37 +91,20 @@ .filter(u => u != null && u.trim().length > 0 && u.trim().length < 360) .map(u => u.trim().toLowerCase()); - const mapped = [...new Set(normalizedUrls)].map(u => { - return { - text: u, - createdAt: moment().valueOf() - }; - }); - - service.chatModerationData.urlAllowlist = - service.chatModerationData.urlAllowlist.concat(mapped); - - backendCommunicator.fireEvent("addAllowedUrls", mapped); - }; - - service.removeAllowedUrlAtIndex = (index) => { - const word = service.chatModerationData.urlAllowlist[index]; - if (word) { - backendCommunicator.fireEvent("removeAllowedUrl", word.text); - service.chatModerationData.urlAllowlist.splice(index, 1); - } + backendCommunicator.fireEvent("chat-moderation:add-allowed-urls", normalizedUrls); }; service.removeAllowedUrlByText = (text) => { - const index = service.chatModerationData.urlAllowlist.findIndex(u => u.text === text); - if (index > -1) { - service.removeAllowedUrlAtIndex(index); - } + backendCommunicator.fireEvent("chat-moderation:remove-allowed-url", text); }; service.removeAllAllowedUrls = () => { - service.chatModerationData.urlAllowlist = []; - backendCommunicator.fireEvent("removeAllAllowedUrls"); + backendCommunicator.fireEvent("chat-moderation:remove-all-allowed-urls"); + }; + + /** @param {import("../../../backend/chat/moderation/chat-moderation-manager").BannedWordImportRequest} request */ + service.importUrlAllowlist = async (request) => { + return await backendCommunicator.fireEventAsync("chat-moderation:import-url-allowlist", request); }; service.registerPermitCommand = () => { @@ -199,6 +115,22 @@ backendCommunicator.fireEvent("unregisterPermitCommand"); }; + backendCommunicator.on("chat-moderation:chat-moderation-settings-updated", (settings) => { + service.chatModerationData.settings = settings; + }); + + backendCommunicator.on("chat-moderation:banned-word-list-updated", (terms) => { + service.chatModerationData.bannedWords = terms; + }); + + backendCommunicator.on("chat-moderation:banned-regex-list-updated", (terms) => { + service.chatModerationData.bannedRegularExpressions = terms; + }); + + backendCommunicator.on("chat-moderation:url-allowlist-updated", (urls) => { + service.chatModerationData.urlAllowlist = urls; + }); + return service; }); -}()); +}()); \ No newline at end of file