feat(openai): add chat streaming and model commands

- add /openai (/chatgpt) chat command using OpenAI Responses API (streaming + incremental edits)
- add /openAIListModels, /openAIGetModel, /openAISetModel
- introduce base Command class and migrate non-chat commands to it
- wire OpenAI client + env vars (OPENAI_API_KEY, OPENAI_MODEL)
- bump deps (@google/genai, systeminformation, @types/node) and add openai
This commit is contained in:
2026-02-03 13:39:01 +03:00
parent 810151263d
commit a736f786c2
58 changed files with 512 additions and 211 deletions
+3 -57
View File
@@ -1,58 +1,4 @@
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "./requirements";
import {Command} from "./command";
export type ArgsMode = "none" | "optional" | "required";
export abstract class ChatCommand {
regexp?: RegExp | null;
command?: string | string[];
argsMode: ArgsMode = "none";
requirements?: Requirements = null;
title?: string;
description?: string;
get finalRegexp(): RegExp {
if (!this.regexp) {
const inferred = name(this.constructor.name);
const names = this.command ?? inferred;
this.regexp = createCommandRegExp(names, this.argsMode);
}
return this.regexp;
}
abstract execute(
msg: Message,
match?: RegExpExecArray
): Promise<void>;
}
export function name(s: string) {
return s
.replace(/Command$/, "")
.replace(/([a-z0-9])([A-Z])/g, "$1$2")
.toLowerCase();
}
function escapeRe(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function createCommandRegExp(
names: string | string[],
argsMode: ArgsMode = "optional",
) {
const list = Array.isArray(names) ? names : [names];
const group = list.map(escapeRe).join("|");
const base = `^\\/(${group})(?:@([\\w_]+))?`; // (1)=cmd, (2)=bot
const tail =
argsMode === "none"
? "\\s*$"
: argsMode === "required"
? "\\s+([\\s\\S]+)\\s*$" // (3)=args обязателен
: "(?:\\s+([\\s\\S]+))?\\s*$"; // (3)=args опционален
return new RegExp(base + tail, "i");
}
export abstract class ChatCommand extends Command {
}
+58
View File
@@ -0,0 +1,58 @@
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "./requirements";
export type ArgsMode = "none" | "optional" | "required";
export abstract class Command {
regexp?: RegExp | null;
command?: string | string[];
argsMode: ArgsMode = "none";
requirements?: Requirements = null;
title?: string;
description?: string;
get finalRegexp(): RegExp {
if (!this.regexp) {
const inferred = name(this.constructor.name);
const names = this.command ?? inferred;
this.regexp = createCommandRegExp(names, this.argsMode);
}
return this.regexp;
}
abstract execute(
msg: Message,
match?: RegExpExecArray
): Promise<void>;
}
export function name(s: string) {
return s
.replace(/Command$/, "")
.replace(/([a-z0-9])([A-Z])/g, "$1$2")
.toLowerCase();
}
function escapeRe(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function createCommandRegExp(
names: string | string[],
argsMode: ArgsMode = "optional",
) {
const list = Array.isArray(names) ? names : [names];
const group = list.map(escapeRe).join("|");
const base = `^\\/(${group})(?:@([\\w_]+))?`; // (1)=cmd, (2)=bot
const tail =
argsMode === "none"
? "\\s*$"
: argsMode === "required"
? "\\s+([\\s\\S]+)\\s*$" // (3)=args обязателен
: "(?:\\s+([\\s\\S]+))?\\s*$"; // (3)=args опционален
return new RegExp(base + tail, "i");
}
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
@@ -6,7 +6,7 @@ import {fullName, logError, oldSendMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {botUser} from "../index";
export class AdminsAdd extends ChatCommand {
export class AdminsAdd extends Command {
command = "addAdmin";
title = "/addAdmin";
description = "Add user to admins";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
@@ -6,7 +6,7 @@ import {fullName, logError, oldSendMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {botUser} from "../index";
export class AdminsRemove extends ChatCommand {
export class AdminsRemove extends Command {
command = "removeAdmin";
title = "/removeAdmin";
description = "Remove user from admins";
+2 -2
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {errorPlaceholder, logError, oldSendMessage} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
export class Ae extends ChatCommand {
export class Ae extends Command {
argsMode = "required" as const;
title = "/ae";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -6,7 +6,7 @@ import {bot, botUser} from "../index";
import {fullName, logError, oldSendMessage, oldReplyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Ban extends ChatCommand {
export class Ban extends Command {
title = "/ban [reply]";
description = "ban user from chat";
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
export class Choice extends ChatCommand {
export class Choice extends Command {
command = "choice";
argsMode = "required" as const;
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils";
export class Coin extends ChatCommand {
export class Coin extends Command {
title = "/coin";
description = "Heads or tails";
+2 -2
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {logError, replyToMessage} from "../util/utils";
export class Debug extends ChatCommand {
export class Debug extends Command {
title = "/debug";
description = "Returns msg (or reply) as json";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, randomValue} from "../util/utils";
import {bot} from "../index";
@@ -6,7 +6,7 @@ import {bot} from "../index";
type DiceEmoji = "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰";
const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"];
export class Dice extends ChatCommand {
export class Dice extends Command {
title = "/dice";
description = "Sends random or specific dice";
+6 -3
View File
@@ -1,9 +1,9 @@
import {ChatCommand} from "../base/chat-command";
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";
export class Distort extends ChatCommand {
export class Distort extends Command {
command = "distort";
argsMode = "optional" as const;
@@ -40,7 +40,10 @@ export class Distort extends ChatCommand {
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
const file = await bot.getFile({file_id: fileId});
if (!file.file_path) throw new Error("No file_path in Telegram getFile response");
if (!file.file_path) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("No file_path in Telegram getFile response");
}
const inputBuf = await downloadTelegramFile(file.file_path);
+1 -1
View File
@@ -1,4 +1,3 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {bot, googleAi} from "../index";
@@ -13,6 +12,7 @@ import {
oldReplyToMessage,
startIntervalEditor
} from "../util/utils";
import {ChatCommand} from "../base/chat-command";
export class GeminiChat extends ChatCommand {
command = "gemini";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -6,7 +6,7 @@ import {googleAi} from "../index";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class GeminiGenerateImage extends ChatCommand {
export class GeminiGenerateImage extends Command {
command = "geminiGenImage";
argsMode = "required" as const;
+2 -2
View File
@@ -1,9 +1,9 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class GeminiGetModel extends ChatCommand {
export class GeminiGetModel extends Command {
title = "/geminiGetModel";
description = "Get current Gemini model";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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 ChatCommand {
export class GeminiListModels extends Command {
title = "/geminiListModels";
description = "List all Gemini models";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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 ChatCommand {
export class GeminiSetModel extends Command {
argsMode = "required" as const;
title = "/geminiSetModel";
+4 -4
View File
@@ -1,10 +1,10 @@
import {Message} from "typescript-telegram-bot-api";
import {chatCommandToString, delay, logError, sendMessage} from "../util/utils";
import {ChatCommand} from "../base/chat-command";
import {chatCommands} from "../index";
import {Command} from "../base/command";
import {commands} from "../index";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
export class Help extends ChatCommand {
export class Help extends Command {
command = ["h", "help"];
title = "/help";
@@ -13,7 +13,7 @@ export class Help extends ChatCommand {
async execute(msg: Message) {
let text = "Commands:\n\n";
chatCommands.forEach(c => {
commands.forEach(c => {
text += `${chatCommandToString(c)}\n`;
});
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage} from "../util/utils";
export class Id extends ChatCommand {
export class Id extends Command {
title = "/id";
description = "ID of chat, user and reply (if replied to any message)";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -6,7 +6,7 @@ import {fullName, logError, oldSendMessage} from "../util/utils";
import {botUser} from "../index";
import {Environment} from "../common/environment";
export class Ignore extends ChatCommand {
export class Ignore extends Command {
title = "/ignore";
description = "Bot will ignore user";
+2 -2
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
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";
export class Leave extends ChatCommand {
export class Leave extends Command {
title = "/leave";
description = "Bot will leave current chat";
+1 -1
View File
@@ -1,4 +1,3 @@
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -12,6 +11,7 @@ import {
import {Environment} from "../common/environment";
import {bot, mistralAi} from "../index";
import {MessageStore} from "../common/message-store";
import {ChatCommand} from "../base/chat-command";
export class MistralChat extends ChatCommand {
command = "mistral";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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";
export class MistralGetModel extends ChatCommand {
export class MistralGetModel extends Command {
title = "/mistralGetModel";
description = "Get current Mistral model";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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";
export class MistralListModels extends ChatCommand {
export class MistralListModels extends Command {
title = "/mistralListModels";
description = "List all Mistral models";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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 MistralSetModel extends ChatCommand {
export class MistralSetModel extends Command {
argsMode = "required" as const;
title = "/mistralSetModel";
+5 -5
View File
@@ -1,6 +1,5 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, chatCommands, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {abortOllamaRequest, bot, commands, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {
collectReplyChainText,
escapeMarkdownV2Text,
@@ -14,9 +13,10 @@ 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";
export class OllamaChat extends ChatCommand {
command = ["ollama", "ollamathink"];
command = ["ollamaThink", "ollama"];
argsMode = "required" as const;
title = "/ollama";
@@ -57,7 +57,7 @@ export class OllamaChat extends ChatCommand {
if (!think && imagesCount) {
try {
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
if (modelInfo) {
const caps = modelInfo.capabilities || [];
if (!caps.includes("vision")) {
@@ -75,7 +75,7 @@ export class OllamaChat extends ChatCommand {
if (think) {
try {
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
const modelInfo = await commands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
if (modelInfo) {
const caps = modelInfo.capabilities || [];
if (!caps.includes("thinking")) {
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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 {ShowResponse} from "ollama";
export class OllamaGetModel extends ChatCommand {
export class OllamaGetModel extends Command {
title = "/ollamaGetModel";
description = "Ollama model info";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
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";
export class OllamaListModels extends ChatCommand {
export class OllamaListModels extends Command {
title = "/ollamaListModels";
description = "List all Ollama models";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
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";
@@ -9,7 +9,7 @@ import {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {MessageStore} from "../common/message-store";
export class OllamaPrompt extends ChatCommand {
export class OllamaPrompt extends Command {
command = "ollamaPrompt";
argsMode = "required" as const;
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -7,7 +7,7 @@ import {WebSearchResponse} from "../model/web-search-response";
import {editMessageText, logError} from "../util/utils";
import {Environment} from "../common/environment";
export class OllamaSearch extends ChatCommand {
export class OllamaSearch extends Command {
command = ["s", "search"];
argsMode = "required" as const;
+2 -2
View File
@@ -1,12 +1,12 @@
import {Message} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command";
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";
export class OllamaSetModel extends ChatCommand {
export class OllamaSetModel extends Command {
argsMode = "required" as const;
title = "/ollamaSetModel";
+160
View File
@@ -0,0 +1,160 @@
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 {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {ChatCommand} from "../base/chat-command";
export class OpenAIChat extends ChatCommand {
command = ["openai", "chatgpt"];
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
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: "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",
};
});
chatMessages.reverse();
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.waitText,
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: "Markdown"
}
).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);
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
} catch (error) {
logError(error);
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${error.toString()}`
}).catch(logError);
}
}
}
+13
View File
@@ -0,0 +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";
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);
}
}
+37
View File
@@ -0,0 +1,37 @@
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";
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);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
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 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);
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import {logError, sendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
export class Ping extends ChatCommand {
export class Ping extends Command {
title = "/ping";
description = "Ping between received and sent message";
+2 -2
View File
@@ -1,9 +1,9 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, randomValue, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class PrefixResponse extends ChatCommand {
export class PrefixResponse extends Command {
async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix)}).catch(logError);
}
+2 -2
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {extractMessagePayload, logError, replyToMessage} from "../util/utils";
import {bot, botUser} from "../index";
import QRCode from "qrcode";
export class Qr extends ChatCommand {
export class Qr extends Command {
argsMode = "optional" as const;
+2 -2
View File
@@ -4,7 +4,7 @@ import emojiRegex from "emoji-regex";
import {createCanvas, GlobalFonts, type Image as CanvasImage, loadImage, SKRSContext2D} from "@napi-rs/canvas";
import {Message, MessageEntity, PhotoSize} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {bot, botUser} from "../index";
import {
getChatAvatar,
@@ -37,7 +37,7 @@ try {
logError(e);
}
export class Quote extends ChatCommand {
export class Quote extends Command {
command = ["cit", "citation", "q", "quote"];
argsMode = "none" as const;
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {getRandomInt, getRangedRandomInt, logError, oldSendMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
export class RandomInt extends ChatCommand {
export class RandomInt extends Command {
argsMode = "optional" as const;
title = "/randomInt";
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {getRandomInt, logError, replyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
export class RandomString extends ChatCommand {
export class RandomString extends Command {
argsMode = "optional" as const;
title = "/randomString";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
@@ -15,7 +15,7 @@ const texts = [
const timings = [1500, 2500];
const timer = [3, 2, 1];
export class Shutdown extends ChatCommand {
export class Shutdown extends Command {
title = "/shutdown";
description = "Self-destruction sequence for bot (shutdown)";
+4 -4
View File
@@ -1,13 +1,13 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {chatCommands} from "../index";
import {commands} from "../index";
import {Help} from "./help";
export class Start extends ChatCommand {
export class Start extends Command {
title = "/start";
description = "Start the bot";
async execute(msg: Message): Promise<void> {
await chatCommands.find(e => e instanceof Help).execute(msg);
await commands.find(e => e instanceof Help).execute(msg);
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {logError, replyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
export class SystemInfo extends ChatCommand {
export class SystemInfo extends Command {
title = "/systemInfo";
description = "System information";
+2 -2
View File
@@ -1,9 +1,9 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
import {Environment} from "../common/environment";
export class Test extends ChatCommand {
export class Test extends Command {
regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i;
title = "тест";
description = "System functionality check";
+2 -2
View File
@@ -1,11 +1,11 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {logError, oldReplyToMessage} from "../util/utils";
import {bot} from "../index";
export class Title extends ChatCommand {
export class Title extends Command {
command = "title";
argsMode = "required" as const;
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
@@ -74,7 +74,7 @@ export function fixLayoutAuto(
return text;
}
export class Transliteration extends ChatCommand {
export class Transliteration extends Command {
command = ["transliteration", "tr"];
title = "/tr [text or reply]";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
@@ -6,7 +6,7 @@ import {bot, botUser} from "../index";
import {fullName, logError, oldReplyToMessage, oldSendMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class Unban extends ChatCommand {
export class Unban extends Command {
title = "/unban [reply]";
description = "unban user from chat";
+2 -2
View File
@@ -1,4 +1,4 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {fullName, logError, oldSendMessage} from "../util/utils";
@@ -6,7 +6,7 @@ import {Message} from "typescript-telegram-bot-api";
import {botUser} from "../index";
import {Environment} from "../common/environment";
export class Unignore extends ChatCommand {
export class Unignore extends Command {
title = "/unignore";
description = "Bot will start responding to the user";
requirements = Requirements.Build(
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {getUptime, logError, oldSendMessage} from "../util/utils";
export class Uptime extends ChatCommand {
export class Uptime extends Command {
title = "/uptime";
description = "Bot's uptime";
+2 -2
View File
@@ -1,9 +1,9 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {logError, oldSendMessage, randomValue} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
export class WhatBetter extends ChatCommand {
export class WhatBetter extends Command {
command = ["what", "что"];
argsMode = "required" as const;
+2 -2
View File
@@ -1,8 +1,8 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {getRandomInt, getRangedRandomInt, logError, oldReplyToMessage} from "../util/utils";
import {Message} from "typescript-telegram-bot-api";
export class When extends ChatCommand {
export class When extends Command {
command = ["when", "когда"];
argsMode = "required" as const;
+2 -2
View File
@@ -1,10 +1,10 @@
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {bot} from "../index";
import {downloadVideoFromYouTube} from "../util/ytdl";
export class YouTubeDownload extends ChatCommand {
export class YouTubeDownload extends Command {
command = ["ytdl", "youtube"];
argsMode = "required" as const;
+10
View File
@@ -39,6 +39,9 @@ export class Environment {
static MISTRAL_API_KEY?: string;
static MISTRAL_MODEL: string;
static OPENAI_API_KEY?: string;
static OPENAI_MODEL: string;
static waitText = "⏳ Дайте-ка подумать...";
static analyzingPictureText = "🔍 Внимательно изучаю изображение...";
static analyzingPicturesText = "🔍 Внимательно изучаю изображения...";
@@ -75,6 +78,9 @@ export class Environment {
Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-small-latest";
Environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
Environment.OPENAI_MODEL = process.env.OPENAI_MODEL || "gpt-4o-mini";
}
static setAdmins(admins: Set<number>) {
@@ -135,4 +141,8 @@ export class Environment {
static setMistralModel(newModel: string) {
Environment.MISTRAL_MODEL = newModel;
}
static setOpenAIModel(newModel: string) {
Environment.OPENAI_MODEL = newModel;
}
}
+23 -8
View File
@@ -1,7 +1,7 @@
import "dotenv/config";
import {Environment} from "./common/environment";
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
import {ChatCommand} from "./base/chat-command";
import {Command} from "./base/command";
import {
delay,
extractTextMessage,
@@ -73,6 +73,11 @@ import fs from "node:fs";
import path from "node:path";
import {setInterval} from "node:timers";
import {clearUpVideoFolder} from "./util/files";
import {OpenAI} from "openai";
import {OpenAIChat} from "./commands/openai-chat";
import {OpenAIListModels} from "./commands/openai-list-models";
import {OpenAIGetModel} from "./commands/openai-get-model";
import {OpenAISetModel} from "./commands/openai-set-model";
process.setUncaughtExceptionCaptureCallback(logError);
@@ -87,6 +92,7 @@ export let botUser: User;
export const googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY});
export const mistralAi = new Mistral({apiKey: Environment.MISTRAL_API_KEY});
export const openAi = new OpenAI({apiKey: Environment.OPENAI_API_KEY});
export const ollama = new Ollama({
host: Environment.OLLAMA_ADDRESS,
@@ -120,7 +126,7 @@ export function abortOllamaRequest(uuid: string): boolean {
}
}
export const chatCommands: ChatCommand[] = [
export const commands: Command[] = [
new Start(),
new Help(),
new Test(),
@@ -164,7 +170,7 @@ export const callbackCommands: CallbackCommand[] = [
];
if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) {
chatCommands.push(
commands.push(
new OllamaChat(),
new OllamaPrompt(),
new OllamaListModels(),
@@ -174,11 +180,11 @@ if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM
}
if (Environment.OLLAMA_API_KEY) {
chatCommands.push(new OllamaSearch());
commands.push(new OllamaSearch());
}
if (Environment.GEMINI_API_KEY) {
chatCommands.push(
commands.push(
new GeminiChat(),
new GeminiListModels(),
new GeminiGetModel(),
@@ -188,7 +194,7 @@ if (Environment.GEMINI_API_KEY) {
}
if (Environment.MISTRAL_API_KEY) {
chatCommands.push(
commands.push(
new MistralChat(),
new MistralListModels(),
new MistralGetModel(),
@@ -196,6 +202,15 @@ if (Environment.MISTRAL_API_KEY) {
);
}
if (Environment.OPENAI_API_KEY) {
commands.push(
new OpenAIChat(),
new OpenAIListModels(),
new OpenAIGetModel(),
new OpenAISetModel(),
);
}
export const photoDir = path.join(Environment.DATA_PATH, "photo");
export const videoDir = path.join(Environment.DATA_PATH, "video");
@@ -228,7 +243,7 @@ async function main() {
}, 1000 * 60 * 60 * 24);
});
const commands = chatCommands.filter(cmd => {
const cmds = commands.filter(cmd => {
return cmd.title && cmd.title.startsWith("/") && cmd.title.split(" ").length === 1 && cmd.description;
}).map(cmd => {
return {
@@ -242,7 +257,7 @@ async function main() {
[
initSystemSpecs(), readData(), retrieveAnswers(),
bot.getMe(),
bot.setMyCommands({commands: commands, scope: {type: "default"}})
bot.setMyCommands({commands: cmds, scope: {type: "default"}})
]
);
botUser = results[3];
+28 -24
View File
@@ -1,5 +1,5 @@
import * as si from "systeminformation";
import {ChatCommand} from "../base/chat-command";
import {Command} from "../base/command";
import {CallbackCommand} from "../base/callback-command";
import {
CallbackQuery,
@@ -12,7 +12,7 @@ import {
} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
import {bot, botUser, chatCommands, messageDao} from "../index";
import {bot, botUser, commands, messageDao} from "../index";
import os from "os";
import axios from "axios";
import {MessagePart} from "../common/message-part";
@@ -29,6 +29,7 @@ import {PrefixResponse} from "../commands/prefix-response";
import {OllamaChat} from "../commands/ollama-chat";
import {getYouTubeVideoId} from "./ytdl";
import {YouTubeDownload} from "../commands/youtube-download";
import {ChatCommand} from "../base/chat-command";
export const ignore = () => {
};
@@ -54,10 +55,10 @@ export const errorPlaceholder = async (msg: Message) => {
};
export function searchChatCommand(
commands: ChatCommand[],
commands: Command[],
text: string,
botUsername: string = botUser.username
): ChatCommand | null {
): Command | null {
for (const command of commands) {
const match = command.finalRegexp.exec(text);
if (!match) continue;
@@ -85,7 +86,7 @@ export function searchCallbackCommand(commands: CallbackCommand[], data: string)
return null;
}
export async function checkRequirements(cmd: ChatCommand | CallbackCommand | null, msg?: Message, cb?: CallbackQuery): Promise<boolean> {
export async function checkRequirements(cmd: Command | CallbackCommand | null, msg?: Message, cb?: CallbackQuery): Promise<boolean> {
if (!cmd) return false;
if (!msg && !cb) return false;
@@ -202,7 +203,7 @@ export async function checkRequirements(cmd: ChatCommand | CallbackCommand | nul
return true;
}
export async function executeChatCommand(cmd: ChatCommand | null, msg: Message, text: string): Promise<boolean> {
export async function executeChatCommand(cmd: Command | null, msg: Message, text: string): Promise<boolean> {
if (!cmd) return false;
if (!await checkRequirements(cmd, msg)) return false;
@@ -353,7 +354,7 @@ export function randomValue<T>(list: T[]): T {
return list[Math.floor(Math.random() * list.length)];
}
export function chatCommandToString(cmd: ChatCommand): string {
export function chatCommandToString(cmd: Command): string {
if (!cmd.title && !cmd.description) {
return "";
}
@@ -469,17 +470,22 @@ export function extractTextMessage(msg: Message | StoredMessage | string): strin
}
export function cutPrefixes(msg: Message | StoredMessage | string): string {
const prefixes = [
Environment.BOT_PREFIX,
`/ollamathink@${botUser.username}`,
"/ollamathink",
`/ollama@${botUser.username}`,
"/ollama",
`/gemini@${botUser.username}`,
"/gemini",
`/mistral@${botUser.username}`,
"/mistral",
];
const chatCommands = commands.filter(c => c instanceof ChatCommand);
const prefixes = [Environment.BOT_PREFIX];
const pushPrefix = (c: string) => {
prefixes.push(`/${c}@${botUser.username}`);
prefixes.push(`/${c}`);
};
chatCommands.forEach((cmd) => {
const command = cmd.command;
if (Array.isArray(command)) {
command.forEach(pushPrefix);
} else {
pushPrefix(command);
}
});
const text = extractTextMessage(msg);
let newText = text;
@@ -974,13 +980,11 @@ export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environmen
return photos[0];
}
const max = photos.reduce((prev, cur) => {
return photos.reduce((prev, cur) => {
if (!prev) return cur;
return cur.width * cur.height > prev.width * prev.height ? cur : prev;
}, null);
return max;
}
export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize | null> {
@@ -1084,7 +1088,7 @@ export async function processNewMessage(msg: Message) {
const then = Date.now();
const cmd = searchChatCommand(chatCommands, cmdText);
const cmd = searchChatCommand(commands, cmdText);
const executed = await executeChatCommand(cmd, msg, cmdText);
const now = Date.now();
@@ -1114,7 +1118,7 @@ export async function processNewMessage(msg: Message) {
try {
getYouTubeVideoId(url);
const yt = chatCommands.find(e => e instanceof YouTubeDownload);
const yt = commands.find(e => e instanceof YouTubeDownload);
if (await checkRequirements(yt, msg)) {
await yt.downloadYouTubeVideo(msg, url);
}
@@ -1129,7 +1133,7 @@ export async function processNewMessage(msg: Message) {
if (!startsWithPrefix && msg.chat.type !== "private") return;
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
const chat = chatCommands.find(e => e instanceof OllamaChat);
const chat = commands.find(e => e instanceof OllamaChat);
if (await checkRequirements(chat, msg)) {
await chat.executeOllama(msg, textToCheck);
}