Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 105 additions & 20 deletions src/backend/chat/moderation/chat-moderation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand All @@ -23,8 +36,9 @@ export interface BannedRegularExpressions {
regularExpressions: ModerationTerm[];
}

export interface UrlAllowList {
urls: ModerationTerm[]
export interface AllowList {
urls: ModerationTerm[];
users: AllowedUser[];
}

export interface ModerationImportRequest {
Expand Down Expand Up @@ -59,7 +73,10 @@ export interface ChatModerationSettings {
class ChatModerationManager {
bannedWords: BannedWords = { words: [] };
bannedRegularExpressions: BannedRegularExpressions = { regularExpressions: [] };
urlAllowlist: UrlAllowList = { urls: [] };
allowlist: AllowList = {
urls: [],
users: []
};
chatModerationSettings: ChatModerationSettings = {
bannedWordList: {
enabled: false,
Expand Down Expand Up @@ -129,6 +146,18 @@ class ChatModerationManager {
return await this.importUrlAllowlist(request);
});

frontendCommunicator.on("chat-moderation:add-allowed-user", (user: ModerationUser): boolean => {
return this.addAllowedUser(user);
});

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.on("chat-moderation:update-chat-moderation-settings", (settings: ChatModerationSettings): boolean => {
return this.saveChatModerationSettings(settings);
});
Expand All @@ -138,7 +167,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
};
});
}
Expand Down Expand Up @@ -269,22 +299,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()
Expand All @@ -294,12 +324,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();
}

Expand Down Expand Up @@ -334,19 +364,73 @@ class ChatModerationManager {
let success = false;

try {
this.getUrlAllowlistDb().push("/", this.urlAllowlist);
this.getAllowlistDb().push("/", this.allowlist);
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);
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.username);
}

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(id: string): boolean {
this.allowlist.users = this.allowlist.users.filter(u => u.id !== id);
return this.saveUserAllowlist();
}

private removeAllAllowedUsers(): boolean {
this.allowlist.users = [];
return this.saveUserAllowlist();
}

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

Expand Down Expand Up @@ -496,9 +580,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') {
Expand Down Expand Up @@ -568,12 +652,13 @@ class ChatModerationManager {
logger.debug("URL moderation: Found URL in message");

const settings = this.chatModerationSettings.urlModeration;
let outputMessage = settings.outputMessage || "";
const userAllowed = this.getUserAllowlist().find(u => u === chatMessage.username.toLowerCase());

let outputMessage = settings.outputMessage || "";
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);
Expand All @@ -593,7 +678,7 @@ class ChatModerationManager {
}
}

if (disallowedUrlFound) {
if (disallowedUrlFound && !userAllowed) {
if (settings.viewTime && settings.viewTime.enabled) {
const viewer = await viewerDatabase.getViewerByUsername(chatMessage.username);

Expand Down
8 changes: 8 additions & 0 deletions src/gui/app/controllers/moderation.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,13 @@
resolveObj: {}
});
};

$scope.showEditUserAllowlistModal = () => {
utilityService.showModal({
component: "editUserAllowlistModal",
backdrop: true,
resolveObj: {}
});
};
});
}());
113 changes: 113 additions & 0 deletions src/gui/app/directives/modals/misc/edit-user-allowlist-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use strict";

(function() {
angular.module("firebotApp")
.component("editUserAllowlistModal", {
template: `
<div class="modal-header">
<button type="button" class="close" ng-click="$ctrl.dismiss()"><span>&times;</span></button>
<h4 class="modal-title">Edit User Allowlist</h4>
</div>
<div class="modal-body">
<p class="muted">Users on this list will automatically be allowed to post URLs in chat.</p>
<div class="flex flex-row justify-between my-10">
<button class="btn btn-primary" type="button" aria-haspopup="true" ng-click="$ctrl.showViewerSearchModal()">
<span class="dropdown-text"><i class="fas fa-plus-circle"></i> Add User</span>
</button>

<div class="ml-auto">
<searchbar placeholder-text="Search users..." query="$ctrl.search" class="basis-250"></searchbar>
</div>
</div>
<div>
<sortable-table
table-data-set="$ctrl.cms.chatModerationData.userAllowlist"
headers="$ctrl.userHeaders"
query="$ctrl.search"
clickable="false"
starting-sort-field="createdAt"
sort-initially-reversed="true"
page-size="5"
no-data-message="No allowed users have been saved.">
</sortable-table>
</div>
</div>
<div class="modal-footer">
<button ng-show="$ctrl.cms.chatModerationData.userAllowlist.length > 0" type="button" class="btn btn-danger pull-left" ng-click="$ctrl.deleteAllUsers()">Delete All Allowed Users</button>
</div>
`,
bindings: {
resolve: "<",
close: "&",
dismiss: "&"
},
controller: function(chatModerationService, utilityService, logger) {
const $ctrl = this;

$ctrl.search = "";

$ctrl.cms = chatModerationService;

$ctrl.userHeaders = [
{
name: "USER",
icon: "fa-user",
dataField: "username",
headerStyles: {
'width': '375px'
},
sortable: true,
cellTemplate: `{{data.displayName}}`,
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: `<i class="fal fa-trash-alt clickable" style="color:#ff3737;" ng-click="clicked()" uib-tooltip="Delete" tooltip-append-to-body="true"></i>`,
cellController: ($scope, chatModerationService) => {
$scope.clicked = () => {
chatModerationService.removeAllowedUserById($scope.data.id);
};
}
}
];

$ctrl.showViewerSearchModal = () => {
utilityService.openViewerSearchModal(
{
label: "Add User",
saveText: "Add"
},
(user) => {
chatModerationService.addAllowedUser(user);
});
};

$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();
}
});
};
}
});
}());
Loading