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"; command = "gemini";
argsMode = "required" as const; argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/gemini"; title = "/gemini";
description = "Chat with AI (Gemini)"; description = "Chat with AI (Gemini)";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); console.log("match", match);
return this.executeGemini(msg, match?.[3]); return this.executeGemini(msg, match?.[3]);
+2 -2
View File
@@ -17,11 +17,11 @@ export class MistralChat extends ChatCommand {
command = "mistral"; command = "mistral";
argsMode = "required" as const; argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/mistral"; title = "/mistral";
description = "Chat with AI (Mistral)"; description = "Chat with AI (Mistral)";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); console.log("match", match);
return this.executeMistral(msg, match?.[3]); return this.executeMistral(msg, match?.[3]);
-2
View File
@@ -133,8 +133,6 @@ export class OpenAIChat extends ChatCommand {
break; break;
} }
} }
} }
} finally { } finally {
await editor.tick(); await editor.tick();
+16
View File
@@ -3,6 +3,13 @@ import {saveData} from "../db/database";
import {Answers} from "../model/answers"; import {Answers} from "../model/answers";
import {ifTrue} from "../util/utils"; import {ifTrue} from "../util/utils";
export enum AiProvider {
OLLAMA = "OLLAMA",
GEMINI = "GEMINI",
MISTRAL = "MISTRAL",
OPENAI = "OPENAI",
}
export class Environment { export class Environment {
static BOT_TOKEN: string; static BOT_TOKEN: string;
static TEST_ENVIRONMENT: boolean; static TEST_ENVIRONMENT: boolean;
@@ -24,6 +31,8 @@ export class Environment {
static MAX_PHOTO_SIZE: number; static MAX_PHOTO_SIZE: number;
static DEFAULT_AI_PROVIDER: AiProvider;
static SYSTEM_PROMPT?: string; static SYSTEM_PROMPT?: string;
static OLLAMA_ADDRESS?: string; static OLLAMA_ADDRESS?: string;
@@ -65,6 +74,13 @@ export class Environment {
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280"); 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.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS; Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
+11 -74
View File
@@ -1,14 +1,16 @@
import "dotenv/config"; import "dotenv/config";
import {Environment} from "./common/environment"; 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 {Command} from "./base/command";
import { import {
delay, delay,
extractTextMessage,
findAndExecuteCallbackCommand,
ignore, ignore,
initSystemSpecs, initSystemSpecs,
logError, logError,
processCallbackQuery,
processEditedMessage,
processInlineQuery,
processMyChatMember,
processNewMessage processNewMessage
} from "./util/utils"; } from "./util/utils";
import {Ae} from "./commands/ae"; import {Ae} from "./commands/ae";
@@ -27,7 +29,6 @@ import {RandomInt} from "./commands/random-int";
import {Ban} from "./commands/ban"; import {Ban} from "./commands/ban";
import {Quote} from "./commands/quote"; import {Quote} from "./commands/quote";
import {Ollama} from "ollama"; import {Ollama} from "ollama";
import {WebSearchResponse} from "./model/web-search-response";
import {OllamaSearch} from "./commands/ollama-search"; import {OllamaSearch} from "./commands/ollama-search";
import {Id} from "./commands/id"; import {Id} from "./commands/id";
import {OllamaPrompt} from "./commands/ollama-prompt"; import {OllamaPrompt} from "./commands/ollama-prompt";
@@ -37,7 +38,6 @@ import {Shutdown} from "./commands/shutdown";
import {Leave} from "./commands/leave"; import {Leave} from "./commands/leave";
import {OllamaChat} from "./commands/ollama-chat"; import {OllamaChat} from "./commands/ollama-chat";
import {Start} from "./commands/start"; import {Start} from "./commands/start";
import {MessageStore} from "./common/message-store";
import {GeminiChat} from "./commands/gemini-chat"; import {GeminiChat} from "./commands/gemini-chat";
import {Choice} from "./commands/choice"; import {Choice} from "./commands/choice";
import {Coin} from "./commands/coin"; import {Coin} from "./commands/coin";
@@ -221,7 +221,8 @@ async function main() {
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
`DATA_PATH: ${Environment.DATA_PATH}\n` + `DATA_PATH: ${Environment.DATA_PATH}\n` +
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\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); fs.mkdir(photoDir, ignore);
@@ -272,74 +273,10 @@ async function main() {
} }
} }
bot.on("my_chat_member", async (u) => { bot.on("my_chat_member", processMyChatMember);
console.log("my_chat_member", u); bot.on("edited_message", processEditedMessage);
});
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("message", processNewMessage); bot.on("message", processNewMessage);
bot.on("inline_query", processInlineQuery);
bot.on("inline_query", async (query) => { bot.on("callback_query", processCallbackQuery);
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);
});
main().catch(logError); main().catch(logError);
+111 -22
View File
@@ -4,15 +4,18 @@ import {CallbackCommand} from "../base/callback-command";
import { import {
CallbackQuery, CallbackQuery,
ChatMember, ChatMember,
ChatMemberUpdated,
InlineKeyboardMarkup, InlineKeyboardMarkup,
InlineQuery,
InlineQueryResult,
Message, Message,
ParseMode, ParseMode,
PhotoSize, PhotoSize,
User User
} from "typescript-telegram-bot-api"; } 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 {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 os from "os";
import axios from "axios"; import axios from "axios";
import {MessagePart} from "../common/message-part"; import {MessagePart} from "../common/message-part";
@@ -30,6 +33,10 @@ import {OllamaChat} from "../commands/ollama-chat";
import {getYouTubeVideoId} from "./ytdl"; import {getYouTubeVideoId} from "./ytdl";
import {YouTubeDownload} from "../commands/youtube-download"; import {YouTubeDownload} from "../commands/youtube-download";
import {ChatCommand} from "../base/chat-command"; 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 = () => { 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 const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
export async function processNewMessage(msg: Message) { async function processAlbum(groupId: string): Promise<string[]> {
console.log("message", msg); 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; let storedMsg: StoredMessage | null = null;
@@ -1133,30 +1166,86 @@ export async function processNewMessage(msg: Message) {
if (!startsWithPrefix && msg.chat.type !== "private") return; if (!startsWithPrefix && msg.chat.type !== "private") return;
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return; if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
const chat = commands.find(e => e instanceof OllamaChat); switch (Environment.DEFAULT_AI_PROVIDER) {
if (await checkRequirements(chat, msg)) { case AiProvider.OLLAMA: {
await chat.executeOllama(msg, textToCheck); 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[]> { export async function processEditedMessage(msg: Message): Promise<void> {
const entry = albumCache.get(groupId); console.log("Edited Message", msg);
if (!entry) return;
const allPhotos = entry.messages await UserStore.put(msg.from);
.filter(m => m.photo)
.map(m => m.photo);
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo))); if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
console.log(`Received album ${groupId} with ${ids.length} photos.`); await MessageStore.put(msg);
console.log("File IDs:", ids);
albumCache.delete(groupId);
return ids;
} }
export function photoPathByUniqueId(uniqueId: string): string { export async function processInlineQuery(query: InlineQuery): Promise<void> {
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg"); 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);
} }