From cec4edf1f911fcfa77dbefe8d89065f2b6c049ec Mon Sep 17 00:00:00 2001 From: CaveMobster Date: Sat, 8 Feb 2025 22:44:38 +0100 Subject: [PATCH 1/5] Add a button for the user allowlist to the moderation tab --- src/gui/app/controllers/moderation.controller.js | 8 ++++++++ src/gui/app/templates/_moderation.html | 1 + 2 files changed, 9 insertions(+) diff --git a/src/gui/app/controllers/moderation.controller.js b/src/gui/app/controllers/moderation.controller.js index e1b84c2a3..8913500fa 100644 --- a/src/gui/app/controllers/moderation.controller.js +++ b/src/gui/app/controllers/moderation.controller.js @@ -36,5 +36,13 @@ resolveObj: {} }); }; + + $scope.showEditUserAllowlistModal = () => { + utilityService.showModal({ + component: "editUserAllowlistModal", + backdrop: true, + resolveObj: {} + }); + }; }); }()); diff --git a/src/gui/app/templates/_moderation.html b/src/gui/app/templates/_moderation.html index e421e6730..23e8f0ba2 100644 --- a/src/gui/app/templates/_moderation.html +++ b/src/gui/app/templates/_moderation.html @@ -130,6 +130,7 @@ +
Date: Sat, 8 Feb 2025 22:55:48 +0100 Subject: [PATCH 2/5] Add edit user allowlist modal --- .../modals/misc/edit-user-allowlist-modal.js | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js diff --git a/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js b/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js new file mode 100644 index 000000000..6478585ad --- /dev/null +++ b/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js @@ -0,0 +1,160 @@ +"use strict"; + +(function() { + angular.module("firebotApp") + .component("editUserAllowlistModal", { + template: ` + + + + `, + bindings: { + resolve: "<", + close: "&", + dismiss: "&" + }, + controller: function(chatModerationService, utilityService) { + const $ctrl = this; + + $ctrl.search = ""; + + $ctrl.cms = chatModerationService; + + $ctrl.$onInit = function() { + // When the component is initialized + // This is where you can start to access bindings, such as variables stored in 'resolve' + // IE $ctrl.resolve.shouldDelete or whatever + }; + + $ctrl.userHeaders = [ + { + name: "USER", + icon: "fa-user", + dataField: "text", + headerStyles: { + 'width': '375px' + }, + sortable: true, + cellTemplate: `{{data.text}}`, + cellController: () => {} + }, + { + name: "ADDED AT", + icon: "fa-calendar", + dataField: "createdAt", + sortable: true, + cellTemplate: `{{data.createdAt | prettyDate}}`, + cellController: () => {} + }, + { + headerStyles: { + 'width': '15px' + }, + cellStyles: { + 'width': '15px' + }, + sortable: false, + cellTemplate: ``, + cellController: ($scope, chatModerationService) => { + $scope.clicked = () => { + chatModerationService.removeAllowedUserByText($scope.data.text); + }; + } + } + ]; + + $ctrl.addUser = () => { + utilityService.openGetInputModal( + { + model: "", + label: "Add allowed User", + saveText: "Add", + inputPlaceholder: "Enter allowed User", + validationFn: (value) => { + return new Promise((resolve) => { + if (value == null || value.trim().length < 1 || value.trim().length > 359) { + resolve(false); + } else if (chatModerationService.chatModerationData.userAllowlist + .some(u => u.text === value.toLowerCase())) { + resolve(false); + } else { + resolve(true); + } + }); + }, + validationText: "Allowed user can't be empty and can't already exist." + + }, + (newUser) => { + chatModerationService.addAllowedUsers([newUser.trim()]); + }); + }; + + $ctrl.showImportModal = () => { + utilityService.showModal({ + component: "txtFileWordImportModal", + size: 'sm', + resolveObj: {}, + closeCallback: async (data) => { + const success = await chatModerationService.importUserAllowlist(data); + + if (!success) { + utilityService.showErrorModal("There was an error importing the user allowlist. Please check the log for more info."); + } + + return success; + } + }); + }; + + $ctrl.deleteAllUsers = function() { + utilityService.showConfirmationModal({ + title: "Delete All Allowed Users", + question: `Are you sure you want to delete all allowed users?`, + confirmLabel: "Delete", + confirmBtnType: "btn-danger" + }).then((confirmed) => { + if (confirmed) { + chatModerationService.removeAllAllowedUsers(); + } + }); + }; + } + }); +}()); \ No newline at end of file From 68d1381aa6a50ca9b7a99bdd32233e0ea7ee0620 Mon Sep 17 00:00:00 2001 From: CaveMobster Date: Sun, 9 Feb 2025 11:06:25 +0100 Subject: [PATCH 3/5] Add data processing for the user allowlist --- .../moderation/chat-moderation-manager.ts | 137 +++++++++++++++--- .../app/services/chat-moderation.service.js | 30 +++- 2 files changed, 146 insertions(+), 21 deletions(-) diff --git a/src/backend/chat/moderation/chat-moderation-manager.ts b/src/backend/chat/moderation/chat-moderation-manager.ts index 91e81eef5..ad0b25651 100644 --- a/src/backend/chat/moderation/chat-moderation-manager.ts +++ b/src/backend/chat/moderation/chat-moderation-manager.ts @@ -23,8 +23,9 @@ export interface BannedRegularExpressions { regularExpressions: ModerationTerm[]; } -export interface UrlAllowList { - urls: ModerationTerm[] +export interface AllowList { + urls: ModerationTerm[]; + users: ModerationTerm[]; } export interface ModerationImportRequest { @@ -59,7 +60,10 @@ export interface ChatModerationSettings { class ChatModerationManager { bannedWords: BannedWords = { words: [] }; bannedRegularExpressions: BannedRegularExpressions = { regularExpressions: [] }; - urlAllowlist: UrlAllowList = { urls: [] }; + allowlist: AllowList = { + urls: [], + users: [] + }; chatModerationSettings: ChatModerationSettings = { bannedWordList: { enabled: false, @@ -129,6 +133,22 @@ class ChatModerationManager { return await this.importUrlAllowlist(request); }); + frontendCommunicator.on("chat-moderation:add-allowed-users", (users: string[]): boolean => { + return this.addAllowedUsers(users); + }); + + frontendCommunicator.on("chat-moderation:remove-allowed-user", (userText: string): boolean => { + return this.removeAllowedUser(userText); + }); + + frontendCommunicator.on("chat-moderation:remove-all-allowed-users", (): boolean => { + return this.removeAllAllowedUsers(); + }); + + frontendCommunicator.onAsync("chat-moderation:import-user-allowlist", async (request: ModerationImportRequest) => { + return await this.importUserAllowlist(request); + }); + frontendCommunicator.on("chat-moderation:update-chat-moderation-settings", (settings: ChatModerationSettings): boolean => { return this.saveChatModerationSettings(settings); }); @@ -138,7 +158,8 @@ class ChatModerationManager { settings: this.chatModerationSettings, bannedWords: this.bannedWords.words, bannedRegularExpressions: this.bannedRegularExpressions.regularExpressions, - urlAllowlist: this.urlAllowlist.urls + urlAllowlist: this.allowlist.urls, + userAllowlist: this.allowlist.users }; }); } @@ -269,22 +290,22 @@ class ChatModerationManager { return success; } - - // URL Allow List - - private getUrlAllowlistDb(): JsonDB { + private getAllowlistDb(): JsonDB { return profileManager.getJsonDbInProfile("/chat/moderation/url-allowlist", false); } + + // URL Allow List + private getUrlAllowlist(): string[] { - if (!this.urlAllowlist || !this.urlAllowlist.urls) { + if (!this.allowlist || !this.allowlist.urls) { return []; } - return this.urlAllowlist.urls.map(u => u.text); + return this.allowlist.urls.map(u => u.text); } private addAllowedUrls(urls: string[]): boolean { - this.urlAllowlist.urls = this.urlAllowlist.urls.concat(urls.map((u) => { + this.allowlist.urls = this.allowlist.urls.concat(urls.map((u) => { return { text: u, createdAt: new Date().valueOf() @@ -294,12 +315,12 @@ class ChatModerationManager { } private removeAllowedUrl(urlText: string): boolean { - this.urlAllowlist.urls = this.urlAllowlist.urls.filter(u => u.text.toLowerCase() !== urlText); + this.allowlist.urls = this.allowlist.urls.filter(u => u.text.toLowerCase() !== urlText); return this.saveUrlAllowlist(); } private removeAllAllowedUrls(): boolean { - this.urlAllowlist.urls = []; + this.allowlist.urls = []; return this.saveUrlAllowlist(); } @@ -334,7 +355,7 @@ class ChatModerationManager { let success = false; try { - this.getUrlAllowlistDb().push("/", this.urlAllowlist); + this.getAllowlistDb().push("/", this.allowlist); success = true; } catch (error) { if (error.name === 'DatabaseError') { @@ -342,11 +363,87 @@ class ChatModerationManager { } } - frontendCommunicator.send("chat-moderation:url-allowlist-updated", this.urlAllowlist.urls); + frontendCommunicator.send("chat-moderation:url-allowlist-updated", this.allowlist.urls); return success; } + // User Allowlist + + private getUserAllowlist(): string[] { + if (!this.allowlist || !this.allowlist.users) { + return []; + } + return this.allowlist.users.map(u => u.text); + } + + private addAllowedUsers(users: string[]): boolean { + this.allowlist.users = this.allowlist.users.concat(users.map((u) => { + return { + text: u, + createdAt: new Date().valueOf() + }; + })); + return this.saveUserAllowlist(); + } + + private removeAllowedUser(userText: string): boolean { + this.allowlist.users = this.allowlist.users.filter(u => u.text.toLowerCase() !== userText.toLowerCase()); + return this.saveUserAllowlist(); + } + + private removeAllAllowedUsers(): boolean { + this.allowlist.users = []; + return this.saveUserAllowlist(); + } + + private async importUserAllowlist(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 users", err); + return false; + } + + let users: string[] = []; + if (delimiter === 'newline') { + users = contents.replace(/\r\n/g, "\n").split("\n"); + } else if (delimiter === "comma") { + users = contents.split(","); + } else if (delimiter === "space") { + users = contents.split(" "); + } + + this.allowlist.users.forEach(user => { + users = users.filter(u => u.toLowerCase() !== user.text.toLowerCase()); + }); + + if (users?.length) { + this.addAllowedUsers(users); + } + + return true; + } + + private saveUserAllowlist(): boolean { + let success = false; + + try { + this.getAllowlistDb().push("/", this.allowlist); + success = true; + } catch (error) { + if (error.name === 'DatabaseError') { + logger.error("Error saving user allowlist data", error); + } + } + + frontendCommunicator.send("chat-moderation:user-allowlist-updated", this.allowlist.users); + + return success; + } // Moderation Settings @@ -496,9 +593,9 @@ class ChatModerationManager { this.bannedRegularExpressions = regularExpressions; } - const allowlist: UrlAllowList = this.getUrlAllowlistDb().getData("/"); + const allowlist: AllowList = this.getAllowlistDb().getData("/"); if (allowlist && Object.keys(allowlist).length > 0) { - this.urlAllowlist = allowlist; + this.allowlist = allowlist; } } catch (error) { if (error.name === 'DatabaseError') { @@ -569,11 +666,11 @@ class ChatModerationManager { const settings = this.chatModerationSettings.urlModeration; let outputMessage = settings.outputMessage || ""; - + let userAllowed = this.getUserAllowlist().find(u => u.toLowerCase() === chatMessage.username.toLowerCase()); let disallowedUrlFound = false; // If the urlAllowlist is empty, ANY URL is disallowed - if (this.urlAllowlist.urls.length === 0) { + if (this.allowlist.urls.length === 0) { disallowedUrlFound = true; } else { const urlsFound = message.match(regex); @@ -593,7 +690,7 @@ class ChatModerationManager { } } - if (disallowedUrlFound) { + if (disallowedUrlFound && !userAllowed) { if (settings.viewTime && settings.viewTime.enabled) { const viewer = await viewerDatabase.getViewerByUsername(chatMessage.username); diff --git a/src/gui/app/services/chat-moderation.service.js b/src/gui/app/services/chat-moderation.service.js index da7fa0b5c..da8c8190d 100644 --- a/src/gui/app/services/chat-moderation.service.js +++ b/src/gui/app/services/chat-moderation.service.js @@ -39,7 +39,10 @@ bannedRegularExpressions: [], /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ModerationTerm[]} */ - urlAllowlist: [] + urlAllowlist: [], + + /** @type {import("../../../backend/chat/moderation/chat-moderation-manager").ModerationTerm[]} */ + userAllowlist: [] }; service.loadChatModerationData = () => { @@ -107,6 +110,27 @@ return await backendCommunicator.fireEventAsync("chat-moderation:import-url-allowlist", request); }; + service.addAllowedUsers = (users) => { + const normalizedUsers = users + .filter(u => u != null && u.trim().length > 0 && u.trim().length < 360) + .map(u => u.trim()); + + backendCommunicator.fireEvent("chat-moderation:add-allowed-users", normalizedUsers); + }; + + service.removeAllowedUserByText = (text) => { + backendCommunicator.fireEvent("chat-moderation:remove-allowed-user", text); + }; + + service.removeAllAllowedUsers = () => { + backendCommunicator.fireEvent("chat-moderation:remove-all-allowed-users"); + }; + + /** @param {import("../../../backend/chat/moderation/chat-moderation-manager").BannedWordImportRequest} request */ + service.importUserAllowlist = async (request) => { + return await backendCommunicator.fireEventAsync("chat-moderation:import-user-allowlist", request); + }; + service.registerPermitCommand = () => { backendCommunicator.fireEvent("registerPermitCommand"); }; @@ -131,6 +155,10 @@ service.chatModerationData.urlAllowlist = urls; }); + backendCommunicator.on("chat-moderation:user-allowlist-updated", (users) => { + service.chatModerationData.userAllowlist = users; + }); + return service; }); }()); \ No newline at end of file From ed76d12c271342dc852b9140e11d3a691eaee7e5 Mon Sep 17 00:00:00 2001 From: CaveMobster Date: Tue, 11 Feb 2025 12:45:33 +0100 Subject: [PATCH 4/5] Use the viewer search modal to add new users --- .../moderation/chat-moderation-manager.ts | 89 ++++++++----------- .../modals/misc/edit-user-allowlist-modal.js | 75 ++++------------ .../app/services/chat-moderation.service.js | 17 +--- 3 files changed, 59 insertions(+), 122 deletions(-) diff --git a/src/backend/chat/moderation/chat-moderation-manager.ts b/src/backend/chat/moderation/chat-moderation-manager.ts index ad0b25651..7030d1b48 100644 --- a/src/backend/chat/moderation/chat-moderation-manager.ts +++ b/src/backend/chat/moderation/chat-moderation-manager.ts @@ -15,6 +15,19 @@ export interface ModerationTerm { createdAt: number; } +export interface ModerationUser { + id: string; + username: string; + displayName: string; +} + +export interface AllowedUser { + id: string; + username: string; + displayName: string; + createdAt: number; +} + export interface BannedWords { words: ModerationTerm[]; } @@ -25,7 +38,7 @@ export interface BannedRegularExpressions { export interface AllowList { urls: ModerationTerm[]; - users: ModerationTerm[]; + users: AllowedUser[]; } export interface ModerationImportRequest { @@ -133,22 +146,18 @@ class ChatModerationManager { return await this.importUrlAllowlist(request); }); - frontendCommunicator.on("chat-moderation:add-allowed-users", (users: string[]): boolean => { - return this.addAllowedUsers(users); + frontendCommunicator.on("chat-moderation:add-allowed-user", (user: ModerationUser): boolean => { + return this.addAllowedUser(user); }); - frontendCommunicator.on("chat-moderation:remove-allowed-user", (userText: string): boolean => { - return this.removeAllowedUser(userText); + frontendCommunicator.on("chat-moderation:remove-allowed-user", (id: string): boolean => { + return this.removeAllowedUser(id); }); frontendCommunicator.on("chat-moderation:remove-all-allowed-users", (): boolean => { return this.removeAllAllowedUsers(); }); - frontendCommunicator.onAsync("chat-moderation:import-user-allowlist", async (request: ModerationImportRequest) => { - return await this.importUserAllowlist(request); - }); - frontendCommunicator.on("chat-moderation:update-chat-moderation-settings", (settings: ChatModerationSettings): boolean => { return this.saveChatModerationSettings(settings); }); @@ -374,21 +383,30 @@ class ChatModerationManager { if (!this.allowlist || !this.allowlist.users) { return []; } - return this.allowlist.users.map(u => u.text); + return this.allowlist.users.map(u => u.username); } - private addAllowedUsers(users: string[]): boolean { - this.allowlist.users = this.allowlist.users.concat(users.map((u) => { - return { - text: u, - createdAt: new Date().valueOf() - }; - })); + private addAllowedUser(user: ModerationUser): boolean { + if (!this.allowlist.users) { + this.allowlist.users = []; + } + + if (this.allowlist.users.find(u => u.id === user.id)) { + return; + } + + this.allowlist.users.push({ + id: user.id, + username: user.username, + displayName: user.displayName, + createdAt: new Date().valueOf() + }); + return this.saveUserAllowlist(); } - private removeAllowedUser(userText: string): boolean { - this.allowlist.users = this.allowlist.users.filter(u => u.text.toLowerCase() !== userText.toLowerCase()); + private removeAllowedUser(id: string): boolean { + this.allowlist.users = this.allowlist.users.filter(u => u.id !== id.toLowerCase()); return this.saveUserAllowlist(); } @@ -397,37 +415,6 @@ class ChatModerationManager { return this.saveUserAllowlist(); } - private async importUserAllowlist(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 users", err); - return false; - } - - let users: string[] = []; - if (delimiter === 'newline') { - users = contents.replace(/\r\n/g, "\n").split("\n"); - } else if (delimiter === "comma") { - users = contents.split(","); - } else if (delimiter === "space") { - users = contents.split(" "); - } - - this.allowlist.users.forEach(user => { - users = users.filter(u => u.toLowerCase() !== user.text.toLowerCase()); - }); - - if (users?.length) { - this.addAllowedUsers(users); - } - - return true; - } - private saveUserAllowlist(): boolean { let success = false; @@ -666,7 +653,7 @@ class ChatModerationManager { const settings = this.chatModerationSettings.urlModeration; let outputMessage = settings.outputMessage || ""; - let userAllowed = this.getUserAllowlist().find(u => u.toLowerCase() === chatMessage.username.toLowerCase()); + let userAllowed = this.getUserAllowlist().find(u => u === chatMessage.username.toLowerCase()); let disallowedUrlFound = false; // If the urlAllowlist is empty, ANY URL is disallowed diff --git a/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js b/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js index 6478585ad..08569feef 100644 --- a/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js +++ b/src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js @@ -9,22 +9,14 @@