Do some cleanup on the bot and prepare handling reactions

This commit is contained in:
MTRNord 2022-12-20 19:10:45 +01:00
parent 7f97efbe71
commit 924a2bf858
No known key found for this signature in database
3 changed files with 87 additions and 33 deletions

View File

@ -6,7 +6,8 @@
"type": "module",
"scripts": {
"build": "tsc",
"lint": "eslint src/**/*.ts"
"lint": "eslint src/**/*.ts",
"run": "node --experimental-specifier-resolution=node ./dist/index.js"
},
"author": "MTRNord <mtrnord@nordgedanken.dev>",
"license": "Apache-2.0",

View File

@ -0,0 +1,45 @@
import { RoomEvent } from "matrix-bot-sdk";
/**
* The defintion for the relation
* @category Matrix event contents
* @see ReactionEventContent
*/
export interface RelatesTo {
rel_type: "m.annotation";
event_id: string;
key: string;
}
/**
* The content definition for m.reaction events
* @category Matrix event contents
* @see ReactionEvent
*/
export interface ReactionEventContent {
"m.relates_to": RelatesTo | undefined;
}
/**
* Represents an m.reaction room event
* @category Matrix events
*/
export class ReactionEvent<T extends ReactionEventContent> extends RoomEvent<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(event: any) {
super(event);
}
/**
* Whether or not the event is redacted (or looked redacted).
*/
public get isRedacted(): boolean {
// Presume the event redacted if we're missing content
return this.content["m.relates_to"] === undefined;
}
public get relatesTo(): RelatesTo {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-extra-non-null-assertion
return this.content["m.relates_to"]!!;
}
}

View File

