shitton of the ai changes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
]]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user