feat(ai): add configurable default provider + refactor telegram handlers

- add AiProvider enum and DEFAULT_AI_PROVIDER env var (fallback: OLLAMA)
- route chat execution based on selected provider (Ollama/Gemini/Mistral/OpenAI)
- move inline query / callback / edited message / my_chat_member handlers into utils
- minor cleanup (command requirements placement, whitespace)
This commit is contained in:
2026-02-04 12:55:04 +03:00
parent 3c7d56b213
commit 9ba3d81a21
6 changed files with 142 additions and 102 deletions
+2 -2
View File
@@ -18,11 +18,11 @@ export class GeminiChat extends ChatCommand {
command = "gemini";
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/gemini";
description = "Chat with AI (Gemini)";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeGemini(msg, match?.[3]);
+2 -2
View File
@@ -17,11 +17,11 @@ export class MistralChat extends ChatCommand {
command = "mistral";
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/mistral";
description = "Chat with AI (Mistral)";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeMistral(msg, match?.[3]);
-2
View File
@@ -133,8 +133,6 @@ export class OpenAIChat extends ChatCommand {
break;
}
}
}
} finally {
await editor.tick();
+16
View File
@@ -3,6 +3,13 @@ import {saveData} from "../db/database";
import {Answers} from "../model/answers";
import {ifTrue} from "../util/utils";
export enum AiProvider {
OLLAMA = "OLLAMA",
GEMINI = "GEMINI",
MISTRAL = "MISTRAL",
OPENAI = "OPENAI",
}
export class Environment {
static BOT_TOKEN: string;
static TEST_ENVIRONMENT: boolean;
@@ -24,6 +31,8 @@ export class Environment {
static MAX_PHOTO_SIZE: number;
static DEFAULT_AI_PROVIDER: AiProvider;
static SYSTEM_PROMPT?: string;
static OLLAMA_ADDRESS?: string;
@@ -65,6 +74,13 @@ export class Environment {
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA";
if (Object.values(AiProvider).includes(aiProvider as AiProvider)) {
Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider;
} else {
Environment.DEFAULT_AI_PROVIDER = AiProvider.OLLAMA;
}
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
+11 -74
View File
@@ -1,14 +1,16 @@
import "dotenv/config";
import {Environment} from "./common/environment";
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
import {TelegramBot, User} from "typescript-telegram-bot-api";
import {Command} from "./base/command";
import {
delay,
extractTextMessage,
findAndExecuteCallbackCommand,
ignore,
initSystemSpecs,
logError,
processCallbackQuery,
processEditedMessage,
processInlineQuery,
processMyChatMember,
processNewMessage
} from "./util/utils";
import {Ae} from "./commands/ae";
@@ -27,7 +29,6 @@ import {RandomInt} from "./commands/random-int";
import {Ban} from "./commands/ban";
import {Quote} from "./commands/quote";
import {Ollama} from "ollama";
import {WebSearchResponse} from "./model/web-search-response";
import {OllamaSearch} from "./commands/ollama-search";
import {Id} from "./commands/id";
import {OllamaPrompt} from "./commands/ollama-prompt";
@@ -37,7 +38,6 @@ import {Shutdown} from "./commands/shutdown";
import {Leave} from "./commands/leave";
import {OllamaChat} from "./commands/ollama-chat";
import {Start} from "./commands/start";
import {MessageStore} from "./common/message-store";
import {GeminiChat} from "./commands/gemini-chat";
import {Choice} from "./commands/choice";
import {Coin} from "./commands/coin";
@@ -221,7 +221,8 @@ async function main() {
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
`DATA_PATH: ${Environment.DATA_PATH}\n` +
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` +
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}`
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}\n` +
`DEFAULT_AI_PROVIDER: ${Environment.DEFAULT_AI_PROVIDER}`
);
fs.mkdir(photoDir, ignore);
@@ -272,74 +273,10 @@ async function main() {
}
}
bot.on("my_chat_member", async (u) => {
console.log("my_chat_member", u);
});
bot.on("edited_message", async (msg) => {
console.log("edited_message", msg);
await UserStore.put(msg.from);
if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
await MessageStore.put(msg);
});
bot.on("my_chat_member", processMyChatMember);
bot.on("edited_message", processEditedMessage);
bot.on("message", processNewMessage);
bot.on("inline_query", async (query) => {
console.log("query", query);
if (Environment.CREATOR_ID !== query.from.id) {
await bot.answerInlineQuery({
inline_query_id: query.id,
results: [],
button: {
text: "No access",
start_parameter: "nope"
}
}).catch(logError);
return;
}
if (query.query.trim().length !== 0) {
try {
const queryResults: InlineQueryResult[] = [];
const results = await ollama.webSearch({query: query.query});
console.log("results", results);
results.results.forEach((result, i) => {
const r = result as WebSearchResponse;
queryResults.push({
type: "article",
id: `${i}`,
title: `${r.title}`,
input_message_content: {
message_text: `${r.title}\n\n${r.url}`
}
});
});
await bot.answerInlineQuery({
inline_query_id: query.id,
results: queryResults,
});
} catch (e) {
logError(e);
}
} else {
await bot.answerInlineQuery({
inline_query_id: query.id,
results: [],
}).catch(logError);
}
});
bot.on("callback_query", async (query) => {
console.log(query);
await findAndExecuteCallbackCommand(callbackCommands, query);
});
bot.on("inline_query", processInlineQuery);
bot.on("callback_query", processCallbackQuery);
main().catch(logError);
+111 -22
View File
@@ -4,15 +4,18 @@ import {CallbackCommand} from "../base/callback-command";
import {
CallbackQuery,
ChatMember,
ChatMemberUpdated,
InlineKeyboardMarkup,
InlineQuery,
InlineQueryResult,
Message,
ParseMode,
PhotoSize,
User
} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {AiProvider, Environment} from "../common/environment";
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
import {bot, botUser, commands, messageDao} from "../index";
import {bot, botUser, callbackCommands, commands, messageDao, ollama} from "../index";
import os from "os";
import axios from "axios";
import {MessagePart} from "../common/message-part";
@@ -30,6 +33,10 @@ import {OllamaChat} from "../commands/ollama-chat";
import {getYouTubeVideoId} from "./ytdl";
import {YouTubeDownload} from "../commands/youtube-download";
import {ChatCommand} from "../base/chat-command";
import {WebSearchResponse} from "../model/web-search-response";
import {GeminiChat} from "../commands/gemini-chat";
import {MistralChat} from "../commands/mistral-chat";
import {OpenAIChat} from "../commands/openai-chat";
export const ignore = () => {
};
@@ -1028,8 +1035,34 @@ export function boolToEmoji(bool: boolean): string {
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
export async function processNewMessage(msg: Message) {
console.log("message", msg);
async function processAlbum(groupId: string): Promise<string[]> {
const entry = albumCache.get(groupId);
if (!entry) return;
const allPhotos = entry.messages
.filter(m => m.photo)
.map(m => m.photo);
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
console.log(`Received album ${groupId} with ${ids.length} photos.`);
console.log("File IDs:", ids);
albumCache.delete(groupId);
return ids;
}
export function photoPathByUniqueId(uniqueId: string): string {
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
}
export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
console.log("my_chat_member", u);
}
export async function processNewMessage(msg: Message): Promise<void> {
console.log("New Message", msg);
let storedMsg: StoredMessage | null = null;
@@ -1133,30 +1166,86 @@ 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 = commands.find(e => e instanceof OllamaChat);
if (await checkRequirements(chat, msg)) {
await chat.executeOllama(msg, textToCheck);
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA: {
await commands.find(e => e instanceof OllamaChat).executeOllama(msg, textToCheck);
break;
}
case AiProvider.GEMINI: {
await commands.find(e => e instanceof GeminiChat).executeGemini(msg, textToCheck);
break;
}
case AiProvider.MISTRAL: {
await commands.find(e => e instanceof MistralChat).executeMistral(msg, textToCheck);
break;
}
case AiProvider.OPENAI: {
await commands.find(e => e instanceof OpenAIChat).executeOpenAI(msg, textToCheck);
break;
}
}
}
async function processAlbum(groupId: string): Promise<string[]> {
const entry = albumCache.get(groupId);
if (!entry) return;
export async function processEditedMessage(msg: Message): Promise<void> {
console.log("Edited Message", msg);
const allPhotos = entry.messages
.filter(m => m.photo)
.map(m => m.photo);
await UserStore.put(msg.from);
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
console.log(`Received album ${groupId} with ${ids.length} photos.`);
console.log("File IDs:", ids);
albumCache.delete(groupId);
return ids;
await MessageStore.put(msg);
}
export function photoPathByUniqueId(uniqueId: string): string {
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
export async function processInlineQuery(query: InlineQuery): Promise<void> {
console.log("InlineQuery", query);
if (Environment.CREATOR_ID !== query.from.id) {
await bot.answerInlineQuery({
inline_query_id: query.id,
results: [],
button: {
text: "No access",
start_parameter: "nope"
}
}).catch(logError);
return;
}
if (query.query.trim().length !== 0) {
try {
const queryResults: InlineQueryResult[] = [];
const results = await ollama.webSearch({query: query.query});
console.log("results", results);
results.results.forEach((result, i) => {
const r = result as WebSearchResponse;
queryResults.push({
type: "article",
id: `${i}`,
title: `${r.title}`,
input_message_content: {
message_text: `${r.title}\n\n${r.url}`
}
});
});
await bot.answerInlineQuery({
inline_query_id: query.id,
results: queryResults,
});
} catch (e) {
logError(e);
}
} else {
await bot.answerInlineQuery({
inline_query_id: query.id,
results: [],
}).catch(logError);
}
}
export async function processCallbackQuery(query: CallbackQuery): Promise<void> {
console.log("CallbackQuery", query);
await findAndExecuteCallbackCommand(callbackCommands, query);
}