diff --git a/src/commands/gemini-chat.ts b/src/commands/gemini-chat.ts index b227484..778adf9 100644 --- a/src/commands/gemini-chat.ts +++ b/src/commands/gemini-chat.ts @@ -34,7 +34,8 @@ export class GeminiChat extends ChatCommand { const chatId = msg.chat.id; - const messageParts = await collectReplyChainText(msg); + const storedMsg = await MessageStore.get(chatId, msg.message_id); + const messageParts = await collectReplyChainText(storedMsg); console.log("MESSAGE PARTS", messageParts); const chatMessages = messageParts.map(part => { @@ -64,7 +65,7 @@ export class GeminiChat extends ChatCommand { if (messageParts[0].images?.length) { const images = messageParts[0].images; - images.forEach(image=>{ + images.forEach(image => { const base64Image = Buffer.from(fs.readFileSync(image)).toString("base64"); input.push({ type: "image", @@ -144,6 +145,10 @@ export class GeminiChat extends ChatCommand { } break; } + case "image": { + const image = event.delta.data; + console.log("image", image); + } } } } diff --git a/src/commands/gemini-generate-image.ts b/src/commands/gemini-generate-image.ts new file mode 100644 index 0000000..3cb1b1b --- /dev/null +++ b/src/commands/gemini-generate-image.ts @@ -0,0 +1,60 @@ +import {ChatCommand} from "../base/chat-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 ChatCommand { + 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 { + console.log("match", match); + + const prompt = match?.[3]; + return this.executeGenImage(msg, prompt); + } + + async executeGenImage(msg: Message, text: string): Promise { + 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()}`, + disableLinkPreview: true + }).catch(logError); + } + } +} \ No newline at end of file diff --git a/src/commands/ignore.ts b/src/commands/ignore.ts index 7eee354..cb6d7ae 100644 --- a/src/commands/ignore.ts +++ b/src/commands/ignore.ts @@ -1,4 +1,3 @@ -import {addMute} from "../db/database"; import {ChatCommand} from "../base/chat-command"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; @@ -35,7 +34,7 @@ export class Ignore extends ChatCommand { return; } - if (await addMute(id)) { + if (await Environment.addMute(id)) { await oldSendMessage(msg, text + " в муте! 🔇").catch(logError); } else { await oldSendMessage(msg, text + " уже в муте 🤔").catch(logError); diff --git a/src/commands/mistral-chat.ts b/src/commands/mistral-chat.ts index 9f9e385..5d566e4 100644 --- a/src/commands/mistral-chat.ts +++ b/src/commands/mistral-chat.ts @@ -7,6 +7,7 @@ import { escapeMarkdownV2Text, logError, oldReplyToMessage, + photoPathByUniqueId, startIntervalEditor } from "../util/utils"; import {Environment} from "../common/environment"; @@ -33,7 +34,8 @@ export class MistralChat extends ChatCommand { const chatId = msg.chat.id; - const messageParts = await collectReplyChainText(msg); + const storedMsg = await MessageStore.get(chatId, msg.message_id); + const messageParts = await collectReplyChainText(storedMsg); console.log("MESSAGE PARTS", messageParts); const chatMessages = messageParts.map(part => { @@ -43,8 +45,8 @@ export class MistralChat extends ChatCommand { text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content, }); - if (part.images && part.images.length > 0) { - const base64Image = Buffer.from(fs.readFileSync(part.images[0])).toString("base64"); + for (const image of part.images) { + const base64Image = Buffer.from(fs.readFileSync(photoPathByUniqueId(image))).toString("base64"); content.push({ type: "image_url", imageUrl: "data:image/jpeg;base64," + base64Image diff --git a/src/commands/ollama-chat.ts b/src/commands/ollama-chat.ts index 863d183..bd9d3b5 100644 --- a/src/commands/ollama-chat.ts +++ b/src/commands/ollama-chat.ts @@ -1,17 +1,19 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {abortOllamaRequest, bot, getOllamaRequest, ollama, ollamaRequests} from "../index"; +import {abortOllamaRequest, bot, chatCommands, 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"; export class OllamaChat extends ChatCommand { command = "ollama"; @@ -30,7 +32,8 @@ export class OllamaChat extends ChatCommand { const chatId = msg.chat.id; - const messageParts = await collectReplyChainText(msg); + const storedMsg = await MessageStore.get(chatId, msg.message_id); + const messageParts = await collectReplyChainText(storedMsg); console.log("MESSAGE PARTS", messageParts); const chatMessages = messageParts.map(part => { @@ -52,19 +55,32 @@ export class OllamaChat extends ChatCommand { return total + (curr.images?.length ?? 0); }, 0); + if (imagesCount) { + try { + const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadModelInfo(); + if (modelInfo) { + const caps = modelInfo.capabilities || []; + if (!caps.includes("vision")) { + 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 bot.sendMessage({ - chat_id: chatId, + waitMessage = await replyToMessage({ + message: msg, text: imagesCount ? imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText - : Environment.waitText, - - reply_parameters: { - chat_id: chatId, - message_id: msg.message_id - } + : Environment.waitText }); const stream = await ollama.chat({ diff --git a/src/commands/ollama-get-model.ts b/src/commands/ollama-get-model.ts index 24f87cf..095650d 100644 --- a/src/commands/ollama-get-model.ts +++ b/src/commands/ollama-get-model.ts @@ -3,6 +3,7 @@ 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 { title = "/ollamaGetModel"; @@ -10,7 +11,7 @@ export class OllamaGetModel extends ChatCommand { async execute(msg: Message): Promise { try { - const showResponse = await ollama.show({model: Environment.OLLAMA_MODEL}); + const showResponse = await this.loadModelInfo(); const caps = showResponse.capabilities; @@ -27,4 +28,8 @@ export class OllamaGetModel extends ChatCommand { await replyToMessage({message: msg, text: e.toString()}).catch(logError); } } + + async loadModelInfo(): Promise { + return ollama.show({model: Environment.OLLAMA_MODEL}); + } } \ No newline at end of file diff --git a/src/commands/prefix-response.ts b/src/commands/prefix-response.ts index 69ff4e1..3c4a7bc 100644 --- a/src/commands/prefix-response.ts +++ b/src/commands/prefix-response.ts @@ -1,10 +1,10 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, randomValue, oldReplyToMessage} from "../util/utils"; -import {prefixAnswers} from "../db/database"; +import {logError, randomValue, replyToMessage} from "../util/utils"; +import {Environment} from "../common/environment"; export class PrefixResponse extends ChatCommand { async execute(msg: Message): Promise { - await oldReplyToMessage(msg, randomValue(prefixAnswers)).catch(logError); + await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix)}).catch(logError); } } \ No newline at end of file diff --git a/src/commands/test.ts b/src/commands/test.ts index 4ddce59..d774dcd 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,7 +1,7 @@ import {ChatCommand} from "../base/chat-command"; import {Message} from "typescript-telegram-bot-api"; -import {logError, randomValue, oldReplyToMessage} from "../util/utils"; -import {testAnswers} from "../db/database"; +import {logError, oldReplyToMessage, randomValue} from "../util/utils"; +import {Environment} from "../common/environment"; export class Test extends ChatCommand { regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i; @@ -9,6 +9,6 @@ export class Test extends ChatCommand { description = "System functionality check"; async execute(msg: Message) { - await oldReplyToMessage(msg, randomValue(testAnswers) || "а").catch(logError); + await oldReplyToMessage(msg, randomValue(Environment.ANSWERS.test) || "а").catch(logError); } } \ No newline at end of file diff --git a/src/commands/unignore.ts b/src/commands/unignore.ts index caeb7e6..6b7bfd9 100644 --- a/src/commands/unignore.ts +++ b/src/commands/unignore.ts @@ -1,4 +1,3 @@ -import {removeMute} from "../db/database"; import {ChatCommand} from "../base/chat-command"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; @@ -34,7 +33,7 @@ export class Unignore extends ChatCommand { return; } - if (await removeMute(id)) { + if (await Environment.removeMute(id)) { await oldSendMessage(msg, text + " больше не в муте! 🔈").catch(logError); } else { await oldSendMessage(msg, text + " не был в муте 🤔").catch(logError); diff --git a/src/commands/what-better.ts b/src/commands/what-better.ts index 17a05d1..974d6ae 100644 --- a/src/commands/what-better.ts +++ b/src/commands/what-better.ts @@ -1,7 +1,7 @@ import {ChatCommand} from "../base/chat-command"; import {logError, oldSendMessage, randomValue} from "../util/utils"; -import {betterAnswers} from "../db/database"; import {Message} from "typescript-telegram-bot-api"; +import {Environment} from "../common/environment"; export class WhatBetter extends ChatCommand { command = ["what", "что"]; @@ -19,7 +19,7 @@ export class WhatBetter extends ChatCommand { const a = m[2].trim(); const b = m[4].trim(); - const text = `${randomValue(betterAnswers)} ${randomValue([a, b])}`; + const text = `${randomValue(Environment.ANSWERS.better)} ${randomValue([a, b])}`; await oldSendMessage(msg, text).catch(logError); } diff --git a/src/common/environment.ts b/src/common/environment.ts index c7fee12..538fe75 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -1,10 +1,12 @@ import path from "node:path"; import {saveData} from "../db/database"; +import {Answers} from "../model/answers"; export class Environment { static BOT_TOKEN: string; static TEST_ENVIRONMENT: boolean; static ADMIN_IDS: Set = new Set(); + static MUTED_IDS: Set = new Set(); static CHAT_IDS_WHITELIST: Set = new Set(); static BOT_PREFIX: string; static CREATOR_ID: number; @@ -15,9 +17,7 @@ export class Environment { static ONLY_FOR_CREATOR_MODE: boolean; - static USE_MOM: boolean; - static USE_DAD: boolean; - static USE_FU: boolean; + static ANSWERS: Answers; static USE_NAMES_IN_PROMPT: boolean; @@ -31,6 +31,7 @@ export class Environment { static GEMINI_API_KEY?: string; static GEMINI_MODEL: string; + static GEMINI_IMAGE_MODEL: string; static MISTRAL_API_KEY?: string; static MISTRAL_MODEL: string; @@ -38,6 +39,7 @@ export class Environment { static waitText = "⏳ Дайте-ка подумать..."; static analyzingPictureText = "🔍 Внимательно изучаю изображение..."; static analyzingPicturesText = "🔍 Внимательно изучаю изображения..."; + static genImageText = "👨‍🎨 Генерирую изображение..."; static ollamaCancelledText = "```Ollama\n❌ Отменено```"; static load() { @@ -52,10 +54,6 @@ export class Environment { Environment.ONLY_FOR_CREATOR_MODE = process.env.ONLY_FOR_CREATOR_MODE == "true"; - Environment.USE_MOM = process.env.USE_MOM == "true"; - Environment.USE_DAD = process.env.USE_DAD == "true"; - Environment.USE_FU = process.env.USE_FU == "true"; - Environment.USE_NAMES_IN_PROMPT = process.env.USE_NAMES_IN_PROMPT == "true"; Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280"); @@ -68,6 +66,7 @@ export class Environment { Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY; Environment.GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-2.5-flash-lite"; + Environment.GEMINI_IMAGE_MODEL = process.env.GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image"; Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY; Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-small-latest"; @@ -97,6 +96,29 @@ export class Environment { return has; } + static setMuted(muted: Set) { + this.MUTED_IDS = muted; + } + + static async addMute(id: number): Promise { + if (this.MUTED_IDS.has(id)) return Promise.resolve(false); + + this.MUTED_IDS.add(id); + await saveData(); + return Promise.resolve(true); + } + + static async removeMute(id: number): Promise { + if (!this.MUTED_IDS.has(id)) return Promise.resolve(false); + this.MUTED_IDS.delete(id); + await saveData(); + return Promise.resolve(true); + } + + static setAnswers(answers: Answers) { + this.ANSWERS = answers; + } + static setOllamaModel(newModel: string) { Environment.OLLAMA_MODEL = newModel; } diff --git a/src/common/message-store.ts b/src/common/message-store.ts index 7540052..cfed829 100644 --- a/src/common/message-store.ts +++ b/src/common/message-store.ts @@ -14,7 +14,7 @@ export class MessageStore { return this.map; } - static async put(m: Message | StoredMessage) { + static async put(m: Message | StoredMessage): Promise { const msg: StoredMessage = isStoredMessage(m) ? m : { chatId: m.chat.id, id: m.message_id, @@ -26,6 +26,7 @@ export class MessageStore { this.map.set(this.key(msg.chatId, msg.id), msg); await messageDao.insert(messageDao.mapStoredTo([msg])); + return msg; } static async get(chatId: number, messageId: number): Promise { diff --git a/src/db/database.ts b/src/db/database.ts index f19d02b..f913b18 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -1,9 +1,7 @@ import * as fs from "fs"; import {Environment} from "../common/environment"; import {logError} from "../util/utils"; - - -export let muted: Set = new Set(); +import {Answers} from "../model/answers"; type DataJsonFile = { admins: number[] @@ -12,39 +10,6 @@ type DataJsonFile = { export let jsonFile: DataJsonFile; -type AnswersJsonFile = { - test: string[] - prefix: string[] - better: string[] - who: string[] - kick: string[] - invite: string[] - day: number[] -} - -export const testAnswers: string[] = []; -export const prefixAnswers: string[] = []; -export const betterAnswers: string[] = []; -export const whoAnswers: string[] = []; -export const kickAnswers: string[] = []; -export const inviteAnswers: string[] = []; -export const dayAnswers: number[] = []; - -export async function addMute(id: number): Promise { - if (muted.has(id)) return Promise.resolve(false); - - muted.add(id); - await saveData(); - return Promise.resolve(true); -} - -export async function removeMute(id: number): Promise { - if (!muted.has(id)) return Promise.resolve(false); - muted.delete(id); - await saveData(); - return Promise.resolve(true); -} - export async function readData(): Promise { try { jsonFile = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/data.json`).toString()); @@ -53,8 +18,7 @@ export async function readData(): Promise { admins.unshift(Environment.CREATOR_ID); Environment.setAdmins(new Set(admins)); - - muted = new Set(jsonFile.muted || []); + Environment.setMuted(new Set(jsonFile.muted || [])); return Promise.resolve(); } catch (e) { @@ -69,7 +33,7 @@ export async function saveData(): Promise { jsonFile.admins = adminIds; const mutedList: number[] = []; - muted.forEach(id => mutedList.push(id)); + Environment.MUTED_IDS.forEach(id => mutedList.push(id)); jsonFile.muted = mutedList; try { @@ -82,14 +46,8 @@ export async function saveData(): Promise { export async function retrieveAnswers(): Promise { try { - const json: AnswersJsonFile = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/answers.json`).toString()); - json.test.forEach(e => testAnswers.push(e)); - json.prefix.forEach(e => prefixAnswers.push(e)); - json.better.forEach(e => betterAnswers.push(e)); - json.who.forEach(e => whoAnswers.push(e)); - json.kick.forEach(e => kickAnswers.push(e)); - json.invite.forEach(e => inviteAnswers.push(e)); - json.day.forEach(e => dayAnswers.push(e)); + const json: Answers = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/answers.json`).toString()); + Environment.setAnswers(json); return Promise.resolve(); } catch (e) { logError(e); diff --git a/src/index.ts b/src/index.ts index d8075cc..4a1f717 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,11 @@ import {Environment} from "./common/environment"; import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api"; import {ChatCommand} from "./base/chat-command"; import { - checkRequirements, - executeChatCommand, extractTextMessage, findAndExecuteCallbackCommand, - ignore, initSystemSpecs, logError, - randomValue, - searchChatCommand + processNewMessage } from "./util/utils"; import {Ae} from "./commands/ae"; import {Help} from "./commands/help"; @@ -21,7 +17,7 @@ import {Ping} from "./commands/ping"; import {RandomString} from "./commands/random-string"; import {SystemInfo} from "./commands/system-info"; import {Test} from "./commands/test"; -import {inviteAnswers, kickAnswers, muted, readData, retrieveAnswers} from "./db/database"; +import {readData, retrieveAnswers} from "./db/database"; import {Uptime} from "./commands/uptime"; import {WhatBetter} from "./commands/what-better"; import {When} from "./commands/when"; @@ -40,7 +36,6 @@ import {Leave} from "./commands/leave"; import {OllamaChat} from "./commands/ollama-chat"; import {Start} from "./commands/start"; import {MessageStore} from "./common/message-store"; -import {PrefixResponse} from "./commands/prefix-response"; import {GeminiChat} from "./commands/gemini-chat"; import {Choice} from "./commands/choice"; import {Coin} from "./commands/coin"; @@ -70,6 +65,7 @@ import {GeminiListModels} from "./commands/gemini-list-models"; import {GeminiGetModel} from "./commands/gemini-get-model"; import {GeminiSetModel} from "./commands/gemini-set-model"; import {Debug} from "./commands/debug"; +import {GeminiGenerateImage} from "./commands/gemini-generate-image"; process.setUncaughtExceptionCaptureCallback(logError); @@ -177,7 +173,8 @@ if (Environment.GEMINI_API_KEY) { new GeminiChat(), new GeminiListModels(), new GeminiGetModel(), - new GeminiSetModel() + new GeminiSetModel(), + new GeminiGenerateImage() ); } @@ -239,57 +236,7 @@ bot.on("edited_message", async (msg) => { await MessageStore.put(msg); }); -bot.on("message", async (msg) => { - console.log("message", msg); - - Promise.all([MessageStore.put(msg), UserStore.put(msg.from)]).then(ignore); - - if ((msg.new_chat_members?.length || 0 > 0)) { - await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(inviteAnswers)}).catch(logError); - return; - } - - if (msg.left_chat_member && msg.left_chat_member.id !== botUser.id) { - await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(kickAnswers)}).catch(logError); - return; - } - - if (muted.has(msg.from.id)) return; - - if (msg.forward_origin) return; - - const cmdText = msg.text || msg.caption || ""; - - const then = Date.now(); - - const cmd = searchChatCommand(chatCommands, cmdText); - const executed = await executeChatCommand(cmd, msg, cmdText); - - const now = Date.now(); - const diff = now - then; - console.log("diff", diff); - - if (executed || !cmdText) return; - - const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase()); - const messageWithoutPrefix = cmdText.substring(Environment.BOT_PREFIX.length).trim(); - - if (startsWithPrefix && messageWithoutPrefix.length === 0) { - const prefixResponse = new PrefixResponse(); - if (await checkRequirements(prefixResponse, msg)) { - await prefixResponse.execute(msg); - } - return; - } - - 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); - if (await checkRequirements(chat, msg)) { - await chat.executeOllama(msg, startsWithPrefix ? messageWithoutPrefix : cmdText); - } -}); +bot.on("message", processNewMessage); bot.on("inline_query", async (query) => { console.log("query", query); diff --git a/src/model/answers.ts b/src/model/answers.ts new file mode 100644 index 0000000..cd84def --- /dev/null +++ b/src/model/answers.ts @@ -0,0 +1,9 @@ +export type Answers = { + test: string[] + prefix: string[] + better: string[] + who: string[] + kick: string[] + invite: string[] + day: number[] +} \ No newline at end of file diff --git a/src/util/utils.ts b/src/util/utils.ts index e4f1cc6..6d617a0 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -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, messageDao} from "../index"; +import {bot, botUser, chatCommands, messageDao} from "../index"; import os from "os"; import axios from "axios"; import {MessagePart} from "../common/message-part"; @@ -25,6 +25,8 @@ import fs from "node:fs"; import path from "node:path"; import {MessageStore} from "../common/message-store"; import {SystemInfo} from "../commands/system-info"; +import {PrefixResponse} from "../commands/prefix-response"; +import {OllamaChat} from "../commands/ollama-chat"; export const ignore = () => { }; @@ -525,7 +527,35 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise< return imageFilePaths; } -export async function collectReplyChainText(triggerMsg: Message, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise { +export async function loadImagesFromFileIds(maxSizes: PhotoMaxSize[]): Promise { + if (!maxSizes?.length) return null; + + const dataPath = path.join(Environment.DATA_PATH, "temp"); + if (!fs.existsSync(dataPath)) { + fs.mkdirSync(dataPath); + } + + const imagePromises = maxSizes.map((size) => { + return axios.get(size.url, {responseType: "arraybuffer"}); + }); + + const responses = await Promise.all(imagePromises); + const paths = responses.map((res, index) => { + try { + const imageFilePath = path.join(dataPath, maxSizes[index].unique_file_id + ".jpg"); + const src = Buffer.from(res.data); + fs.writeFileSync(imageFilePath, src); + return imageFilePath; + } catch (e) { + logError(e); + return null; + } + }); + + return paths.filter(p => p); +} + +export async function collectReplyChainText(triggerMsg: Message | StoredMessage, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise { const parts: MessagePart[] = []; const pushPart = async (msg: Message | StoredMessage, textRequired: boolean = false) => { @@ -548,19 +578,21 @@ export async function collectReplyChainText(triggerMsg: Message, limit: number = }); }; - const chatId = triggerMsg.chat.id as number; + const chatId = isStoredMessage(triggerMsg) ? triggerMsg.chatId as number : triggerMsg.chat.id; if (includeTrigger) { await pushPart(triggerMsg); } - const first = triggerMsg.reply_to_message; + const first = isStoredMessage(triggerMsg) ? + (await MessageStore.get(chatId, triggerMsg.replyToMessageId)) : + triggerMsg.reply_to_message; if (!first) { return parts; } await pushPart(first, false); - let curId = first.message_id; + let curId = isStoredMessage(first) ? first.id : first.message_id; while (parts.length < limit) { const cur = await messageDao.getById({chatId: chatId, id: curId}); @@ -898,7 +930,7 @@ export function getRuntimeInfo(): RuntimeInfo { return {runtime: "unknown", version: String((process as any).version ?? "")}; } -export type PhotoMaxSize = { width: number, height: number, url: string; unique_file_id: string; }; +export type PhotoMaxSize = { width: number, height: number, url: string; file_id: string; unique_file_id: string; }; export async function getPhotoMaxSize(photos: PhotoSize[], target: number = Environment.MAX_PHOTO_SIZE): Promise { if (!photos) return null; @@ -927,6 +959,7 @@ export async function mapPhotoSizeToMax(size: PhotoSize): Promise(); + +export async function processNewMessage(msg: Message) { + console.log("message", msg); + + let storedMsg: StoredMessage | null = null; + Promise.all([ + MessageStore.put(msg).then(r => { + storedMsg = r; + console.log("storedMsg", storedMsg); + }), + UserStore.put(msg.from) + ] + ).catch(logError); + + if ((msg.new_chat_members?.length || 0 > 0)) { + await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(Environment.ANSWERS.invite)}).catch(logError); + return; + } + + if (msg.left_chat_member && msg.left_chat_member.id !== botUser.id) { + await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(Environment.ANSWERS.kick)}).catch(logError); + return; + } + + if (Environment.MUTED_IDS.has(msg.from.id)) return; + + if (msg.forward_origin) return; + + const groupId = msg.media_group_id; + if (groupId) { + await new Promise(resolve => { + if (!albumCache.has(groupId)) { + albumCache.set(groupId, { + messages: [msg], + timer: setTimeout(async () => { + const photos = await processAlbum(groupId); + console.log("processedAlbum", photos); + + storedMsg.photoMaxSizeFilePath = photos; + await MessageStore.put(storedMsg).catch(logError); + resolve(true); + }, 1000) + }); + } else { + const entry = albumCache.get(groupId); + entry.messages.push(msg); + } + }); + } + + const cmdText = msg.text || msg.caption || ""; + + const then = Date.now(); + + const cmd = searchChatCommand(chatCommands, cmdText); + const executed = await executeChatCommand(cmd, msg, cmdText); + + const now = Date.now(); + const diff = now - then; + console.log("diff", diff); + + if (executed || !cmdText) return; + + const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase()); + const messageWithoutPrefix = cmdText.substring(Environment.BOT_PREFIX.length).trim(); + + if (startsWithPrefix && messageWithoutPrefix.length === 0) { + const prefixResponse = new PrefixResponse(); + if (await checkRequirements(prefixResponse, msg)) { + await prefixResponse.execute(msg); + } + return; + } + + 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); + if (await checkRequirements(chat, msg)) { + await chat.executeOllama(msg, startsWithPrefix ? messageWithoutPrefix : cmdText); + } +} + +async function processAlbum(groupId: string): Promise { + 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 = allPhotoMaxSizes.map(p => p.unique_file_id); + + 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, "temp", uniqueId + ".jpg"); } \ No newline at end of file