2022-12-25 23:35:19 +00:00
|
|
|
import { ChatGPTAPIBrowser, ChatResponse } from "chatgpt";
|
2023-01-06 23:59:52 +00:00
|
|
|
import { LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
2023-01-07 21:58:30 +00:00
|
|
|
import { CHATGPT_TIMEOUT, MATRIX_DEFAULT_PREFIX_REPLY, MATRIX_DEFAULT_PREFIX, MATRIX_BLACKLIST, MATRIX_WHITELIST} from "./env.js";
|
2023-01-07 18:17:13 +00:00
|
|
|
import { RelatesTo, MessageEvent, StoredConversation, StoredConversationConfig } from "./interfaces.js";
|
2023-01-06 23:59:52 +00:00
|
|
|
import { sendError, sendThreadReply } from "./utils.js";
|
2022-12-25 23:35:19 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
export default class CommandHandler {
|
2023-01-06 20:30:45 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
// Variables so we can cache the bot's display name and ID for command matching later.
|
|
|
|
private displayName: string;
|
|
|
|
private userId: string;
|
|
|
|
private localpart: string;
|
2023-01-05 16:54:37 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
constructor(private client: MatrixClient, private chatGPT:ChatGPTAPIBrowser) {}
|
2022-12-29 11:11:52 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
public async start() {
|
|
|
|
await this.prepareProfile(); // Populate the variables above (async)
|
|
|
|
this.client.on("room.message", this.onMessage.bind(this)); // Set up the event handler
|
|
|
|
}
|
|
|
|
|
|
|
|
private async prepareProfile() {
|
|
|
|
this.userId = await this.client.getUserId();
|
|
|
|
this.localpart = new UserID(this.userId).localpart;
|
|
|
|
try {
|
|
|
|
const profile = await this.client.getUserProfile(this.userId);
|
|
|
|
if (profile && profile['displayname']) this.displayName = profile['displayname'];
|
|
|
|
} catch (e) {
|
|
|
|
// Non-fatal error - we'll just log it and move on.
|
|
|
|
LogService.warn("CommandHandler", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-01-07 00:30:09 +00:00
|
|
|
* Run when `message` room event is received. The bot only sends a message if needed.
|
2023-01-06 23:59:52 +00:00
|
|
|
* @returns Room event handler, which itself returnings nothing
|
|
|
|
*/
|
2023-01-07 18:17:13 +00:00
|
|
|
private async onMessage(roomId: string, event: MessageEvent) {
|
2023-01-06 23:59:52 +00:00
|
|
|
try {
|
2023-01-07 00:30:09 +00:00
|
|
|
if (event.sender === this.userId) return; // Ignore ourselves
|
2023-01-06 23:59:52 +00:00
|
|
|
if (Date.now() - event.origin_server_ts > 10000) return; // Ignore old messages
|
|
|
|
const relatesTo: RelatesTo | undefined = event.content["m.relates_to"];
|
|
|
|
if ((relatesTo !== undefined) && (relatesTo["rel_type"] === "m.replace")) return; // Ignore edits
|
2023-01-07 21:58:30 +00:00
|
|
|
if (MATRIX_BLACKLIST !== undefined){
|
|
|
|
if (MATRIX_BLACKLIST.split(" ").find(b => event.sender.endsWith(b))) return; // Ignore if on blacklist if set
|
|
|
|
}
|
|
|
|
if (MATRIX_WHITELIST !== undefined){
|
|
|
|
if (!MATRIX_WHITELIST.split(" ").find(w => event.sender.endsWith(w))) return; // Ignore if not on whitelist if set
|
|
|
|
}
|
2023-01-06 23:59:52 +00:00
|
|
|
const rootEventId: string = (relatesTo !== undefined && relatesTo.event_id !== undefined) ? relatesTo.event_id : event.event_id;
|
|
|
|
const storedValue: string = await this.client.storageProvider.readValue('gpt-' + rootEventId)
|
|
|
|
const storedConversation: StoredConversation = (storedValue !== undefined) ? JSON.parse(storedValue) : undefined;
|
|
|
|
const config: StoredConversationConfig = (storedConversation !== undefined && storedConversation.config !== undefined) ? storedConversation.config : {};
|
|
|
|
const MATRIX_PREFIX: string = (config.MATRIX_PREFIX === undefined) ? MATRIX_DEFAULT_PREFIX : config.MATRIX_PREFIX
|
|
|
|
const MATRIX_PREFIX_REPLY:boolean = (config.MATRIX_PREFIX_REPLY === undefined) ? MATRIX_DEFAULT_PREFIX_REPLY : config.MATRIX_PREFIX_REPLY
|
2022-12-25 23:35:19 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
const shouldBePrefixed: boolean = ((MATRIX_PREFIX) && (relatesTo === undefined)) || (MATRIX_PREFIX_REPLY && (relatesTo !== undefined));
|
|
|
|
const prefixes = [MATRIX_PREFIX, `${this.localpart}:`, `${this.displayName}:`, `${this.userId}:`];
|
2023-01-07 18:17:13 +00:00
|
|
|
if ((relatesTo !== undefined) && !MATRIX_PREFIX_REPLY) {
|
|
|
|
if(relatesTo.event_id !== undefined){
|
|
|
|
const rootEvent: MessageEvent = await this.client.getEvent(roomId, relatesTo.event_id) // relatesTo is root event.
|
|
|
|
const rootPrefixUsed = prefixes.find(p => rootEvent.content.body.startsWith(p));
|
|
|
|
if (!rootPrefixUsed) return; // Ignore unrelated threads
|
|
|
|
} else {
|
|
|
|
// reply not a thread, we don't currently support looking back for a prefix
|
|
|
|
return; // Ignore if no relatesTo EventID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const prefixUsed: string = prefixes.find(p => event.content.body.startsWith(p));
|
2023-01-06 23:59:52 +00:00
|
|
|
if (shouldBePrefixed && !prefixUsed) return; // Ignore without prefix if prefixed
|
|
|
|
await Promise.all([this.client.sendReadReceipt(roomId, event.event_id), this.client.setTyping(roomId, true, 10000)]);
|
2022-12-22 14:57:34 +00:00
|
|
|
|
2023-01-06 23:59:52 +00:00
|
|
|
const trimLength: number = shouldBePrefixed ? prefixUsed.length : 0
|
|
|
|
const question: string = event.content.body.slice(trimLength).trimStart();
|
|
|
|
|
|
|
|
if ((question === undefined) || !question) {
|
|
|
|
await sendError(this.client, "Error with question: " + question, roomId, event.event_id);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let result: ChatResponse
|
|
|
|
if (storedConversation !== undefined) {
|
|
|
|
result = await this.chatGPT.sendMessage(question, {
|
|
|
|
timeoutMs: CHATGPT_TIMEOUT,
|
|
|
|
conversationId: storedConversation.conversationId,
|
|
|
|
parentMessageId: storedConversation.messageId
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
result = await this.chatGPT.sendMessage(question, {timeoutMs: CHATGPT_TIMEOUT});
|
2022-12-09 09:41:17 +00:00
|
|
|
}
|
2023-01-06 23:59:52 +00:00
|
|
|
|
|
|
|
await Promise.all([this.client.setTyping(roomId, false, 500), sendThreadReply(this.client, `${result.response}`, roomId, rootEventId)]);
|
|
|
|
|
|
|
|
await this.client.storageProvider.storeValue('gpt-' + rootEventId, JSON.stringify({
|
|
|
|
conversationId: result.conversationId,
|
|
|
|
messageId: result.messageId,
|
|
|
|
config: ((storedConversation !== undefined && storedConversation.config !== undefined) ? storedConversation.config : {}),
|
|
|
|
}));
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
await sendError(this.client, "Bot error, terminating.", roomId, event.event_id);
|
2022-12-09 09:41:17 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-22 14:57:34 +00:00
|
|
|
}
|