@ -2,15 +2,18 @@ import {
MatrixClient,
SimpleFsStorageProvider,
AutojoinRoomsMixin,
// MessageEvent,
// TextualMessageEventContent,
RustSdkCryptoStorageProvider,
LogService,
RichConsoleLogger,
MessageEvent,
MessageEventContent,
} from "matrix-bot-sdk";
import { readFile } from "fs/promises";
import { load } from "js-yaml";
import * as tf from "@tensorflow/tfjs-node";
import { Rank, Tensor } from "@tensorflow/tfjs-node";
import { TFSavedModel } from "@tensorflow/tfjs-node/dist/saved_model";
import { ReactionEvent } from "./events/ReactionEvent";
type Config = {
homeserver: string;
@ -22,51 +25,45 @@ type Config = {
warningsRoom: string;
};
type HelperMessageEvent = {
content: {
msgtype: string | undefined,
body: string | undefined
} | undefined,
sender: string
};
class Bot {
public static async createBot() {
LogService.setLogger(new RichConsoleLogger());
LogService.muteModule("Metrics");
const config = load(await readFile("./config.yaml", "utf8")) as Config;
// Add some divider for clarity after tensorflow loaded up
const line = '-'.repeat(process.stdout.columns);
console.log(line);
LogService.info("index", line);
// Check if required fields are set
if (config.homeserver == undefined && config.accessToken == undefined) {
console.error("Missing homeserver and accessToken config values");
LogService.error("index", "Missing homeserver and accessToken config values");
process.exit(1);
} else if (config.homeserver == undefined) {
console.error("Missing homeserver config value");
LogService.error("index", "Missing homeserver config value");
process.exit(1);
} else if (config.accessToken == undefined) {
console.error("Missing accessToken config value");
LogService.error("index", "Missing accessToken config value");
process.exit(1);
}
if (config.adminRoom == undefined && config.warningsRoom == undefined) {
console.error("Missing adminRoom and warningsRoom config values");
LogService.error("index", "Missing adminRoom and warningsRoom config values");
process.exit(1);
} else if (config.adminRoom == undefined) {
console.error("Missing adminRoom config value");
LogService.error("index", "Missing adminRoom config value");
process.exit(1);
} else if (config.warningsRoom == undefined) {
console.error("Missing warningsRoom config value");
LogService.error("index", "Missing warningsRoom config value");
process.exit(1);
}
if (config.adminRoom.startsWith("#")) {
console.error("adminRoom config value needs to be a roomid starting with a \"!\"");
LogService.error("index", "adminRoom config value needs to be a roomid starting with a \"!\"");
process.exit(1);
}
if (config.warningsRoom.startsWith("#")) {
console.error("warningsRoom config value needs to be a roomid starting with a \"!\"");
LogService.error("index", "warningsRoom config value needs to be a roomid starting with a \"!\"");
process.exit(1);
}
@ -92,7 +89,7 @@ class Bot {
// they can be easily added to any room.
const client = new MatrixClient(homeserverUrl, accessToken, storage, cryptoProvider);
// TODO replace with manual handling to need admin approval
// TODO: replace with manual handling to need admin approval
AutojoinRoomsMixin.setupOnClient(client);
// Join rooms as needed but crash if missing
@ -106,17 +103,29 @@ class Bot {
// Before we start the bot, register our command handler
// eslint-disable-next-line @typescript-eslint/no-misused-promises
client.on("room.message", this.handleMessage.bind(this));
// eslint-disable-next-line @typescript-eslint/no-misused-promises
client.on("room.event", this.handleEvents.bind(this));
// Now that everything is set up, start the bot. This will start the sync loop and run until killed.
client.start().then(() => console.log("Bot started!")).catch(console.error);
client.start().then(() => LogService.info("index", "Bot started!")).catch(console.error);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async handleEvents(_roomId: string, ev: any): Promise<void> {
// For now only handle reactions
const event = new ReactionEvent(ev);
if (event.sender === await this.client.getUserId()) return; // Ignore ourselves
// TODO: parse actions taken and handle them
}
// This is the command handler we registered a few lines up
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async handleMessage(roomId: string, event: HelperMessageEvent): Promise<void> {
// Don't handle unhelpful events (ones that aren't text messages, are redacted, or sent by us)
if (event.content?.msgtype !== 'm.text') return;
if (event.sender === await this.client.getUserId()) return;
private async handleMessage(roomId: string, ev: any): Promise<void> {
const event = new MessageEvent(ev);
if (event.isRedacted) return; // Ignore redacted events that come through
if (event.sender === await this.client.getUserId()) return; // Ignore ourselves
if (event.messageType !== "m.text") return; // Ignore non-text messages
if (roomId !== this.config.adminRoom && roomId !== this.config.warningsRoom) {
await this.checkSpam(event, event.content?.body ?? "", roomId);
@ -124,22 +133,22 @@ class Bot {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async checkSpam(event: HelperMessageEvent, body: string, roomId: string) {
private async checkSpam(event: MessageEvent<MessageEventContent>, body: string, roomId: string) {
// Check if spam
const data = tf.tensor([body])
const prediction: Tensor<Rank.R2> = this.model.predict(data) as Tensor<Rank.R2>;
const prediction_data: number[][] = await prediction.array();
console.log(`Checking: "${body}"`);
console.log(`Prediction: ${prediction_data.toString()}`);
LogService.info("index", `Checking: "${body}"`);
LogService.info("index", `Prediction: ${prediction_data.toString()}`);
const prediction_value = ((prediction_data[0] ?? [])[0] ?? 0);
if (prediction_value > 0.8) {
// TODO reverse resolve alias for the roomID and make pill.
// TODO: reverse resolve alias for the roomID and make pill.
const alert_event_id = await this.client.sendHtmlText(this.config.warningsRoom, `<blockquote>\n<p>${body}</p>\n</blockquote>\n<p>Above message was detected as spam. See json file for full event and use reactions to take action or no action.</p>\n<p>It was sent in ${roomId}</p>\n<p>Spam Score is: ${prediction_value.toFixed(3)}</p>\n`);
await this.client.unstableApis.addReactionToEvent(this.config.warningsRoom, alert_event_id, "🚨 Ban User");
await this.client.unstableApis.addReactionToEvent(this.config.warningsRoom, alert_event_id, "⚠️ Kick User");
await this.client.unstableApis.addReactionToEvent(this.config.warningsRoom, alert_event_id, "✅ False positive");
const eventContent = Buffer.from(JSON.stringify(event), 'utf8');
const eventContent = Buffer.from(JSON.stringify(event.raw), 'utf8');
const media = await this.client.uploadContent(eventContent, "application/json", "event.json");
await this.client.sendMessage(this.config.warningsRoom, {
msgtype: "m.file",
@ -152,8 +161,7 @@ class Bot {
url: media,
})
} else {
// const message = new MessageEvent(event);
// const textEvent = new MessageEvent<TextualMessageEventContent>(message.raw);
// const textEvent = new MessageEvent<TextualMessageEventContent>(event.raw);
//await this.client.unstableApis.addReactionToEvent(roomId, textEvent.eventId, "Classified Not Spam")
}
}