diff --git a/BadWordsApp.ts b/BadWordsApp.ts index a01acc9..64f821d 100644 --- a/BadWordsApp.ts +++ b/BadWordsApp.ts @@ -1,12 +1,176 @@ import { IAppAccessors, + IConfigurationExtend, + IConfigurationModify, + IEnvironmentRead, + IHttp, ILogger, -} from '@rocket.chat/apps-engine/definition/accessors'; -import { App } from '@rocket.chat/apps-engine/definition/App'; -import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; + IMessageBuilder, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { App } from "@rocket.chat/apps-engine/definition/App"; +import { + IMessage, + IPreMessageSentModify, + IPreMessageSentPrevent, + IPreMessageUpdatedModify, +} from "@rocket.chat/apps-engine/definition/messages"; +import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; +import { ISetting } from "@rocket.chat/apps-engine/definition/settings"; +import { BadWordsCommand } from "./commands/BadWordsCommand"; +import { Settings } from "./config/Settings"; +import { CheckPreMessageSentHandler } from "./handlers/CheckPreMessageSentHandler"; +import { OnSettingsUpdatedHandler } from "./handlers/OnSettingsUpdatedHandler"; +import { PreMessageSentHandler } from "./handlers/PreMessageSentHandler"; +import { getBlockedWords } from "./lib/Settings"; +import { PreMessageSentPreventHandler } from "./handlers/PreMessageSentPreventHandler"; +import { + IUIKitInteractionHandler, + IUIKitResponse, + UIKitBlockInteractionContext, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { BlockActionHandler } from "./handlers/BlockActionHandler"; -export class BadWordsApp extends App { +export class BadWordsApp + extends App + implements + IPreMessageSentModify, + IPreMessageUpdatedModify, + IPreMessageSentPrevent, + IUIKitInteractionHandler +{ constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } + public blockedWords: Array; + + async executePreMessageSentPrevent( + message: IMessage, + read: IRead + ): Promise { + const preMessageSentPreventHandler = new PreMessageSentPreventHandler( + message, + read + ); + return preMessageSentPreventHandler.run(); + } + + async checkPreMessageSentModify( + message: IMessage, + read: IRead, + http: IHttp + ): Promise { + const checkPreMessageSentHandler = new CheckPreMessageSentHandler( + this, + message, + read, + http, + this.blockedWords + ); + return checkPreMessageSentHandler.check(); + } + + async executePreMessageSentModify( + message: IMessage, + builder: IMessageBuilder, + read: IRead, + http: IHttp, + persist: IPersistence + ): Promise { + const preMessageSentHandler = new PreMessageSentHandler( + this, + message, + builder, + read, + http, + persist, + this.blockedWords + ); + return preMessageSentHandler.run(); + } + + async checkPreMessageUpdatedModify( + message: IMessage, + read: IRead, + http: IHttp + ): Promise { + const checkPreMessageUpdatedHandler = new CheckPreMessageSentHandler( + this, + message, + read, + http, + this.blockedWords + ); + return checkPreMessageUpdatedHandler.check(); + } + + async executePreMessageUpdatedModify( + message: IMessage, + builder: IMessageBuilder, + read: IRead, + http: IHttp, + persist: IPersistence + ): Promise { + const preMessageUpdatedHandler = new PreMessageSentHandler( + this, + message, + builder, + read, + http, + persist, + this.blockedWords + ); + return preMessageUpdatedHandler.run(); + } + + async executeBlockActionHandler( + context: UIKitBlockInteractionContext, + read: IRead, + http: IHttp, + persist: IPersistence, + modify: IModify + ): Promise { + const blockActionHandler = new BlockActionHandler( + context, + read, + persist + ); + return blockActionHandler.run(); + } + + public async onSettingUpdated( + setting: ISetting, + configurationModify: IConfigurationModify, + read: IRead, + http: IHttp + ): Promise { + const settingsUpdated = new OnSettingsUpdatedHandler( + this, + setting, + configurationModify, + read, + http + ); + this.blockedWords = await settingsUpdated.run(); + } + + protected async extendConfiguration( + configuration: IConfigurationExtend, + environmentRead: IEnvironmentRead + ): Promise { + await Promise.all( + Settings.map((setting) => + configuration.settings.provideSetting(setting) + ) + ); + await configuration.slashCommands.provideSlashCommand( + new BadWordsCommand() + ); + this.blockedWords = await getBlockedWords( + environmentRead, + this.getAccessors().http + ); + } } diff --git a/app.json b/app.json index 9528939..f6091d7 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "id": "925a2e1e-d239-4ab8-a583-57203a8c3754", "version": "0.0.1", - "requiredApiVersion": "^1.4.0", + "requiredApiVersion": "^1.19.0", "iconFile": "icon.png", "author": { "name": "Rocket.Chat", @@ -11,5 +11,11 @@ "name": "BadWords", "nameSlug": "badwords", "classFile": "BadWordsApp.ts", - "description": "A simple app to block badwords and apply moderation policies in channels" + "description": "A simple app to block badwords and apply moderation policies in channels", + "implements": [ + "IPreMessageSentModify", + "IPreMessageUpdatedModify", + "IPreMessageSentPrevent", + "IUIKitInteractionHandler" + ] } \ No newline at end of file diff --git a/commands/BadWordsCommand.ts b/commands/BadWordsCommand.ts new file mode 100644 index 0000000..880d821 --- /dev/null +++ b/commands/BadWordsCommand.ts @@ -0,0 +1,173 @@ +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + ISlashCommand, + SlashCommandContext, +} from "@rocket.chat/apps-engine/definition/slashcommands"; +import { sendNotifyMessage } from "../lib/sendNotifyMessage"; +import { showModal } from "../lib/showModal"; +import { banUser, clearStats, unBanUser } from "../lib/storeStats"; + +export class BadWordsCommand implements ISlashCommand { + public command = "bad-words"; + public i18nParamsExample = "example params"; + public i18nDescription = "command description"; + public providesPreview = false; + + public async executor( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise { + const triggerId = context.getTriggerId(); + const room = context.getRoom(); + const sender = context.getSender(); + const threadId = context.getThreadId(); + + if (sender.roles.includes("admin") || sender.roles.includes("owner")) { + const args = context.getArguments(); + console.log("the args are = ", args); + switch (args[0]) { + case "stats": { + if (triggerId) { + const modal = await showModal(room, read, modify); + await modify + .getUiController() + .openModalView(modal, { triggerId }, sender); + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "Something went wrong!!" + ); + } + break; + } + + case "clear": { + if (args[1]) { + const username = args[1]; + const user = await read + .getUserReader() + .getByUsername(username); + if (user) { + clearStats(room.id, user.id, persis, read); + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + `*${user.username}'s* record has been cleared` + ); + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "User not found! Please enter a valid username." + ); + } + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "Please provide a username after clear keyword!" + ); + } + break; + } + + case "ban": { + if (args[1]) { + const username = args[1]; + const user = await read + .getUserReader() + .getByUsername(username); + if (user) { + banUser(room.id, user.id, persis, read); + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + `*${user.username}* has been banned from this room!` + ); + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "User not found! Please enter a valid username." + ); + } + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "Please provide a username after ban keyword!" + ); + } + break; + } + + case "unban": { + if (args[1]) { + const username = args[1]; + const user = await read + .getUserReader() + .getByUsername(username); + if (user) { + unBanUser(room.id, user.id, persis, read); + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + `*${user.username}* has been un-banned from this room!` + ); + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "User not found! Please enter a valid username." + ); + } + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "Please provide a username after unban keyword!" + ); + } + break; + } + } + } else { + sendNotifyMessage( + room, + sender, + threadId, + modify.getNotifier(), + "This slash command can be only used by admins and owners" + ); + } + } +} diff --git a/config/Settings.ts b/config/Settings.ts new file mode 100644 index 0000000..d9510f8 --- /dev/null +++ b/config/Settings.ts @@ -0,0 +1,101 @@ +import { + ISetting, + SettingType, +} from "@rocket.chat/apps-engine/definition/settings"; + +export enum AppSetting { + LinkToExtractBadWords = "link_to_extract_bad_words", + ApplyFilterToAllChannels = "apply_filter_to_all_channels", + ApplyFilterToDirectMessages = "apply_filter_to_direct_messages", + ApplyFilterToLivechatMessages = "apply_filter_to_livechat_messages", + ListOfBlockedWords = "list_of_blocked_words", + ListOfAllowededWords = "list_of_Allowed_words", + IncludeChannels = "include_channels", + ExcludeChannels = "exclude_channels", + SendWarningMessage = "send_warning_message", + BanUserAfterThisLimit = "ban_user_after_this_limit", +} + +export const Settings: Array = [ + { + id: AppSetting.LinkToExtractBadWords, + public: true, + type: SettingType.STRING, + packageValue: + "https://raw.githubusercontent.com/web-mech/badwords/master/lib/lang.json", + i18nLabel: "link_to_extract_bad_words", + required: false, + }, + { + id: AppSetting.ListOfBlockedWords, + public: true, + type: SettingType.STRING, + packageValue: "", + i18nLabel: "list_of_blocked_words", + required: false, + }, + { + id: AppSetting.ListOfAllowededWords, + public: true, + type: SettingType.STRING, + packageValue: "", + i18nLabel: "list_of_Allowed_words", + required: false, + }, + { + id: AppSetting.SendWarningMessage, + public: true, + type: SettingType.BOOLEAN, + packageValue: true, + i18nLabel: "send_warning_message", + required: false, + }, + { + id: AppSetting.ApplyFilterToDirectMessages, + public: true, + type: SettingType.BOOLEAN, + packageValue: true, + i18nLabel: "apply_filter_to_direct_messages", + required: false, + }, + { + id: AppSetting.ApplyFilterToLivechatMessages, + public: true, + type: SettingType.BOOLEAN, + packageValue: true, + i18nLabel: "apply_filter_to_livechat_messages", + required: false, + }, + { + id: AppSetting.ApplyFilterToAllChannels, + public: true, + type: SettingType.BOOLEAN, + packageValue: true, + i18nLabel: "apply_filter_to_all_channels", + required: false, + }, + { + id: AppSetting.IncludeChannels, + public: true, + type: SettingType.STRING, + packageValue: "", + i18nLabel: "include_channels", + required: false, + }, + { + id: AppSetting.ExcludeChannels, + public: true, + type: SettingType.STRING, + packageValue: "", + i18nLabel: "exclude_channels", + required: false, + }, + { + id: AppSetting.BanUserAfterThisLimit, + public: true, + type: SettingType.NUMBER, + packageValue: 0, + i18nLabel: "ban_user_after_this_limit", + required: false, + }, +]; diff --git a/handlers/BlockActionHandler.ts b/handlers/BlockActionHandler.ts new file mode 100644 index 0000000..5bab04f --- /dev/null +++ b/handlers/BlockActionHandler.ts @@ -0,0 +1,50 @@ +import { + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { UIKitBlockInteractionContext } from "@rocket.chat/apps-engine/definition/uikit"; +import { banUser, clearStats, unBanUser } from "../lib/storeStats"; + +export class BlockActionHandler { + constructor( + private context: UIKitBlockInteractionContext, + private read: IRead, + private persist: IPersistence + ) {} + + public async run() { + const data = this.context.getInteractionData(); + const { actionId, value } = data; + + const args = value?.split("."); + const rid = args && args[0]; + const uid = args && args[1]; + + switch (actionId) { + case "clear": { + clearStats(rid || "", uid || "", this.persist, this.read); + return { + success: true, + }; + } + + case "ban": { + banUser(rid || "", uid || "", this.persist, this.read); + return { + success: true, + }; + } + + case "unban": { + unBanUser(rid || "", uid || "", this.persist, this.read); + return { + success: true, + }; + } + } + + return { + success: false, + }; + } +} diff --git a/handlers/CheckPreMessageSentHandler.ts b/handlers/CheckPreMessageSentHandler.ts new file mode 100644 index 0000000..53c6b49 --- /dev/null +++ b/handlers/CheckPreMessageSentHandler.ts @@ -0,0 +1,69 @@ +import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IApp } from "@rocket.chat/apps-engine/definition/IApp"; +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { AppSetting } from "../config/Settings"; +import { getSettingValue } from "../lib/Settings"; + +export class CheckPreMessageSentHandler { + constructor( + private app: IApp, + private message: IMessage, + private read: IRead, + private http: IHttp, + private blockedWords: Array + ) {} + + private async checkForChannels(room: IRoom): Promise { + const { displayName: roomName } = room; + + const applyToAllChannels = await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.ApplyFilterToAllChannels + ); + + if (applyToAllChannels) { + const excludedChannels = ( + await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.ExcludeChannels + ) + ) + .split(",") + .map((e: string) => e.trim()); + return !excludedChannels.includes(roomName); + } else { + const includedChannels = ( + await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.IncludeChannels + ) + ) + .split(",") + .map((e) => e.trim()); + return includedChannels.includes(roomName); + } + } + + public async check(): Promise { + if (this.blockedWords.length == 0) return false; + + const { room, sender } = this.message; + const { type } = room; + + switch (type) { + case "d": + return await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.ApplyFilterToDirectMessages + ); + case "l": + return await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.ApplyFilterToLivechatMessages + ); + default: + return this.checkForChannels(room); + } + } +} diff --git a/handlers/OnSettingsUpdatedHandler.ts b/handlers/OnSettingsUpdatedHandler.ts new file mode 100644 index 0000000..fcb30ff --- /dev/null +++ b/handlers/OnSettingsUpdatedHandler.ts @@ -0,0 +1,27 @@ +import { + IConfigurationModify, + IHttp, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { IApp } from "@rocket.chat/apps-engine/definition/IApp"; +import { ISetting } from "@rocket.chat/apps-engine/definition/settings"; +import { getBlockedWords } from "../lib/Settings"; + +export class OnSettingsUpdatedHandler { + constructor( + private readonly app: IApp, + private readonly settings: ISetting, + private readonly configurationModify: IConfigurationModify, + private readonly read: IRead, + private readonly http: IHttp + ) {} + + public async run(): Promise> { + const blockedWords = getBlockedWords( + this.read.getEnvironmentReader(), + this.http + ); + + return blockedWords; + } +} diff --git a/handlers/PreMessageSentHandler.ts b/handlers/PreMessageSentHandler.ts new file mode 100644 index 0000000..0e7da03 --- /dev/null +++ b/handlers/PreMessageSentHandler.ts @@ -0,0 +1,81 @@ +import { + IHttp, + IMessageBuilder, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { IApp } from "@rocket.chat/apps-engine/definition/IApp"; +import { + IMessage, + IMessageAttachment, +} from "@rocket.chat/apps-engine/definition/messages"; +import { clean } from "../lib/Messages"; +import { storeStatsForOffendingUsers } from "../lib/storeStats"; +import { sendNotifyMessage } from "../lib/sendNotifyMessage"; +import { AppSetting } from "../config/Settings"; +import { getSettingValue } from "../lib/Settings"; + +export class PreMessageSentHandler { + constructor( + private app: IApp, + private message: IMessage, + private builder: IMessageBuilder, + private read: IRead, + private http: IHttp, + private persist: IPersistence, + private blockedWords: Array + ) {} + + public async run() { + if (this.blockedWords.length == 0) { + return this.message; + } + + const { text = "", room, sender, attachments = [] } = this.message; + const isAttachment = attachments.length > 0; + const messageText = text ? text : attachments[0].description; + + const { cleanText, isAnyWordProfane } = clean( + this.blockedWords, + messageText ? messageText : "" + ); + + if (!isAnyWordProfane) { + return this.message; + } + + const filteredMessage = this.builder.setRoom(room).setSender(sender); + + if (isAttachment) { + const filteredAttachment = [ + { + ...attachments[0], + description: cleanText, + }, + ]; + + filteredMessage.setAttachments(filteredAttachment); + } else { + filteredMessage.setText(cleanText); + } + + const SendWarningMessage = await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.SendWarningMessage + ); + + if (SendWarningMessage) { + sendNotifyMessage( + room, + sender, + this.message.threadId, + this.read.getNotifier(), + `*${sender.username}*, Please watch your Language!` + ); + } + + storeStatsForOffendingUsers(room, sender, this.persist, this.read); + + return filteredMessage.getMessage(); + } +} diff --git a/handlers/PreMessageSentPreventHandler.ts b/handlers/PreMessageSentPreventHandler.ts new file mode 100644 index 0000000..5975355 --- /dev/null +++ b/handlers/PreMessageSentPreventHandler.ts @@ -0,0 +1,40 @@ +import { IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; +import { AppSetting } from "../config/Settings"; +import { getStatsForOffendingUser } from "../lib/getStats"; +import { sendNotifyMessage } from "../lib/sendNotifyMessage"; +import { getSettingValue } from "../lib/Settings"; + +export class PreMessageSentPreventHandler { + constructor(private message: IMessage, private read: IRead) {} + + public async run() { + const { room, sender } = this.message; + + const record: any = await getStatsForOffendingUser( + room.id, + sender.id, + this.read + ); + const badWordsCount = record.badWordsCount; + const badWordsLimit = await getSettingValue( + this.read.getEnvironmentReader(), + AppSetting.BanUserAfterThisLimit + ); + const banned = record.banned; + + if (badWordsLimit === 0 || (badWordsLimit > badWordsCount && banned === false)) { + return false; + } + + sendNotifyMessage( + room, + sender, + this.message.threadId, + this.read.getNotifier(), + `*${sender.username}*, You are banned from this channel for using harsh language! You cannot send anymore messages in this channel until and unless you are unbanned by the admin or the owner.` + ); + + return true; + } +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..37dad58 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,12 @@ +{ + "link_to_extract_bad_words": "URL to extract Blocked Words", + "apply_filter_to_all_channels": "Apply Bad Words filtration Channels, Teams, Groups and discussions", + "list_of_blocked_words": "List of Blocked Words", + "list_of_Allowed_words": "List of Allowed Words", + "include_channels": "Include these channels", + "exclude_channels": "Exclude these channels", + "apply_filter_to_direct_messages": "Apply Bad Words filtration to Direct Messages", + "apply_filter_to_livechat_messages": "Apply Bad Words filtration to LiveChat Messages", + "send_warning_message": "Send Warning Message to users upon usage of Bad Words", + "ban_user_after_this_limit": "Ban user from sending messages after this Limit of Bad Words" +} diff --git a/icon.png b/icon.png index a7c0cc4..32e8db6 100644 Binary files a/icon.png and b/icon.png differ diff --git a/lib/Messages.ts b/lib/Messages.ts new file mode 100644 index 0000000..28e38d3 --- /dev/null +++ b/lib/Messages.ts @@ -0,0 +1,38 @@ +let regex = /[^a-zA-Z0-9|\$|\@]|\^/g; +let splitRegex = /\b/; +let placeHolder = "*"; +let replaceRegex = /\w/g; +let exclude: Array = []; + +const isProfane = (blockedWords: Array, string: string): boolean => { + return ( + blockedWords.filter((word) => { + const wordExp = new RegExp( + `\\b${word.replace(/(\W)/g, "\\$1")}\\b`, + "gi" + ); + return ( + !exclude.includes(word.toLowerCase()) && wordExp.test(string) + ); + }).length > 0 || false + ); +}; + +const replaceWord = (string: string): string => { + return string.replace(regex, "").replace(replaceRegex, placeHolder); +}; + +export const clean = (blockedWords: Array, string: string) => { + let isAnyWordProfane = false; + let cleanText: string = string + .split(splitRegex) + .map((word) => { + if (isProfane(blockedWords, word)) { + isAnyWordProfane = true; + word = replaceWord(word); + } + return word; + }) + .join(splitRegex.exec(string)![0]); + return { isAnyWordProfane, cleanText }; +}; diff --git a/lib/Settings.ts b/lib/Settings.ts new file mode 100644 index 0000000..7055ef4 --- /dev/null +++ b/lib/Settings.ts @@ -0,0 +1,55 @@ +import { + IEnvironmentRead, + IHttp, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { AppSetting } from "../config/Settings"; + +export const getBlockedWords = async ( + environmentRead: IEnvironmentRead, + http: IHttp +) => { + const badWordsURL: string = await environmentRead + .getSettings() + .getValueById(AppSetting.LinkToExtractBadWords); + + const listOfBlockedWordsSetting: string = await environmentRead + .getSettings() + .getValueById(AppSetting.ListOfBlockedWords); + + const listOfAllowedWordsSetting: string = await environmentRead + .getSettings() + .getValueById(AppSetting.ListOfAllowededWords); + + const listOfAllowedWords: Array = listOfAllowedWordsSetting + .split(",") + .map((word) => word.trim()); + + const blockedWordsFromSettings: Array = listOfBlockedWordsSetting + .split(",") + .map((word) => word.trim()); + + const fetchBlockedWordsFromURL = await http.get(badWordsURL); + const blockedWordsFromURL = JSON.parse( + fetchBlockedWordsFromURL.content || "{}" + ); + const blockedWords: Array = blockedWordsFromURL.words; + blockedWords.push(...blockedWordsFromSettings); + + listOfAllowedWords.map((word) => { + const idx = blockedWords.indexOf(word); + if (idx > -1) { + blockedWords.splice(idx, 1); + } + }); + + return blockedWords; +}; + +export const getSettingValue = async ( + environmentRead: IEnvironmentRead, + id: string +) => { + return await ( + await environmentRead.getSettings().getById(id) + ).value; +}; diff --git a/lib/getStats.ts b/lib/getStats.ts new file mode 100644 index 0000000..5ee4756 --- /dev/null +++ b/lib/getStats.ts @@ -0,0 +1,39 @@ +import { IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { + RocketChatAssociationModel, + RocketChatAssociationRecord, +} from "@rocket.chat/apps-engine/definition/metadata"; + +export async function getStatsForOffendingUser( + rid: string, + uid: string, + read: IRead +) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + const userAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + uid + ); + + const [record] = await read + .getPersistenceReader() + .readByAssociations([roomAssociation, userAssociation]); + + return record; +} + +export async function getStatsForRoom(rid: string, read: IRead) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + + const records = await read + .getPersistenceReader() + .readByAssociation(roomAssociation); + + return records; +} diff --git a/lib/sendNotifyMessage.ts b/lib/sendNotifyMessage.ts new file mode 100644 index 0000000..c6f4797 --- /dev/null +++ b/lib/sendNotifyMessage.ts @@ -0,0 +1,24 @@ +import { INotifier } from "@rocket.chat/apps-engine/definition/accessors"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; + +export async function sendNotifyMessage( + room: IRoom, + user: IUser, + threadId: string | undefined, + notify: INotifier, + message: string +) { + const builder = notify.getMessageBuilder(); + + builder + .setRoom(room) + .setUsernameAlias("Bad Words Moderator") + .setText(message); + + if (threadId) { + builder.setThreadId(threadId); + } + + await notify.notifyUser(user, builder.getMessage()); +} diff --git a/lib/showModal.ts b/lib/showModal.ts new file mode 100644 index 0000000..465ffdb --- /dev/null +++ b/lib/showModal.ts @@ -0,0 +1,62 @@ +import { IModify, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { ButtonStyle } from "@rocket.chat/apps-engine/definition/uikit"; +import { IUIKitModalViewParam } from "@rocket.chat/apps-engine/definition/uikit/UIKitInteractionResponder"; +import { getStatsForRoom } from "./getStats"; + +export async function showModal( + room: IRoom, + read: IRead, + modify: IModify +): Promise { + const block = modify.getCreator().getBlockBuilder(); + + const records: any = await getStatsForRoom(room.id, read); + console.log("Records = ", records); + if (records.length == 0) { + block.addContextBlock({ + elements: [ + block.newPlainTextObject( + `No data found for Room - ${room.displayName}` + ), + ], + }); + } else { + records.forEach((record) => { + block.addActionsBlock({ + elements: [ + block.newButtonElement({ + text: block.newPlainTextObject( + `${record.userName} - ${record.badWordsCount}` + ), + }), + block.newButtonElement({ + actionId: "clear", + text: block.newPlainTextObject("Clear"), + value: `${record.rid}.${record.uid}`, + style: ButtonStyle.PRIMARY, + }), + block.newButtonElement({ + actionId: record.banned ? "unban" : "ban", + text: record.banned + ? block.newPlainTextObject("Un-Ban") + : block.newPlainTextObject("Ban"), + value: `${record.rid}.${record.uid}`, + style: ButtonStyle.DANGER, + }), + ], + }); + }); + } + + return { + id: "122121", + title: block.newPlainTextObject( + `Offending Users in ${room.displayName}` + ), + close: block.newButtonElement({ + text: block.newPlainTextObject("Cancel"), + }), + blocks: block.getBlocks(), + }; +} diff --git a/lib/storeStats.ts b/lib/storeStats.ts new file mode 100644 index 0000000..529e1b4 --- /dev/null +++ b/lib/storeStats.ts @@ -0,0 +1,143 @@ +import { + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + RocketChatAssociationModel, + RocketChatAssociationRecord, +} from "@rocket.chat/apps-engine/definition/metadata"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { getStatsForOffendingUser } from "./getStats"; + +export async function storeStatsForOffendingUsers( + { id: rid, displayName }: IRoom, + { id: uid, username }: IUser, + persist: IPersistence, + read: IRead +) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + const userAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + uid + ); + const record: any = await getStatsForOffendingUser(rid, uid, read); + + if (!record) { + const newRecord = { + badWordsCount: 1, + banned: false, + roomName: displayName || "", + userName: username, + rid, + uid, + }; + await persist.createWithAssociations(newRecord, [ + roomAssociation, + userAssociation, + ]); + } else { + const updatedRecord = { + badWordsCount: record.badWordsCount + 1, + roomName: displayName || "", + userName: username, + rid, + uid, + banned: record.banned, + }; + await persist.updateByAssociations( + [roomAssociation, userAssociation], + updatedRecord + ); + } +} + +export async function clearStats( + rid: string, + uid: string, + persist: IPersistence, + read: IRead +) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + const userAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + uid + ); + + const record: any = await getStatsForOffendingUser(rid, uid, read); + + if (record) { + const clearRecord = { + ...record, + badWordsCount: 0, + }; + await persist.updateByAssociations( + [roomAssociation, userAssociation], + clearRecord + ); + } +} + +export async function banUser( + rid: string, + uid: string, + persist: IPersistence, + read: IRead +) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + const userAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + uid + ); + + const record: any = await getStatsForOffendingUser(rid, uid, read); + + if (record) { + const banRecord = { + ...record, + banned: true, + }; + await persist.updateByAssociations( + [roomAssociation, userAssociation], + banRecord + ); + } +} + +export async function unBanUser( + rid: string, + uid: string, + persist: IPersistence, + read: IRead +) { + const roomAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.ROOM, + rid + ); + const userAssociation = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + uid + ); + + const record: any = await getStatsForOffendingUser(rid, uid, read); + + if (record) { + const banRecord = { + ...record, + banned: false, + }; + await persist.updateByAssociations( + [roomAssociation, userAssociation], + banRecord + ); + } +}