From 32baaebb93757de714fd7b1dd7c6065aa1ff1be6 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 20 Jan 2026 08:31:05 +0300 Subject: [PATCH] update regex matching - now bot responds only for base commands (like /ping) and with mention (like /ping@panfilovi4_bot) and ignores command if it mentions other bot; Add ability to see, change and list gemini, mistral and ollama models --- src/base/chat-command.ts | 47 ++++++++++- src/commands/admins-add.ts | 2 +- src/commands/admins-remove.ts | 2 +- src/commands/ae.ts | 9 +- src/commands/ban.ts | 9 +- src/commands/choice.ts | 10 ++- src/commands/coin.ts | 5 +- src/commands/dice.ts | 3 +- src/commands/distort.ts | 18 ++-- src/commands/gemini-chat.ts | 30 +++---- src/commands/gemini-get-model.ts | 13 +++ src/commands/gemini-list-models.ts | 36 ++++++++ src/commands/gemini-set-model.ts | 25 ++++++ src/commands/help.ts | 7 +- src/commands/id.ts | 5 +- src/commands/leave.ts | 1 - src/commands/mistral-chat.ts | 56 ++++--------- src/commands/mistral-get-model.ts | 17 ++++ src/commands/mistral-list-models.ts | 37 ++++++++ src/commands/mistral-set-model.ts | 25 ++++++ src/commands/mute.ts | 3 +- src/commands/ollama-chat.ts | 65 +++++---------- src/commands/ollama-get-model.ts | 27 ++++++ src/commands/ollama-list-models.ts | 35 ++++++++ src/commands/ollama-prompt.ts | 10 ++- src/commands/ollama-search.ts | 6 +- src/commands/ollama-set-model.ts | 25 ++++++ src/commands/ping.ts | 3 +- src/commands/prefix-response.ts | 6 +- src/commands/qr.ts | 7 +- src/commands/quote.ts | 12 +-- src/commands/random-int.ts | 1 - src/commands/random-string.ts | 3 +- src/commands/shutdown.ts | 1 - src/commands/start.ts | 1 - src/commands/system-specs.ts | 3 +- src/commands/test.ts | 6 +- src/commands/title.ts | 10 ++- src/commands/transliteration.ts | 5 +- src/commands/unban.ts | 9 +- src/commands/unmute.ts | 3 +- src/commands/uptime.ts | 1 - src/commands/what-better.ts | 15 +++- src/commands/when.ts | 8 +- src/common/environment.ts | 20 ++++- src/common/message-part.ts | 1 + src/common/message-store.ts | 2 +- src/db/message-dao.ts | 13 +-- src/db/schema.ts | 1 + src/index.ts | 49 +++++++++-- src/model/stored-message.ts | 3 +- src/util/utils.ts | 125 +++++++++++++++++++++++----- 52 files changed, 605 insertions(+), 231 deletions(-) create mode 100644 src/commands/gemini-get-model.ts create mode 100644 src/commands/gemini-list-models.ts create mode 100644 src/commands/gemini-set-model.ts create mode 100644 src/commands/mistral-get-model.ts create mode 100644 src/commands/mistral-list-models.ts create mode 100644 src/commands/mistral-set-model.ts create mode 100644 src/commands/ollama-get-model.ts create mode 100644 src/commands/ollama-list-models.ts create mode 100644 src/commands/ollama-set-model.ts diff --git a/src/base/chat-command.ts b/src/base/chat-command.ts index a451156..c0ab79b 100644 --- a/src/base/chat-command.ts +++ b/src/base/chat-command.ts @@ -1,15 +1,58 @@ import {Message} from "typescript-telegram-bot-api"; import {Requirements} from "./requirements"; +export type ArgsMode = "none" | "optional" | "required"; + export abstract class ChatCommand { - abstract regexp: RegExp; + regexp?: RegExp | null; + command?: string | string[]; + argsMode: ArgsMode = "none"; + requirements?: Requirements = null; title?: string; description?: string; + get finalRegexp(): RegExp { + if (!this.regexp) { + const inferred = name(this.constructor.name); + const names = this.command ?? inferred; + this.regexp = createCommandRegExp(names, this.argsMode); + } + return this.regexp; + } + abstract execute( msg: Message, match?: RegExpExecArray ): Promise; -} \ No newline at end of file +} + +export function name(s: string) { + return s + .replace(/Command$/, "") + .replace(/([a-z0-9])([A-Z])/g, "$1$2") + .toLowerCase(); +} + +function escapeRe(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function createCommandRegExp( + names: string | string[], + argsMode: ArgsMode = "optional", +) { + const list = Array.isArray(names) ? names : [names]; + const group = list.map(escapeRe).join("|"); + + const base = `^\\/(${group})(?:@([\\w_]+))?`; // (1)=cmd, (2)=bot + const tail = + argsMode === "none" + ? "\\s*$" + : argsMode === "required" + ? "\\s+([\\s\\S]+)\\s*$" // (3)=args обязателен + : "(?:\\s+([\\s\\S]+))?\\s*$"; // (3)=args опционален + + return new RegExp(base + tail, "i"); +} diff --git a/src/commands/admins-add.ts b/src/commands/admins-add.ts index 8a5b2be..519cd27 100644 --- a/src/commands/admins-add.ts +++ b/src/commands/admins-add.ts @@ -7,7 +7,7 @@ import {Environment} from "../common/environment"; import {botUser} from "../index"; export class AdminsAdd extends ChatCommand { - regexp = /^\/addadmin/i; + command = "addAdmin"; title = "/addAdmin"; description = "Add user to admins"; diff --git a/src/commands/admins-remove.ts b/src/commands/admins-remove.ts index 6ad113d..68fdc86 100644 --- a/src/commands/admins-remove.ts +++ b/src/commands/admins-remove.ts @@ -7,7 +7,7 @@ import {Environment} from "../common/environment"; import {botUser} from "../index"; export class AdminsRemove extends ChatCommand { - regexp = /^\/removeadmin/i; + command = "removeAdmin"; title = "/removeAdmin"; description = "Remove user from admins"; diff --git a/src/commands/ae.ts b/src/commands/ae.ts index 551acb8..52c06da 100644 --- a/src/commands/ae.ts +++ b/src/commands/ae.ts @@ -4,15 +4,16 @@ import {errorPlaceholder, logError, oldSendMessage} from "../util/utils"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; -export class Ae implements ChatCommand { - regexp = /^\/ae\s([^]+)/i; +export class Ae extends ChatCommand { + argsMode = "required" as const; + title = "/ae"; description = "evaluation"; requirements = Requirements.Build(Requirement.BOT_CREATOR); - async execute(msg: Message, params: string[]) { - const match = params[1]; + async execute(msg: Message, params?: RegExpExecArray) { + const match = params?.[3]; try { let e = eval(match); diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 05d121d..db4989e 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -3,11 +3,10 @@ import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; import {Message} from "typescript-telegram-bot-api"; import {bot, botUser} from "../index"; -import {fullName, logError, oldSendMessage, replyToMessage} from "../util/utils"; +import {fullName, logError, oldSendMessage, oldReplyToMessage} from "../util/utils"; import {Environment} from "../common/environment"; export class Ban extends ChatCommand { - regexp = /^\/ban/i; title = "/ban [reply]"; description = "ban user from chat"; @@ -25,17 +24,17 @@ export class Ban extends ChatCommand { const userId = user.id; if (userId === botUser.id) { - await replyToMessage(msg, "Используй /leave").catch(logError); + await oldReplyToMessage(msg, "Используй /leave").catch(logError); return; } if (userId === Environment.CREATOR_ID) { - await replyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError); + await oldReplyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError); return; } if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) { - await replyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError); + await oldReplyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError); return; } diff --git a/src/commands/choice.ts b/src/commands/choice.ts index 67ceaa8..54425d1 100644 --- a/src/commands/choice.ts +++ b/src/commands/choice.ts @@ -1,16 +1,18 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, randomValue, replyToMessage} from "../util/utils"; +import {logError, oldReplyToMessage, randomValue} from "../util/utils"; export class Choice extends ChatCommand { - regexp = /^\/choice\b\s*(.*)$/i; + command = "choice"; + argsMode = "required" as const; + title = "/choice a, b, ..., c"; description = "Выбор случайного значения"; async execute(msg: Message, match?: RegExpExecArray): Promise { console.log("match", match); - const payload = match[1]; + const payload = match[3]; const re = /\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+?))\s*(?:,|$)/g; @@ -32,6 +34,6 @@ export class Choice extends ChatCommand { const random = randomValue(out); - await replyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError); + await oldReplyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError); } } \ No newline at end of file diff --git a/src/commands/coin.ts b/src/commands/coin.ts index 2b6dfa6..02ec46a 100644 --- a/src/commands/coin.ts +++ b/src/commands/coin.ts @@ -1,14 +1,13 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {getRangedRandomInt, logError, replyToMessage} from "../util/utils"; +import {getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils"; export class Coin extends ChatCommand { - regexp = /^\/coin$/i; title = "/coin"; description = "Heads or tails"; async execute(msg: Message): Promise { const random = getRangedRandomInt(0, 2); const headsOrTails = random === 1 ? "Выпал *Орёл* 🪙" : "Выпала *Решка* 🪙"; - await replyToMessage(msg, headsOrTails, "Markdown").catch(logError); } + await oldReplyToMessage(msg, headsOrTails, "Markdown").catch(logError); } } \ No newline at end of file diff --git a/src/commands/dice.ts b/src/commands/dice.ts index d232960..50a516d 100644 --- a/src/commands/dice.ts +++ b/src/commands/dice.ts @@ -7,8 +7,7 @@ type DiceEmoji = "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰"; const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"]; export class Dice extends ChatCommand { - regexp = /^\/dice/i; - title = "/dice [emoji]"; + title = "/dice"; description = "Sends random or specific dice"; async execute(msg: Message): Promise { diff --git a/src/commands/distort.ts b/src/commands/distort.ts index 93334b9..d1d0de8 100644 --- a/src/commands/distort.ts +++ b/src/commands/distort.ts @@ -1,10 +1,12 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {downloadTelegramFile, extractImageFileId, logError, replyToMessage, waveDistortSharp} from "../util/utils"; +import {downloadTelegramFile, extractImageFileId, logError, oldReplyToMessage, waveDistortSharp} from "../util/utils"; import {bot} from "../index"; export class Distort extends ChatCommand { - regexp = /^\/distort(?:@[\w_]+)?(?:\s+(\d+))?(?:\s+(\d+))?\s*$/i; + command = "distort"; + argsMode = "optional" as const; + title = "/distort [amp] [wavelength]"; description = "Distortion of picture"; @@ -13,7 +15,7 @@ export class Distort extends ChatCommand { const reply = msg.reply_to_message; if (!reply) { - await replyToMessage( + await oldReplyToMessage( msg, "Ответь командой /distort на сообщение с картинкой (фото, документ или стикер).\n" + "Пример: /distort 16 80" ); @@ -22,15 +24,17 @@ export class Distort extends ChatCommand { const fileId = extractImageFileId(reply); if (!fileId) { - await replyToMessage( + await oldReplyToMessage( msg, "В реплае не вижу картинку. Пришли фото или файл-изображение." ); return; } - const amp = match?.[1] ? parseInt(match[1], 10) : 14; - const wavelength = match?.[2] ? parseInt(match[2], 10) : 72; + const args = (match?.[3] ?? "").trim(); + const [a, b] = args ? args.split(/\s+/) : []; + const amp = a ? Number(a) : 14; + const wavelength = b ? Number(b) : 72; try { await bot.sendChatAction({chat_id: chatId, action: "upload_photo"}); @@ -48,7 +52,7 @@ export class Distort extends ChatCommand { caption: `Искажение готово ✅ (amp=${amp}, wavelength=${wavelength})`, }); } catch (e) { - await replyToMessage( + await oldReplyToMessage( msg, `Не получилось исказить изображение: ${e?.message ?? String(e)}` ).catch(logError); } diff --git a/src/commands/gemini-chat.ts b/src/commands/gemini-chat.ts index 3c836eb..30a9c19 100644 --- a/src/commands/gemini-chat.ts +++ b/src/commands/gemini-chat.ts @@ -3,30 +3,31 @@ import {Message} from "typescript-telegram-bot-api"; import { collectReplyChainText, editMessageText, - escapeMarkdownV2Text, extractText, + escapeMarkdownV2Text, + extractText, logError, - replyToMessage, + oldReplyToMessage, startIntervalEditor } from "../util/utils"; import {Environment} from "../common/environment"; -import {bot} from "../index"; +import {bot, googleAi} from "../index"; import {MessageStore} from "../common/message-store"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; -import {ApiError, GoogleGenAI} from "@google/genai"; +import {ApiError} from "@google/genai"; export class GeminiChat extends ChatCommand { - regexp = /^\/gemini\s([^]+)/i; + command = "gemini"; + argsMode = "required" as const; + title = "/gemini"; description = "Chat with AI (Gemini)"; requirements = Requirements.Build(Requirement.BOT_CREATOR); - private googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY}); - async execute(msg: Message, match?: RegExpExecArray): Promise { console.log("match", match); - return this.executeGemini(msg, match?.[1]); + return this.executeGemini(msg, match?.[3]); } async executeGemini(msg: Message, text: string): Promise { @@ -67,8 +68,8 @@ export class GeminiChat extends ChatCommand { } }); - const stream = await this.googleAi.models.generateContentStream({ - model: Environment.GEMINI_MODEL || "gemini-2.5-flash", + const stream = await googleAi.models.generateContentStream({ + model: Environment.GEMINI_MODEL, contents: chatContent, }); @@ -85,6 +86,7 @@ export class GeminiChat extends ChatCommand { onStop: async () => { } }); + await editor.tick(); try { for await (const chunk of stream) { @@ -123,21 +125,21 @@ export class GeminiChat extends ChatCommand { waitMessage.reply_to_message = msg; waitMessage.text = messageText; - MessageStore.put(waitMessage); + await MessageStore.put(waitMessage); - await replyToMessage(waitMessage, `⏱️ ${diff}s`); + await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`); } } catch (error) { console.error(error); if (error instanceof ApiError) { if (error.status === 429) { - await replyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError); + await oldReplyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError); return; } } - await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); + await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); } } } \ No newline at end of file diff --git a/src/commands/gemini-get-model.ts b/src/commands/gemini-get-model.ts new file mode 100644 index 0000000..79c0713 --- /dev/null +++ b/src/commands/gemini-get-model.ts @@ -0,0 +1,13 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {logError, replyToMessage} from "../util/utils"; +import {Environment} from "../common/environment"; + +export class GeminiGetModel extends ChatCommand { + title = "/geminiGetModel"; + description = "Get current Gemini model"; + + async execute(msg: Message): Promise { + await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/gemini-list-models.ts b/src/commands/gemini-list-models.ts new file mode 100644 index 0000000..11fcdff --- /dev/null +++ b/src/commands/gemini-list-models.ts @@ -0,0 +1,36 @@ +import {ChatCommand} from "../base/chat-command"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {Message} from "typescript-telegram-bot-api"; +import {googleAi} from "../index"; +import {logError, replyToMessage} from "../util/utils"; + +export class GeminiListModels extends ChatCommand { + title = "/geminiListModels"; + description = "List all Gemini models"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message): Promise { + try { + const listResponse = await googleAi.models.list(); + console.log(listResponse); + + const modelsString = listResponse.page + .sort((a, b) => a.name.localeCompare(b.name)) + .map(e => `\`${e.name}\``) + .join("\n"); + + const text = "Доступные модели:\n\n" + modelsString; + + await replyToMessage({ + message: msg, + text: text, + parse_mode: "Markdown" + }); + } catch (e) { + console.error(e); + await replyToMessage({message: msg, text: "Не получилось загрузить список моделей"}).catch(logError); + } + } +} \ No newline at end of file diff --git a/src/commands/gemini-set-model.ts b/src/commands/gemini-set-model.ts new file mode 100644 index 0000000..8ee2c52 --- /dev/null +++ b/src/commands/gemini-set-model.ts @@ -0,0 +1,25 @@ +import {ChatCommand} from "../base/chat-command"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {Message} from "typescript-telegram-bot-api"; +import {Environment} from "../common/environment"; +import {logError, replyToMessage} from "../util/utils"; + +export class GeminiSetModel extends ChatCommand { + argsMode = "required" as const; + + title = "/geminiSetModel"; + description = "Set Gemini model"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message, match?: RegExpExecArray | null): Promise { + const newModel = match?.[1]; + Environment.setGeminiModel(newModel || Environment.GEMINI_MODEL); + + const text = newModel ? `Выбрана модель "${newModel}"` + : `Модель не задана. Будет использоваться стандартная модель "${Environment.GEMINI_MODEL}".`; + + await replyToMessage({message: msg, text: text}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/help.ts b/src/commands/help.ts index 5687c93..a65132a 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -4,8 +4,9 @@ import {ChatCommand} from "../base/chat-command"; import {chatCommands} from "../index"; import {TelegramError} from "typescript-telegram-bot-api/dist/errors"; -export class Help implements ChatCommand { - regexp = /^\/(h|help)/i; +export class Help extends ChatCommand { + command = ["h", "help"]; + title = "/help"; description = "Show list of commands"; @@ -16,7 +17,7 @@ export class Help implements ChatCommand { text += `${chatCommandToString(c)}\n`; }); - await sendMessage({chatId: msg.from.id, text: text}) + await sendMessage({chat_id: msg.from.id, text: text}) .then(async () => { if (msg.chat.type !== "private") { await sendMessage({message: msg, text: "Отправил команды в ЛС 😎"}).catch(logError); diff --git a/src/commands/id.ts b/src/commands/id.ts index ae97dba..356b49b 100644 --- a/src/commands/id.ts +++ b/src/commands/id.ts @@ -1,9 +1,8 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, replyToMessage} from "../util/utils"; +import {logError, oldReplyToMessage} from "../util/utils"; export class Id extends ChatCommand { - regexp = /^\/id/i; title = "/id"; description = "ID of chat, user and reply (if replied to any message)"; @@ -13,6 +12,6 @@ export class Id extends ChatCommand { text += ` \nreply id: \n\`\`\`${msg.reply_to_message.from.id}\`\`\``; } - await replyToMessage(msg, text, "MarkdownV2").catch(logError); + await oldReplyToMessage(msg, text, "MarkdownV2").catch(logError); } } \ No newline at end of file diff --git a/src/commands/leave.ts b/src/commands/leave.ts index a559763..bc994c1 100644 --- a/src/commands/leave.ts +++ b/src/commands/leave.ts @@ -5,7 +5,6 @@ import {Requirement} from "../base/requirement"; import {bot} from "../index"; export class Leave extends ChatCommand { - regexp = /^\/leave/i; title = "/leave"; description = "Bot will leave current chat"; diff --git a/src/commands/mistral-chat.ts b/src/commands/mistral-chat.ts index f3ccf71..7c14019 100644 --- a/src/commands/mistral-chat.ts +++ b/src/commands/mistral-chat.ts @@ -7,31 +7,27 @@ import { editMessageText, escapeMarkdownV2Text, extractText, - getPhotoMaxSize, logError, - replyToMessage, + oldReplyToMessage, startIntervalEditor } from "../util/utils"; import {Environment} from "../common/environment"; -import {bot} from "../index"; +import {bot, mistralAi} from "../index"; import {MessageStore} from "../common/message-store"; -import {Mistral} from "@mistralai/mistralai"; -import path from "node:path"; import fs from "node:fs"; -import axios from "axios"; export class MistralChat extends ChatCommand { - regexp = /^\/mistral\s([^]+)/i; + command = "mistral"; + argsMode = "required" as const; + title = "/mistral"; description = "Chat with AI (Mistral)"; requirements = Requirements.Build(Requirement.BOT_CREATOR); - private mistralAi = new Mistral({apiKey: Environment.MISTRAL_API_KEY}); - async execute(msg: Message, match?: RegExpExecArray): Promise { console.log("match", match); - return this.executeMistral(msg, match?.[1]); + return this.executeMistral(msg, match?.[3]); } async executeMistral(msg: Message, text: string): Promise { @@ -39,41 +35,18 @@ export class MistralChat extends ChatCommand { const chatId = msg.chat.id; - let imageFilePath: string | null = null; - - const maxSize = await getPhotoMaxSize(msg.photo); - if (maxSize) { - const imagePath = path.join(Environment.DATA_PATH, "temp"); - if (!fs.existsSync(imagePath)) { - fs.mkdirSync(imagePath); - } - - imageFilePath = path.join(imagePath, maxSize.unique_file_id + ".jpg"); - if (!fs.existsSync(imageFilePath)) { - const res = await axios.get(maxSize.url, {responseType: "arraybuffer"}); - const src = Buffer.from(res.data); - - try { - fs.writeFileSync(imageFilePath, src); - } catch (e) { - console.error(e); - imageFilePath = null; - } - } - } - const messageParts = await collectReplyChainText(msg, "/mistral"); console.log("MESSAGE PARTS", messageParts); - const chatMessages = messageParts.map((part, i) => { + const chatMessages = messageParts.map(part => { const content = []; content.push({ type: "text", text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + extractText(part.content, Environment.BOT_PREFIX), }); - if (imageFilePath && i === 0) { - const base64Image = Buffer.from(fs.readFileSync(imageFilePath)).toString("base64"); + if (part.images && part.images.length > 0) { + const base64Image = Buffer.from(fs.readFileSync(part.images[0])).toString("base64"); content.push({ type: "image_url", imageUrl: "data:image/jpeg;base64," + base64Image @@ -102,8 +75,8 @@ export class MistralChat extends ChatCommand { } }); - const stream = await this.mistralAi.chat.stream({ - model: Environment.MISTRAL_MODEL || "mistral-small-latest", + const stream = await mistralAi.chat.stream({ + model: Environment.MISTRAL_MODEL, messages: chatMessages as any }); @@ -120,14 +93,13 @@ export class MistralChat extends ChatCommand { onStop: async () => { } }); + await editor.tick(); try { for await (const chunk of stream) { - // const text = chunk.text; const text = chunk.data.choices[0].delta.content; console.log("chunk", chunk); - // const text = ""; const length = (messageText + text).length; if (length > 4096) { messageText = messageText.slice(0, 4093) + "..."; @@ -163,7 +135,7 @@ export class MistralChat extends ChatCommand { waitMessage.text = messageText; MessageStore.put(waitMessage); - await replyToMessage(waitMessage, `⏱️ ${diff}s`); + await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`); } } catch (error) { console.error(error); @@ -175,7 +147,7 @@ export class MistralChat extends ChatCommand { // } // } - await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); + await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); } } } \ No newline at end of file diff --git a/src/commands/mistral-get-model.ts b/src/commands/mistral-get-model.ts new file mode 100644 index 0000000..a67356b --- /dev/null +++ b/src/commands/mistral-get-model.ts @@ -0,0 +1,17 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {logError, replyToMessage} from "../util/utils"; +import {Environment} from "../common/environment"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; + +export class MistralGetModel extends ChatCommand { + title = "/mistralGetModel"; + description = "Get current Mistral model"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message): Promise { + await replyToMessage({message: msg, text: `Текущая модель: "${Environment.MISTRAL_MODEL}"`}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/mistral-list-models.ts b/src/commands/mistral-list-models.ts new file mode 100644 index 0000000..fde9e6b --- /dev/null +++ b/src/commands/mistral-list-models.ts @@ -0,0 +1,37 @@ +import {ChatCommand} from "../base/chat-command"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {Message} from "typescript-telegram-bot-api"; +import {mistralAi} from "../index"; +import {logError, oldReplyToMessage, replyToMessage} from "../util/utils"; + +export class MistralListModels extends ChatCommand { + title = "/mistralListModels"; + description = "List all Mistral models"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message): Promise { + try { + const listResponse = await mistralAi.models.list(); + console.log(listResponse); + + const modelsString = listResponse.data + .sort((a, b) => a.name.localeCompare(b.name)) + .map(e => `\`${e.name}\``) + .join("\n"); + + const text = "Доступные модели:\n\n" + modelsString; + + await replyToMessage({ + chat_id: msg.chat.id, + text: text, + parse_mode: "Markdown", + message: msg + }); + } catch (e) { + console.error(e); + await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError); + } + } +} \ No newline at end of file diff --git a/src/commands/mistral-set-model.ts b/src/commands/mistral-set-model.ts new file mode 100644 index 0000000..24ad6ac --- /dev/null +++ b/src/commands/mistral-set-model.ts @@ -0,0 +1,25 @@ +import {ChatCommand} from "../base/chat-command"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {Message} from "typescript-telegram-bot-api"; +import {Environment} from "../common/environment"; +import {logError, replyToMessage} from "../util/utils"; + +export class MistralSetModel extends ChatCommand { + argsMode = "required" as const; + + title = "/mistralSetModel"; + description = "Set Mistral model"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message, match?: RegExpExecArray | null): Promise { + const newModel = match?.[1]; + Environment.setMistralModel(newModel || Environment.MISTRAL_MODEL); + + const text = newModel ? `Выбрана модель "${newModel}"` + : `Модель не задана. Будет использоваться стандартная модель "${Environment.MISTRAL_MODEL}".`; + + await replyToMessage({message: msg, text: text}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/mute.ts b/src/commands/mute.ts index 5327f42..f42d0a9 100644 --- a/src/commands/mute.ts +++ b/src/commands/mute.ts @@ -7,8 +7,7 @@ import {fullName, logError, oldSendMessage} from "../util/utils"; import {botUser} from "../index"; import {Environment} from "../common/environment"; -export class Mute implements ChatCommand { - regexp = /^\/mute/i; +export class Mute extends ChatCommand { title = "/mute"; description = "Bot will ignore user"; diff --git a/src/commands/ollama-chat.ts b/src/commands/ollama-chat.ts index 7dabc51..22c83de 100644 --- a/src/commands/ollama-chat.ts +++ b/src/commands/ollama-chat.ts @@ -6,27 +6,25 @@ import { editMessageText, escapeMarkdownV2Text, extractText, - getPhotoMaxSize, logError, - replyToMessage, + oldReplyToMessage, startIntervalEditor } from "../util/utils"; import {Environment} from "../common/environment"; import {MessageStore} from "../common/message-store"; -import axios from "axios"; -import * as fs from "node:fs"; -import path from "node:path"; import {Cancel} from "../callback_commands/cancel"; import {OllamaCancel} from "../callback_commands/ollama-cancel"; export class OllamaChat extends ChatCommand { - regexp = /^\/ollama\s([^]+)/; + command = "ollama"; + argsMode = "required" as const; + title = "/ollama"; - description = "talk to AI (Ollama)"; + description = "Chat with AI (Ollama)"; async execute(msg: Message, match?: RegExpExecArray | null): Promise { console.log("match", match); - return this.executeOllama(msg, match?.[1]); + return this.executeOllama(msg, match?.[3]); } async executeOllama(msg: Message, text: string): Promise { @@ -34,41 +32,18 @@ export class OllamaChat extends ChatCommand { const chatId = msg.chat.id; - let imageFilePath: string | null = null; - - const maxSize = await getPhotoMaxSize(msg.photo); - if (maxSize) { - const imagePath = path.join(Environment.DATA_PATH, "temp"); - if (!fs.existsSync(imagePath)) { - fs.mkdirSync(imagePath); - } - - imageFilePath = path.join(imagePath, maxSize.unique_file_id + ".jpg"); - if (!fs.existsSync(imageFilePath)) { - const res = await axios.get(maxSize.url, {responseType: "arraybuffer"}); - const src = Buffer.from(res.data); - - try { - fs.writeFileSync(imageFilePath, src); - } catch (e) { - console.error(e); - imageFilePath = null; - } - } - } - const messageParts = await collectReplyChainText(msg); console.log("MESSAGE PARTS", messageParts); - const chatMessages = messageParts.map((part, i) => { + const chatMessages = messageParts.map(part => { return { role: part.bot ? "assistant" : "user", content: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + extractText(part.content, Environment.BOT_PREFIX), - images: imageFilePath && i === 0 ? [imageFilePath] : null + images: part.images }; }); chatMessages.reverse(); - chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: null}); + chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []}); let waitMessage: Message; @@ -81,7 +56,8 @@ export class OllamaChat extends ChatCommand { waitMessage = await bot.sendMessage({ chat_id: chatId, - text: maxSize !== null ? `🔍 Внимательно изучаю изображение...\n🤓 ${maxSize.width}x${maxSize.height}px` : Environment.waitText, + text: Environment.waitText, + // text: maxSize !== null ? `🔍 Внимательно изучаю изображение...\n🤓 ${maxSize.width}x${maxSize.height}px` : Environment.waitText, reply_parameters: { chat_id: chatId, message_id: msg.message_id @@ -117,17 +93,18 @@ export class OllamaChat extends ChatCommand { onStop: async () => { } }); + await editor.tick(); try { for await (const chunk of stream) { - if (!getOllamaRequest(uuid).done) { - currentText += chunk.message.content; + currentText += chunk.message.content; - if (currentText.length > 4096) { - currentText = currentText.slice(0, 4093) + "..."; - shouldBreak = true; - } - } else { + if (currentText.length > 4096) { + currentText = currentText.slice(0, 4093) + "..."; + shouldBreak = true; + } + + if (getOllamaRequest(uuid).done) { shouldBreak = true; } @@ -154,7 +131,7 @@ export class OllamaChat extends ChatCommand { waitMessage.text = currentText; await MessageStore.put(waitMessage); - await replyToMessage(waitMessage, `⏱️ ${diff}s` + (maxSize !== null ? `\n🤓 ${maxSize.width}x${maxSize.height}px` : "")); + await oldReplyToMessage(waitMessage, `⏱️ ${diff}s` /*+ (maxSize !== null ? `\n🤓 ${maxSize.width}x${maxSize.height}px` : "")*/); break; } } @@ -173,7 +150,7 @@ export class OllamaChat extends ChatCommand { }).catch(logError); console.error(error); - await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); + await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); } } } \ No newline at end of file diff --git a/src/commands/ollama-get-model.ts b/src/commands/ollama-get-model.ts new file mode 100644 index 0000000..b035760 --- /dev/null +++ b/src/commands/ollama-get-model.ts @@ -0,0 +1,27 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {boolToEmoji, logError, replyToMessage} from "../util/utils"; +import {Environment} from "../common/environment"; +import {ollama} from "../index"; + +export class OllamaGetModel extends ChatCommand { + title = "/ollamaGetModel"; + description = "Ollama model info"; + + async execute(msg: Message): Promise { + + const showResponse = await ollama.show({model: Environment.OLLAMA_MODEL}); + console.log(showResponse); + + const caps = showResponse.capabilities; + + const text = "```ollama\n" + + `model: ${Environment.OLLAMA_MODEL}\n\n` + + `vision: ${boolToEmoji(caps.includes("vision"))}\n` + + `thinking: ${boolToEmoji(caps.includes("thinking"))}\n` + + `tools: ${boolToEmoji(caps.includes("tools"))}` + + "```"; + + await replyToMessage({message: msg, text: text, parse_mode: "Markdown"}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/ollama-list-models.ts b/src/commands/ollama-list-models.ts new file mode 100644 index 0000000..13d7e7e --- /dev/null +++ b/src/commands/ollama-list-models.ts @@ -0,0 +1,35 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {ollama} from "../index"; +import {logError, oldReplyToMessage, sendMessage} from "../util/utils"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; + +export class OllamaListModels extends ChatCommand { + title = "/ollamaListModels"; + description = "List all Ollama models"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message): Promise { + try { + const listResponse = await ollama.list(); + + const modelsString = listResponse.models + .sort((a, b) => a.name.localeCompare(b.name)) + .map(e => `\`${e.model}\``) + .join("\n"); + + const message = "Доступные модели:\n\n" + modelsString; + + await sendMessage({ + chat_id: msg.chat.id, + text: message, + parse_mode: "Markdown", + }); + } catch (e) { + console.error(e); + await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError); + } + } +} \ No newline at end of file diff --git a/src/commands/ollama-prompt.ts b/src/commands/ollama-prompt.ts index 259f4e6..071faea 100644 --- a/src/commands/ollama-prompt.ts +++ b/src/commands/ollama-prompt.ts @@ -1,13 +1,15 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; import {bot, ollama} from "../index"; -import {editMessageText, ignore, replyToMessage} from "../util/utils"; +import {editMessageText, ignore, oldReplyToMessage} from "../util/utils"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; import {Environment} from "../common/environment"; export class OllamaPrompt extends ChatCommand { - regexp = /^\/ollamaprompt\s([^]+)/i; + command = "ollamaPrompt"; + argsMode = "required" as const; + title = "/ollamaPrompt"; description = "Custom prompt for AI (Ollama)"; @@ -86,7 +88,7 @@ export class OllamaPrompt extends ChatCommand { messageText += `\n\nДумал ${diff}s`; await editMessageText(chatId, waitMessage.message_id, messageText); - await replyToMessage(waitMessage, "Закончил лишь часть 😉"); + await oldReplyToMessage(waitMessage, "Закончил лишь часть 😉"); break; } @@ -101,7 +103,7 @@ export class OllamaPrompt extends ChatCommand { messageText += `\n\nДумал ${diff}s`; await editMessageText(chatId, waitMessage.message_id, messageText); - await replyToMessage(waitMessage, "Закончил 😉"); + await oldReplyToMessage(waitMessage, "Закончил 😉"); } } } catch (error) { diff --git a/src/commands/ollama-search.ts b/src/commands/ollama-search.ts index e119cd4..775d240 100644 --- a/src/commands/ollama-search.ts +++ b/src/commands/ollama-search.ts @@ -8,7 +8,9 @@ import {editMessageText} from "../util/utils"; import {Environment} from "../common/environment"; export class OllamaSearch extends ChatCommand { - regexp = /^\/(s|search)\s([^]+)/; + command = ["s", "search"]; + argsMode = "required" as const; + title = "/search"; description = "Web search via Ollama"; @@ -29,7 +31,7 @@ export class OllamaSearch extends ChatCommand { parse_mode: "Markdown" }); - const results = await ollama.webSearch({query: match?.[2]}); + const results = await ollama.webSearch({query: match?.[3]}); console.log("results", results); let message = "Результаты:\n\n"; diff --git a/src/commands/ollama-set-model.ts b/src/commands/ollama-set-model.ts new file mode 100644 index 0000000..44ee691 --- /dev/null +++ b/src/commands/ollama-set-model.ts @@ -0,0 +1,25 @@ +import {Message} from "typescript-telegram-bot-api"; +import {ChatCommand} from "../base/chat-command"; +import {Environment} from "../common/environment"; +import {logError, replyToMessage} from "../util/utils"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; + +export class OllamaSetModel extends ChatCommand { + argsMode = "required" as const; + + title = "/ollamaSetModel"; + description = "Set Ollama model"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message, match?: RegExpExecArray | null): Promise { + const newModel = match?.[1]; + Environment.setOllamaModel(newModel || Environment.OLLAMA_MODEL); + + const text = newModel ? `Выбрана модель "${newModel}"` + : `Модель не задана. Будет использоваться стандартная модель "${Environment.OLLAMA_MODEL}".`; + + await replyToMessage({message: msg, text: text}).catch(logError); + } +} \ No newline at end of file diff --git a/src/commands/ping.ts b/src/commands/ping.ts index a489c1a..851c4da 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -2,8 +2,7 @@ import {logError, oldSendMessage} from "../util/utils"; import {Message} from "typescript-telegram-bot-api"; import {ChatCommand} from "../base/chat-command"; -export class Ping implements ChatCommand { - regexp = /^\/ping/i; +export class Ping extends ChatCommand { title = "/ping"; description = "Ping between received and sent message"; diff --git a/src/commands/prefix-response.ts b/src/commands/prefix-response.ts index a202950..69ff4e1 100644 --- a/src/commands/prefix-response.ts +++ b/src/commands/prefix-response.ts @@ -1,12 +1,10 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, randomValue, replyToMessage} from "../util/utils"; +import {logError, randomValue, oldReplyToMessage} from "../util/utils"; import {prefixAnswers} from "../db/database"; export class PrefixResponse extends ChatCommand { - regexp: RegExp; - async execute(msg: Message): Promise { - await replyToMessage(msg, randomValue(prefixAnswers)).catch(logError); + await oldReplyToMessage(msg, randomValue(prefixAnswers)).catch(logError); } } \ No newline at end of file diff --git a/src/commands/qr.ts b/src/commands/qr.ts index f50beaa..c1d010e 100644 --- a/src/commands/qr.ts +++ b/src/commands/qr.ts @@ -5,7 +5,6 @@ import {bot} from "../index"; import QRCode from "qrcode"; export class Qr extends ChatCommand { - regexp = /^\/qr/i; title = "/qr"; description = "Generates QR-code from text you sent or replied to."; @@ -18,7 +17,7 @@ export class Qr extends ChatCommand { const payload = extractMessagePayload(msg, matchText); if (!payload) { await sendMessage({ - chatId: chatId, + chat_id: chatId, text: "Отправь: /qr <текст или ссылка>\n" + "или ответь командой /qr на сообщение, из которого взять текст." }); return; @@ -26,7 +25,7 @@ export class Qr extends ChatCommand { if (payload.length > 1500) { await sendMessage({ - chatId: chatId, + chat_id: chatId, text: `Слишком длинный текст для QR (${payload.length} символов). Максимум 1500 символов.` }); return; @@ -51,7 +50,7 @@ export class Qr extends ChatCommand { } }); } catch (e) { - await sendMessage({chatId: chatId, text: `Не получилось сгенерировать QR: ${e?.message ?? String(e)}`}).catch(logError); + await sendMessage({chat_id: chatId, text: `Не получилось сгенерировать QR: ${e?.message ?? String(e)}`}).catch(logError); } } } \ No newline at end of file diff --git a/src/commands/quote.ts b/src/commands/quote.ts index eb95fb8..00417b5 100644 --- a/src/commands/quote.ts +++ b/src/commands/quote.ts @@ -13,8 +13,8 @@ import { getUserAvatar, logError, makeDarkGradientBgFancy, - oldSendMessage, - replyToMessage + oldReplyToMessage, + oldSendMessage } from "../util/utils"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; @@ -34,7 +34,9 @@ try { } export class Quote extends ChatCommand { - regexp = /^\/(cit|q|quote)$/i; + command = ["cit", "citation", "q", "quote"]; + argsMode = "none" as const; + title = "/quote"; description = "Make quote from text (or quote)"; @@ -45,14 +47,14 @@ export class Quote extends ChatCommand { const reply = msg.reply_to_message; if (!reply) { - await replyToMessage(msg, "Сделай /quote реплаем на сообщение 🙂").catch(logError); + await oldReplyToMessage(msg, "Сделай /quote реплаем на сообщение 🙂").catch(logError); return; } try { const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim(); if (quoteRaw.length === 0) { - await replyToMessage(msg, "Не нашёл в сообщении текста 😢").catch(logError); + await oldReplyToMessage(msg, "Не нашёл в сообщении текста 😢").catch(logError); return; } diff --git a/src/commands/random-int.ts b/src/commands/random-int.ts index 5ed1a33..becd58d 100644 --- a/src/commands/random-int.ts +++ b/src/commands/random-int.ts @@ -3,7 +3,6 @@ import {getRandomInt, getRangedRandomInt, logError, oldSendMessage} from "../uti import {Message} from "typescript-telegram-bot-api"; export class RandomInt extends ChatCommand { - regexp = /^\/randomInt/i; title = "/randomInt [min] [max]"; description = "Ranged random integer from parameters"; diff --git a/src/commands/random-string.ts b/src/commands/random-string.ts index 046cc8b..aa75b98 100644 --- a/src/commands/random-string.ts +++ b/src/commands/random-string.ts @@ -2,8 +2,7 @@ import {ChatCommand} from "../base/chat-command"; import {getRandomInt, logError, oldSendMessage} from "../util/utils"; import {Message} from "typescript-telegram-bot-api"; -export class RandomString implements ChatCommand { - regexp = /^\/randomString/i; +export class RandomString extends ChatCommand { title = "/randomString [length]"; description = "literally random string (up to 4096 symbols)"; diff --git a/src/commands/shutdown.ts b/src/commands/shutdown.ts index 9e9db91..11710d7 100644 --- a/src/commands/shutdown.ts +++ b/src/commands/shutdown.ts @@ -16,7 +16,6 @@ const timings = [1500, 2500]; const timer = [3, 2, 1]; export class Shutdown extends ChatCommand { - regexp = /^\/shutdown/i; title = "/shutdown"; description = "Self-destruction sequence for bot (shutdown)"; diff --git a/src/commands/start.ts b/src/commands/start.ts index 3caf5e7..9ba6d2a 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -4,7 +4,6 @@ import {chatCommands} from "../index"; import {Help} from "./help"; export class Start extends ChatCommand { - regexp = /^\/start/i; title = "/start"; description = "Start the bot"; diff --git a/src/commands/system-specs.ts b/src/commands/system-specs.ts index a8541eb..436be3e 100644 --- a/src/commands/system-specs.ts +++ b/src/commands/system-specs.ts @@ -3,8 +3,7 @@ import {logError, oldSendMessage} from "../util/utils"; import {Message} from "typescript-telegram-bot-api"; import {systemInfoText} from "../index"; -export class SystemSpecs implements ChatCommand { - regexp = /^\/systeminfo/i; +export class SystemSpecs extends ChatCommand { title = "/systemInfo"; description = "System information"; diff --git a/src/commands/test.ts b/src/commands/test.ts index f285e3c..4ddce59 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,14 +1,14 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, randomValue, replyToMessage} from "../util/utils"; +import {logError, randomValue, oldReplyToMessage} from "../util/utils"; import {testAnswers} from "../db/database"; -export class Test implements ChatCommand { +export class Test extends ChatCommand { regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i; title = "тест"; description = "System functionality check"; async execute(msg: Message) { - await replyToMessage(msg, randomValue(testAnswers) || "а").catch(logError); + await oldReplyToMessage(msg, randomValue(testAnswers) || "а").catch(logError); } } \ No newline at end of file diff --git a/src/commands/title.ts b/src/commands/title.ts index 26e3f15..d7c0577 100644 --- a/src/commands/title.ts +++ b/src/commands/title.ts @@ -2,12 +2,14 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; -import {logError, replyToMessage} from "../util/utils"; +import {logError, oldReplyToMessage} from "../util/utils"; import {bot} from "../index"; export class Title extends ChatCommand { - regexp = /^\/title\s([^]+)/; - title = "/title [title]"; + command = "title"; + argsMode = "required" as const; + + title = "/title"; description = "Change group title"; requirements = Requirements.Build( @@ -19,7 +21,7 @@ export class Title extends ChatCommand { async execute(msg: Message, match?: RegExpExecArray): Promise { const title = (match?.[1] ?? "").trim(); if (title.length === 0) { - await replyToMessage(msg, "Не нашёл название...").catch(logError); + await oldReplyToMessage(msg, "Не нашёл название...").catch(logError); return; } diff --git a/src/commands/transliteration.ts b/src/commands/transliteration.ts index baf7203..0214806 100644 --- a/src/commands/transliteration.ts +++ b/src/commands/transliteration.ts @@ -1,6 +1,6 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, replyToMessage} from "../util/utils"; +import {logError, oldReplyToMessage} from "../util/utils"; const EN = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./" + @@ -98,7 +98,6 @@ export function fixLayoutAuto( } export class Transliteration extends ChatCommand { - regexp = /^\/tr/i; title = "/tr [text or reply]"; description = "Transliteration EN <--> RU"; @@ -120,6 +119,6 @@ export class Transliteration extends ChatCommand { const newText = fixLayoutAuto(text, toRuLayout, toEnLayout); - await replyToMessage(msg, newText).catch(logError); + await oldReplyToMessage(msg, newText).catch(logError); } } \ No newline at end of file diff --git a/src/commands/unban.ts b/src/commands/unban.ts index 9ef0b67..570c0a0 100644 --- a/src/commands/unban.ts +++ b/src/commands/unban.ts @@ -3,11 +3,10 @@ import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; import {Message} from "typescript-telegram-bot-api"; import {bot, botUser} from "../index"; -import {fullName, logError, oldSendMessage, replyToMessage} from "../util/utils"; +import {fullName, logError, oldSendMessage, oldReplyToMessage} from "../util/utils"; import {Environment} from "../common/environment"; export class Unban extends ChatCommand { - regexp = /^\/unban/i; title = "/unban [reply]"; description = "unban user from chat"; @@ -25,17 +24,17 @@ export class Unban extends ChatCommand { const userId = user.id; if (userId === botUser.id) { - await replyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError); + await oldReplyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError); return; } if (userId === Environment.CREATOR_ID) { - await replyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError); + await oldReplyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError); return; } if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) { - await replyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError); + await oldReplyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError); return; } diff --git a/src/commands/unmute.ts b/src/commands/unmute.ts index 412a178..6b20f2e 100644 --- a/src/commands/unmute.ts +++ b/src/commands/unmute.ts @@ -7,8 +7,7 @@ import {Message} from "typescript-telegram-bot-api"; import {botUser} from "../index"; import {Environment} from "../common/environment"; -export class Unmute implements ChatCommand { - regexp = /^\/unmute/i; +export class Unmute extends ChatCommand { title = "/unmute"; description = "Bot will start responding to the user"; requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.CHAT, Requirement.REPLY); diff --git a/src/commands/uptime.ts b/src/commands/uptime.ts index 13a55f9..e204be8 100644 --- a/src/commands/uptime.ts +++ b/src/commands/uptime.ts @@ -3,7 +3,6 @@ import {Message} from "typescript-telegram-bot-api"; import {getUptime, logError, oldSendMessage} from "../util/utils"; export class Uptime extends ChatCommand { - regexp = /^\/uptime/i; title = "/uptime"; description = "Bot's uptime"; diff --git a/src/commands/what-better.ts b/src/commands/what-better.ts index 498d51d..17a05d1 100644 --- a/src/commands/what-better.ts +++ b/src/commands/what-better.ts @@ -1,16 +1,23 @@ import {ChatCommand} from "../base/chat-command"; -import {logError, randomValue, oldSendMessage} from "../util/utils"; +import {logError, oldSendMessage, randomValue} from "../util/utils"; import {betterAnswers} from "../db/database"; import {Message} from "typescript-telegram-bot-api"; export class WhatBetter extends ChatCommand { - regexp = /^\/(what|что)\s(better|лучше)\s([^]+)\s(or|или)\s([^]+)/i; + command = ["what", "что"]; + argsMode = "required" as const; + title = "/what better [a] or [b]"; description = "either a or b randomly (50% chance)"; + private argsRe = /^(better|лучше)\s+([\s\S]+?)\s+(or|или)\s+([\s\S]+)$/i; + async execute(msg: Message, match?: RegExpExecArray) { - const a = match[3]; - const b = match[5].trimStart(); + const args = (match?.[3] ?? "").trim(); + const m = this.argsRe.exec(args); + if (!m) return; + const a = m[2].trim(); + const b = m[4].trim(); const text = `${randomValue(betterAnswers)} ${randomValue([a, b])}`; diff --git a/src/commands/when.ts b/src/commands/when.ts index 643cb23..f6d03f6 100644 --- a/src/commands/when.ts +++ b/src/commands/when.ts @@ -1,9 +1,11 @@ import {ChatCommand} from "../base/chat-command"; -import {getRandomInt, getRangedRandomInt, logError, replyToMessage} from "../util/utils"; +import {getRandomInt, getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils"; import {Message} from "typescript-telegram-bot-api"; export class When extends ChatCommand { - regexp = /^\/(when|когда)\s([^]+)/i; + command = ["when", "когда"]; + argsMode = "required" as const; + title = "/when [value]"; description = "random date"; @@ -85,6 +87,6 @@ export class When extends ChatCommand { } } - await replyToMessage(msg, text).catch(logError); + await oldReplyToMessage(msg, text).catch(logError); } } \ No newline at end of file diff --git a/src/common/environment.ts b/src/common/environment.ts index fd87a9a..0b8d9b3 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -30,10 +30,10 @@ export class Environment { static OLLAMA_API_KEY?: string; static GEMINI_API_KEY?: string; - static GEMINI_MODEL?: string; + static GEMINI_MODEL: string; static MISTRAL_API_KEY?: string; - static MISTRAL_MODEL?: string; + static MISTRAL_MODEL: string; static waitText = "⏳ Дайте-ка подумать..."; @@ -64,10 +64,10 @@ export class Environment { Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY; Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY; - Environment.GEMINI_MODEL = process.env.GEMINI_MODEL; + Environment.GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-2.5-flash"; Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY; - Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL; + Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-small-latest"; } static setAdmins(admins: Set) { @@ -93,4 +93,16 @@ export class Environment { return has; } + + static setOllamaModel(newModel: string) { + Environment.OLLAMA_MODEL = newModel; + } + + static setGeminiModel(newModel: string) { + Environment.GEMINI_MODEL = newModel; + } + + static setMistralModel(newModel: string) { + Environment.MISTRAL_MODEL = newModel; + } } \ No newline at end of file diff --git a/src/common/message-part.ts b/src/common/message-part.ts index 37b4e23..e6e4958 100644 --- a/src/common/message-part.ts +++ b/src/common/message-part.ts @@ -2,4 +2,5 @@ export type MessagePart = { bot: boolean; name?: string; content: string; + images: string[]; } \ No newline at end of file diff --git a/src/common/message-store.ts b/src/common/message-store.ts index 032884f..c663bca 100644 --- a/src/common/message-store.ts +++ b/src/common/message-store.ts @@ -18,7 +18,7 @@ export class MessageStore { static async put(m: Message, prefix: string = Environment.BOT_PREFIX) { const msg: StoredMessage = { chatId: m.chat.id, - messageId: m.message_id, + id: m.message_id, replyToMessageId: m.reply_to_message?.message_id ?? null, fromId: m.from.id, text: extractTextMessage(m, prefix), diff --git a/src/db/message-dao.ts b/src/db/message-dao.ts index 260a1d7..598a618 100644 --- a/src/db/message-dao.ts +++ b/src/db/message-dao.ts @@ -1,4 +1,4 @@ -import {messagesTable} from "./schema"; +import {MessageInsert, messagesTable} from "./schema"; import {DatabaseManager} from "./database-manager"; import {StoredMessage} from "../model/stored-message"; import {and, eq} from "drizzle-orm"; @@ -66,7 +66,7 @@ export class MessageDao extends Dao { return this.mapFrom(messages); } - async insert(values: typeof messagesTable.$inferInsert[]): Promise { + async insert(values: MessageInsert[]): Promise { const then = Date.now(); const r = await DatabaseManager.db .insert(messagesTable) @@ -82,7 +82,7 @@ export class MessageDao extends Dao { return true; } - mapTo(messages: Message[]): typeof messagesTable.$inferInsert[] { + mapTo(messages: Message[]): MessageInsert[] { return messages.map(msg => { return { chatId: msg.chat.id, @@ -95,15 +95,16 @@ export class MessageDao extends Dao { }); } - mapFrom(messages: typeof messagesTable.$inferInsert[]): StoredMessage[] { + mapFrom(messages: MessageInsert[]): StoredMessage[] { return messages.map(m => { return { chatId: m.chatId, - messageId: m.id, + id: m.id, replyToMessageId: m.replyToMessageId, fromId: m.fromId, text: m.text, - date: m.date + date: m.date, + photoMaxSizeFilePath: m.photoMaxSizeFilePath }; }); } diff --git a/src/db/schema.ts b/src/db/schema.ts index 80c5be2..9a4ef17 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -7,6 +7,7 @@ export const messagesTable = sqliteTable("messages", { fromId: int().notNull(), text: text(), date: int().notNull(), + photoMaxSizeFilePath: text(), }); export type MessageInsert = typeof messagesTable.$inferInsert; diff --git a/src/index.ts b/src/index.ts index e571d68..c0f61ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { executeChatCommand, extractTextMessage, findAndExecuteCallbackCommand, + ignore, initSystemSpecs, logError, randomValue, @@ -57,6 +58,17 @@ import {CallbackCommand} from "./base/callback-command"; import {OllamaCancel} from "./callback_commands/ollama-cancel"; import {MistralChat} from "./commands/mistral-chat"; import {Transliteration} from "./commands/transliteration"; +import {OllamaListModels} from "./commands/ollama-list-models"; +import {OllamaGetModel} from "./commands/ollama-get-model"; +import {OllamaSetModel} from "./commands/ollama-set-model"; +import {Mistral} from "@mistralai/mistralai"; +import {GoogleGenAI} from "@google/genai"; +import {MistralGetModel} from "./commands/mistral-get-model"; +import {MistralSetModel} from "./commands/mistral-set-model"; +import {MistralListModels} from "./commands/mistral-list-models"; +import {GeminiListModels} from "./commands/gemini-list-models"; +import {GeminiGetModel} from "./commands/gemini-get-model"; +import {GeminiSetModel} from "./commands/gemini-set-model"; process.setUncaughtExceptionCaptureCallback(console.error); @@ -69,6 +81,9 @@ export const userDao = new UserDao(); export const bot = new TelegramBot({botToken: Environment.BOT_TOKEN, testEnvironment: Environment.TEST_ENVIRONMENT}); export let botUser: User; +export const googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY}); +export const mistralAi = new Mistral({apiKey: Environment.MISTRAL_API_KEY}); + export const ollama = new Ollama({ host: Environment.OLLAMA_ADDRESS, headers: {"Authorization": `Bearer ${Environment.OLLAMA_API_KEY}`} @@ -148,7 +163,13 @@ export const callbackCommands: CallbackCommand[] = [ ]; if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) { - chatCommands.push(new OllamaChat(), new OllamaPrompt()); + chatCommands.push( + new OllamaChat(), + new OllamaPrompt(), + new OllamaListModels(), + new OllamaGetModel(), + new OllamaSetModel() + ); } if (Environment.OLLAMA_API_KEY) { @@ -156,18 +177,29 @@ if (Environment.OLLAMA_API_KEY) { } if (Environment.GEMINI_API_KEY) { - chatCommands.push(new GeminiChat()); + chatCommands.push( + new GeminiChat(), + new GeminiListModels(), + new GeminiGetModel(), + new GeminiSetModel() + ); } if (Environment.MISTRAL_API_KEY) { - chatCommands.push(new MistralChat()); + chatCommands.push( + new MistralChat(), + new MistralListModels(), + new MistralGetModel(), + new MistralSetModel() + ); } async function main() { console.log( `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + `DATA_PATH: ${Environment.DATA_PATH}\n` + - `MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}` + `MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` + + `ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}` ); const commands = chatCommands.filter(cmd => { @@ -214,7 +246,7 @@ bot.on("edited_message", async (msg) => { bot.on("message", async (msg) => { console.log("message", msg); - await Promise.all([MessageStore.put(msg), UserStore.put(msg.from)]); + Promise.all([MessageStore.put(msg), UserStore.put(msg.from)]).then(ignore); if ((msg.new_chat_members?.length || 0 > 0)) { await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(inviteAnswers)}).catch(logError); @@ -232,8 +264,15 @@ bot.on("message", async (msg) => { const cmdText = msg.text || msg.caption || ""; + const then = Date.now(); + const cmd = searchChatCommand(chatCommands, cmdText); const executed = await executeChatCommand(cmd, msg, cmdText); + + const now = Date.now(); + const diff = now - then; + console.log("diff", diff); + if (executed || !cmdText) return; const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase()); diff --git a/src/model/stored-message.ts b/src/model/stored-message.ts index d19dcc7..79bf9e1 100644 --- a/src/model/stored-message.ts +++ b/src/model/stored-message.ts @@ -1,8 +1,9 @@ export type StoredMessage = { chatId: number; - messageId: number; + id: number; replyToMessageId?: number | null; fromId: number; text?: string; date: number; + photoMaxSizeFilePath?: string | null; }; \ No newline at end of file diff --git a/src/util/utils.ts b/src/util/utils.ts index 07fe342..1de7d2e 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -14,6 +14,7 @@ import {UserStore} from "../common/user-store"; import * as orm from "drizzle-orm"; import {sql, type SQL} from "drizzle-orm"; import fs from "node:fs"; +import path from "node:path"; export const ignore = () => { }; @@ -38,12 +39,21 @@ export const errorPlaceholder = async (msg: Message) => { await sendErrorPlaceholder(msg).catch(logError); }; -export function searchChatCommand(commands: ChatCommand[], text: string): ChatCommand | null { - for (let i = 0; i < commands.length; i++) { - const command = commands[i]; - if (command?.regexp.test(text)) { - return command; +export function searchChatCommand( + commands: ChatCommand[], + text: string, + botUsername: string = botUser.username +): ChatCommand | null { + for (const command of commands) { + const match = command.finalRegexp.exec(text); + if (!match) continue; + + const mentioned = match[2]?.toLowerCase(); + if (botUsername && mentioned && mentioned !== botUsername.toLowerCase()) { + continue; } + + return command; } return null; @@ -81,13 +91,13 @@ export async function checkRequirements(cmd: ChatCommand | null, msg: Message): if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) { console.log(`${cmd.title}: creatorId is bad`); - await replyToMessage(msg, "Вы не являетесь создателем бота."); + await oldReplyToMessage(msg, "Вы не являетесь создателем бота."); return false; } if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) { console.log(`${cmd.title}: adminId is bad`); - await replyToMessage(msg, "Вы не являетесь администратором бота."); + await oldReplyToMessage(msg, "Вы не являетесь администратором бота."); return false; } @@ -97,20 +107,20 @@ export async function checkRequirements(cmd: ChatCommand | null, msg: Message): if (!isAdmin) { console.log(`${cmd.title}: chatAdminId is bad`); - await replyToMessage(msg, "Бот не является администратором чата."); + await oldReplyToMessage(msg, "Бот не является администратором чата."); return false; } } if (reqs.isRequiresChat() && msg.chat.type === "private") { console.log(`${cmd.title}: chatId is bad`); - await replyToMessage(msg, "Тут Вам не чат."); + await oldReplyToMessage(msg, "Тут Вам не чат."); return false; } if (reqs.isRequiresReply() && !msg.reply_to_message) { console.log(`${cmd.title}: replyMessage is bad`); - await replyToMessage(msg, "Отсутствует ответ на сообщение."); + await oldReplyToMessage(msg, "Отсутствует ответ на сообщение."); return false; } @@ -179,10 +189,11 @@ export async function editMessageText(chatId: number, messageId: number, message } export type SendOptions = { - chatId?: number; + chat_id?: number; message?: Message, + message_id?: number; text: string, - parseMode?: ParseMode, + parse_mode?: ParseMode, }; export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise { @@ -197,15 +208,28 @@ export async function oldSendMessage(message: Message, text: string, parseMode?: export async function sendMessage(options: SendOptions): Promise { const response = await bot.sendMessage({ - chat_id: options.chatId ?? options.message?.chat?.id, + chat_id: options.chat_id ?? options.message?.chat?.id, text: options.text, - parse_mode: options.parseMode + parse_mode: options.parse_mode }); return Promise.resolve(response); } -export async function replyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise { +export async function replyToMessage(options: SendOptions): Promise { + const response = await bot.sendMessage({ + chat_id: options.chat_id ?? options.message?.chat?.id, + text: options.text, + parse_mode: options.parse_mode, + reply_parameters: { + message_id: options.message_id || options.message?.message_id + } + }); + + return Promise.resolve(response); +} + +export async function oldReplyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise { const response = await bot.sendMessage({ chat_id: message.chat.id, text: text, @@ -382,6 +406,7 @@ export function extractTextStored(msg: StoredMessage, prefix: string): string { } export function extractText(text: string, prefix: string): string { + if (!text) return ""; if (text.toLowerCase().startsWith(prefix.toLowerCase())) { text = text.substring(prefix.length).trim(); } @@ -389,17 +414,57 @@ export function extractText(text: string, prefix: string): string { return text; } +export function isStoredMessage(msg: Message | StoredMessage): msg is StoredMessage { + return "id" in msg; +} + +export async function loadImageIfExists(msg: Message | StoredMessage): Promise { + if (isStoredMessage(msg)) { + return msg.photoMaxSizeFilePath; + } + + let imageFilePath: string | null = null; + + const maxSize = await getPhotoMaxSize(msg.photo); + if (maxSize) { + const imagePath = path.join(Environment.DATA_PATH, "temp"); + if (!fs.existsSync(imagePath)) { + fs.mkdirSync(imagePath); + } + + imageFilePath = path.join(imagePath, maxSize.unique_file_id + ".jpg"); + if (!fs.existsSync(imageFilePath)) { + const res = await axios.get(maxSize.url, {responseType: "arraybuffer"}); + const src = Buffer.from(res.data); + + try { + fs.writeFileSync(imageFilePath, src); + } catch (e) { + console.error(e); + imageFilePath = null; + } + } + } + + return imageFilePath; +} + export async function collectReplyChainText(triggerMsg: Message, prefix: string = Environment.BOT_PREFIX, limit: number = 40, includeTrigger = true): Promise { const chatId = triggerMsg.chat.id as number; const parts: MessagePart[] = []; if (includeTrigger) { const t = extractTextMessage(triggerMsg, prefix); - if (t) parts.push({ - bot: triggerMsg.from.id === botUser.id, - content: t, - name: triggerMsg.from.first_name - }); + const img = (await loadImageIfExists(triggerMsg)) /*|| triggerMsg.reply_to_message ? + (await loadImageIfExists(triggerMsg.reply_to_message)) : null*/; + if (t) { + parts.push({ + bot: triggerMsg.from.id === botUser.id, + content: t, + name: triggerMsg.from.first_name, + images: img ? [img] : [] + }); + } } const first = triggerMsg.reply_to_message; @@ -408,7 +473,15 @@ export async function collectReplyChainText(triggerMsg: Message, prefix: string } const firstText = extractTextMessage(first, prefix); - if (firstText) parts.push({bot: first.from.id === botUser.id, content: firstText, name: first.from.first_name}); + if (firstText || first.photo) { + const img = await loadImageIfExists(first); + parts.push({ + bot: first.from.id === botUser.id, + content: firstText, + name: first.from.first_name, + images: img ? [img] : [] + }); + } let curId = first.message_id; @@ -418,14 +491,16 @@ export async function collectReplyChainText(triggerMsg: Message, prefix: string if (!parentId) break; const parent = await messageDao.getById({chatId: chatId, id: parentId}); - if (!parent?.text) break; + if (!parent?.text && !parent?.photoMaxSizeFilePath) break; const user = await UserStore.get(parent.fromId); + const img = await loadImageIfExists(parent); parts.push({ bot: parent.fromId === botUser.id, content: extractTextStored(parent, prefix), - name: user?.firstName + name: user?.firstName, + images: img ? [img] : [] }); curId = parentId; } @@ -806,4 +881,8 @@ export function ifTrue(exp?: never): boolean { if (!exp) return false; return ["true", "t", "y", 1, "1"].includes(exp); +} + +export function boolToEmoji(bool: boolean): string { + return bool ? "✅" : "❌"; } \ No newline at end of file