shitton of the ai changes

This commit is contained in:
2026-05-01 04:54:11 +03:00
parent d95c37a322
commit 8cff086a8e
194 changed files with 29409 additions and 8841 deletions
+8 -8
View File
@@ -8,8 +8,8 @@ import {botUser} from "../index";
export class AdminsAdd extends Command {
command = "addAdmin";
title = "/addAdmin";
description = "Add user to admins";
title = Environment.commandTitles.adminsAdd;
description = Environment.commandDescriptions.adminsAdd;
requirements = Requirements.Build(
Requirement.BOT_CREATOR,
@@ -18,25 +18,25 @@ export class AdminsAdd extends Command {
);
async execute(msg: Message): Promise<void> {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.reply_to_message.from) return;
const id = msg.reply_to_message.from.id;
const text = fullName(msg.reply_to_message.from);
if (id === botUser.id) {
await oldSendMessage(msg, "Бот не может сам себя сделать админом").catch(logError);
await oldSendMessage(msg, Environment.botCannotMakeItselfAdminText).catch(logError);
return;
}
if (id === Environment.CREATOR_ID) {
await oldSendMessage(msg, "Создатель бота и так является админом").catch(logError);
await oldSendMessage(msg, Environment.botCreatorAlreadyAdminText).catch(logError);
return;
}
if (await Environment.addAdmin(id)) {
await oldSendMessage(msg, text + " теперь админ!").catch(logError);
await oldSendMessage(msg, Environment.getUserIsNowAdminText(text)).catch(logError);
} else {
await oldSendMessage(msg, text + " и так уже админ 🤔").catch(logError);
await oldSendMessage(msg, Environment.getUserAlreadyAdminText(text)).catch(logError);
}
}
}
}
+6 -6
View File
@@ -3,7 +3,7 @@ import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {fullName, logError, replyToMessage, sendErrorPlaceholder} from "../util/utils";
import {escapePlainMarkdownV2, fullName, logError, replyToMessage, sendErrorPlaceholder} from "../util/utils";
import {StoredUser} from "../model/stored-user";
import {UserStore} from "../common/user-store";
@@ -29,14 +29,14 @@ export class AdminsList extends Command {
}
}
let text = "*Администраторы*:\n\n";
let text = Environment.administratorsHeaderText;
users.forEach(user => {
text += "\\* ";
if (user) {
text += `[${fullName(user)}](tg://user?id=${user.id})`;
text += `[${escapePlainMarkdownV2(fullName(user))}](tg://user?id=${user.id})`;
} else {
text += "Нет информации о пользователе";
text += Environment.noUserInfoText;
}
text += "\n";
@@ -48,8 +48,8 @@ export class AdminsList extends Command {
parse_mode: "MarkdownV2"
});
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
await sendErrorPlaceholder(msg).catch(logError);
}
}
}
}
+8 -8
View File
@@ -8,8 +8,8 @@ import {botUser} from "../index";
export class AdminsRemove extends Command {
command = "removeAdmin";
title = "/removeAdmin";
description = "Remove user from admins";
title = Environment.commandTitles.adminsRemove;
description = Environment.commandDescriptions.adminsRemove;
requirements = Requirements.Build(
Requirement.BOT_CREATOR,
@@ -18,25 +18,25 @@ export class AdminsRemove extends Command {
);
async execute(msg: Message): Promise<void> {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.reply_to_message.from) return;
const id = msg.reply_to_message.from.id;
const text = fullName(msg.reply_to_message.from);
if (id === botUser.id) {
await oldSendMessage(msg, "Бот не может сам себя убрать из админов").catch(logError);
await oldSendMessage(msg, Environment.botCannotRemoveItselfFromAdminsText).catch(logError);
return;
}
if (id === Environment.CREATOR_ID) {
await oldSendMessage(msg, "Создатель бота не может перестать быть админом").catch(logError);
await oldSendMessage(msg, Environment.botCreatorCannotStopBeingAdminText).catch(logError);
return;
}
if (await Environment.removeAdmin(id)) {
await oldSendMessage(msg, text + " больше не админ!").catch(logError);
await oldSendMessage(msg, Environment.getUserNoLongerAdminText(text)).catch(logError);
} else {
await oldSendMessage(msg, text + " и так не был админом 🤔").catch(logError);
await oldSendMessage(msg, Environment.getUserWasNotAdminText(text)).catch(logError);
}
}
}
}
+36 -13
View File
@@ -3,36 +3,59 @@ import {Message} from "typescript-telegram-bot-api";
import {errorPlaceholder, logError, oldSendMessage} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
export class Ae extends Command {
argsMode = "required" as const;
title = "/ae";
description = "evaluation";
command = ["ae"];
title = Environment.commandTitles.ae;
description = Environment.commandDescriptions.ae;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, params?: RegExpExecArray) {
const match = params?.[3];
const match = params?.[3] || "";
try {
let e = eval(match);
e = ((typeof e == "string") ? e : JSON.stringify(e));
await oldSendMessage(msg, e).catch(async () => await errorPlaceholder(msg));
} catch (e) {
const text = e.message.toString();
let result = this.executeEvaluation(match);
await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg));
} catch (error) {
const normalizedError = error instanceof Error ? error : new Error(String(error));
const text = normalizedError.message.toString();
if (text.includes("is not defined")) {
await oldSendMessage(msg, "variable is not defined").catch(logError);
await oldSendMessage(msg, Environment.variableNotDefinedText).catch(logError);
return;
}
logError(`${text}
* Stacktrace: ${e.stack}`);
* Stacktrace: ${normalizedError.stack}`);
await oldSendMessage(msg, text).catch(logError);
}
}
}
executeEvaluation(evaluation: string): string {
try {
let e = eval(evaluation);
e = ((typeof e == "string") ? e : JSON.stringify(e));
return e;
} catch (error) {
const normalizedError = error instanceof Error ? error : new Error(String(error));
const text = normalizedError.message.toString();
if (text.includes("is not defined")) {
return Environment.evaluationVariableNotDefinedText;
}
logError(`${text}
* Stacktrace: ${normalizedError.stack}`);
return text;
}
}
}
+14 -10
View File
@@ -5,10 +5,11 @@ import {Message} from "typescript-telegram-bot-api";
import {bot, botUser} from "../index";
import {fullName, logError, oldSendMessage, oldReplyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
export class Ban extends Command {
title = "/ban [reply]";
description = "ban user from chat";
title = Environment.commandTitles.ban;
description = Environment.commandDescriptions.ban;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
@@ -19,32 +20,35 @@ export class Ban extends Command {
);
async execute(msg: Message) {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.from || ! msg.reply_to_message.from) return;
const user = msg.reply_to_message.from;
const userId = user.id;
if (userId === botUser.id) {
await oldReplyToMessage(msg, "Используй /leave").catch(logError);
await oldReplyToMessage(msg, Environment.useLeaveCommandText).catch(logError);
return;
}
if (userId === Environment.CREATOR_ID) {
await oldReplyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError);
await oldReplyToMessage(msg, Environment.botWillNotBanCreatorText).catch(logError);
return;
}
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
await oldReplyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError);
await oldReplyToMessage(msg, Environment.botWillNotBanAdminsText).catch(logError);
return;
}
bot.banChatMember({chat_id: msg.chat.id, user_id: userId})
enqueueTelegramApiCall(
() => bot.banChatMember({chat_id: msg.chat.id, user_id: userId}),
{method: "banChatMember", chatId: msg.chat.id, chatType: msg.chat.type}
)
.then(async () => {
await oldSendMessage(msg, `${fullName(user)} забанен 🚫`).catch(logError);
await oldSendMessage(msg, Environment.getUserBannedText(fullName(user))).catch(logError);
})
.catch(async () => {
await oldSendMessage(msg, `Не смог забанить ${fullName(user)} ☹️`).catch(logError);
await oldSendMessage(msg, Environment.getUserBanFailedText(fullName(user))).catch(logError);
});
}
}
}
+19 -6
View File
@@ -1,18 +1,23 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
import {Environment} from "../common/environment";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("command:choice");
export class Choice extends Command {
command = "choice";
argsMode = "required" as const;
title = "/choice a, b, ..., c";
description = "Выбор случайного значения";
title = Environment.commandTitles.choice;
description = Environment.commandDescriptions.choice;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
logger.debug("execute", {chatId: msg.chat?.id, messageId: msg.message_id, match});
const payload = match[3];
const payload = match?.[3] || "";
const re =
/\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+?))\s*(?:,|$)/g;
@@ -33,7 +38,15 @@ export class Choice extends Command {
}
const random = randomValue(out);
if (!random) {
await oldReplyToMessage(msg, Environment.noChoicesText).catch(logError);
return;
}
await oldReplyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError);
await oldReplyToMessage(
msg,
Environment.getChoiceText(prepareTelegramMarkdownV2(random, {mode: "final"})),
"MarkdownV2"
).catch(logError);
}
}
}
+7 -5
View File
@@ -1,13 +1,15 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Coin extends Command {
title = "/coin";
description = "Heads or tails";
title = Environment.commandTitles.coin;
description = Environment.commandDescriptions.coin;
async execute(msg: Message): Promise<void> {
const random = getRangedRandomInt(0, 2);
const headsOrTails = random === 1 ? "Выпал *Орёл* 🪙" : "Выпала *Решка* 🪙";
await oldReplyToMessage(msg, headsOrTails, "Markdown").catch(logError); }
}
const headsOrTails = Environment.getCoinResultText(random === 1 ? Environment.coinHeadsText : Environment.coinTailsText) + " 🪙";
await oldReplyToMessage(msg, headsOrTails, "Markdown").catch(logError);
}
}
+4 -3
View File
@@ -3,10 +3,11 @@ import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Debug extends Command {
title = "/debug";
description = "Returns msg (or reply) as json";
title = Environment.commandTitles.debug;
description = Environment.commandDescriptions.debug;
requirements = Requirements.Build(Requirement.BOT_ADMIN);
@@ -17,4 +18,4 @@ export class Debug extends Command {
const text = `\`\`\`json\n${json}\n\`\`\``;
await replyToMessage({message: msg, text: text, parse_mode: "Markdown"}).catch(logError);
}
}
}
+20 -15
View File
@@ -2,26 +2,31 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, randomValue} from "../util/utils";
import {bot} from "../index";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
type DiceEmoji = "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰";
const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"];
const emojis: readonly DiceEmoji[] = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"];
export class Dice extends Command {
title = "/dice";
description = "Sends random or specific dice";
title = Environment.commandTitles.dice;
description = Environment.commandDescriptions.dice;
async execute(msg: Message): Promise<void> {
const split = msg.text.split("/dice ");
const secondPart = split[1]?.trim();
const emojiIndex = emojis.indexOf(secondPart);
const emojiToDice: DiceEmoji = (emojiIndex >= 0 ? emojis[emojiIndex] : randomValue(emojis)) as DiceEmoji;
const split = msg.text?.split("/dice ");
const secondPart = split?.[1]?.trim() || "";
const requestedEmoji = secondPart as DiceEmoji;
const emojiToDice = emojis.includes(requestedEmoji) ? requestedEmoji : randomValue(emojis) ?? "🎲";
await bot.sendDice({
chat_id: msg.chat.id,
emoji: emojiToDice,
reply_parameters: {
message_id: msg.message_id
}
}).catch(logError);
await enqueueTelegramApiCall(
() => bot.sendDice({
chat_id: msg.chat.id,
emoji: emojiToDice,
reply_parameters: {
message_id: msg.message_id
}
}),
{method: "sendDice", chatId: msg.chat.id, chatType: msg.chat.type}
).catch(logError);
}
}
}
+22 -14
View File
@@ -2,13 +2,15 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {downloadTelegramFile, extractImageFileId, logError, oldReplyToMessage, waveDistortSharp} from "../util/utils";
import {bot} from "../index";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
export class Distort extends Command {
command = "distort";
argsMode = "optional" as const;
title = "/distort [amp] [wavelength]";
description = "Distortion of picture";
title = Environment.commandTitles.distort;
description = Environment.commandDescriptions.distort;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const chatId = msg.chat.id;
@@ -17,7 +19,7 @@ export class Distort extends Command {
if (!reply) {
await oldReplyToMessage(
msg,
"Ответь командой /distort на сообщение с картинкой (фото, документ или стикер).\n" + "Пример: /distort 16 80"
Environment.distortReplyInstructionText
);
return;
}
@@ -26,7 +28,7 @@ export class Distort extends Command {
if (!fileId) {
await oldReplyToMessage(
msg,
"В реплае не вижу картинку. Пришли фото или файл-изображение."
Environment.distortMissingImageText
);
return;
}
@@ -37,7 +39,10 @@ export class Distort extends Command {
const wavelength = b ? Number(b) : 72;
try {
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
await enqueueTelegramApiCall(
() => bot.sendChatAction({chat_id: chatId, action: "upload_photo"}),
{method: "sendChatAction", chatId, chatType: msg.chat.type}
);
const file = await bot.getFile({file_id: fileId});
if (!file.file_path) {
@@ -47,17 +52,20 @@ export class Distort extends Command {
const inputBuf = await downloadTelegramFile(file.file_path);
const outBuf = await waveDistortSharp(inputBuf, amp, wavelength);
const outBuf = await waveDistortSharp(<Buffer>inputBuf, amp, wavelength);
await bot.sendPhoto({
chat_id: chatId,
photo: outBuf,
caption: `Искажение готово ✅ (amp=${amp}, wavelength=${wavelength})`,
});
} catch (e) {
await enqueueTelegramApiCall(
() => bot.sendPhoto({
chat_id: chatId,
photo: outBuf,
caption: Environment.getDistortionReadyCaption(amp, wavelength),
}),
{method: "sendPhoto", chatId, chatType: msg.chat.type}
);
} catch (error) {
await oldReplyToMessage(
msg, `Не получилось исказить изображение: ${e?.message ?? String(e)}`
msg, Environment.getDistortFailedText(error instanceof Error ? error : String(error))
).catch(logError);
}
}
}
}
+27 -15
View File
@@ -6,6 +6,8 @@ import {Environment} from "../common/environment";
import fs from "node:fs";
import {logError, replyToMessage, sendErrorPlaceholder} from "../util/utils";
import {bot} from "../index";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {DatabaseManager, type DatabaseBackupArtifact} from "../db/database-manager";
export class ExportDb extends Command {
@@ -16,27 +18,37 @@ export class ExportDb extends Command {
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> {
const fullPath = Environment.DB_PATH.substring(5);
if (!fs.existsSync(fullPath)) {
await sendErrorPlaceholder(msg);
return;
}
let backups: DatabaseBackupArtifact[] = [];
try {
const buffer = fs.readFileSync(fullPath);
backups = await DatabaseManager.exportBackupArtifacts();
if (!backups.length) {
throw new Error("Database backup artifacts were not created.");
}
await bot.sendDocument({
chat_id: Environment.CREATOR_ID,
document: new FileOptions(buffer, {filename: "database.db", contentType: "application/sql"}),
caption: "Бэкап базы данных",
});
for (const backup of backups) {
await enqueueTelegramApiCall(
() => bot.sendDocument({
chat_id: Environment.CREATOR_ID,
document: new FileOptions(
fs.createReadStream(backup.filePath),
{filename: backup.fileName, contentType: backup.contentType},
),
caption: Environment.databaseBackupCaption,
}),
{method: "sendDocument", chatId: Environment.CREATOR_ID, chatType: "private"}
);
}
if (msg.chat.id !== Environment.CREATOR_ID) {
await replyToMessage({message: msg, text: "Успешно отправлено в ЛС создателю!"});
await replyToMessage({message: msg, text: Environment.databaseBackupSentText});
}
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
await sendErrorPlaceholder(msg);
} finally {
for (const backup of backups) {
await backup.cleanup();
}
}
}
}
}
-188
View File
@@ -1,188 +0,0 @@
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {bot, googleAi} from "../index";
import {MessageStore} from "../common/message-store";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {ApiError} from "@google/genai";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage, replyToMessage,
startIntervalEditor
} from "../util/utils";
import {ChatCommand} from "../base/chat-command";
export class GeminiChat extends ChatCommand {
command = "gemini";
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/gemini";
description = "Chat with AI (Gemini)";
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeGemini(msg, match?.[3]);
}
async executeGemini(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
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` : "") + part.content
};
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT});
}
let chatContent = "";
for (const part of chatMessages) {
chatContent += `${part.role.toUpperCase()}:\n${part.content}\n\n`;
}
chatContent = chatContent.trim();
const input = [];
input.push(
{
type: "text",
text: chatContent
}
);
// TODO: 12/02/2026, Danil Nikolaev: support for multiple images
if (messageParts.some(p => p.images?.length)) {
const firstImages = messageParts.find(p => p.images?.length)?.images ?? [];
firstImages.forEach(image => {
input.push({
type: "image",
data: image,
mime_type: "image/png"
});
});
}
let waitMessage: Message;
const startTime = Date.now();
try {
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await googleAi.interactions.create({
model: Environment.GEMINI_MODEL,
input: input,
stream: true
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const event of stream) {
switch (event.event_type) {
case "content.delta":
switch (event.delta?.type) {
case "text": {
const text = event.delta.text;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
break;
}
case "image": {
const image = event.delta.data;
console.log("image", image);
}
}
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
if (error instanceof ApiError) {
if (error.status === 429) {
await oldReplyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError);
return;
}
}
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
}
}
}
-60
View File
@@ -1,60 +0,0 @@
import {Command} from "../base/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";
import {Environment} from "../common/environment";
export class GeminiGenerateImage extends Command {
command = "geminiGenImage";
argsMode = "required" as const;
title = "/geminiGenImage";
description = "Generate image with Gemini";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
const prompt = match?.[3];
return this.executeGenImage(msg, prompt);
}
async executeGenImage(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
let waitMessage: Message;
try {
waitMessage = await replyToMessage({
message: msg,
text: Environment.genImageText,
});
const interaction = await googleAi.interactions.create({
model: Environment.GEMINI_IMAGE_MODEL,
response_modalities: ["image"],
input: text,
});
interaction.outputs?.forEach((output, index) => {
if (output.type === "image") {
// const image = output.data;
console.log(`Image output ${index + 1}:`, output);
} else {
console.log(`Output ${index + 1}: ${output}`);
}
});
} catch (e) {
logError(e);
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`,
link_preview_options: {is_disabled: true}
}).catch(logError);
}
}
}
-32
View File
@@ -1,32 +0,0 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {googleAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class GeminiGetModel extends Command {
title = "/geminiGetModel";
description = "Get current Gemini model";
async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError);
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await googleAi.models.get({model: Environment.GEMINI_MODEL});
console.log(info);
return {
vision: {supported: true},
ocr: null,
thinking: {supported: info.thinking},
tools: null
};
} catch (e) {
logError(e);
return null;
}
}
}
-36
View File
@@ -1,36 +0,0 @@
import {Command} from "../base/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 Command {
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" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: "Не получилось загрузить список моделей"}).catch(logError);
}
}
}
-25
View File
@@ -1,25 +0,0 @@
import {Command} from "../base/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 Command {
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?.[3];
Environment.setGeminiModel(newModel || Environment.GEMINI_MODEL);
const text = newModel ? `Выбрана модель "${newModel}"`
: `Модель не задана. Будет использоваться стандартная модель "${Environment.GEMINI_MODEL}".`;
await replyToMessage({message: msg, text: text}).catch(logError);
}
}
+8 -6
View File
@@ -3,15 +3,17 @@ import {chatCommandToString, delay, logError, sendMessage} from "../util/utils";
import {Command} from "../base/command";
import {commands} from "../index";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
import {Environment} from "../common/environment";
export class Help extends Command {
command = ["h", "help"];
title = "/help";
description = "Show list of commands";
title = Environment.commandTitles.help;
description = Environment.commandDescriptions.help;
async execute(msg: Message) {
let text = "Commands:\n\n";
if (!msg.from) return;
let text = Environment.commandsHeaderText;
commands.forEach(c => {
text += `${chatCommandToString(c)}\n`;
@@ -20,7 +22,7 @@ export class Help extends Command {
await sendMessage({chat_id: msg.from.id, text: text})
.then(async () => {
if (msg.chat.type !== "private") {
await sendMessage({message: msg, text: "Отправил команды в ЛС 😎"}).catch(logError);
await sendMessage({message: msg, text: Environment.sentCommandsInDmText}).catch(logError);
}
})
.catch(async (e) => {
@@ -28,7 +30,7 @@ export class Help extends Command {
if (e.response?.error_code === 403) {
await sendMessage({
message: msg,
text: "Не смог отправить команды в ЛС ☹️\nТогда отправлю сюда"
text: Environment.couldNotSendCommandsInDmText
}).catch(logError);
await delay(1000);
@@ -37,4 +39,4 @@ export class Help extends Command {
}
});
}
}
}
+9 -9
View File
@@ -1,17 +1,17 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Id extends Command {
title = "/id";
description = "ID of chat, user and reply (if replied to any message)";
title = Environment.commandTitles.id;
description = Environment.commandDescriptions.id;
async execute(msg: Message): Promise<void> {
let text = `chat id: \n\`\`\`${msg.chat.id}\`\`\` \nfrom id: \n\`\`\`${msg.from.id}\`\`\``;
if (msg.reply_to_message) {
text += ` \nreply id: \n\`\`\`${msg.reply_to_message.from.id}\`\`\``;
}
await oldReplyToMessage(msg, text, "MarkdownV2").catch(logError);
await oldReplyToMessage(
msg,
Environment.getIdText(msg.chat.id, msg.from?.id, msg.reply_to_message?.from?.id),
"MarkdownV2",
).catch(logError);
}
}
}
+8 -8
View File
@@ -7,8 +7,8 @@ import {botUser} from "../index";
import {Environment} from "../common/environment";
export class Ignore extends Command {
title = "/ignore";
description = "Bot will ignore user";
title = Environment.commandTitles.ignore;
description = Environment.commandDescriptions.ignore;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
@@ -19,25 +19,25 @@ export class Ignore extends Command {
);
async execute(msg: Message) {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.reply_to_message.from) return;
const id = msg.reply_to_message.from.id;
const text = fullName(msg.reply_to_message.from);
if (id === botUser.id) {
await oldSendMessage(msg, "Бот не может сам себя игнорировать").catch(logError);
await oldSendMessage(msg, Environment.botWillNotIgnoreItselfText).catch(logError);
return;
}
if (id === Environment.CREATOR_ID) {
await oldSendMessage(msg, "Бот не будет игнорировать своего создателя").catch(logError);
await oldSendMessage(msg, Environment.botWillNotIgnoreCreatorText).catch(logError);
return;
}
if (await Environment.addMute(id)) {
await oldSendMessage(msg, text + " в муте! 🔇").catch(logError);
await oldSendMessage(msg, Environment.getUserIgnoredText(text)).catch(logError);
} else {
await oldSendMessage(msg, text + " уже в муте 🤔").catch(logError);
await oldSendMessage(msg, Environment.getUserAlreadyIgnoredText(text)).catch(logError);
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {bot} from "../index";
import {DatabaseManager, type DatabaseBackupPayload} from "../db/database-manager";
import {downloadTelegramFile, logError, replyToMessage, sendErrorPlaceholder} from "../util/utils";
import {Environment} from "../common/environment";
import {MessageStore} from "../common/message-store";
import {UserStore} from "../common/user-store";
export class ImportDb extends Command {
command = ["importdb"];
argsMode = "optional" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
try {
const payloadText = await this.resolvePayloadText(msg, match);
if (!payloadText) {
await replyToMessage({message: msg, text: Environment.databaseImportNeedJsonText});
return;
}
const payload = JSON.parse(payloadText) as DatabaseBackupPayload;
const result = await DatabaseManager.importBackupFromJsonPayload(payload);
MessageStore.clear();
UserStore.clear();
await replyToMessage({
message: msg,
text: `${Environment.databaseImportDoneText} Users: ${result.users}, messages: ${result.messages}.`,
});
} catch (error) {
logError(error instanceof Error ? error : String(error));
await sendErrorPlaceholder(msg);
}
}
private async resolvePayloadText(msg: Message, match?: RegExpExecArray | null): Promise<string | null> {
const argText = match?.[3]?.trim();
if (argText) return argText;
const document = msg.document ?? msg.reply_to_message?.document;
if (!document) return null;
const file = await bot.getFile({file_id: document.file_id});
const buffer = await downloadTelegramFile(file.file_path);
return buffer ? buffer.toString("utf8").trim() : null;
}
}
+55 -51
View File
@@ -2,66 +2,70 @@ import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {callbackCommands, commands} from "../index";
import {Environment} from "../common/environment";
import {boolToEmoji, getCurrentModel, getCurrentModelCapabilities, logError, replyToMessage} from "../util/utils";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {Command} from "../base/command";
import {getProviderTools} from "../ai/tool-mappers";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
import {resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
import {getFormattedCapabilities} from "../ai/provider-model-runtime";
export class Info extends Command {
command = ["info", "v"];
title = "/info";
description = "Info about bot";
title = Environment.commandTitles.info;
description = Environment.commandDescriptions.info;
async execute(msg: Message): Promise<void> {
const aiProvider = Environment.DEFAULT_AI_PROVIDER;
const aiModel = getCurrentModel();
let aiModelCapabilities: AiModelCapabilities = {};
if (!msg.from) return;
try {
aiModelCapabilities = await getCurrentModelCapabilities();
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: `Произошла ошибка: ${e}`}).catch(logError);
return;
}
const getToolsInfo = async () => {
const tools = getProviderTools(provider);
return Environment.getInfoToolsBlockText(tools.map(t => t.function.name));
};
const getCommandsInfo = async () => {
const cmds = commands.filter(c => !(c instanceof ChatCommand));
const chatCmds = commands.filter(c => c instanceof ChatCommand);
const callbackCmds = callbackCommands;
const publicCmdsLength = cmds.filter(c => c.requirements?.isPublic()).length;
const privateCmdsLength = cmds.length - publicCmdsLength;
const chatCmdsLength = chatCmds.length;
const callbackCmdsLength = callbackCmds.length;
return Environment.getInfoCommandsBlockText({
publicCommands: publicCmdsLength,
privateCommands: privateCmdsLength,
chatCommands: chatCmdsLength,
callbackCommands: callbackCmdsLength,
});
};
const provider = await resolveEffectiveAiProviderForUser(msg.from.id);
// const aiProvidersLength = Object.keys(AiProvider).filter(key => isNaN(Number(key))).length;
const aiProviders = Object.keys(AiProvider).map(p => p.toLowerCase());
const finalText = [
`\`\`\`${Environment.runtimeProviderLabelText}`,
`${Environment.infoSupportedProvidersLabelText}: ${aiProviders.join(", ")}`,
`${Environment.runtimeProviderCurrentLabelText}: ${provider.toLowerCase()}`,
"```",
"",
`\`\`\`${Environment.runtimeCapabilitiesLabelText}`,
(await getFormattedCapabilities(provider)).join("\n"),
"```",
"",
await getToolsInfo(),
await getCommandsInfo()
].join("\n");
const aiInfo = "```" +
"AI\n" +
`supported providers: ${Object.keys(AiProvider).filter(key => isNaN(Number(key))).length}\n\n` +
`provider: ${aiProvider.toLowerCase()}\n` +
`model: ${aiModel}\n\n` +
`vision${aiModelCapabilities.vision?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.vision?.supported)}\n` +
`ocr${aiModelCapabilities.ocr?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.ocr?.supported)}\n` +
`thinking${aiModelCapabilities.thinking?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.thinking?.supported)}\n` +
`tools${aiModelCapabilities.tools?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities.tools?.supported)}` +
"```";
const cmds = commands.filter(c => !(c instanceof ChatCommand));
const chatCmds = commands.filter(c => c instanceof ChatCommand);
const callbackCmds = callbackCommands;
const publicCmdsLength = cmds.filter(c => c.requirements?.isPublic()).length;
const privateCmdsLength = cmds.length - publicCmdsLength;
const chatCmdsLength = chatCmds.length;
const callbackCmdsLength = callbackCmds.length;
const text =
aiInfo + "\n\n" +
"```" +
"Commands\n" +
`Public: ${publicCmdsLength}\n` +
`Private: ${privateCmdsLength}\n` +
`Chat: ${chatCmdsLength}\n` +
`Callback: ${callbackCmdsLength}\n` +
"```"
;
await replyToMessage({message: msg, text: text, parse_mode: "Markdown"}).catch(logError);
await replyToMessage({
message: msg,
text: prepareTelegramMarkdownV2(finalText, {mode: "final"}),
parse_mode: "MarkdownV2"
}).catch(logError);
}
}
}
+9 -4
View File
@@ -3,10 +3,12 @@ import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {bot} from "../index";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
export class Leave extends Command {
title = "/leave";
description = "Bot will leave current chat";
title = Environment.commandTitles.leave;
description = Environment.commandDescriptions.leave;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
@@ -14,6 +16,9 @@ export class Leave extends Command {
);
async execute(msg: Message): Promise<void> {
await bot.leaveChat({chat_id: msg.chat.id});
await enqueueTelegramApiCall(
() => bot.leaveChat({chat_id: msg.chat.id}),
{method: "leaveChat", chatId: msg.chat.id, chatType: msg.chat.type}
);
}
}
}
+15 -167
View File
@@ -1,180 +1,28 @@
import {Message} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {runUnifiedAi} from "../ai/unified-ai-runner";
import {Environment} from "../common/environment";
import {bot, commands, mistralAi} from "../index";
import {MessageStore} from "../common/message-store";
import {ChatCommand} from "../base/chat-command";
import {MistralGetModel} from "./mistral-get-model";
export class MistralChat extends ChatCommand {
command = "mistral";
command = ["mistral", "mistral-chat", "mistral-voice"];
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/mistral";
description = "Chat with AI (Mistral)";
title = Environment.commandTitles.mistralChat;
description = Environment.commandDescriptions.mistralChat;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeMistral(msg, match?.[3]);
}
async executeMistral(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
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` : "") + part.content,
});
for (const image of part.images) {
content.push({
type: "image_url",
imageUrl: "data:image/jpeg;base64," + image
});
}
return {
role: part.bot ? "assistant" : "user",
content: content,
};
const command = match?.[1]?.toLowerCase() ?? "";
await runUnifiedAi({
provider: AiProvider.MISTRAL,
msg: msg,
text: match?.[3] ?? "",
stream: true,
synthesizeSpeechResponse: command.endsWith("-voice"),
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]});
}
let waitMessage: Message;
const startTime = Date.now();
try {
const imagesCount = chatMessages.reduce((total, curr) => {
return total + (curr.content.filter(c => c.type === "image_url")?.length ?? 0);
}, 0);
if (imagesCount) {
try {
const modelInfo = await commands.find(c => c instanceof MistralGetModel).getModelCapabilities();
if (modelInfo) {
if (!modelInfo.vision?.supported) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: imagesCount ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await mistralAi.chat.stream({
model: Environment.MISTRAL_MODEL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
messages: chatMessages as any
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const chunk of stream) {
console.log("chunk", chunk);
const text = chunk.data.choices[0].delta.content;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
}
}
}
}
+10 -33
View File
@@ -1,36 +1,13 @@
import {Command} from "../base/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";
import {mistralAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class MistralGetModel extends Command {
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);
export class MistralGetModel extends ProviderGetModelCommand {
constructor() {
super({
provider: AiProvider.MISTRAL,
title: Environment.commandTitles.mistralGetModel,
description: Environment.commandDescriptions.mistralGetModel,
});
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await mistralAi.models.retrieve({modelId: Environment.MISTRAL_MODEL});
console.log(info);
return {
vision: {supported: info.capabilities.vision},
ocr: {supported: info.capabilities.ocr},
thinking: null,
tools: {supported: info.capabilities.functionCalling}
};
} catch (e) {
logError(e);
return null;
}
}
}
}
+11 -34
View File
@@ -1,36 +1,13 @@
import {Command} from "../base/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";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class MistralListModels extends Command {
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.id}`)
.join("\n");
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) {
logError(e);
await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError);
}
export class MistralListModels extends ProviderListModelsCommand {
constructor() {
super({
provider: AiProvider.MISTRAL,
title: Environment.commandTitles.mistralListModels,
description: Environment.commandDescriptions.mistralListModels,
});
}
}
}
+10 -22
View File
@@ -1,25 +1,13 @@
import {Command} from "../base/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";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class MistralSetModel extends Command {
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?.[3];
Environment.setMistralModel(newModel || Environment.MISTRAL_MODEL);
const text = newModel ? `Выбрана модель "${newModel}"`
: `Модель не задана. Будет использоваться стандартная модель "${Environment.MISTRAL_MODEL}".`;
await replyToMessage({message: msg, text: text}).catch(logError);
export class MistralSetModel extends ProviderSetModelCommand {
constructor() {
super({
provider: AiProvider.MISTRAL,
title: Environment.commandTitles.mistralSetModel,
description: Environment.commandDescriptions.mistralSetModel,
});
}
}
}
+19 -240
View File
@@ -1,250 +1,29 @@
import {Message} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, commands, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment";
import {MessageStore} from "../common/message-store";
import {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {OllamaGetModel} from "./ollama-get-model";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {AiProvider} from "../model/ai-provider";
import {runUnifiedAi} from "../ai/unified-ai-runner";
import {Environment} from "../common/environment";
export class OllamaChat extends ChatCommand {
command = ["ollamaThink", "ollama"];
command = ["ollama", "ollama-chat", "ollama-voice", "think", "think-voice"];
argsMode = "required" as const;
title = "/ollama";
description = "Chat with AI (Ollama)";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
console.log("match", match);
return this.executeOllama(msg, match?.[3], match?.[1]?.toLowerCase()?.startsWith("ollamathink"));
}
title = Environment.commandTitles.ollamaChat;
description = Environment.commandDescriptions.ollamaChat;
async executeOllama(msg: Message, text: string, think: boolean = false): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
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` : "") + part.content,
images: part.images
};
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const command = match?.[1]?.toLowerCase() ?? "";
await runUnifiedAi({
provider: AiProvider.OLLAMA,
msg: msg,
text: match?.[3] ?? "",
stream: true,
think: command.startsWith("think"),
synthesizeSpeechResponse: command.endsWith("-voice"),
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []});
}
let waitMessage: Message;
const startTime = Date.now();
try {
const imagesCount = chatMessages.reduce((total, curr) => {
return total + (curr.images?.length ?? 0);
}, 0);
if (!think && imagesCount) {
try {
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
if (modelInfo) {
if (!modelInfo.vision?.supported) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
if (think) {
try {
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
if (modelInfo) {
if (!modelInfo.thinking?.supported) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет размышлять 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
const uuid = crypto.randomUUID();
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
waitMessage = await replyToMessage({
message: msg,
text: (!think && imagesCount) ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitThinkText
});
const stream = await ollama.chat({
model: think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL,
stream: true,
think: think,
messages: chatMessages,
});
const newRequest = {
uuid: uuid,
stream: stream,
done: false,
fromId: msg.from.id,
chatId: msg.chat.id,
};
console.log("Pushing new request", newRequest);
ollamaRequests.push(newRequest);
await bot.editMessageReplyMarkup(
{
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: cancelMarkup
}
).catch(logError);
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
uuid: uuid,
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
if (getOllamaRequest(uuid)?.done) return;
try {
await bot.editMessageText({
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2",
reply_markup: cancelMarkup
}).catch(logError);
console.log("editMessageText", text);
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
} catch (e) {
logError(e);
}
}
});
await editor.tick();
try {
let isThinking = false;
for await (const chunk of stream) {
const content = chunk.message.content;
if (content === "<think>" || chunk.message.thinking) {
if (!isThinking) {
await bot.editMessageText({
chat_id: chatId,
message_id: waitMessage.message_id,
text: "🤔 Размышляю...",
parse_mode: "Markdown",
reply_markup: cancelMarkup
}).catch(logError);
}
isThinking = true;
}
if (!isThinking) {
currentText += content;
}
if (isThinking && !chunk.message.thinking) {
currentText += content;
}
if (content === "</think>" || !chunk.message.thinking) {
isThinking = false;
}
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
if (getOllamaRequest(uuid).done) {
shouldBreak = true;
}
if (shouldBreak || chunk.done) {
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
} else {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000;
await editor.tick();
await editor.stop();
console.log(`aborted request ${uuid}:`, abortOllamaRequest(uuid));
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
break;
}
}
} finally {
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
}
} catch (error) {
if (error.message.toLowerCase().includes("aborted")) return;
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
logError(error);
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
}
}
}
}
+10 -109
View File
@@ -1,112 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {boolToEmoji, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {ollama} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class OllamaGetModel extends Command {
title = "/ollamaGetModel";
description = "Ollama model info";
async execute(msg: Message): Promise<void> {
try {
const model = Environment.OLLAMA_MODEL;
const imageModel = Environment.OLLAMA_IMAGE_MODEL;
const thinkModel = Environment.OLLAMA_THINK_MODEL;
const promises: (Promise<AiModelCapabilities | null> | null)[] = [this.getModelCapabilities()];
if (imageModel && imageModel !== model) {
promises.push(this.loadImageModelInfo());
} else {
promises.push(null);
}
if (thinkModel && thinkModel !== model) {
promises.push(this.loadThinkModelInfo());
} else {
promises.push(null);
}
const infos = await Promise.all(promises);
let modelInfo = infos[0];
const modelText = "```Text\n" + this.getModelText(model, modelInfo) + "```";
modelInfo = infos[1];
const imageModelText = modelInfo ?
"```Image\n" + this.getModelText(imageModel, modelInfo) + "```" : null;
modelInfo = infos[2];
const thinkModelText = modelInfo ?
"```Think\n" + this.getModelText(thinkModel, modelInfo) + "```" : null;
const modelInfos = [modelText];
if (imageModelText) {
modelInfos.push(imageModelText);
}
if (thinkModelText) {
modelInfos.push(thinkModelText);
}
await replyToMessage({
message: msg,
text: modelInfos.join("\n\n"),
parse_mode: "Markdown"
}).catch(logError);
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
}
export class OllamaGetModel extends ProviderGetModelCommand {
constructor() {
super({
provider: AiProvider.OLLAMA,
title: Environment.commandTitles.ollamaGetModel,
description: Environment.commandDescriptions.ollamaGetModel,
});
}
private getModelText(model: string, info: AiModelCapabilities): string {
return `model: ${model}\n\n` +
`vision: ${boolToEmoji(info.vision?.supported)}\n` +
`thinking: ${boolToEmoji(info.thinking?.supported)}\n` +
`tools: ${boolToEmoji(info.tools?.supported)}`;
}
async getModelCapabilities(model: string = Environment.OLLAMA_MODEL): Promise<AiModelCapabilities | null> {
try {
const info = await ollama.show({model: model});
console.log(info);
return {
vision: {
supported: info.capabilities.includes("vision"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
ocr: {
supported: info.capabilities.includes("ocr"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
thinking: {
supported: info.capabilities.includes("thinking"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
tools: {
supported: info.capabilities.includes("tools"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
};
} catch (e) {
logError(e);
return null;
}
}
async loadImageModelInfo(): Promise<AiModelCapabilities | null> {
return this.getModelCapabilities(Environment.OLLAMA_IMAGE_MODEL);
}
async loadThinkModelInfo(): Promise<AiModelCapabilities | null> {
return this.getModelCapabilities(Environment.OLLAMA_THINK_MODEL);
}
}
}
+11 -34
View File
@@ -1,36 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {ollama} from "../index";
import {logError, oldReplyToMessage, replyToMessage} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class OllamaListModels extends Command {
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();
console.log(listResponse);
const modelsString = listResponse.models
.sort((a, b) => a.name.localeCompare(b.name))
.map(e => `${e.model}`)
.join("\n");
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) {
logError(e);
await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError);
}
export class OllamaListModels extends ProviderListModelsCommand {
constructor() {
super({
provider: AiProvider.OLLAMA,
title: Environment.commandTitles.ollamaListModels,
description: Environment.commandDescriptions.ollamaListModels,
});
}
}
}
-189
View File
@@ -1,189 +0,0 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {escapeMarkdownV2Text, logError, oldReplyToMessage, startIntervalEditor} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {MessageStore} from "../common/message-store";
export class OllamaPrompt extends Command {
command = "ollamaPrompt";
argsMode = "required" as const;
title = "/ollamaPrompt";
description = "Custom prompt for AI (Ollama)";
requirements = Requirements.Build(Requirement.BOT_ADMIN);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeOllama(msg, match?.[3]);
}
async executeOllama(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
let waitMessage: Message;
const startTime = Date.now();
try {
const uuid = crypto.randomUUID();
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await ollama.generate({
model: Environment.OLLAMA_MODEL,
stream: true,
think: false,
prompt: text
});
const newRequest = {
uuid: uuid,
stream: stream,
done: false,
fromId: msg.from.id,
chatId: msg.chat.id,
};
console.log("Pushing new request", newRequest);
ollamaRequests.push(newRequest);
await bot.editMessageReplyMarkup(
{
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: cancelMarkup
}
).catch(logError);
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
uuid: uuid,
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
if (getOllamaRequest(uuid)?.done) return;
try {
await bot.editMessageText({
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown",
reply_markup: cancelMarkup
}).catch(logError);
console.log("editMessageText", text);
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
} catch (e) {
logError(e);
}
}
});
await editor.tick();
try {
let isThinking = false;
for await (const chunk of stream) {
const content = chunk.response;
if (content === "<think>" || chunk.thinking) {
if (!isThinking) {
await bot.editMessageText({
chat_id: chatId,
message_id: waitMessage.message_id,
text: "🤔 Размышляю...",
parse_mode: "Markdown",
}).catch(logError);
}
isThinking = true;
}
if (!isThinking) {
currentText += content;
}
if (isThinking && !chunk.thinking) {
currentText += content;
}
if (content === "</think>" || !chunk.thinking) {
isThinking = false;
}
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
if (getOllamaRequest(uuid).done) {
shouldBreak = true;
}
if (shouldBreak || chunk.done) {
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
} else {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000;
await editor.tick();
await editor.stop();
console.log(`aborted request ${uuid}:`, abortOllamaRequest(uuid));
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
break;
}
}
} finally {
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
}
} catch (error) {
if (error.message.toLowerCase().includes("aborted")) return;
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
logError(error);
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
}
}
}
+20 -29
View File
@@ -2,48 +2,39 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {bot, ollama} from "../index";
import {WebSearchResponse} from "../model/web-search-response";
import {oldEditMessageText, logError} from "../util/utils";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
import {AiProvider} from "../model/ai-provider";
export class OllamaSearch extends Command {
command = ["s", "search"];
argsMode = "required" as const;
title = "/search";
description = "Web search via Ollama";
title = Environment.commandTitles.ollamaSearch;
description = Environment.commandDescriptions.ollamaSearch;
override requirements = Requirements.Build(Requirement.BOT_ADMIN);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
console.log("match", match);
const chatId = msg.chat.id;
const query = match?.[3] || "";
if (!query || !query.length) return;
try {
const wait = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
},
parse_mode: "Markdown"
const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
const result = await createOllamaClient(target).webSearch({query, maxResults: 10});
const body = (result.results ?? [])
.map((item, index) => `${index + 1}. ${item.content}`)
.join("\n\n");
await replyToMessage({
message: msg,
text: Environment.searchResultsHeaderText + "<blockquote expandable>" + escapeHtml(body) + "</blockquote>",
parse_mode: "HTML",
});
const results = await ollama.webSearch({query: match?.[3]});
console.log("results", results);
let message = "Результаты:\n\n";
results.results.forEach((result, index) => {
const r = result as WebSearchResponse;
message += `${index + 1}. ${r.url}\n`;
});
await oldEditMessageText(chatId, wait.message_id, message);
} catch (error) {
logError(error);
logError(error instanceof Error ? error : String(error));
await replyToMessage({message: msg, text: Environment.errorText}).catch(logError);
}
return Promise.resolve();
}
}
}
+10 -31
View File
@@ -1,34 +1,13 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {Environment} from "../common/environment";
import {logError, replyToMessage} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {ollama} from "../index";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class OllamaSetModel extends Command {
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?.[3];
try {
await ollama.show({model: newModel});
Environment.setOllamaModel(newModel || Environment.OLLAMA_MODEL);
const text = newModel ? `Выбрана модель "${newModel}"`
: `Модель не задана. Будет использоваться стандартная модель "${Environment.OLLAMA_MODEL}".`;
await replyToMessage({message: msg, text: text}).catch(logError);
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
}
export class OllamaSetModel extends ProviderSetModelCommand {
constructor() {
super({
provider: AiProvider.OLLAMA,
title: Environment.commandTitles.ollamaSetModel,
description: Environment.commandDescriptions.ollamaSetModel,
});
}
}
}
+16 -154
View File
@@ -1,167 +1,29 @@
import {Message} from "typescript-telegram-bot-api";
import {MessageStore} from "../common/message-store";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment";
import {bot, openAi} from "../index";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {ChatCommand} from "../base/chat-command";
import {AiProvider} from "../model/ai-provider";
import {runUnifiedAi} from "../ai/unified-ai-runner";
import {Environment} from "../common/environment";
export class OpenAIChat extends ChatCommand {
command = ["openai", "chatgpt"];
command = ["openai", "chatgpt", "openai-voice", "chatgpt-voice"];
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/openAI";
description = "Chat with AI (OpenAI)";
title = Environment.commandTitles.openAiChat;
description = Environment.commandDescriptions.openAiChat;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("OpenAI Chat: ", match);
return this.executeOpenAI(msg, match?.[3]);
}
async executeOpenAI(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
const content = [];
content.push({
type: part.bot ? "output_text" : "input_text",
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content,
});
// TODO: 03/02/2026, Danil Nikolaev: upload file then add here
// for (const image of part.images) {
// content.push({
// type: "image_url",
// imageUrl: "data:image/jpeg;base64," + image
// });
// }
return {
role: part.bot ? "assistant" : "user",
content: content,
type: "message",
};
const command = match?.[1]?.toLowerCase() ?? "";
await runUnifiedAi({
provider: AiProvider.OPENAI,
msg: msg,
text: match?.[3] ?? "",
stream: true,
think: true,
synthesizeSpeechResponse: command.endsWith("-voice"),
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({
role: "system",
content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}],
type: "message"
});
}
let waitMessage: Message;
const startTime = Date.now();
try {
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await openAi.responses.create({
model: Environment.OPENAI_MODEL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
input: chatMessages as any,
stream: true
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const chunk of stream) {
console.log("chunk", chunk);
if (chunk.type === "response.output_text.delta") {
const text = chunk.delta;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${error.toString()}`
}).catch(logError);
}
}
}
}
-117
View File
@@ -1,117 +0,0 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {bot, openAi, photoGenDir} from "../index";
import fs from "node:fs";
import path from "node:path";
import {oldEditMessageText, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {APIError} from "openai";
export class OpenAIGenImage extends ChatCommand {
command = ["openAiGenImage", "chatGPTGenImage", "imgen"];
title = "/openAIGenImage";
description = "Generate image from OpenAI";
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const prompt = match?.[3]?.trim();
if (!prompt?.length) return;
let waitMessage: Message | null = null;
try {
const totalParts = 3;
const model = Environment.OPENAI_IMAGE_MODEL;
const fileFullName = `${msg.chat.id}_${msg.message_id}.png`;
const getFileLocation = (fn: string) => {
return path.join(photoGenDir, fn);
};
waitMessage = await replyToMessage({message: msg, text: "🌈 Генерирую изображение..."});
const stream = await openAi.images.generate({
model: model,
prompt: prompt,
n: 1,
size: "auto",
stream: true,
partial_images: totalParts,
moderation: "low",
output_format: "png",
});
const then = Date.now();
for await (const event of stream) {
switch (event.type) {
case "image_generation.partial_image": {
console.log(` Partial image ${event.partial_image_index + 1}/3 received`);
console.log(` Size: ${event.b64_json.length} characters (base64)`);
const fileName = `partial_${event.partial_image_index + 1}_${fileFullName}`;
const imageBuffer = Buffer.from(event.b64_json, "base64");
const fileLocation = getFileLocation(fileName);
fs.writeFileSync(fileLocation, imageBuffer);
console.log(` 💾 Saved to: ${path.resolve(fileLocation)}`);
await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "photo",
media: imageBuffer,
caption: `🌈 Генерирую изображение (${(event.partial_image_index + 1)}/${totalParts})...`
}
});
break;
}
case "image_generation.completed": {
console.log("\n✅ Final image completed!");
console.log(` Size: ${event.b64_json.length} characters (base64)`);
const imageBuffer = Buffer.from(event.b64_json, "base64");
const fileLocation = getFileLocation(fileFullName);
fs.writeFileSync(fileLocation, imageBuffer);
console.log(` Saved to: ${path.resolve(fileLocation)}`);
const diff = Date.now() - then;
await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "photo",
media: imageBuffer,
caption: `🌈 Изображение по запросу "${prompt}" сгенерировано моделью "${model}" размеров ${event.size} за ${diff}ms`
}
});
break;
}
default:
console.log(`❓ Unknown event: ${event}`);
}
}
} catch (e) {
logError(e);
if (e instanceof APIError && e.error.code === "moderation_blocked") {
const text = "❌ Мне запрещено такое генерировать 😠";
if (waitMessage) {
await oldEditMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError);
} else {
await replyToMessage({message: msg, text: text}).catch(logError);
}
} else {
await replyToMessage({
message: waitMessage ? waitMessage : msg,
text: `Произошла ошибка: ${e}`
}).catch(logError);
}
}
}
}
+10 -26
View File
@@ -1,29 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class OpenAIGetModel extends Command {
title = "/openAIGetModel";
description = "Get current OpenAI model";
async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.OPENAI_MODEL}"`}).catch(logError);
export class OpenAIGetModel extends ProviderGetModelCommand {
constructor() {
super({
provider: AiProvider.OPENAI,
title: Environment.commandTitles.openAiGetModel,
description: Environment.commandDescriptions.openAiGetModel,
});
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
// TODO: 12/02/2026, Danil Nikolaev: find solution
try {
return {
vision: {supported: true},
ocr: null,
thinking: {supported: true},
tools: {supported: true},
};
} catch (e) {
logError(e);
return null;
}
}
}
}
+11 -35
View File
@@ -1,37 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {openAi} from "../index";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class OpenAIListModels extends Command {
title = "/openAIListModels";
description = "List all OpenAI models";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> {
try {
const listResponse = await openAi.models.list();
console.log(listResponse);
const modelsString = listResponse.data
.map(e => `${e.id}`)
.sort((a, b) => a.localeCompare(b))
.join("\n")
.substring(0, 4000);
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: "Не получилось загрузить список моделей"}).catch(logError);
}
export class OpenAIListModels extends ProviderListModelsCommand {
constructor() {
super({
provider: AiProvider.OPENAI,
title: Environment.commandTitles.openAiListModels,
description: Environment.commandDescriptions.openAiListModels,
});
}
}
}
+10 -22
View File
@@ -1,25 +1,13 @@
import {Command} from "../base/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";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class OpenAISetModel extends Command {
argsMode = "required" as const;
title = "/openAISetModel";
description = "Set OpenAI model";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3];
Environment.setOpenAIModel(newModel || Environment.OPENAI_MODEL);
const text = newModel ? `Выбрана модель "${newModel}"`
: `Модель не задана. Будет использоваться стандартная модель "${Environment.OPENAI_MODEL}".`;
await replyToMessage({message: msg, text: text}).catch(logError);
export class OpenAISetModel extends ProviderSetModelCommand {
constructor() {
super({
provider: AiProvider.OPENAI,
title: Environment.commandTitles.openAiSetModel,
description: Environment.commandDescriptions.openAiSetModel,
});
}
}
}
+9 -17
View File
@@ -1,46 +1,38 @@
import {logError, sendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {Environment} from "../common/environment";
export class Ping extends Command {
title = "/ping";
description = "Ping between received and sent message";
title = Environment.commandTitles.ping;
description = Environment.commandDescriptions.ping;
async execute(msg: Message) {
let d = new Date();
const u = (n: number): string => n > 9 ? n.toString() : `0${n}`;
const date = `${u(d.getDay())}.${u(d.getMonth() + 1)}.${d.getFullYear()}`;
const date = `${u(d.getDate())}.${u(d.getMonth() + 1)}.${d.getFullYear()}`;
const time = `${u(d.getHours())}:${u(d.getMinutes())}:${u(d.getSeconds())}:${u(d.getMilliseconds())}`;
const mDate = msg.date;
const nowDate = new Date().getTime() / 1000;
const diff = nowDate - mDate;
const tgPing = diff.toFixed(2);
const tgPing = (diff * 1000).toFixed(0);
d = new Date(mDate * 1000);
const msgDate = `${u(d.getDay())}.${u(d.getMonth() + 1)}.${d.getFullYear()}`;
const msgDate = `${u(d.getDate())}.${u(d.getMonth() + 1)}.${d.getFullYear()}`;
const msgTime = `${u(d.getHours())}:${u(d.getMinutes())}:${u(d.getSeconds())}:${u(d.getMilliseconds())}`;
const then = Date.now();
await sendMessage({message: msg, text: "pong"}).catch(logError);
await sendMessage({message: msg, text: Environment.pongText}).catch(logError);
const now = Date.now();
const msgSendDiff = (now - then).toFixed(2);
await sendMessage(
{
message: msg,
text:
"```ping\n" +
`TG: ${tgPing}ms\n` +
`API ${msgSendDiff}ms\n\n` +
`🗓️ Message date: ${msgDate}\n` +
`🕒 Message time: ${msgTime}\n\n` +
`🗓️ Local date : ${date}\n` +
`🕒 Local time: ${time}` +
"```",
text: Environment.getPingReportText(tgPing, msgSendDiff, msgDate, msgTime, date, time),
parse_mode: "Markdown"
}
).catch(logError);
}
}
}
+2 -2
View File
@@ -5,6 +5,6 @@ import {Environment} from "../common/environment";
export class PrefixResponse extends Command {
async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix)}).catch(logError);
await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix) ?? Environment.prefixFallbackText}).catch(logError);
}
}
}
+98
View File
@@ -0,0 +1,98 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {Requirement} from "../base/requirement";
import {Requirements} from "../base/requirements";
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
import {formatRuntimeModelInfo, getRuntimeModel, listProviderModels, setRuntimeModel} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {appLogger} from "../logging/logger";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
const logger = appLogger.child("commands:models");
type ProviderModelCommandOptions = {
provider: AiProvider;
title: string;
description: string;
};
export abstract class ProviderModelCommand extends Command {
protected readonly provider: AiProvider;
title: string;
description: string;
protected constructor(options: ProviderModelCommandOptions) {
super();
this.provider = options.provider;
this.title = options.title;
this.description = options.description;
}
}
export class ProviderGetModelCommand extends ProviderModelCommand {
async execute(msg: Message): Promise<void> {
logger.debug("get_model", {provider: this.provider, chatId: msg.chat?.id, messageId: msg.message_id});
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(this.provider)}).catch(logError);
}
}
export class ProviderSetModelCommand extends ProviderModelCommand {
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]?.trim();
logger.info("set_model.request", {provider: this.provider, hasModel: !!newModel, chatId: msg.chat?.id, messageId: msg.message_id});
if (newModel) setRuntimeModel(this.provider, newModel);
const model = getRuntimeModel(this.provider);
const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(this.provider))
: Environment.getModelIsNotSetCurrentText(model);
logger.debug("set_model.reply", {provider: this.provider, model});
await replyToMessage({message: msg, text}).catch(logError);
}
}
export class ProviderListModelsCommand extends ProviderModelCommand {
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> {
try {
logger.info("list_models.request", {provider: this.provider, chatId: msg.chat?.id, messageId: msg.message_id});
const models = (await listProviderModels(this.provider)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = await this.buildListText(modelsString);
logger.debug("list_models.reply", {provider: this.provider, count: models.length, textChars: text.length});
await replyToMessage({message: msg, text, parse_mode: "HTML"});
} catch (e) {
logger.error("list_models.failed", {provider: this.provider, error: e instanceof Error ? e : String(e)});
logError(e instanceof Error ? e : String(e));
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
}
}
private async buildListText(modelsString: string): Promise<string> {
if (this.provider !== AiProvider.OLLAMA) {
return Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
}
const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
const loadedModels = ((await createOllamaClient(target).ps())?.models ?? [])
.map(model => model.model || model.name)
.filter((model): model is string => !!model);
logger.debug("list_models.loaded", {provider: this.provider, loaded: loadedModels.length});
return Environment.getLoadedModelsText(loadedModels)
+ "\n\n"
+ Environment.modelListHeaderText
+ "<blockquote expandable>"
+ modelsString
+ "</blockquote>";
}
}
+32 -24
View File
@@ -1,15 +1,17 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {extractMessagePayload, logError, replyToMessage} from "../util/utils";
import {escapeHtml, extractMessagePayload, logError, replyToMessage} from "../util/utils";
import {bot, botUser} from "../index";
import QRCode from "qrcode";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
export class Qr extends Command {
argsMode = "optional" as const;
title = "/qr";
description = "Generates QR-code from text you sent or replied to.";
title = Environment.commandTitles.qr;
description = Environment.commandDescriptions.qr;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const chatId = msg.chat.id;
@@ -19,27 +21,29 @@ export class Qr extends Command {
await replyToMessage(
{
message: msg,
text: "Не найден текст для генерации QR-кода."
text: Environment.qrCodeMissingTextText
}
);
return;
}
// TODO: 16/02/2026, Danil Nikolaev: escape html symbols in payload
if (payload.length > 1500) {
payload = payload.slice(0, 1500);
const maxQrPayloadLength = 1500;
if (payload.length > maxQrPayloadLength) {
payload = payload.slice(0, maxQrPayloadLength);
await replyToMessage(
{
message: msg,
text: `Слишком длинный текст для QR (${payload.length} символов). Текст будет обрезан до 1500 символов.`
text: Environment.getQrCodeTextTooLongText(payload.length, maxQrPayloadLength)
}
);
}
try {
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
await enqueueTelegramApiCall(
() => bot.sendChatAction({chat_id: chatId, action: "upload_photo"}),
{method: "sendChatAction", chatId, chatType: msg.chat.type}
);
const pngBuffer = await QRCode.toBuffer(payload, {
type: "png",
@@ -49,23 +53,27 @@ export class Qr extends Command {
});
const maxCaptionLength = botUser.is_premium ? 4096 : 1024;
const visiblePayload = payload.length > maxCaptionLength - 80
? payload.slice(0, maxCaptionLength - 83) + "..."
: payload;
await bot.sendPhoto({
chat_id: chatId,
photo: pngBuffer,
caption: "QR-код готов ✅\nСодержимое:\n<blockquote expandable>" +
`${payload.length > maxCaptionLength ? payload.slice(0, maxCaptionLength - 40) + "..." : payload}` +
"</blockquote>",
reply_parameters: {
message_id: msg.message_id,
},
parse_mode: "HTML"
});
} catch (e) {
await enqueueTelegramApiCall(
() => bot.sendPhoto({
chat_id: chatId,
photo: pngBuffer,
caption: Environment.getQrCodeReadyText(escapeHtml(visiblePayload)),
reply_parameters: {
message_id: msg.message_id,
},
parse_mode: "HTML"
}),
{method: "sendPhoto", chatId, chatType: msg.chat.type}
);
} catch (error) {
await replyToMessage({
message: msg,
text: `Не получилось сгенерировать QR: ${e?.message ?? String(e)}`
text: Environment.getQrCodeFailedText(error instanceof Error ? error : String(error))
}).catch(logError);
}
}
}
}
+51 -33
View File
@@ -17,9 +17,15 @@ import {
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import twemoji from "twemoji";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {AsyncSemaphore} from "../util/async-lock";
import {Environment} from "../common/environment";
import {getLruMapValue, setLruMapValue} from "../util/lru-map";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("command:quote");
try {
GlobalFonts.registerFromPath("./assets/Inter_18pt-ExtraThin.ttf", "InterExtraThin");
GlobalFonts.registerFromPath("./assets/Inter_18pt-Thin.ttf", "InterThin");
GlobalFonts.registerFromPath("./assets/Inter_18pt-Light.ttf", "InterLight");
GlobalFonts.registerFromPath("./assets/Inter_18pt-Regular.ttf", "Inter");
@@ -33,50 +39,60 @@ try {
GlobalFonts.registerFromPath("./assets/JetBrainsMono-Italic.ttf", "JetBrainsMonoItalic");
GlobalFonts.registerFromPath("./assets/JetBrainsMono-Regular.ttf", "JetBrainsMonoRegular");
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
}
export class Quote extends Command {
command = ["cit", "citation", "q", "quote"];
argsMode = "none" as const;
title = "/quote";
description = "Make quote from text (or quote)";
title = Environment.commandTitles.quote;
description = Environment.commandDescriptions.quote;
requirements = Requirements.Build(Requirement.REPLY);
async execute(msg: Message): Promise<void> {
const chatId = msg.chat.id;
const reply = msg.reply_to_message;
if (!reply) return;
try {
const startedAt = Date.now();
logger.debug("execute.start", {chatId, messageId: msg.message_id, replyMessageId: reply.message_id});
const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim();
if (quoteRaw.length === 0) {
await replyToMessage({message: msg, text: "Не нашёл в сообщении текста 😢"}).catch(logError);
await replyToMessage({message: msg, text: Environment.quoteMissingTextText}).catch(logError);
return;
}
const quote = quoteRaw.length ? quoteRaw : "…";
const entities = msg.quote ? msg.quote.entities : reply.entities ?? reply.caption_entities ?? [];
const entities = msg.quote ? msg.quote.entities ?? [] : reply.entities ?? reply.caption_entities ?? [];
const png = await renderQuoteCard(msg, quote, reply, entities);
await bot.sendPhoto({
chat_id: chatId,
photo: png,
reply_parameters: {
message_id: msg.message_id,
},
}).catch(logError);
const png = await quoteRenderSemaphore.runExclusive(() => renderQuoteCard(msg, quote, reply, entities));
await enqueueTelegramApiCall(
() => bot.sendPhoto({
chat_id: chatId,
photo: png,
reply_parameters: {
message_id: msg.message_id,
},
}),
{method: "sendPhoto", chatId, chatType: msg.chat.type}
);
logger.debug("execute.done", {chatId, messageId: msg.message_id, bytes: png.length, duration: logger.duration(startedAt)});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: "Не смог собрать цитату 😢"}).catch(logError);
logError(e instanceof Error ? e : String(e));
await replyToMessage({message: msg, text: Environment.quoteBuildFailedText}).catch(logError);
}
}
}
const emojiCache = new Map<string, CanvasImage>();
const customEmojiCache = new Map<string, CanvasImage>();
const quoteRenderSemaphore = new AsyncSemaphore(2);
const EMOJI_CACHE_MAX_ENTRIES = 256;
const CUSTOM_EMOJI_CACHE_MAX_ENTRIES = 512;
function appleEmojiUrl(emoji: string): string {
const codePoints = [...emoji]
@@ -97,17 +113,19 @@ function twemojiUrl(emoji: string) {
return `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/${code}.png`;
}
async function loadEmoji(emoji: string): Promise<CanvasImage> {
async function loadEmoji(emoji: string | undefined): Promise<CanvasImage | null> {
if (!emoji) return null;
const downloadAndCache = async (url: string): Promise<Image> => {
const res = await axios.get<ArrayBuffer>(url, {responseType: "arraybuffer"});
const img = await loadImage(Buffer.from(res.data));
emojiCache.set(url, img);
setLruMapValue(emojiCache, url, img, EMOJI_CACHE_MAX_ENTRIES);
return img;
};
const checkIfCached = async (emoji: string, emojiToUrl: (emoji: string) => string): Promise<CanvasImage> => {
const url = emojiToUrl(emoji);
const cached = emojiCache.get(url);
const cached = getLruMapValue(emojiCache, url);
if (cached) return cached;
return await downloadAndCache(emojiToUrl(emoji));
};
@@ -117,7 +135,7 @@ async function loadEmoji(emoji: string): Promise<CanvasImage> {
try {
return await checkIfCached(emoji, source);
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
}
}
@@ -125,7 +143,7 @@ async function loadEmoji(emoji: string): Promise<CanvasImage> {
}
async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | null> {
const cached = customEmojiCache.get(customEmojiId);
const cached = getLruMapValue(customEmojiCache, customEmojiId);
if (cached) return cached;
try {
@@ -134,14 +152,14 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
});
if (!stickerSet || stickerSet.length === 0) {
console.warn(`Custom emoji ${customEmojiId} not found`);
logger.warn("custom_emoji.not_found", {customEmojiId});
return null;
}
const sticker = stickerSet[0];
if (sticker.is_animated || sticker.is_video) {
console.warn(`Animated/video custom emoji ${customEmojiId} not supported`);
logger.warn("custom_emoji.unsupported", {customEmojiId});
return loadEmoji(sticker.emoji);
}
@@ -152,14 +170,14 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
try {
buffer = await sharp(buffer).png().toBuffer();
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
}
const img = await loadImage(buffer);
customEmojiCache.set(customEmojiId, img);
setLruMapValue(customEmojiCache, customEmojiId, img, CUSTOM_EMOJI_CACHE_MAX_ENTRIES);
return img;
} catch (e) {
console.warn(`Failed to load custom emoji ${customEmojiId}:`, e);
logger.warn("custom_emoji.load_failed", {customEmojiId, error: e instanceof Error ? e : String(e)});
return null;
}
}
@@ -491,9 +509,9 @@ async function drawLine(ctx: SKRSContext2D, line: Segment[], x: number, baseline
try {
const img = await loadEmoji(seg.v);
const y = baselineY - emojiSize + Math.round(fontSize * 0.2);
ctx.drawImage(img, cx, y, emojiSize, emojiSize);
ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize);
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
ctx.fillText(seg.v, cx, baselineY);
}
cx += emojiSize;
@@ -506,17 +524,17 @@ async function drawLine(ctx: SKRSContext2D, line: Segment[], x: number, baseline
} else {
const img = await loadEmoji("😥");
const y = baselineY - emojiSize + Math.round(fontSize * 0.2);
ctx.drawImage(img, cx, y, emojiSize, emojiSize);
ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize);
}
} catch (e) {
console.warn("Failed to draw custom emoji:", e);
logger.warn("custom_emoji.draw_failed", {error: e instanceof Error ? e : String(e)});
try {
const img = await loadEmoji("😥");
const y = baselineY - emojiSize + Math.round(fontSize * 0.2);
ctx.drawImage(img, cx, y, emojiSize, emojiSize);
ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize);
} catch (e) {
logError(e);
logError(e instanceof Error ? e : String(e));
ctx.fillText(":-(", cx, baselineY);
}
@@ -742,4 +760,4 @@ function getQuoteAuthor(reply: Message): QuoteAuthor {
const u = reply.from!;
const name = [u.first_name, u.last_name].filter(Boolean).join(" ") || u.username || "Unknown";
return {name, username: u.username, userId: u.id};
}
}
+22 -11
View File
@@ -1,25 +1,36 @@
import {Command} from "../base/command";
import {getRandomInt, getRangedRandomInt, logError, oldSendMessage} from "../util/utils";
import {getRandomInt, logError, oldSendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
export class RandomInt extends Command {
argsMode = "optional" as const;
title = "/randomInt";
description = "Ranged random integer from parameters";
title = Environment.commandTitles.randomInt;
description = Environment.commandDescriptions.randomInt;
async execute(msg: Message) {
const split = msg.text.split(" ");
const min = parseInt(split[1]);
const max = parseInt(split[2]);
if (!msg.text) return;
const good = max > min;
const sufficient = !!(min && max) && good;
const args = msg.text.trim().split(/\s+/).slice(1);
const values = args
.map(value => Number(value))
.filter(value => Number.isSafeInteger(value));
const min = values.length === 1 ? 1 : values[0];
const max = values.length === 1 ? values[0] : values[1];
const random = !sufficient ? getRandomInt(Math.pow(2, 60)) : getRangedRandomInt(min, max);
const sufficient = Number.isSafeInteger(min) && Number.isSafeInteger(max);
if (sufficient && min === max) {
await oldSendMessage(msg, Environment.getRandomIntRangeText(min, max, min)).catch(logError);
return;
}
const randomText = !sufficient ? random.toString() : `[${min}; ${max}]: ${random}`;
const from = sufficient ? Math.min(min, max) : 0;
const to = sufficient ? Math.max(min, max) : 1_000_000_000;
const random = getRandomInt(to - from + 1) + from;
const randomText = !sufficient ? random.toString() : Environment.getRandomIntRangeText(from, to, random);
await oldSendMessage(msg, randomText).catch(logError);
}
}
}
+14 -10
View File
@@ -1,31 +1,35 @@
import {Command} from "../base/command";
import {getRandomInt, logError, replyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
export class RandomString extends Command {
argsMode = "optional" as const;
title = "/randomString";
description = "literally random string (up to 4096 symbols)";
title = Environment.commandTitles.randomString;
description = Environment.commandDescriptions.randomString;
async execute(msg: Message) {
const split = msg.text.split(" ");
const l = parseInt(split.length > 1 ? split[1] : "1");
if (!msg.text) return;
const length = (l <= 0 || l > 4096) ? 1 : l;
const [, lengthArg] = msg.text.trim().split(/\s+/);
const requestedLength = Number(lengthArg ?? 1);
const length = Number.isSafeInteger(requestedLength)
? Math.min(4096, Math.max(1, requestedLength))
: 1;
const characters = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789");
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789";
for (let i = 0; i < length; i++) {
result += characters.charAt(getRandomInt(characters.length));
result += characters[getRandomInt(characters.length)];
}
await replyToMessage({
message: msg,
text: "<blockquote expandable>" + result + "</blockquote>",
text: Environment.getExpandableBlockquoteText(result),
parse_mode: "HTML"
}).catch(logError);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {UserStore} from "../common/user-store";
import {
ensureValidUserAiSettings,
normalizeAiContextSizeChoice,
normalizeAiImageOutputMode,
normalizeAiVoiceMode,
setUserAiContextSizeChoice,
setUserAiImageOutputMode,
setUserAiVoiceMode,
} from "../common/user-ai-settings";
import {buildUserSettingsKeyboard, formatUserSettingsText} from "../common/user-settings-view";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Settings extends Command {
command = ["settings", "config"];
argsMode = "optional" as const;
title = Environment.commandTitles.settings;
description = Environment.commandDescriptions.settings;
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
if (!msg.from) return;
await UserStore.put(msg.from);
const args = match?.[3]?.trim();
let settings = await ensureValidUserAiSettings(msg.from.id);
let screen: Parameters<typeof formatUserSettingsText>[1] = "main";
if (args) {
const [name, ...rest] = args.split(/\s+/);
const value = rest.join(" ");
if (name?.toLowerCase() === "context" || name?.toLowerCase() === "ctx") {
const choice = normalizeAiContextSizeChoice(value);
if (choice) {
settings = (await setUserAiContextSizeChoice(msg.from.id, choice)).settings;
screen = "contextSize";
}
}
if (name?.toLowerCase() === "voice" || name?.toLowerCase() === "audio") {
const mode = normalizeAiVoiceMode(value);
if (mode) {
settings = (await setUserAiVoiceMode(msg.from.id, mode)).settings;
screen = "voiceMode";
}
}
if (name?.toLowerCase() === "image" || name?.toLowerCase() === "images" || name?.toLowerCase() === "output") {
const mode = normalizeAiImageOutputMode(value || name);
if (mode) {
settings = (await setUserAiImageOutputMode(msg.from.id, mode)).settings;
screen = "imageOutput";
}
}
}
await replyToMessage({
message: msg,
text: formatUserSettingsText(settings, screen),
reply_markup: buildUserSettingsKeyboard(settings, screen),
}).catch(logError);
}
}
+22 -19
View File
@@ -2,47 +2,50 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {bot} from "../index";
import {bot, shutdown as shutdownApp} from "../index";
import {delay, logError, randomValue} from "../util/utils";
const texts = [
"ну что-же, господа",
"приятно было с вами пообщаться",
"но мне пора на покой",
"всего хорошего"
];
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
const timings = [1500, 2500];
const timer = [3, 2, 1];
export class Shutdown extends Command {
title = "/shutdown";
description = "Self-destruction sequence for bot (shutdown)";
title = Environment.commandTitles.shutdown;
description = Environment.commandDescriptions.shutdown;
argsMode = "optional" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
await bot.sendMessage({chat_id: msg.chat.id, text: "..."}).catch(logError);
const send = async (text: string) => {
await enqueueTelegramApiCall(
() => bot.sendMessage({chat_id: msg.chat.id, text}),
{method: "sendMessage", chatId: msg.chat.id, chatType: msg.chat.type}
).catch(logError);
};
await send(Environment.shutdownFallbackText);
const now = match?.[3]?.toLowerCase() === "now";
if (msg.chat.type !== "private" && !now) {
for (const text of texts) {
await delay(randomValue(timings));
await bot.sendMessage({chat_id: msg.chat.id, text: text}).catch(logError);
for (const text of Environment.shutdownSequenceTexts) {
await delay(randomValue(timings) ?? 1500);
await send(text);
}
await delay(randomValue(timings));
await delay(randomValue(timings) ?? 1500);
for (const t of timer) {
await bot.sendMessage({chat_id: msg.chat.id, text: `${t}`}).catch(logError);
await send(`${t}`);
await delay(1000);
}
}
await bot.sendMessage({chat_id: msg.chat.id, text: "*R.I.P*"}).catch(logError);
await send(Environment.shutdownDoneText);
delay(2000).then(() => process.exit(0));
await delay(2000);
await shutdownApp("manual");
}
}
}
+80
View File
@@ -0,0 +1,80 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {isTranscribableAudioDownload, resolveSpeechToTextProviderForUser, transcribeSpeechDownloads} from "../ai/speech-to-text";
import {attachmentsToDownloadedFiles, cacheMessageAttachments} from "../ai/telegram-attachments";
import {MessageStore} from "../common/message-store";
import {StoredAttachment} from "../model/stored-attachment";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {parseProviderToken} from "../ai/provider-aliases";
const TELEGRAM_LIMIT = 4096;
async function collectStoredAttachments(msg: Message | undefined): Promise<StoredAttachment[]> {
if (!msg) return [];
const stored = await MessageStore.get(msg.chat.id, msg.message_id);
if (stored?.attachments?.length) return stored.attachments;
return cacheMessageAttachments(msg);
}
async function collectAudioDownloads(msg: Message) {
const attachments = [
...await collectStoredAttachments(msg),
...await collectStoredAttachments(msg.reply_to_message),
];
const seen = new Set<string>();
return attachmentsToDownloadedFiles(attachments)
.filter(isTranscribableAudioDownload)
.filter(download => {
const key = `${download.fileId}:${download.path}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
export class SpeechToText extends Command {
command = ["stt", "transcribe"];
argsMode = "optional" as const;
title = Environment.commandTitles.speechToText;
description = Environment.commandDescriptions.speechToText;
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
if (!msg.from) return;
const args = match?.[3]?.trim() ?? "";
const explicitProvider = parseProviderToken(args.split(/\s+/)[0]);
const downloads = await collectAudioDownloads(msg);
if (!downloads.length) {
await replyToMessage({
message: msg,
text: Environment.speechToTextInstructionText,
}).catch(logError);
return;
}
try {
const resolved = await resolveSpeechToTextProviderForUser(msg.from.id, explicitProvider, {
allowFallback: !explicitProvider,
});
const transcript = await transcribeSpeechDownloads(resolved.provider, downloads);
const text = transcript.trim() || Environment.speechToTextEmptyResultText;
await replyToMessage({
message: msg,
text: text.length > TELEGRAM_LIMIT ? text.slice(0, TELEGRAM_LIMIT - 3) + "..." : text,
}).catch(logError);
} catch (e) {
logError(e instanceof Error ? e : String(e));
await replyToMessage({
message: msg,
text: e instanceof Error ? e.message : String(e),
}).catch(logError);
}
}
}
+5 -4
View File
@@ -2,12 +2,13 @@ import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {commands} from "../index";
import {Help} from "./help";
import {Environment} from "../common/environment";
export class Start extends Command {
title = "/start";
description = "Start the bot";
title = Environment.commandTitles.start;
description = Environment.commandDescriptions.start;
async execute(msg: Message): Promise<void> {
await commands.find(e => e instanceof Help).execute(msg);
await commands.find(e => e instanceof Help)?.execute(msg);
}
}
}
+22 -7
View File
@@ -1,18 +1,33 @@
import {Command} from "../base/command";
import {logError, replyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {ShellCommandRunner} from "../util/shell-command-runner";
export class SystemInfo extends Command {
title = "/systemInfo";
description = "System information";
title = Environment.commandTitles.systemInfo;
description = Environment.commandDescriptions.systemInfo;
private static systemInfoText: string;
private static systemInfoParams: Parameters<typeof Environment.getSystemSpecsText>[0] | null = null;
static setSystemInfo(info: string) {
SystemInfo.systemInfoText = info;
static setSystemInfo(params: Parameters<typeof Environment.getSystemSpecsText>[0]) {
SystemInfo.systemInfoParams = params;
}
async execute(msg: Message) {
await replyToMessage({message: msg, text: SystemInfo.systemInfoText}).catch(logError);
if (!SystemInfo.systemInfoParams) return;
const loadAverageResult = await ShellCommandRunner.run("awk '{printf \"%.2f;%.2f;%.2f\\n\", $1, $2, $3}' /proc/loadavg");
const split = loadAverageResult.stdout?.split(";").map(s => parseFloat(s)) ?? [];
const loadAverageText = split.length
? `LOAD_AVERAGE: ${split.map(value => value.toFixed(2)).join(", ")}`
: null;
const finalText = [
Environment.getSystemSpecsText(SystemInfo.systemInfoParams),
loadAverageText,
].filter(Boolean).join("\n");
await replyToMessage({message: msg, text: finalText}).catch(logError);
}
}
}
+4 -4
View File
@@ -5,10 +5,10 @@ import {Environment} from "../common/environment";
export class Test extends Command {
regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))$/i;
title = "тест";
description = "System functionality check";
title = Environment.commandTitles.test;
description = Environment.commandDescriptions.test;
async execute(msg: Message) {
await oldReplyToMessage(msg, randomValue(Environment.ANSWERS.test) || "а").catch(logError);
await oldReplyToMessage(msg, randomValue(Environment.ANSWERS.test) || Environment.defaultTestAnswerText).catch(logError);
}
}
}
+50
View File
@@ -0,0 +1,50 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command";
import {parseProviderToken} from "../ai/provider-aliases";
import {
resolveTextToSpeechProviderForUser,
sendSynthesizedSpeech,
synthesizeSpeech,
} from "../ai/text-to-speech";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class TextToSpeech extends Command {
command = ["tts", "say", "voice"];
argsMode = "optional" as const;
title = Environment.commandTitles.textToSpeech;
description = Environment.commandDescriptions.textToSpeech;
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
if (!msg.from) return;
const args = match?.[3]?.trim() ?? "";
const replyText = (msg.reply_to_message?.text ?? msg.reply_to_message?.caption ?? "").trim();
const [firstToken = "", ...restTokens] = args.split(/\s+/);
const explicitProvider = parseProviderToken(firstToken);
const text = explicitProvider
? (restTokens.join(" ").trim() || replyText)
: (args || replyText);
if (!text.trim()) {
await replyToMessage({
message: msg,
text: Environment.textToSpeechInstructionText,
}).catch(error => logError(error instanceof Error ? error : String(error)));
return;
}
try {
const resolved = await resolveTextToSpeechProviderForUser(msg.from.id, explicitProvider);
const speech = await synthesizeSpeech({provider: resolved.provider, text});
await sendSynthesizedSpeech(msg, speech);
} catch (e) {
logError(e instanceof Error ? e : String(e));
await replyToMessage({
message: msg,
text: e instanceof Error ? e.message : String(e),
}).catch(logError);
}
}
}
+10 -5
View File
@@ -4,13 +4,15 @@ import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {logError, oldReplyToMessage} from "../util/utils";
import {bot} from "../index";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {Environment} from "../common/environment";
export class Title extends Command {
command = "title";
argsMode = "required" as const;
title = "/title";
description = "Change group title";
title = Environment.commandTitles.title;
description = Environment.commandDescriptions.title;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
@@ -22,10 +24,13 @@ export class Title extends Command {
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const title = (match?.[3] ?? "").trim();
if (title.length === 0) {
await oldReplyToMessage(msg, "Не нашёл название...").catch(logError);
await oldReplyToMessage(msg, Environment.titleMissingText).catch(logError);
return;
}
await bot.setChatTitle({chat_id: msg.chat.id, title: title}).catch(logError);
await enqueueTelegramApiCall(
() => bot.setChatTitle({chat_id: msg.chat.id, title: title}),
{method: "setChatTitle", chatId: msg.chat.id, chatType: msg.chat.type}
).catch(logError);
}
}
}
+10 -7
View File
@@ -1,6 +1,7 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
import {Environment} from "../common/environment";
const EN =
"`qwertyuiop[]asdfghjkl;'zxcvbnm,./" +
@@ -38,7 +39,7 @@ export const toEnLayout = (text: string) => swapLayout(text, ruToEn);
const reCyr = /\p{Script=Cyrillic}/u;
const reLat = /\p{Script=Latin}/u;
export type ScriptGuess = "ru" | "en" | "mixed" | "unknown";
export type ScriptGuess = "ru" | "en" | "mixed" | "other";
export function detectScript(text: string): ScriptGuess {
let cyr = 0, lat = 0;
@@ -48,7 +49,7 @@ export function detectScript(text: string): ScriptGuess {
else if (reLat.test(ch)) lat++;
}
if (cyr === 0 && lat === 0) return "unknown";
if (cyr === 0 && lat === 0) return "other";
if (cyr === lat) return "mixed";
return cyr > lat ? "ru" : "en";
}
@@ -60,7 +61,7 @@ export function fixLayoutAuto(
): string {
let guess = detectScript(text);
if (guess === "mixed") {
guess = randomValue([true, false]) ? "ru" : "en";
guess = (randomValue([true, false]) ?? false) ? "ru" : "en";
}
if (guess === "en") {
@@ -77,16 +78,18 @@ export function fixLayoutAuto(
export class Transliteration extends Command {
command = ["transliteration", "tr"];
title = "/tr [text or reply]";
description = "Transliteration EN <--> RU";
title = Environment.commandTitles.transliteration;
description = Environment.commandDescriptions.transliteration;
async execute(msg: Message): Promise<void> {
if (!msg.text && !msg.caption) return;
let text: string = "";
if (msg.reply_to_message) {
text = (msg.reply_to_message.text || msg.reply_to_message.caption || "");
} else {
const split = (msg.text || msg.caption).split("/tr ");
const split = (<string>(msg.text || msg.caption)).split("/tr ");
if (split.length > 1) {
text = split[1].trim();
}
@@ -100,4 +103,4 @@ export class Transliteration extends Command {
await oldReplyToMessage(msg, newText).catch(logError);
}
}
}
+15 -11
View File
@@ -5,10 +5,11 @@ import {Message} from "typescript-telegram-bot-api";
import {bot, botUser} from "../index";
import {fullName, logError, oldReplyToMessage, oldSendMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
export class Unban extends Command {
title = "/unban [reply]";
description = "unban user from chat";
title = Environment.commandTitles.unban;
description = Environment.commandDescriptions.unban;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
@@ -19,32 +20,35 @@ export class Unban extends Command {
);
async execute(msg: Message) {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.reply_to_message.from) return;
const user = msg.reply_to_message.from;
const userId = user.id;
if (userId === botUser.id) {
await oldReplyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError);
await oldReplyToMessage(msg, Environment.botIsNotBannedByItselfText).catch(logError);
return;
}
if (userId === Environment.CREATOR_ID) {
await oldReplyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError);
await oldReplyToMessage(msg, Environment.botCreatorNeverBannedText).catch(logError);
return;
}
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
await oldReplyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError);
if (msg.from?.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
await oldReplyToMessage(msg, Environment.botAdminsNotBannedText).catch(logError);
return;
}
bot.unbanChatMember({chat_id: msg.chat.id, user_id: userId})
enqueueTelegramApiCall(
() => bot.unbanChatMember({chat_id: msg.chat.id, user_id: userId}),
{method: "unbanChatMember", chatId: msg.chat.id, chatType: msg.chat.type}
)
.then(async () => {
await oldSendMessage(msg, `${fullName(user)} разбанен ⛓️‍💥`).catch(logError);
await oldSendMessage(msg, Environment.getUserUnbannedText(fullName(user))).catch(logError);
})
.catch(async () => {
await oldSendMessage(msg, `Не смог разбанить ${fullName(user)} ☹️`).catch(logError);
await oldSendMessage(msg, Environment.getUserUnbanFailedText(fullName(user))).catch(logError);
});
}
}
}
+8 -8
View File
@@ -7,8 +7,8 @@ import {botUser} from "../index";
import {Environment} from "../common/environment";
export class Unignore extends Command {
title = "/unignore";
description = "Bot will start responding to the user";
title = Environment.commandTitles.unignore;
description = Environment.commandDescriptions.unignore;
requirements = Requirements.Build(
Requirement.BOT_ADMIN,
Requirement.CHAT,
@@ -18,25 +18,25 @@ export class Unignore extends Command {
);
async execute(msg: Message) {
if (!msg.reply_to_message) return;
if (!msg.reply_to_message || !msg.reply_to_message.from) return;
const id = msg.reply_to_message.from.id;
const text = fullName(msg.reply_to_message.from);
if (id === botUser.id) {
await oldSendMessage(msg, "Бот и так всегда к себе прислушивается").catch(logError);
await oldSendMessage(msg, Environment.botAlreadyAlwaysListensToItselfText).catch(logError);
return;
}
if (id === Environment.CREATOR_ID) {
await oldSendMessage(msg, "Бот всегда слушает своего создателя").catch(logError);
await oldSendMessage(msg, Environment.botAlwaysListensToCreatorText).catch(logError);
return;
}
if (await Environment.removeMute(id)) {
await oldSendMessage(msg, text + " больше не в муте! 🔈").catch(logError);
await oldSendMessage(msg, Environment.getUserUnignoredText(text)).catch(logError);
} else {
await oldSendMessage(msg, text + " не был в муте 🤔").catch(logError);
await oldSendMessage(msg, Environment.getUserWasNotIgnoredText(text)).catch(logError);
}
}
}
}
+4 -3
View File
@@ -1,12 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {getUptime, logError, oldSendMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Uptime extends Command {
title = "/uptime";
description = "Bot's uptime";
title = Environment.commandTitles.uptime;
description = Environment.commandDescriptions.uptime;
async execute(msg: Message): Promise<void> {
await oldSendMessage(msg, getUptime()).catch(logError);
}
}
}
+4 -4
View File
@@ -7,8 +7,8 @@ export class WhatBetter extends Command {
command = ["what", "что"];
argsMode = "required" as const;
title = "/what better [a] or [b]";
description = "either a or b randomly (50% chance)";
title = Environment.commandTitles.whatBetter;
description = Environment.commandDescriptions.whatBetter;
private argsRe = /^(better|лучше)\s+([\s\S]+?)\s+(or|или)\s+([\s\S]+)$/i;
@@ -19,8 +19,8 @@ export class WhatBetter extends Command {
const a = m[2].trim();
const b = m[4].trim();
const text = `${randomValue(Environment.ANSWERS.better)} ${randomValue([a, b])}`;
const text = `${randomValue(Environment.ANSWERS.better) ?? Environment.betterFallbackText} ${randomValue([a, b]) ?? a}`;
await oldSendMessage(msg, text).catch(logError);
}
}
}
+13 -45
View File
@@ -1,92 +1,60 @@
import {Command} from "../base/command";
import {getRandomInt, getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
export class When extends Command {
command = ["when", "когда"];
argsMode = "required" as const;
title = "/when [value]";
description = "random date";
title = Environment.commandTitles.when;
description = Environment.commandDescriptions.when;
async execute(msg: Message) {
let text = "через ";
let text = Environment.getWhenPrefixText();
const type = getRandomInt(8);
switch (type) {
case 0:
text = "сейчас";
text = Environment.whenNowText;
break;
case 1:
text = "никогда";
text = Environment.whenNeverText;
break;
case 2: //seconds
{
const seconds = getRangedRandomInt(1, 60);
text += `${seconds} `;
text += (
(seconds == 1 || seconds % 10 == 1) ? "секунду" :
((seconds > 1 && seconds < 5) || (seconds % 10 > 1 && seconds % 10 < 5)) ? "секунды" : "секунд"
);
text = Environment.getWhenDurationText(seconds, Environment.whenSecondUnitText);
break;
}
case 3: {
const minutes = getRangedRandomInt(1, 60);
text += `${minutes} `;
text += (
(minutes == 1 || minutes % 10 == 1) ? "минуту" :
((minutes > 1 && minutes < 5) || (minutes % 10 > 1 && minutes % 10 < 5)) ? "минуты" : "минут"
);
text = Environment.getWhenDurationText(minutes, Environment.whenMinuteUnitText);
break;
}
case 4: {
const hours = getRangedRandomInt(1, 24);
text += `${hours} `;
text += (
(hours == 1 || hours % 10 == 1) ? "час" :
((hours > 1 && hours < 5) || (hours % 10 > 1 && hours % 10 < 5)) ? "часа" : "часов"
);
text = Environment.getWhenDurationText(hours, Environment.whenHourUnitText);
break;
}
case 5: {
const weeks = getRangedRandomInt(1, 4);
text += `${weeks} `;
text += (weeks == 1 ? "неделю" : "недель");
text = Environment.getWhenDurationText(weeks, Environment.whenWeekUnitText);
break;
}
case 6: {
const months = getRandomInt(12);
text += `${months} `;
text += (
(months == 1 || months % 10 == 1) ? "месяц" :
((months > 1 && months < 5) || (months % 10 > 1 && months % 10 < 5)) ? "месяца" : "месяцев"
);
text = Environment.getWhenDurationText(months, Environment.whenMonthUnitText);
break;
}
case 7: {
const years = getRangedRandomInt(1, 100);
text += `${years} `;
text += (
(years == 1 || years % 10 == 1) ? "год" :
((years > 1 && years < 5) || (years % 10 > 1 && years % 10 < 5)) ? "года" : "лет"
);
text = Environment.getWhenDurationText(years, Environment.whenYearUnitText);
break;
}
}
await oldReplyToMessage(msg, text).catch(logError);
}
}
}
-66
View File
@@ -1,66 +0,0 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {editMessageText, logError, replyToMessage} from "../util/utils";
import {bot, botUser} from "../index";
import {DownloadOptions, downloadVideoFromYouTube, getYouTubeVideoId} from "../util/ytdl";
import {Environment} from "../common/environment";
import {TryAgain} from "../callback_commands/try-again";
export class YouTubeDownload extends Command {
command = ["ytdl", "youtube"];
argsMode = "required" as const;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const url = match?.[3];
return this.downloadYouTubeVideo(msg, {url: url});
}
async downloadYouTubeVideo(msg: Message, options: DownloadOptions): Promise<void> {
// TODO: 02.03.2026, Danil Nikolaev: add check for date
let waitMessage: Message | null = (msg.from.id === botUser.id) ? msg : null;
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
try {
if (!waitMessage) {
waitMessage = await replyToMessage({message: msg, text: "⏳ Скачиваю видео..."});
} else {
await editMessageText({message: msg, text: "⏳ Скачиваю видео..."});
}
const {time, exists, buffer} = await downloadVideoFromYouTube({videoId: videoId});
if (buffer) {
const start = Date.now();
waitMessage = await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "video",
media: buffer
}
}) as Message;
const diff = Date.now() - start;
waitMessage = await bot.editMessageCaption({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
caption: "✅ Видео" + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
}) as Message;
}
} catch (e) {
logError(e);
if (waitMessage && "text" in waitMessage) {
await bot.editMessageText({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
text: Environment.errorText,
reply_markup: {
inline_keyboard: [[
TryAgain.withData("/ytdl " + videoId).asButton()
]]
}
});
}
}
}
}