mirror of
https://github.com/matrixgpt/matrix-chatgpt-bot.git
synced 2024-09-27 19:45:38 +00:00
Begin dropping keyv
This commit is contained in:
parent
64fc3f2488
commit
8a6fadf729
81
src/env.ts
81
src/env.ts
@ -5,9 +5,6 @@ dotenv.config();
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
DATA_PATH,
|
DATA_PATH,
|
||||||
KEYV_BACKEND,
|
|
||||||
KEYV_URL,
|
|
||||||
KEYV_BOT_STORAGE,
|
|
||||||
/** Matrix Bot Settings */
|
/** Matrix Bot Settings */
|
||||||
MATRIX_HOMESERVER_URL,
|
MATRIX_HOMESERVER_URL,
|
||||||
MATRIX_ACCESS_TOKEN,
|
MATRIX_ACCESS_TOKEN,
|
||||||
@ -39,38 +36,60 @@ export const {
|
|||||||
CHATGPT_REVERSE_PROXY,
|
CHATGPT_REVERSE_PROXY,
|
||||||
CHATGPT_TEMPERATURE,
|
CHATGPT_TEMPERATURE,
|
||||||
} = parseEnv(process.env, {
|
} = parseEnv(process.env, {
|
||||||
DATA_PATH: { schema: z.string().default("./storage"), description: "Set to /storage/ if using docker, ./storage if running without" },
|
DATA_PATH: { schema: z.string().default("./storage"),
|
||||||
KEYV_BACKEND: { schema: z.enum(["file", "other"]).default("file"),description: "Set the Keyv backend to 'file' or 'other' if other set KEYV_URL" },
|
description: "Set to /storage/ if using docker, ./storage if running without" },
|
||||||
KEYV_URL: { schema: z.string().default(""), description: "Set Keyv backend for storage, in-memory if blank, ignored if KEYV_BACKEND set to `file`"},
|
|
||||||
KEYV_BOT_STORAGE: { schema: z.boolean().default(false), description: "Set to true to use a Keyv backend to store bot data. Uses a file if false."},
|
|
||||||
/** Matrix Bot Settings */
|
/** Matrix Bot Settings */
|
||||||
MATRIX_HOMESERVER_URL: { schema: z.string().default("https://matrix.org"), description: "Set matrix homeserver with 'https://' prefix" },
|
MATRIX_HOMESERVER_URL: { schema: z.string().default("https://matrix.org"),
|
||||||
MATRIX_ACCESS_TOKEN: { schema: z.string().optional(), description: "Set MATRIX_BOT_USERNAME & MATRIX_BOT_PASSWORD to print MATRIX_ACCESS_TOKEN or follow https://webapps.stackexchange.com/questions/131056/how-to-get-an-access-token-for-element-riot-matrix" },
|
description: "Set matrix homeserver with 'https://' prefix" },
|
||||||
MATRIX_BOT_USERNAME: { schema: z.string().optional(), description: "Set full username: eg @bot:server.com (superseded by MATRIX_ACCESS_TOKEN if set)" },
|
MATRIX_ACCESS_TOKEN: { schema: z.string().optional(),
|
||||||
MATRIX_BOT_PASSWORD: { schema: z.string().optional(), description: "Set password (superseded by MATRIX_ACCESS_TOKEN if set)" },
|
description: "Set MATRIX_BOT_USERNAME & MATRIX_BOT_PASSWORD to print MATRIX_ACCESS_TOKEN or follow https://webapps.stackexchange.com/questions/131056/how-to-get-an-access-token-for-element-riot-matrix" },
|
||||||
|
MATRIX_BOT_USERNAME: { schema: z.string().optional(),
|
||||||
|
description: "Set full username: eg @bot:server.com (superseded by MATRIX_ACCESS_TOKEN if set)" },
|
||||||
|
MATRIX_BOT_PASSWORD: { schema: z.string().optional(),
|
||||||
|
description: "Set password (superseded by MATRIX_ACCESS_TOKEN if set)" },
|
||||||
/** Matrix Bot Features */
|
/** Matrix Bot Features */
|
||||||
MATRIX_AUTOJOIN: { schema: z.boolean().default(true), description: "Set to true if you want the bot to autojoin when invited" },
|
MATRIX_AUTOJOIN: { schema: z.boolean().default(true),
|
||||||
MATRIX_ENCRYPTION: { schema: z.boolean().default(true), description: "Set to true if you want the bot to support encrypted channels" },
|
description: "Set to true if you want the bot to autojoin when invited" },
|
||||||
MATRIX_THREADS: { schema: z.boolean().default(true), description: "Set to true if you want the bot to answer always in a new thread/conversation" },
|
MATRIX_ENCRYPTION: { schema: z.boolean().default(true),
|
||||||
MATRIX_PREFIX_DM: { schema: z.boolean().default(false), description: "Set to false if you want the bot to answer to all messages in a one-to-one room" },
|
description: "Set to true if you want the bot to support encrypted channels" },
|
||||||
MATRIX_RICH_TEXT: { schema: z.boolean().default(true), description: "Set to true if you want the bot to answer with enriched text" },
|
MATRIX_THREADS: { schema: z.boolean().default(true),
|
||||||
MATRIX_WELCOME: { schema: z.boolean().default(true), description: "Set to true if you want the bot to post a message when it joins a new chat." },
|
description: "Set to true if you want the bot to answer always in a new thread/conversation" },
|
||||||
|
MATRIX_PREFIX_DM: { schema: z.boolean().default(false),
|
||||||
|
description: "Set to false if you want the bot to answer to all messages in a one-to-one room" },
|
||||||
|
MATRIX_RICH_TEXT: { schema: z.boolean().default(true),
|
||||||
|
description: "Set to true if you want the bot to answer with enriched text" },
|
||||||
|
MATRIX_WELCOME: { schema: z.boolean().default(true),
|
||||||
|
description: "Set to true if you want the bot to post a message when it joins a new chat." },
|
||||||
/** Matrix Access Control */
|
/** Matrix Access Control */
|
||||||
MATRIX_BLACKLIST: { schema: z.string().optional(), description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to blacklist users or domains" },
|
MATRIX_BLACKLIST: { schema: z.string().optional(),
|
||||||
MATRIX_WHITELIST: { schema: z.string().optional(), description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to whitelist users or domains" },
|
description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to blacklist users or domains" },
|
||||||
MATRIX_ROOM_BLACKLIST: { schema: z.string().optional(), description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to blacklist rooms or domains" },
|
MATRIX_WHITELIST: { schema: z.string().optional(),
|
||||||
MATRIX_ROOM_WHITELIST: { schema: z.string().optional(), description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to whitelist rooms or domains" },
|
description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to whitelist users or domains" },
|
||||||
|
MATRIX_ROOM_BLACKLIST: { schema: z.string().optional(),
|
||||||
|
description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to blacklist rooms or domains" },
|
||||||
|
MATRIX_ROOM_WHITELIST: { schema: z.string().optional(),
|
||||||
|
description: "Set to a spaces separated string of 'user:homeserver' or a wildcard like ':anotherhomeserver.example' to whitelist rooms or domains" },
|
||||||
/** Matrix Bot Runtime Config */
|
/** Matrix Bot Runtime Config */
|
||||||
MATRIX_DEFAULT_PREFIX: { schema: z.string().default(""), description: "Set to a string if you want the bot to respond only when messages start with this prefix. Trailing space matters. Empty for no prefix." },
|
MATRIX_DEFAULT_PREFIX: { schema: z.string().default(""),
|
||||||
MATRIX_DEFAULT_PREFIX_REPLY: { schema: z.boolean().default(false), description: "Set to false if you want the bot to answer to all messages in a thread/conversation" },
|
description: "Set to a string if you want the bot to respond only when messages start with this prefix. Trailing space matters. Empty for no prefix." },
|
||||||
|
MATRIX_DEFAULT_PREFIX_REPLY: { schema: z.boolean().default(false),
|
||||||
|
description: "Set to false if you want the bot to answer to all messages in a thread/conversation" },
|
||||||
/** ChatGPT Settings */
|
/** ChatGPT Settings */
|
||||||
OPENAI_AZURE: { schema: z.boolean().default(false), description: "Wether or not to use Azure OPENAI"},
|
OPENAI_AZURE: { schema: z.boolean().default(false),
|
||||||
OPENAI_API_KEY: { schema: z.string().default(""), description: "Set to the API key from https://platform.openai.com/account/api-keys"},
|
description: "Wether or not to use Azure OPENAI"},
|
||||||
CHATGPT_TIMEOUT: { schema: z.number().default(2 * 60 * 1000), description: "Set number of milliseconds to wait for ChatGPT responses" },
|
OPENAI_API_KEY: { schema: z.string().default(""),
|
||||||
CHATGPT_CONTEXT: { schema: z.enum(["thread", "room", "both"]).default("thread"), description: "Set the ChatGPT conversation context to 'thread', 'room' or 'both'" },
|
description: "Set to the API key from https://platform.openai.com/account/api-keys"},
|
||||||
CHATGPT_API_MODEL: { schema: z.string().default(""), description: "The model for the ChatGPT-API to use. Keep in mind that these models will charge your OpenAI account depending on their pricing." },
|
CHATGPT_TIMEOUT: { schema: z.number().default(2 * 60 * 1000),
|
||||||
|
description: "Set number of milliseconds to wait for ChatGPT responses" },
|
||||||
|
CHATGPT_CONTEXT: { schema: z.enum(["thread", "room", "both"]).default("thread"),
|
||||||
|
description: "Set the ChatGPT conversation context to 'thread', 'room' or 'both'" },
|
||||||
|
CHATGPT_API_MODEL: { schema: z.string().default(""),
|
||||||
|
description: "The model for the ChatGPT-API to use. Keep in mind that these models will charge your OpenAI account depending on their pricing." },
|
||||||
CHATGPT_PROMPT_PREFIX: { schema: z.string().default('Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.'), description: "Instructions to feed to ChatGPT on startup"},
|
CHATGPT_PROMPT_PREFIX: { schema: z.string().default('Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.'), description: "Instructions to feed to ChatGPT on startup"},
|
||||||
CHATGPT_IGNORE_MEDIA: { schema: z.boolean().default(false), description: "Wether or not the bot should react to non-text messages"},
|
CHATGPT_IGNORE_MEDIA: { schema: z.boolean().default(false),
|
||||||
CHATGPT_REVERSE_PROXY: { schema: z.string().default(""), description: "Change the api url to use another (OpenAI-compatible) API endpoint" },
|
description: "Wether or not the bot should react to non-text messages"},
|
||||||
CHATGPT_TEMPERATURE: { schema: z.number().default(0.8), description: "Set the temperature for the model" }
|
CHATGPT_REVERSE_PROXY: { schema: z.string().default(""),
|
||||||
|
description: "Change the api url to use another (OpenAI-compatible) API endpoint" },
|
||||||
|
CHATGPT_TEMPERATURE: { schema: z.number().default(0.8),
|
||||||
|
description: "Set the temperature for the model" }
|
||||||
});
|
});
|
||||||
|
19
src/index.ts
19
src/index.ts
@ -1,5 +1,4 @@
|
|||||||
import ChatGPTClient from '@waylaidwanderer/chatgpt-api';
|
import ChatGPTClient from '@waylaidwanderer/chatgpt-api';
|
||||||
import Keyv from 'keyv'
|
|
||||||
import { KeyvFile } from 'keyv-file';
|
import { KeyvFile } from 'keyv-file';
|
||||||
import {
|
import {
|
||||||
MatrixAuth, MatrixClient, AutojoinRoomsMixin, LogService, LogLevel, RichConsoleLogger,
|
MatrixAuth, MatrixClient, AutojoinRoomsMixin, LogService, LogLevel, RichConsoleLogger,
|
||||||
@ -8,13 +7,10 @@ import {
|
|||||||
|
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import {
|
import {
|
||||||
DATA_PATH, KEYV_URL, OPENAI_AZURE, OPENAI_API_KEY, MATRIX_HOMESERVER_URL, MATRIX_ACCESS_TOKEN, MATRIX_AUTOJOIN,
|
DATA_PATH, MATRIX_HOMESERVER_URL, MATRIX_ACCESS_TOKEN, MATRIX_AUTOJOIN, MATRIX_WELCOME, MATRIX_BOT_PASSWORD, MATRIX_BOT_USERNAME, MATRIX_ENCRYPTION, MATRIX_THREADS,
|
||||||
MATRIX_BOT_PASSWORD, MATRIX_BOT_USERNAME, MATRIX_ENCRYPTION, MATRIX_THREADS, CHATGPT_CONTEXT,
|
OPENAI_AZURE, OPENAI_API_KEY, CHATGPT_CONTEXT, CHATGPT_API_MODEL, CHATGPT_PROMPT_PREFIX, CHATGPT_REVERSE_PROXY, CHATGPT_TEMPERATURE
|
||||||
CHATGPT_API_MODEL, KEYV_BOT_STORAGE, KEYV_BACKEND, CHATGPT_PROMPT_PREFIX, MATRIX_WELCOME,
|
|
||||||
CHATGPT_REVERSE_PROXY, CHATGPT_TEMPERATURE
|
|
||||||
} from './env.js'
|
} from './env.js'
|
||||||
import CommandHandler from "./handlers.js"
|
import CommandHandler from "./handlers.js"
|
||||||
import { KeyvStorageProvider } from './storage.js'
|
|
||||||
import { parseMatrixUsernamePretty, wrapPrompt } from './utils.js';
|
import { parseMatrixUsernamePretty, wrapPrompt } from './utils.js';
|
||||||
|
|
||||||
LogService.setLogger(new RichConsoleLogger());
|
LogService.setLogger(new RichConsoleLogger());
|
||||||
@ -22,22 +18,15 @@ LogService.setLogger(new RichConsoleLogger());
|
|||||||
LogService.setLevel(LogLevel.INFO);
|
LogService.setLevel(LogLevel.INFO);
|
||||||
// LogService.muteModule("Metrics");
|
// LogService.muteModule("Metrics");
|
||||||
LogService.trace = LogService.debug;
|
LogService.trace = LogService.debug;
|
||||||
if (KEYV_URL && KEYV_BACKEND === 'file') LogService.warn('config', 'KEYV_URL is ignored when KEYV_BACKEND is set to `file`')
|
|
||||||
|
|
||||||
let storage: IStorageProvider
|
let storage: IStorageProvider
|
||||||
if (KEYV_BOT_STORAGE) {
|
storage = new SimpleFsStorageProvider(path.join(DATA_PATH, "bot.json")); // /storage/bot.json
|
||||||
storage = new KeyvStorageProvider('chatgpt-bot-storage');
|
|
||||||
} else {
|
|
||||||
storage = new SimpleFsStorageProvider(path.join(DATA_PATH, "bot.json")); // /storage/bot.json
|
|
||||||
}
|
|
||||||
|
|
||||||
let cryptoStore: ICryptoStorageProvider;
|
let cryptoStore: ICryptoStorageProvider;
|
||||||
if (MATRIX_ENCRYPTION) cryptoStore = new RustSdkCryptoStorageProvider(path.join(DATA_PATH, "encrypted")); // /storage/encrypted
|
if (MATRIX_ENCRYPTION) cryptoStore = new RustSdkCryptoStorageProvider(path.join(DATA_PATH, "encrypted")); // /storage/encrypted
|
||||||
|
|
||||||
let cacheOptions // Options for the Keyv cache, see https://www.npmjs.com/package/keyv
|
let cacheOptions // Options for the Keyv cache, see https://www.npmjs.com/package/keyv
|
||||||
if (KEYV_BACKEND === 'file'){
|
cacheOptions = { store: new KeyvFile({ filename: path.join(DATA_PATH, `chatgpt-bot-api.json`) }) };
|
||||||
cacheOptions = { store: new KeyvFile({ filename: path.join(DATA_PATH, `chatgpt-bot-api.json`) }) };
|
|
||||||
} else { cacheOptions = { uri: KEYV_URL } }
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!MATRIX_ACCESS_TOKEN){
|
if (!MATRIX_ACCESS_TOKEN){
|
||||||
|
130
src/storage.ts
130
src/storage.ts
@ -1,130 +0,0 @@
|
|||||||
import Keyv from 'keyv'
|
|
||||||
import { KeyvFile } from 'keyv-file';
|
|
||||||
import * as sha512 from "hash.js/lib/hash/sha/512.js";
|
|
||||||
import * as path from "path";
|
|
||||||
import { IAppserviceStorageProvider, IFilterInfo, IStorageProvider } from "matrix-bot-sdk";
|
|
||||||
import { DATA_PATH, KEYV_BACKEND, KEYV_URL } from './env.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A storage provider that uses the disk to store information.
|
|
||||||
* @category Storage providers
|
|
||||||
*/
|
|
||||||
export class KeyvStorageProvider implements IStorageProvider, IAppserviceStorageProvider {
|
|
||||||
|
|
||||||
private completedTransactions = [];
|
|
||||||
private db: Keyv;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new simple file system storage provider.
|
|
||||||
* @param {Keyv} keyvStore A Keyv instance for storing data.
|
|
||||||
*/
|
|
||||||
constructor(namespace: string, private trackTransactionsInMemory = true, private maxInMemoryTransactions = 20) {
|
|
||||||
if (KEYV_BACKEND === 'file'){
|
|
||||||
this.db = new Keyv({store: new KeyvFile({ filename: path.join(DATA_PATH, `${namespace}.json`),})})
|
|
||||||
} else {
|
|
||||||
this.db = new Keyv(KEYV_URL, { namespace: namespace });
|
|
||||||
}
|
|
||||||
this.db.set('syncToken', null)
|
|
||||||
this.db.set('filter', null)
|
|
||||||
this.db.set('appserviceUsers', {}) // userIdHash => { data }
|
|
||||||
this.db.set('appserviceTransactions', {}) // txnIdHash => { data }
|
|
||||||
this.db.set('kvStore', {}) // key => value (str)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSyncToken(token: string | null): Promise<any> | void {
|
|
||||||
this.db.set('syncToken', token);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSyncToken(): string | Promise<string | null> | null{
|
|
||||||
return this.db.get('syncToken');
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilter(filter: IFilterInfo): void {
|
|
||||||
this.db.set('filter', filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilter(): IFilterInfo | Promise<IFilterInfo> {
|
|
||||||
return this.db.get('filter');
|
|
||||||
}
|
|
||||||
|
|
||||||
addRegisteredUser(userId: string): Promise<any> | void {
|
|
||||||
const key = sha512().update(userId).digest('hex');
|
|
||||||
this.db.set(`appserviceUsers.${key}.userId`, userId)
|
|
||||||
this.db.set(`appserviceUsers.${key}.registered`, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
isUserRegistered(userId: string): boolean | Promise<boolean> {
|
|
||||||
const key = sha512().update(userId).digest('hex');
|
|
||||||
return this.db.get(`appserviceUsers.${key}.registered`);
|
|
||||||
}
|
|
||||||
|
|
||||||
isTransactionCompleted(transactionId: string): boolean | Promise<boolean> {
|
|
||||||
if (this.trackTransactionsInMemory) {
|
|
||||||
return this.completedTransactions.indexOf(transactionId) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = sha512().update(transactionId).digest('hex');
|
|
||||||
return this.db.get(`appserviceTransactions.${key}.completed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransactionCompleted(transactionId: string): Promise<any> | void {
|
|
||||||
if (this.trackTransactionsInMemory) {
|
|
||||||
if (this.completedTransactions.indexOf(transactionId) === -1) {
|
|
||||||
this.completedTransactions.push(transactionId);
|
|
||||||
}
|
|
||||||
if (this.completedTransactions.length > this.maxInMemoryTransactions) {
|
|
||||||
this.completedTransactions = this.completedTransactions.reverse().slice(0, this.maxInMemoryTransactions).reverse();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = sha512().update(transactionId).digest('hex');
|
|
||||||
this.db.set(`appserviceTransactions.${key}.txnId`, transactionId)
|
|
||||||
this.db.set(`appserviceTransactions.${key}.completed`, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
readValue(key: string): string|Promise<string|null|undefined>|null|undefined{
|
|
||||||
return this.db.get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
storeValue(key: string, value: string): Promise<any> | void {
|
|
||||||
this.db.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
storageForUser(userId: string): IStorageProvider {
|
|
||||||
return new NamespacedKeyvProvider(userId, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A namespaced storage provider that uses the disk to store information.
|
|
||||||
* @category Storage providers
|
|
||||||
*/
|
|
||||||
class NamespacedKeyvProvider implements IStorageProvider {
|
|
||||||
constructor(private prefix: string, private parent: KeyvStorageProvider) {
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilter(filter: IFilterInfo): Promise<any> | void {
|
|
||||||
return this.parent.storeValue(`${this.prefix}_int_filter`, JSON.stringify(filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilter(): IFilterInfo | Promise<IFilterInfo> {
|
|
||||||
return Promise.resolve(this.parent.readValue(`${this.prefix}_int_filter`)).then(r => r ? JSON.parse(r) : r);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSyncToken(token: string | null): Promise<any> | void {
|
|
||||||
return this.parent.storeValue(`${this.prefix}_int_syncToken`, token || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
getSyncToken(): string | Promise<string | null> | null {
|
|
||||||
return Promise.resolve(this.parent.readValue(`${this.prefix}_int_syncToken`)).then(r => r ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
readValue(key: string): string | Promise<string | null | undefined> | null | undefined {
|
|
||||||
return this.parent.readValue(`${this.prefix}_kv_${key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
storeValue(key: string, value: string): Promise<any> | void {
|
|
||||||
return this.parent.storeValue(`${this.prefix}_kv_${key}`, value);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user