mirror of
https://github.com/matrixgpt/matrix-chatgpt-bot.git
synced 2024-10-01 01:25:41 -04:00
commit
953a3dcb43
@ -1,8 +1,6 @@
|
||||
FROM satantime/puppeteer-node:19-slim
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
RUN mkdir -p /usr/src/app/Downloads
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
@ -19,7 +17,6 @@ RUN apt update -qq \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /src/*.deb
|
||||
|
||||
# Install app dependencies
|
||||
COPY package*.json ./
|
||||
RUN yarn install --frozen-lockfile --production && yarn cache clean
|
||||
|
||||
@ -31,4 +28,4 @@ VOLUME /storage
|
||||
ENV DATA_PATH="/storage"
|
||||
|
||||
# We run a fake display and run our script using Xvfb
|
||||
CMD xvfb-run --server-args="-screen 0 1024x768x24" yarn start
|
||||
CMD xvfb-run --server-args="-screen 0 1024x768x16" yarn start
|
152
src/handlers.ts
152
src/handlers.ts
@ -1,8 +1,8 @@
|
||||
import { ChatGPTAPIBrowser, ChatResponse } from "chatgpt";
|
||||
import { LogService, MatrixClient, UserID, } from "matrix-bot-sdk";
|
||||
import { CHATGPT_TIMEOUT, MATRIX_DEFAULT_PREFIX_REPLY, MATRIX_DEFAULT_PREFIX, MATRIX_BLACKLIST, MATRIX_WHITELIST, MATRIX_RICH_TEXT, MATRIX_PREFIX_DM} from "./env.js";
|
||||
import { ChatGPTAPIBrowser } from "chatgpt";
|
||||
import { LogService, MatrixClient, UserID } from "matrix-bot-sdk";
|
||||
import { MATRIX_DEFAULT_PREFIX_REPLY, MATRIX_DEFAULT_PREFIX, MATRIX_BLACKLIST, MATRIX_WHITELIST, MATRIX_RICH_TEXT, MATRIX_PREFIX_DM } from "./env.js";
|
||||
import { RelatesTo, MessageEvent, StoredConversation, StoredConversationConfig } from "./interfaces.js";
|
||||
import { sendError, sendThreadReply } from "./utils.js";
|
||||
import { sendChatGPTMessage, sendError, sendThreadReply } from "./utils.js";
|
||||
|
||||
export default class CommandHandler {
|
||||
|
||||
@ -11,7 +11,7 @@ export default class CommandHandler {
|
||||
private userId: string;
|
||||
private localpart: string;
|
||||
|
||||
constructor(private client: MatrixClient, private chatGPT:ChatGPTAPIBrowser) {}
|
||||
constructor(private client: MatrixClient, private chatGPT: ChatGPTAPIBrowser) { }
|
||||
|
||||
public async start() {
|
||||
await this.prepareProfile(); // Populate the variables above (async)
|
||||
@ -22,85 +22,105 @@ export default class CommandHandler {
|
||||
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'];
|
||||
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);
|
||||
LogService.warn("CommandHandler", e); // Non-fatal error - we'll just log it and move on.
|
||||
}
|
||||
}
|
||||
|
||||
private shouldIgnore(event: MessageEvent): boolean {
|
||||
if (event.sender === this.userId) return true; // Ignore ourselves
|
||||
if (MATRIX_BLACKLIST && MATRIX_BLACKLIST.split(" ").find(b => event.sender.endsWith(b))) return true; // Ignore if on blacklist if set
|
||||
if (MATRIX_WHITELIST && !MATRIX_WHITELIST.split(" ").find(w => event.sender.endsWith(w))) return true; // Ignore if not on whitelist if set
|
||||
if (Date.now() - event.origin_server_ts > 10000) return true; // Ignore old messages
|
||||
if (event.content["m.relates_to"]?.["rel_type"] === "m.replace") return true; // Ignore edits
|
||||
return false;
|
||||
}
|
||||
|
||||
private getRootEventId(event: MessageEvent): string {
|
||||
const relatesTo: RelatesTo | undefined = event.content["m.relates_to"];
|
||||
const isReplyOrThread: boolean = (relatesTo === undefined)
|
||||
return (!isReplyOrThread && relatesTo.event_id !== undefined) ? relatesTo.event_id : event.event_id;
|
||||
}
|
||||
|
||||
private async getStoredConversation(rootEventId: string): Promise<StoredConversation> {
|
||||
const storedValue: string = await this.client.storageProvider.readValue('gpt-' + rootEventId)
|
||||
return (storedValue !== undefined) ? JSON.parse(storedValue) : undefined;
|
||||
}
|
||||
|
||||
private getConfig(storedConversation: StoredConversation): StoredConversationConfig {
|
||||
return (storedConversation !== undefined && storedConversation.config !== undefined) ? storedConversation.config : {};
|
||||
}
|
||||
|
||||
private async shouldBePrefixed(config: StoredConversationConfig, roomId: string, event: MessageEvent): Promise<boolean> {
|
||||
const relatesTo: RelatesTo | undefined = event.content["m.relates_to"];
|
||||
const isReplyOrThread: boolean = (relatesTo === undefined);
|
||||
const isDm: boolean = this.client.dms.isDm(roomId);
|
||||
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
|
||||
let shouldBePrefixed: boolean = (MATRIX_PREFIX && isReplyOrThread) || (MATRIX_PREFIX_REPLY && !isReplyOrThread);
|
||||
if (!MATRIX_PREFIX_DM && isDm) shouldBePrefixed=false
|
||||
const prefixes = [MATRIX_PREFIX, `${this.localpart}:`, `${this.displayName}:`, `${this.userId}:`];
|
||||
if (!isReplyOrThread && !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 && !(!MATRIX_PREFIX_DM && isDm)) return false; // Ignore unrelated threads or certain dms
|
||||
} else { // reply not thread, iterating for a prefix not implemented
|
||||
return false; // Ignore if no relatesTo EventID
|
||||
}
|
||||
}
|
||||
const prefixUsed: string = prefixes.find(p => event.content.body.startsWith(p));
|
||||
if (shouldBePrefixed && !prefixUsed) return false; // Ignore without prefix if prefixed
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getBodyWithoutPrefix(event: MessageEvent, config: StoredConversationConfig, shouldBePrefixed: boolean): Promise<string> {
|
||||
const MATRIX_PREFIX: string = (config.MATRIX_PREFIX === undefined) ? MATRIX_DEFAULT_PREFIX : config.MATRIX_PREFIX
|
||||
const prefixUsed: string = [MATRIX_PREFIX, `${this.localpart}:`, `${this.displayName}:`, `${this.userId}:`].find(p => event.content.body.startsWith(p));
|
||||
const trimLength = (shouldBePrefixed && prefixUsed) ? prefixUsed.length : 0;
|
||||
return event.content.body.slice(trimLength).trimStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run when `message` room event is received. The bot only sends a message if needed.
|
||||
* @returns Room event handler, which itself returnings nothing
|
||||
* @returns Room event handler, which itself returns nothing
|
||||
*/
|
||||
private async onMessage(roomId: string, event: MessageEvent) {
|
||||
try {
|
||||
if (event.sender === this.userId) return; // Ignore ourselves
|
||||
if (Date.now() - event.origin_server_ts > 10000) return; // Ignore old messages
|
||||
const relatesTo: RelatesTo | undefined = event.content["m.relates_to"];
|
||||
const isReplyOrThread: boolean = (relatesTo === undefined)
|
||||
if ((!isReplyOrThread) && (relatesTo["rel_type"] === "m.replace")) return; // Ignore edits
|
||||
if ((MATRIX_BLACKLIST !== undefined) && MATRIX_BLACKLIST){
|
||||
if (MATRIX_BLACKLIST.split(" ").find(b => event.sender.endsWith(b))) return; // Ignore if on blacklist if set
|
||||
}
|
||||
if ((MATRIX_WHITELIST !== undefined) && MATRIX_WHITELIST){
|
||||
if (!MATRIX_WHITELIST.split(" ").find(w => event.sender.endsWith(w))) return; // Ignore if not on whitelist if set
|
||||
}
|
||||
const rootEventId: string = (!isReplyOrThread && 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
|
||||
if (this.shouldIgnore(event)) return;
|
||||
|
||||
const isDm: boolean = this.client.dms.isDm(roomId)
|
||||
let shouldBePrefixed: boolean = (MATRIX_PREFIX && isReplyOrThread) || (MATRIX_PREFIX_REPLY && !isReplyOrThread);
|
||||
if (!MATRIX_PREFIX_DM && isDm) shouldBePrefixed=false
|
||||
const prefixes = [MATRIX_PREFIX, `${this.localpart}:`, `${this.displayName}:`, `${this.userId}:`];
|
||||
if (!isReplyOrThread && !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 && !(!MATRIX_PREFIX_DM && isDm)) return; // Ignore unrelated threads or certain dms
|
||||
} 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));
|
||||
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 rootEventId = this.getRootEventId(event);
|
||||
const storedConversation = await this.getStoredConversation(rootEventId);
|
||||
const config = this.getConfig(storedConversation);
|
||||
|
||||
const trimLength: number = shouldBePrefixed ? prefixUsed.length : 0
|
||||
const question: string = event.content.body.slice(trimLength).trimStart();
|
||||
const shouldBePrefixed = await this.shouldBePrefixed(config, roomId, event)
|
||||
if (!shouldBePrefixed) return;
|
||||
|
||||
if ((question === undefined) || !question) {
|
||||
await sendError(this.client, "Error with question: " + question, roomId, event.event_id);
|
||||
await Promise.all([
|
||||
this.client.sendReadReceipt(roomId, event.event_id),
|
||||
this.client.setTyping(roomId, true, 10000)
|
||||
]);
|
||||
|
||||
const bodyWithoutPrefix = this.getBodyWithoutPrefix(event, config, shouldBePrefixed);
|
||||
if (!bodyWithoutPrefix) {
|
||||
await sendError(this.client, "Error with body: " + event.content.body, 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, roomId, rootEventId,`${result.response}`, MATRIX_RICH_TEXT)]);
|
||||
const result = await sendChatGPTMessage(this.chatGPT, await bodyWithoutPrefix, storedConversation);
|
||||
await Promise.all([
|
||||
this.client.setTyping(roomId, false, 500),
|
||||
sendThreadReply(this.client, roomId, rootEventId, `${result.response}`, MATRIX_RICH_TEXT)
|
||||
]);
|
||||
|
||||
await this.client.storageProvider.storeValue('gpt-' + rootEventId, JSON.stringify({
|
||||
conversationId: result.conversationId,
|
||||
messageId: result.messageId,
|
||||
config: ((storedConversation !== undefined && storedConversation.config !== undefined) ? storedConversation.config : {}),
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ async function main() {
|
||||
LogService.info("index", `Bot joined room ${roomId}`);
|
||||
await client.sendMessage(roomId, {
|
||||
"msgtype": "m.notice",
|
||||
"body": `👋 Hello, I'm the ChatGPT bot! Encrypted message support: ${MATRIX_ENCRYPTION }`,
|
||||
"body": `👋 Hello, I'm the ChatGPT bot! Encrypted message support: ${MATRIX_ENCRYPTION}`,
|
||||
});
|
||||
});
|
||||
|
||||
|
18
src/utils.ts
18
src/utils.ts
@ -1,6 +1,8 @@
|
||||
import { ChatGPTAPIBrowser, ChatResponse } from "chatgpt";
|
||||
import Markdown from 'markdown-it';
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { MessageEvent } from "./interfaces.js";
|
||||
import { MessageEvent, StoredConversation } from "./interfaces.js";
|
||||
import { CHATGPT_TIMEOUT } from "./env.js";
|
||||
|
||||
const md = Markdown();
|
||||
|
||||
@ -68,3 +70,17 @@ export async function sendThreadReply(client: MatrixClient, roomId: string, root
|
||||
|
||||
await client.sendEvent(roomId, "m.room.message", content);
|
||||
}
|
||||
|
||||
export async function sendChatGPTMessage(chatGPT: ChatGPTAPIBrowser, question: string, storedConversation: StoredConversation) {
|
||||
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 });
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user