diff --git a/README.md b/README.md index 0d57c77..8e3cfc0 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ If you want to get an access token without exposing your password to this bot yo OPENAI_EMAIL= OPENAI_PASSWORD= # What type of Login it is, possibility's are google, openai, microsoft -OPENAI_LOGIN_TYPE="google" +OPENAI_LOGIN_TYPE=google # Matrix Static Settings (required, see notes) # Defaults to "https://matrix.org" diff --git a/src/handlers.ts b/src/handlers.ts index 0e36e12..40cbe97 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -1,64 +1,88 @@ import { ChatGPTAPIBrowser, ChatResponse } from "chatgpt"; -import { MatrixClient } from "matrix-bot-sdk"; +import { LogService, MatrixClient, UserID } from "matrix-bot-sdk"; import { CHATGPT_TIMEOUT, MATRIX_BOT_USERNAME, MATRIX_DEFAULT_PREFIX_REPLY, MATRIX_DEFAULT_PREFIX} from "./env.js"; import { RelatesTo, StoredConversation, StoredConversationConfig } from "./interfaces.js"; -import { isEventAMessage, sendError, sendThreadReply } from "./utils.js"; +import { sendError, sendThreadReply } from "./utils.js"; -/** - * Run when *any* room event is received. The bot only sends a message if needed. - * @param client - * @returns Room event handler, which itself returnings nothing - */ -export async function handleRoomEvent(client: MatrixClient, chatGPT: ChatGPTAPIBrowser): Promise<(roomId: string, event: any) => Promise> { - return async (roomId: string, event: any) => { - if (isEventAMessage(event)) { - try { - const relatesTo: RelatesTo | undefined = event.content["m.relates_to"]; - const rootEventId: string = (relatesTo !== undefined && relatesTo.event_id !== undefined) ? relatesTo.event_id : event.event_id; - const storedValue: string = await client.storageProvider.readValue('gpt-' + rootEventId) - const storedConversation: StoredConversation = (storedValue !== undefined) ? JSON.parse(storedValue) : undefined; +export default class CommandHandler { - 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 + // 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; - const shouldBePrefixed: boolean = ((MATRIX_PREFIX) && (relatesTo === undefined)) || (MATRIX_PREFIX_REPLY && (relatesTo !== undefined)); + constructor(private client: MatrixClient, private chatGPT:ChatGPTAPIBrowser) {} - if (event.sender === MATRIX_BOT_USERNAME) return; // Don't reply to ourself - if (Date.now() - event.origin_server_ts > 10000) return; // Don't reply to old messages - if (shouldBePrefixed && !event.content.body.startsWith(MATRIX_PREFIX)) return; // Don't reply without prefix if prefixed + 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 + } - await Promise.all([client.sendReadReceipt(roomId, event.event_id), client.setTyping(roomId, true, 10000)]); - - const trimLength: number = shouldBePrefixed ? MATRIX_DEFAULT_PREFIX.length : 0 - const question: string = event.content.body.slice(trimLength).trimStart(); - if ((question === undefined) || !question) { - await sendError(client, "Error with question: " + question, roomId, event.event_id); - return; - } + 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); + } + } - let result: ChatResponse - if (storedConversation !== undefined) { - result = await chatGPT.sendMessage(question, { - timeoutMs: CHATGPT_TIMEOUT, - conversationId: storedConversation.conversationId, - parentMessageId: storedConversation.messageId - }); - } else { - result = await chatGPT.sendMessage(question, {timeoutMs: CHATGPT_TIMEOUT}); - } + /** + * Run when *any* room message is received. The bot only sends a message if needed. + * @returns Room event handler, which itself returnings nothing + */ + private async onMessage(roomId: string, event: any) { + try { + if (event.sender === MATRIX_BOT_USERNAME) return; // Ignore ourself + 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 - await Promise.all([client.setTyping(roomId, false, 500), sendThreadReply(client, `${result.response}`, roomId, rootEventId)]); + 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 - await 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(client, "Bot error, terminating.", roomId, event.event_id); + const shouldBePrefixed: boolean = ((MATRIX_PREFIX) && (relatesTo === undefined)) || (MATRIX_PREFIX_REPLY && (relatesTo !== undefined)); + const prefixes = [MATRIX_PREFIX, `${this.localpart}:`, `${this.displayName}:`, `${this.userId}:`]; + const prefixUsed = prefixes.find(p => event.content.body.startsWith(p)); + 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)]); + + 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}); + } + + 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); } } } diff --git a/src/index.ts b/src/index.ts index bf01ea9..4f46a10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { import * as path from "path"; import { DATA_PATH, OPENAI_EMAIL, OPENAI_PASSWORD, OPENAI_LOGIN_TYPE, MATRIX_HOMESERVER_URL, MATRIX_ACCESS_TOKEN, MATRIX_AUTOJOIN, MATRIX_BOT_PASSWORD, MATRIX_BOT_USERNAME, MATRIX_ENCRYPTION } from './env.js' import { parseMatrixUsernamePretty } from './utils.js'; -import { handleRoomEvent } from './handlers.js'; +import CommandHandler from "./handlers.js" import { ChatGPTAPIBrowser } from 'chatgpt' LogService.setLogger(new RichConsoleLogger()); @@ -68,7 +68,9 @@ async function main() { }); }); - client.on("room.event", await handleRoomEvent(client, chatGPT)); + // Prepare the command handler + const commands = new CommandHandler(client, chatGPT); + await commands.start(); LogService.info("index", "Starting bot..."); await client.start() diff --git a/src/utils.ts b/src/utils.ts index 046c06b..bdc7be1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,10 +9,6 @@ export function parseMatrixUsernamePretty(matrix_username: string): string { return withoutUrl.split('@')[1] } -export function isEventAMessage(event: any): event is MessageEvent { - return event.type === 'm.room.message' -} - export async function sendError(client: MatrixClient, text: string, roomId: string, eventId: string): Promise { Promise.all([client.setTyping(roomId, false, 500), client.sendText(roomId, text), client.sendReadReceipt(roomId, eventId)]); }