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
This commit is contained in:
2026-01-20 08:31:05 +03:00
parent c31345a3eb
commit 32baaebb93
52 changed files with 605 additions and 231 deletions
+44 -1
View File
@@ -1,15 +1,58 @@
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "./requirements"; import {Requirements} from "./requirements";
export type ArgsMode = "none" | "optional" | "required";
export abstract class ChatCommand { export abstract class ChatCommand {
abstract regexp: RegExp; regexp?: RegExp | null;
command?: string | string[];
argsMode: ArgsMode = "none";
requirements?: Requirements = null; requirements?: Requirements = null;
title?: string; title?: string;
description?: 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( abstract execute(
msg: Message, msg: Message,
match?: RegExpExecArray match?: RegExpExecArray
): Promise<void>; ): Promise<void>;
} }
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");
}
+1 -1
View File
@@ -7,7 +7,7 @@ import {Environment} from "../common/environment";
import {botUser} from "../index"; import {botUser} from "../index";
export class AdminsAdd extends ChatCommand { export class AdminsAdd extends ChatCommand {
regexp = /^\/addadmin/i; command = "addAdmin";
title = "/addAdmin"; title = "/addAdmin";
description = "Add user to admins"; description = "Add user to admins";
+1 -1
View File
@@ -7,7 +7,7 @@ import {Environment} from "../common/environment";
import {botUser} from "../index"; import {botUser} from "../index";
export class AdminsRemove extends ChatCommand { export class AdminsRemove extends ChatCommand {
regexp = /^\/removeadmin/i; command = "removeAdmin";
title = "/removeAdmin"; title = "/removeAdmin";
description = "Remove user from admins"; description = "Remove user from admins";
+5 -4
View File
@@ -4,15 +4,16 @@ import {errorPlaceholder, logError, oldSendMessage} from "../util/utils";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
export class Ae implements ChatCommand { export class Ae extends ChatCommand {
regexp = /^\/ae\s([^]+)/i; argsMode = "required" as const;
title = "/ae"; title = "/ae";
description = "evaluation"; description = "evaluation";
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, params: string[]) { async execute(msg: Message, params?: RegExpExecArray) {
const match = params[1]; const match = params?.[3];
try { try {
let e = eval(match); let e = eval(match);
+4 -5
View File
@@ -3,11 +3,10 @@ import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {bot, botUser} from "../index"; 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"; import {Environment} from "../common/environment";
export class Ban extends ChatCommand { export class Ban extends ChatCommand {
regexp = /^\/ban/i;
title = "/ban [reply]"; title = "/ban [reply]";
description = "ban user from chat"; description = "ban user from chat";
@@ -25,17 +24,17 @@ export class Ban extends ChatCommand {
const userId = user.id; const userId = user.id;
if (userId === botUser.id) { if (userId === botUser.id) {
await replyToMessage(msg, "Используй /leave").catch(logError); await oldReplyToMessage(msg, "Используй /leave").catch(logError);
return; return;
} }
if (userId === Environment.CREATOR_ID) { if (userId === Environment.CREATOR_ID) {
await replyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError); await oldReplyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError);
return; return;
} }
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) { if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
await replyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError); await oldReplyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError);
return; return;
} }
+6 -4
View File
@@ -1,16 +1,18 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; 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 { export class Choice extends ChatCommand {
regexp = /^\/choice\b\s*(.*)$/i; command = "choice";
argsMode = "required" as const;
title = "/choice a, b, ..., c"; title = "/choice a, b, ..., c";
description = "Выбор случайного значения"; description = "Выбор случайного значения";
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); console.log("match", match);
const payload = match[1]; const payload = match[3];
const re = const re =
/\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+?))\s*(?:,|$)/g; /\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+?))\s*(?:,|$)/g;
@@ -32,6 +34,6 @@ export class Choice extends ChatCommand {
const random = randomValue(out); const random = randomValue(out);
await replyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError); await oldReplyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError);
} }
} }
+2 -3
View File
@@ -1,14 +1,13 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; 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 { export class Coin extends ChatCommand {
regexp = /^\/coin$/i;
title = "/coin"; title = "/coin";
description = "Heads or tails"; description = "Heads or tails";
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
const random = getRangedRandomInt(0, 2); const random = getRangedRandomInt(0, 2);
const headsOrTails = random === 1 ? "Выпал *Орёл* 🪙" : "Выпала *Решка* 🪙"; const headsOrTails = random === 1 ? "Выпал *Орёл* 🪙" : "Выпала *Решка* 🪙";
await replyToMessage(msg, headsOrTails, "Markdown").catch(logError); } await oldReplyToMessage(msg, headsOrTails, "Markdown").catch(logError); }
} }
+1 -2
View File
@@ -7,8 +7,7 @@ type DiceEmoji = "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰";
const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"]; const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"];
export class Dice extends ChatCommand { export class Dice extends ChatCommand {
regexp = /^\/dice/i; title = "/dice";
title = "/dice [emoji]";
description = "Sends random or specific dice"; description = "Sends random or specific dice";
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
+11 -7
View File
@@ -1,10 +1,12 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; 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"; import {bot} from "../index";
export class Distort extends ChatCommand { export class Distort extends ChatCommand {
regexp = /^\/distort(?:@[\w_]+)?(?:\s+(\d+))?(?:\s+(\d+))?\s*$/i; command = "distort";
argsMode = "optional" as const;
title = "/distort [amp] [wavelength]"; title = "/distort [amp] [wavelength]";
description = "Distortion of picture"; description = "Distortion of picture";
@@ -13,7 +15,7 @@ export class Distort extends ChatCommand {
const reply = msg.reply_to_message; const reply = msg.reply_to_message;
if (!reply) { if (!reply) {
await replyToMessage( await oldReplyToMessage(
msg, msg,
"Ответь командой /distort на сообщение с картинкой (фото, документ или стикер).\n" + "Пример: /distort 16 80" "Ответь командой /distort на сообщение с картинкой (фото, документ или стикер).\n" + "Пример: /distort 16 80"
); );
@@ -22,15 +24,17 @@ export class Distort extends ChatCommand {
const fileId = extractImageFileId(reply); const fileId = extractImageFileId(reply);
if (!fileId) { if (!fileId) {
await replyToMessage( await oldReplyToMessage(
msg, msg,
"В реплае не вижу картинку. Пришли фото или файл-изображение." "В реплае не вижу картинку. Пришли фото или файл-изображение."
); );
return; return;
} }
const amp = match?.[1] ? parseInt(match[1], 10) : 14; const args = (match?.[3] ?? "").trim();
const wavelength = match?.[2] ? parseInt(match[2], 10) : 72; const [a, b] = args ? args.split(/\s+/) : [];
const amp = a ? Number(a) : 14;
const wavelength = b ? Number(b) : 72;
try { try {
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"}); await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
@@ -48,7 +52,7 @@ export class Distort extends ChatCommand {
caption: `Искажение готово ✅ (amp=${amp}, wavelength=${wavelength})`, caption: `Искажение готово ✅ (amp=${amp}, wavelength=${wavelength})`,
}); });
} catch (e) { } catch (e) {
await replyToMessage( await oldReplyToMessage(
msg, `Не получилось исказить изображение: ${e?.message ?? String(e)}` msg, `Не получилось исказить изображение: ${e?.message ?? String(e)}`
).catch(logError); ).catch(logError);
} }
+16 -14
View File
@@ -3,30 +3,31 @@ import {Message} from "typescript-telegram-bot-api";
import { import {
collectReplyChainText, collectReplyChainText,
editMessageText, editMessageText,
escapeMarkdownV2Text, extractText, escapeMarkdownV2Text,
extractText,
logError, logError,
replyToMessage, oldReplyToMessage,
startIntervalEditor startIntervalEditor
} from "../util/utils"; } from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {bot} from "../index"; import {bot, googleAi} from "../index";
import {MessageStore} from "../common/message-store"; import {MessageStore} from "../common/message-store";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {ApiError, GoogleGenAI} from "@google/genai"; import {ApiError} from "@google/genai";
export class GeminiChat extends ChatCommand { export class GeminiChat extends ChatCommand {
regexp = /^\/gemini\s([^]+)/i; command = "gemini";
argsMode = "required" as const;
title = "/gemini"; title = "/gemini";
description = "Chat with AI (Gemini)"; description = "Chat with AI (Gemini)";
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
private googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY});
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); console.log("match", match);
return this.executeGemini(msg, match?.[1]); return this.executeGemini(msg, match?.[3]);
} }
async executeGemini(msg: Message, text: string): Promise<void> { async executeGemini(msg: Message, text: string): Promise<void> {
@@ -67,8 +68,8 @@ export class GeminiChat extends ChatCommand {
} }
}); });
const stream = await this.googleAi.models.generateContentStream({ const stream = await googleAi.models.generateContentStream({
model: Environment.GEMINI_MODEL || "gemini-2.5-flash", model: Environment.GEMINI_MODEL,
contents: chatContent, contents: chatContent,
}); });
@@ -85,6 +86,7 @@ export class GeminiChat extends ChatCommand {
onStop: async () => { onStop: async () => {
} }
}); });
await editor.tick();
try { try {
for await (const chunk of stream) { for await (const chunk of stream) {
@@ -123,21 +125,21 @@ export class GeminiChat extends ChatCommand {
waitMessage.reply_to_message = msg; waitMessage.reply_to_message = msg;
waitMessage.text = messageText; waitMessage.text = messageText;
MessageStore.put(waitMessage); await MessageStore.put(waitMessage);
await replyToMessage(waitMessage, `⏱️ ${diff}s`); await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (error instanceof ApiError) { if (error instanceof ApiError) {
if (error.status === 429) { if (error.status === 429) {
await replyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError); await oldReplyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError);
return; return;
} }
} }
await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
} }
} }
} }
+13
View File
@@ -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<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError);
}
}
+36
View File
@@ -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<void> {
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);
}
}
}
+25
View File
@@ -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<void> {
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);
}
}
+4 -3
View File
@@ -4,8 +4,9 @@ import {ChatCommand} from "../base/chat-command";
import {chatCommands} from "../index"; import {chatCommands} from "../index";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors"; import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
export class Help implements ChatCommand { export class Help extends ChatCommand {
regexp = /^\/(h|help)/i; command = ["h", "help"];
title = "/help"; title = "/help";
description = "Show list of commands"; description = "Show list of commands";
@@ -16,7 +17,7 @@ export class Help implements ChatCommand {
text += `${chatCommandToString(c)}\n`; text += `${chatCommandToString(c)}\n`;
}); });
await sendMessage({chatId: msg.from.id, text: text}) await sendMessage({chat_id: msg.from.id, text: text})
.then(async () => { .then(async () => {
if (msg.chat.type !== "private") { if (msg.chat.type !== "private") {
await sendMessage({message: msg, text: "Отправил команды в ЛС 😎"}).catch(logError); await sendMessage({message: msg, text: "Отправил команды в ЛС 😎"}).catch(logError);
+2 -3
View File
@@ -1,9 +1,8 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, oldReplyToMessage} from "../util/utils";
export class Id extends ChatCommand { export class Id extends ChatCommand {
regexp = /^\/id/i;
title = "/id"; title = "/id";
description = "ID of chat, user and reply (if replied to any message)"; 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}\`\`\``; text += ` \nreply id: \n\`\`\`${msg.reply_to_message.from.id}\`\`\``;
} }
await replyToMessage(msg, text, "MarkdownV2").catch(logError); await oldReplyToMessage(msg, text, "MarkdownV2").catch(logError);
} }
} }
-1
View File
@@ -5,7 +5,6 @@ import {Requirement} from "../base/requirement";
import {bot} from "../index"; import {bot} from "../index";
export class Leave extends ChatCommand { export class Leave extends ChatCommand {
regexp = /^\/leave/i;
title = "/leave"; title = "/leave";
description = "Bot will leave current chat"; description = "Bot will leave current chat";
+14 -42
View File
@@ -7,31 +7,27 @@ import {
editMessageText, editMessageText,
escapeMarkdownV2Text, escapeMarkdownV2Text,
extractText, extractText,
getPhotoMaxSize,
logError, logError,
replyToMessage, oldReplyToMessage,
startIntervalEditor startIntervalEditor
} from "../util/utils"; } from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {bot} from "../index"; import {bot, mistralAi} from "../index";
import {MessageStore} from "../common/message-store"; import {MessageStore} from "../common/message-store";
import {Mistral} from "@mistralai/mistralai";
import path from "node:path";
import fs from "node:fs"; import fs from "node:fs";
import axios from "axios";
export class MistralChat extends ChatCommand { export class MistralChat extends ChatCommand {
regexp = /^\/mistral\s([^]+)/i; command = "mistral";
argsMode = "required" as const;
title = "/mistral"; title = "/mistral";
description = "Chat with AI (Mistral)"; description = "Chat with AI (Mistral)";
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
private mistralAi = new Mistral({apiKey: Environment.MISTRAL_API_KEY});
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); console.log("match", match);
return this.executeMistral(msg, match?.[1]); return this.executeMistral(msg, match?.[3]);
} }
async executeMistral(msg: Message, text: string): Promise<void> { async executeMistral(msg: Message, text: string): Promise<void> {
@@ -39,41 +35,18 @@ export class MistralChat extends ChatCommand {
const chatId = msg.chat.id; 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<ArrayBuffer>(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"); const messageParts = await collectReplyChainText(msg, "/mistral");
console.log("MESSAGE PARTS", messageParts); console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map((part, i) => { const chatMessages = messageParts.map(part => {
const content = []; const content = [];
content.push({ content.push({
type: "text", type: "text",
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + extractText(part.content, Environment.BOT_PREFIX), text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + extractText(part.content, Environment.BOT_PREFIX),
}); });
if (imageFilePath && i === 0) { if (part.images && part.images.length > 0) {
const base64Image = Buffer.from(fs.readFileSync(imageFilePath)).toString("base64"); const base64Image = Buffer.from(fs.readFileSync(part.images[0])).toString("base64");
content.push({ content.push({
type: "image_url", type: "image_url",
imageUrl: "data:image/jpeg;base64," + base64Image imageUrl: "data:image/jpeg;base64," + base64Image
@@ -102,8 +75,8 @@ export class MistralChat extends ChatCommand {
} }
}); });
const stream = await this.mistralAi.chat.stream({ const stream = await mistralAi.chat.stream({
model: Environment.MISTRAL_MODEL || "mistral-small-latest", model: Environment.MISTRAL_MODEL,
messages: chatMessages as any messages: chatMessages as any
}); });
@@ -120,14 +93,13 @@ export class MistralChat extends ChatCommand {
onStop: async () => { onStop: async () => {
} }
}); });
await editor.tick();
try { try {
for await (const chunk of stream) { for await (const chunk of stream) {
// const text = chunk.text;
const text = chunk.data.choices[0].delta.content; const text = chunk.data.choices[0].delta.content;
console.log("chunk", chunk); console.log("chunk", chunk);
// const text = "";
const length = (messageText + text).length; const length = (messageText + text).length;
if (length > 4096) { if (length > 4096) {
messageText = messageText.slice(0, 4093) + "..."; messageText = messageText.slice(0, 4093) + "...";
@@ -163,7 +135,7 @@ export class MistralChat extends ChatCommand {
waitMessage.text = messageText; waitMessage.text = messageText;
MessageStore.put(waitMessage); MessageStore.put(waitMessage);
await replyToMessage(waitMessage, `⏱️ ${diff}s`); await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
} }
} catch (error) { } catch (error) {
console.error(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);
} }
} }
} }
+17
View File
@@ -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<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.MISTRAL_MODEL}"`}).catch(logError);
}
}
+37
View File
@@ -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<void> {
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);
}
}
}
+25
View File
@@ -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<void> {
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);
}
}
+1 -2
View File
@@ -7,8 +7,7 @@ import {fullName, logError, oldSendMessage} from "../util/utils";
import {botUser} from "../index"; import {botUser} from "../index";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class Mute implements ChatCommand { export class Mute extends ChatCommand {
regexp = /^\/mute/i;
title = "/mute"; title = "/mute";
description = "Bot will ignore user"; description = "Bot will ignore user";
+16 -39
View File
@@ -6,27 +6,25 @@ import {
editMessageText, editMessageText,
escapeMarkdownV2Text, escapeMarkdownV2Text,
extractText, extractText,
getPhotoMaxSize,
logError, logError,
replyToMessage, oldReplyToMessage,
startIntervalEditor startIntervalEditor
} from "../util/utils"; } from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {MessageStore} from "../common/message-store"; 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 {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel"; import {OllamaCancel} from "../callback_commands/ollama-cancel";
export class OllamaChat extends ChatCommand { export class OllamaChat extends ChatCommand {
regexp = /^\/ollama\s([^]+)/; command = "ollama";
argsMode = "required" as const;
title = "/ollama"; title = "/ollama";
description = "talk to AI (Ollama)"; description = "Chat with AI (Ollama)";
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
console.log("match", match); console.log("match", match);
return this.executeOllama(msg, match?.[1]); return this.executeOllama(msg, match?.[3]);
} }
async executeOllama(msg: Message, text: string): Promise<void> { async executeOllama(msg: Message, text: string): Promise<void> {
@@ -34,41 +32,18 @@ export class OllamaChat extends ChatCommand {
const chatId = msg.chat.id; 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<ArrayBuffer>(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); const messageParts = await collectReplyChainText(msg);
console.log("MESSAGE PARTS", messageParts); console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map((part, i) => { const chatMessages = messageParts.map(part => {
return { return {
role: part.bot ? "assistant" : "user", 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), 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.reverse();
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: null}); chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []});
let waitMessage: Message; let waitMessage: Message;
@@ -81,7 +56,8 @@ export class OllamaChat extends ChatCommand {
waitMessage = await bot.sendMessage({ waitMessage = await bot.sendMessage({
chat_id: chatId, 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: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
message_id: msg.message_id message_id: msg.message_id
@@ -117,17 +93,18 @@ export class OllamaChat extends ChatCommand {
onStop: async () => { onStop: async () => {
} }
}); });
await editor.tick();
try { try {
for await (const chunk of stream) { for await (const chunk of stream) {
if (!getOllamaRequest(uuid).done) {
currentText += chunk.message.content; currentText += chunk.message.content;
if (currentText.length > 4096) { if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "..."; currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true; shouldBreak = true;
} }
} else {
if (getOllamaRequest(uuid).done) {
shouldBreak = true; shouldBreak = true;
} }
@@ -154,7 +131,7 @@ export class OllamaChat extends ChatCommand {
waitMessage.text = currentText; waitMessage.text = currentText;
await MessageStore.put(waitMessage); 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; break;
} }
} }
@@ -173,7 +150,7 @@ export class OllamaChat extends ChatCommand {
}).catch(logError); }).catch(logError);
console.error(error); console.error(error);
await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
} }
} }
} }
+27
View File
@@ -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<void> {
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);
}
}
+35
View File
@@ -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<void> {
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);
}
}
}
+6 -4
View File
@@ -1,13 +1,15 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {bot, ollama} from "../index"; 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 {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class OllamaPrompt extends ChatCommand { export class OllamaPrompt extends ChatCommand {
regexp = /^\/ollamaprompt\s([^]+)/i; command = "ollamaPrompt";
argsMode = "required" as const;
title = "/ollamaPrompt"; title = "/ollamaPrompt";
description = "Custom prompt for AI (Ollama)"; description = "Custom prompt for AI (Ollama)";
@@ -86,7 +88,7 @@ export class OllamaPrompt extends ChatCommand {
messageText += `\n\nДумал ${diff}s`; messageText += `\n\nДумал ${diff}s`;
await editMessageText(chatId, waitMessage.message_id, messageText); await editMessageText(chatId, waitMessage.message_id, messageText);
await replyToMessage(waitMessage, "Закончил лишь часть 😉"); await oldReplyToMessage(waitMessage, "Закончил лишь часть 😉");
break; break;
} }
@@ -101,7 +103,7 @@ export class OllamaPrompt extends ChatCommand {
messageText += `\n\nДумал ${diff}s`; messageText += `\n\nДумал ${diff}s`;
await editMessageText(chatId, waitMessage.message_id, messageText); await editMessageText(chatId, waitMessage.message_id, messageText);
await replyToMessage(waitMessage, "Закончил 😉"); await oldReplyToMessage(waitMessage, "Закончил 😉");
} }
} }
} catch (error) { } catch (error) {
+4 -2
View File
@@ -8,7 +8,9 @@ import {editMessageText} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class OllamaSearch extends ChatCommand { export class OllamaSearch extends ChatCommand {
regexp = /^\/(s|search)\s([^]+)/; command = ["s", "search"];
argsMode = "required" as const;
title = "/search"; title = "/search";
description = "Web search via Ollama"; description = "Web search via Ollama";
@@ -29,7 +31,7 @@ export class OllamaSearch extends ChatCommand {
parse_mode: "Markdown" parse_mode: "Markdown"
}); });
const results = await ollama.webSearch({query: match?.[2]}); const results = await ollama.webSearch({query: match?.[3]});
console.log("results", results); console.log("results", results);
let message = "Результаты:\n\n"; let message = "Результаты:\n\n";
+25
View File
@@ -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<void> {
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);
}
}
+1 -2
View File
@@ -2,8 +2,7 @@ import {logError, oldSendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
export class Ping implements ChatCommand { export class Ping extends ChatCommand {
regexp = /^\/ping/i;
title = "/ping"; title = "/ping";
description = "Ping between received and sent message"; description = "Ping between received and sent message";
+2 -4
View File
@@ -1,12 +1,10 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; 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"; import {prefixAnswers} from "../db/database";
export class PrefixResponse extends ChatCommand { export class PrefixResponse extends ChatCommand {
regexp: RegExp;
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage(msg, randomValue(prefixAnswers)).catch(logError); await oldReplyToMessage(msg, randomValue(prefixAnswers)).catch(logError);
} }
} }
+3 -4
View File
@@ -5,7 +5,6 @@ import {bot} from "../index";
import QRCode from "qrcode"; import QRCode from "qrcode";
export class Qr extends ChatCommand { export class Qr extends ChatCommand {
regexp = /^\/qr/i;
title = "/qr"; title = "/qr";
description = "Generates QR-code from text you sent or replied to."; 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); const payload = extractMessagePayload(msg, matchText);
if (!payload) { if (!payload) {
await sendMessage({ await sendMessage({
chatId: chatId, chat_id: chatId,
text: "Отправь: /qr <текст или ссылка>\n" + "или ответь командой /qr на сообщение, из которого взять текст." text: "Отправь: /qr <текст или ссылка>\n" + "или ответь командой /qr на сообщение, из которого взять текст."
}); });
return; return;
@@ -26,7 +25,7 @@ export class Qr extends ChatCommand {
if (payload.length > 1500) { if (payload.length > 1500) {
await sendMessage({ await sendMessage({
chatId: chatId, chat_id: chatId,
text: `Слишком длинный текст для QR (${payload.length} символов). Максимум 1500 символов.` text: `Слишком длинный текст для QR (${payload.length} символов). Максимум 1500 символов.`
}); });
return; return;
@@ -51,7 +50,7 @@ export class Qr extends ChatCommand {
} }
}); });
} catch (e) { } 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);
} }
} }
} }
+7 -5
View File
@@ -13,8 +13,8 @@ import {
getUserAvatar, getUserAvatar,
logError, logError,
makeDarkGradientBgFancy, makeDarkGradientBgFancy,
oldSendMessage, oldReplyToMessage,
replyToMessage oldSendMessage
} from "../util/utils"; } from "../util/utils";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
@@ -34,7 +34,9 @@ try {
} }
export class Quote extends ChatCommand { export class Quote extends ChatCommand {
regexp = /^\/(cit|q|quote)$/i; command = ["cit", "citation", "q", "quote"];
argsMode = "none" as const;
title = "/quote"; title = "/quote";
description = "Make quote from text (or quote)"; description = "Make quote from text (or quote)";
@@ -45,14 +47,14 @@ export class Quote extends ChatCommand {
const reply = msg.reply_to_message; const reply = msg.reply_to_message;
if (!reply) { if (!reply) {
await replyToMessage(msg, "Сделай /quote реплаем на сообщение 🙂").catch(logError); await oldReplyToMessage(msg, "Сделай /quote реплаем на сообщение 🙂").catch(logError);
return; return;
} }
try { try {
const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim(); const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim();
if (quoteRaw.length === 0) { if (quoteRaw.length === 0) {
await replyToMessage(msg, "Не нашёл в сообщении текста 😢").catch(logError); await oldReplyToMessage(msg, "Не нашёл в сообщении текста 😢").catch(logError);
return; return;
} }
-1
View File
@@ -3,7 +3,6 @@ import {getRandomInt, getRangedRandomInt, logError, oldSendMessage} from "../uti
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
export class RandomInt extends ChatCommand { export class RandomInt extends ChatCommand {
regexp = /^\/randomInt/i;
title = "/randomInt [min] [max]"; title = "/randomInt [min] [max]";
description = "Ranged random integer from parameters"; description = "Ranged random integer from parameters";
+1 -2
View File
@@ -2,8 +2,7 @@ import {ChatCommand} from "../base/chat-command";
import {getRandomInt, logError, oldSendMessage} from "../util/utils"; import {getRandomInt, logError, oldSendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
export class RandomString implements ChatCommand { export class RandomString extends ChatCommand {
regexp = /^\/randomString/i;
title = "/randomString [length]"; title = "/randomString [length]";
description = "literally random string (up to 4096 symbols)"; description = "literally random string (up to 4096 symbols)";
-1
View File
@@ -16,7 +16,6 @@ const timings = [1500, 2500];
const timer = [3, 2, 1]; const timer = [3, 2, 1];
export class Shutdown extends ChatCommand { export class Shutdown extends ChatCommand {
regexp = /^\/shutdown/i;
title = "/shutdown"; title = "/shutdown";
description = "Self-destruction sequence for bot (shutdown)"; description = "Self-destruction sequence for bot (shutdown)";
-1
View File
@@ -4,7 +4,6 @@ import {chatCommands} from "../index";
import {Help} from "./help"; import {Help} from "./help";
export class Start extends ChatCommand { export class Start extends ChatCommand {
regexp = /^\/start/i;
title = "/start"; title = "/start";
description = "Start the bot"; description = "Start the bot";
+1 -2
View File
@@ -3,8 +3,7 @@ import {logError, oldSendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {systemInfoText} from "../index"; import {systemInfoText} from "../index";
export class SystemSpecs implements ChatCommand { export class SystemSpecs extends ChatCommand {
regexp = /^\/systeminfo/i;
title = "/systemInfo"; title = "/systemInfo";
description = "System information"; description = "System information";
+3 -3
View File
@@ -1,14 +1,14 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; 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"; import {testAnswers} from "../db/database";
export class Test implements ChatCommand { export class Test extends ChatCommand {
regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i; regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i;
title = "тест"; title = "тест";
description = "System functionality check"; description = "System functionality check";
async execute(msg: Message) { async execute(msg: Message) {
await replyToMessage(msg, randomValue(testAnswers) || "а").catch(logError); await oldReplyToMessage(msg, randomValue(testAnswers) || "а").catch(logError);
} }
} }
+6 -4
View File
@@ -2,12 +2,14 @@ import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {logError, replyToMessage} from "../util/utils"; import {logError, oldReplyToMessage} from "../util/utils";
import {bot} from "../index"; import {bot} from "../index";
export class Title extends ChatCommand { export class Title extends ChatCommand {
regexp = /^\/title\s([^]+)/; command = "title";
title = "/title [title]"; argsMode = "required" as const;
title = "/title";
description = "Change group title"; description = "Change group title";
requirements = Requirements.Build( requirements = Requirements.Build(
@@ -19,7 +21,7 @@ export class Title extends ChatCommand {
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const title = (match?.[1] ?? "").trim(); const title = (match?.[1] ?? "").trim();
if (title.length === 0) { if (title.length === 0) {
await replyToMessage(msg, "Не нашёл название...").catch(logError); await oldReplyToMessage(msg, "Не нашёл название...").catch(logError);
return; return;
} }
+2 -3
View File
@@ -1,6 +1,6 @@
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, oldReplyToMessage} from "../util/utils";
const EN = const EN =
"`qwertyuiop[]asdfghjkl;'zxcvbnm,./" + "`qwertyuiop[]asdfghjkl;'zxcvbnm,./" +
@@ -98,7 +98,6 @@ export function fixLayoutAuto(
} }
export class Transliteration extends ChatCommand { export class Transliteration extends ChatCommand {
regexp = /^\/tr/i;
title = "/tr [text or reply]"; title = "/tr [text or reply]";
description = "Transliteration EN <--> RU"; description = "Transliteration EN <--> RU";
@@ -120,6 +119,6 @@ export class Transliteration extends ChatCommand {
const newText = fixLayoutAuto(text, toRuLayout, toEnLayout); const newText = fixLayoutAuto(text, toRuLayout, toEnLayout);
await replyToMessage(msg, newText).catch(logError); await oldReplyToMessage(msg, newText).catch(logError);
} }
} }
+4 -5
View File
@@ -3,11 +3,10 @@ import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {bot, botUser} from "../index"; 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"; import {Environment} from "../common/environment";
export class Unban extends ChatCommand { export class Unban extends ChatCommand {
regexp = /^\/unban/i;
title = "/unban [reply]"; title = "/unban [reply]";
description = "unban user from chat"; description = "unban user from chat";
@@ -25,17 +24,17 @@ export class Unban extends ChatCommand {
const userId = user.id; const userId = user.id;
if (userId === botUser.id) { if (userId === botUser.id) {
await replyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError); await oldReplyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError);
return; return;
} }
if (userId === Environment.CREATOR_ID) { if (userId === Environment.CREATOR_ID) {
await replyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError); await oldReplyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError);
return; return;
} }
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) { if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
await replyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError); await oldReplyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError);
return; return;
} }
+1 -2
View File
@@ -7,8 +7,7 @@ import {Message} from "typescript-telegram-bot-api";
import {botUser} from "../index"; import {botUser} from "../index";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class Unmute implements ChatCommand { export class Unmute extends ChatCommand {
regexp = /^\/unmute/i;
title = "/unmute"; title = "/unmute";
description = "Bot will start responding to the user"; description = "Bot will start responding to the user";
requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.CHAT, Requirement.REPLY); requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.CHAT, Requirement.REPLY);
-1
View File
@@ -3,7 +3,6 @@ import {Message} from "typescript-telegram-bot-api";
import {getUptime, logError, oldSendMessage} from "../util/utils"; import {getUptime, logError, oldSendMessage} from "../util/utils";
export class Uptime extends ChatCommand { export class Uptime extends ChatCommand {
regexp = /^\/uptime/i;
title = "/uptime"; title = "/uptime";
description = "Bot's uptime"; description = "Bot's uptime";
+11 -4
View File
@@ -1,16 +1,23 @@
import {ChatCommand} from "../base/chat-command"; 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 {betterAnswers} from "../db/database";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
export class WhatBetter extends ChatCommand { 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]"; title = "/what better [a] or [b]";
description = "either a or b randomly (50% chance)"; 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) { async execute(msg: Message, match?: RegExpExecArray) {
const a = match[3]; const args = (match?.[3] ?? "").trim();
const b = match[5].trimStart(); 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])}`; const text = `${randomValue(betterAnswers)} ${randomValue([a, b])}`;
+5 -3
View File
@@ -1,9 +1,11 @@
import {ChatCommand} from "../base/chat-command"; 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"; import {Message} from "typescript-telegram-bot-api";
export class When extends ChatCommand { export class When extends ChatCommand {
regexp = /^\/(when|когда)\s([^]+)/i; command = ["when", "когда"];
argsMode = "required" as const;
title = "/when [value]"; title = "/when [value]";
description = "random date"; description = "random date";
@@ -85,6 +87,6 @@ export class When extends ChatCommand {
} }
} }
await replyToMessage(msg, text).catch(logError); await oldReplyToMessage(msg, text).catch(logError);
} }
} }
+16 -4
View File
@@ -30,10 +30,10 @@ export class Environment {
static OLLAMA_API_KEY?: string; static OLLAMA_API_KEY?: string;
static GEMINI_API_KEY?: string; static GEMINI_API_KEY?: string;
static GEMINI_MODEL?: string; static GEMINI_MODEL: string;
static MISTRAL_API_KEY?: string; static MISTRAL_API_KEY?: string;
static MISTRAL_MODEL?: string; static MISTRAL_MODEL: string;
static waitText = "⏳ Дайте-ка подумать..."; static waitText = "⏳ Дайте-ка подумать...";
@@ -64,10 +64,10 @@ export class Environment {
Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY; Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
Environment.GEMINI_API_KEY = process.env.GEMINI_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_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<number>) { static setAdmins(admins: Set<number>) {
@@ -93,4 +93,16 @@ export class Environment {
return has; 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;
}
} }
+1
View File
@@ -2,4 +2,5 @@ export type MessagePart = {
bot: boolean; bot: boolean;
name?: string; name?: string;
content: string; content: string;
images: string[];
} }
+1 -1
View File
@@ -18,7 +18,7 @@ export class MessageStore {
static async put(m: Message, prefix: string = Environment.BOT_PREFIX) { static async put(m: Message, prefix: string = Environment.BOT_PREFIX) {
const msg: StoredMessage = { const msg: StoredMessage = {
chatId: m.chat.id, chatId: m.chat.id,
messageId: m.message_id, id: m.message_id,
replyToMessageId: m.reply_to_message?.message_id ?? null, replyToMessageId: m.reply_to_message?.message_id ?? null,
fromId: m.from.id, fromId: m.from.id,
text: extractTextMessage(m, prefix), text: extractTextMessage(m, prefix),
+7 -6
View File
@@ -1,4 +1,4 @@
import {messagesTable} from "./schema"; import {MessageInsert, messagesTable} from "./schema";
import {DatabaseManager} from "./database-manager"; import {DatabaseManager} from "./database-manager";
import {StoredMessage} from "../model/stored-message"; import {StoredMessage} from "../model/stored-message";
import {and, eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
@@ -66,7 +66,7 @@ export class MessageDao extends Dao<StoredMessage> {
return this.mapFrom(messages); return this.mapFrom(messages);
} }
async insert(values: typeof messagesTable.$inferInsert[]): Promise<true> { async insert(values: MessageInsert[]): Promise<true> {
const then = Date.now(); const then = Date.now();
const r = await DatabaseManager.db const r = await DatabaseManager.db
.insert(messagesTable) .insert(messagesTable)
@@ -82,7 +82,7 @@ export class MessageDao extends Dao<StoredMessage> {
return true; return true;
} }
mapTo(messages: Message[]): typeof messagesTable.$inferInsert[] { mapTo(messages: Message[]): MessageInsert[] {
return messages.map(msg => { return messages.map(msg => {
return { return {
chatId: msg.chat.id, chatId: msg.chat.id,
@@ -95,15 +95,16 @@ export class MessageDao extends Dao<StoredMessage> {
}); });
} }
mapFrom(messages: typeof messagesTable.$inferInsert[]): StoredMessage[] { mapFrom(messages: MessageInsert[]): StoredMessage[] {
return messages.map(m => { return messages.map(m => {
return { return {
chatId: m.chatId, chatId: m.chatId,
messageId: m.id, id: m.id,
replyToMessageId: m.replyToMessageId, replyToMessageId: m.replyToMessageId,
fromId: m.fromId, fromId: m.fromId,
text: m.text, text: m.text,
date: m.date date: m.date,
photoMaxSizeFilePath: m.photoMaxSizeFilePath
}; };
}); });
} }
+1
View File
@@ -7,6 +7,7 @@ export const messagesTable = sqliteTable("messages", {
fromId: int().notNull(), fromId: int().notNull(),
text: text(), text: text(),
date: int().notNull(), date: int().notNull(),
photoMaxSizeFilePath: text(),
}); });
export type MessageInsert = typeof messagesTable.$inferInsert; export type MessageInsert = typeof messagesTable.$inferInsert;
+44 -5
View File
@@ -7,6 +7,7 @@ import {
executeChatCommand, executeChatCommand,
extractTextMessage, extractTextMessage,
findAndExecuteCallbackCommand, findAndExecuteCallbackCommand,
ignore,
initSystemSpecs, initSystemSpecs,
logError, logError,
randomValue, randomValue,
@@ -57,6 +58,17 @@ import {CallbackCommand} from "./base/callback-command";
import {OllamaCancel} from "./callback_commands/ollama-cancel"; import {OllamaCancel} from "./callback_commands/ollama-cancel";
import {MistralChat} from "./commands/mistral-chat"; import {MistralChat} from "./commands/mistral-chat";
import {Transliteration} from "./commands/transliteration"; 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); 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 const bot = new TelegramBot({botToken: Environment.BOT_TOKEN, testEnvironment: Environment.TEST_ENVIRONMENT});
export let botUser: User; 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({ export const ollama = new Ollama({
host: Environment.OLLAMA_ADDRESS, host: Environment.OLLAMA_ADDRESS,
headers: {"Authorization": `Bearer ${Environment.OLLAMA_API_KEY}`} 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) { 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) { if (Environment.OLLAMA_API_KEY) {
@@ -156,18 +177,29 @@ if (Environment.OLLAMA_API_KEY) {
} }
if (Environment.GEMINI_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) { if (Environment.MISTRAL_API_KEY) {
chatCommands.push(new MistralChat()); chatCommands.push(
new MistralChat(),
new MistralListModels(),
new MistralGetModel(),
new MistralSetModel()
);
} }
async function main() { async function main() {
console.log( console.log(
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
`DATA_PATH: ${Environment.DATA_PATH}\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 => { const commands = chatCommands.filter(cmd => {
@@ -214,7 +246,7 @@ bot.on("edited_message", async (msg) => {
bot.on("message", async (msg) => { bot.on("message", async (msg) => {
console.log("message", 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)) { if ((msg.new_chat_members?.length || 0 > 0)) {
await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(inviteAnswers)}).catch(logError); 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 cmdText = msg.text || msg.caption || "";
const then = Date.now();
const cmd = searchChatCommand(chatCommands, cmdText); const cmd = searchChatCommand(chatCommands, cmdText);
const executed = await executeChatCommand(cmd, msg, cmdText); const executed = await executeChatCommand(cmd, msg, cmdText);
const now = Date.now();
const diff = now - then;
console.log("diff", diff);
if (executed || !cmdText) return; if (executed || !cmdText) return;
const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase()); const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase());
+2 -1
View File
@@ -1,8 +1,9 @@
export type StoredMessage = { export type StoredMessage = {
chatId: number; chatId: number;
messageId: number; id: number;
replyToMessageId?: number | null; replyToMessageId?: number | null;
fromId: number; fromId: number;
text?: string; text?: string;
date: number; date: number;
photoMaxSizeFilePath?: string | null;
}; };
+99 -20
View File
@@ -14,6 +14,7 @@ import {UserStore} from "../common/user-store";
import * as orm from "drizzle-orm"; import * as orm from "drizzle-orm";
import {sql, type SQL} from "drizzle-orm"; import {sql, type SQL} from "drizzle-orm";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path";
export const ignore = () => { export const ignore = () => {
}; };
@@ -38,12 +39,21 @@ export const errorPlaceholder = async (msg: Message) => {
await sendErrorPlaceholder(msg).catch(logError); await sendErrorPlaceholder(msg).catch(logError);
}; };
export function searchChatCommand(commands: ChatCommand[], text: string): ChatCommand | null { export function searchChatCommand(
for (let i = 0; i < commands.length; i++) { commands: ChatCommand[],
const command = commands[i]; text: string,
if (command?.regexp.test(text)) { botUsername: string = botUser.username
return command; ): 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; return null;
@@ -81,13 +91,13 @@ export async function checkRequirements(cmd: ChatCommand | null, msg: Message):
if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) { if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) {
console.log(`${cmd.title}: creatorId is bad`); console.log(`${cmd.title}: creatorId is bad`);
await replyToMessage(msg, "Вы не являетесь создателем бота."); await oldReplyToMessage(msg, "Вы не являетесь создателем бота.");
return false; return false;
} }
if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) { if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) {
console.log(`${cmd.title}: adminId is bad`); console.log(`${cmd.title}: adminId is bad`);
await replyToMessage(msg, "Вы не являетесь администратором бота."); await oldReplyToMessage(msg, "Вы не являетесь администратором бота.");
return false; return false;
} }
@@ -97,20 +107,20 @@ export async function checkRequirements(cmd: ChatCommand | null, msg: Message):
if (!isAdmin) { if (!isAdmin) {
console.log(`${cmd.title}: chatAdminId is bad`); console.log(`${cmd.title}: chatAdminId is bad`);
await replyToMessage(msg, "Бот не является администратором чата."); await oldReplyToMessage(msg, "Бот не является администратором чата.");
return false; return false;
} }
} }
if (reqs.isRequiresChat() && msg.chat.type === "private") { if (reqs.isRequiresChat() && msg.chat.type === "private") {
console.log(`${cmd.title}: chatId is bad`); console.log(`${cmd.title}: chatId is bad`);
await replyToMessage(msg, "Тут Вам не чат."); await oldReplyToMessage(msg, "Тут Вам не чат.");
return false; return false;
} }
if (reqs.isRequiresReply() && !msg.reply_to_message) { if (reqs.isRequiresReply() && !msg.reply_to_message) {
console.log(`${cmd.title}: replyMessage is bad`); console.log(`${cmd.title}: replyMessage is bad`);
await replyToMessage(msg, "Отсутствует ответ на сообщение."); await oldReplyToMessage(msg, "Отсутствует ответ на сообщение.");
return false; return false;
} }
@@ -179,10 +189,11 @@ export async function editMessageText(chatId: number, messageId: number, message
} }
export type SendOptions = { export type SendOptions = {
chatId?: number; chat_id?: number;
message?: Message, message?: Message,
message_id?: number;
text: string, text: string,
parseMode?: ParseMode, parse_mode?: ParseMode,
}; };
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> { export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
@@ -197,15 +208,28 @@ export async function oldSendMessage(message: Message, text: string, parseMode?:
export async function sendMessage(options: SendOptions): Promise<Message> { export async function sendMessage(options: SendOptions): Promise<Message> {
const response = await bot.sendMessage({ 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, text: options.text,
parse_mode: options.parseMode parse_mode: options.parse_mode
}); });
return Promise.resolve(response); return Promise.resolve(response);
} }
export async function replyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> { export async function replyToMessage(options: SendOptions): Promise<Message> {
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<Message> {
const response = await bot.sendMessage({ const response = await bot.sendMessage({
chat_id: message.chat.id, chat_id: message.chat.id,
text: text, text: text,
@@ -382,6 +406,7 @@ export function extractTextStored(msg: StoredMessage, prefix: string): string {
} }
export function extractText(text: string, prefix: string): string { export function extractText(text: string, prefix: string): string {
if (!text) return "";
if (text.toLowerCase().startsWith(prefix.toLowerCase())) { if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
text = text.substring(prefix.length).trim(); text = text.substring(prefix.length).trim();
} }
@@ -389,18 +414,58 @@ export function extractText(text: string, prefix: string): string {
return text; return text;
} }
export function isStoredMessage(msg: Message | StoredMessage): msg is StoredMessage {
return "id" in msg;
}
export async function loadImageIfExists(msg: Message | StoredMessage): Promise<string | null> {
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<ArrayBuffer>(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<MessagePart[]> { export async function collectReplyChainText(triggerMsg: Message, prefix: string = Environment.BOT_PREFIX, limit: number = 40, includeTrigger = true): Promise<MessagePart[]> {
const chatId = triggerMsg.chat.id as number; const chatId = triggerMsg.chat.id as number;
const parts: MessagePart[] = []; const parts: MessagePart[] = [];
if (includeTrigger) { if (includeTrigger) {
const t = extractTextMessage(triggerMsg, prefix); const t = extractTextMessage(triggerMsg, prefix);
if (t) parts.push({ 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, bot: triggerMsg.from.id === botUser.id,
content: t, content: t,
name: triggerMsg.from.first_name name: triggerMsg.from.first_name,
images: img ? [img] : []
}); });
} }
}
const first = triggerMsg.reply_to_message; const first = triggerMsg.reply_to_message;
if (!first) { if (!first) {
@@ -408,7 +473,15 @@ export async function collectReplyChainText(triggerMsg: Message, prefix: string
} }
const firstText = extractTextMessage(first, prefix); 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; let curId = first.message_id;
@@ -418,14 +491,16 @@ export async function collectReplyChainText(triggerMsg: Message, prefix: string
if (!parentId) break; if (!parentId) break;
const parent = await messageDao.getById({chatId: chatId, id: parentId}); 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 user = await UserStore.get(parent.fromId);
const img = await loadImageIfExists(parent);
parts.push({ parts.push({
bot: parent.fromId === botUser.id, bot: parent.fromId === botUser.id,
content: extractTextStored(parent, prefix), content: extractTextStored(parent, prefix),
name: user?.firstName name: user?.firstName,
images: img ? [img] : []
}); });
curId = parentId; curId = parentId;
} }
@@ -807,3 +882,7 @@ export function ifTrue(exp?: never): boolean {
return ["true", "t", "y", 1, "1"].includes(exp); return ["true", "t", "y", 1, "1"].includes(exp);
} }
export function boolToEmoji(bool: boolean): string {
return bool ? "✅" : "❌";
}