refactor(bot): centralize runtime state; support albums + safer vision handling

- make MessageStore.put() return StoredMessage and allow collectReplyChainText() to work with StoredMessage
- move muted users + answers loading into Environment (add Answers model + GEMINI_IMAGE_MODEL)
- extract message handling into processNewMessage() and add media-group (album) caching/downloading by unique_file_id
- for Ollama: check model capabilities before sending images; use replyToMessage helper consistently
- add /geminiGenImage command stub for Gemini image generation
This commit is contained in:
2026-01-29 20:01:30 +03:00
parent 5f4405c9ee
commit b74e0a0f3e
16 changed files with 312 additions and 148 deletions
+7 -2
View File
@@ -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);
}
}
}
}
+60
View File
@@ -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<void> {
console.log("match", match);
const prompt = match?.[3];
return this.executeGenImage(msg, prompt);
}
async executeGenImage(msg: Message, text: string): Promise<void> {
if (!text || text.trim().length === 0) return;
let waitMessage: Message;
try {
waitMessage = await replyToMessage({
message: msg,
text: Environment.genImageText,
});
const interaction = await googleAi.interactions.create({
model: Environment.GEMINI_IMAGE_MODEL,
response_modalities: ["image"],
input: text,
});
interaction.outputs?.forEach((output, index) => {
if (output.type === "image") {
// const image = output.data;
console.log(`Image output ${index + 1}:`, output);
} else {
console.log(`Output ${index + 1}: ${output}`);
}
});
} catch (e) {
logError(e);
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`,
disableLinkPreview: true
}).catch(logError);
}
}
}
+1 -2
View File
@@ -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);
+5 -3
View File
@@ -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
+26 -10
View File
@@ -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({
+6 -1
View File
@@ -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<void> {
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<ShowResponse | null> {
return ollama.show({model: Environment.OLLAMA_MODEL});
}
}
+3 -3
View File
@@ -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<void> {
await oldReplyToMessage(msg, randomValue(prefixAnswers)).catch(logError);
await replyToMessage({message: msg, text: randomValue(Environment.ANSWERS.prefix)}).catch(logError);
}
}
+3 -3
View File
@@ -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);
}
}
+1 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
}
+29 -7
View File
@@ -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<number> = new Set<number>();
static MUTED_IDS: Set<number> = new Set<number>();
static CHAT_IDS_WHITELIST: Set<number> = new Set<number>();
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<number>) {
this.MUTED_IDS = muted;
}
static async addMute(id: number): Promise<boolean> {
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<boolean> {
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;
}
+2 -1
View File
@@ -14,7 +14,7 @@ export class MessageStore {
return this.map;
}
static async put(m: Message | StoredMessage) {
static async put(m: Message | StoredMessage): Promise<StoredMessage> {
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<StoredMessage | null> {
+5 -47
View File
@@ -1,9 +1,7 @@
import * as fs from "fs";
import {Environment} from "../common/environment";
import {logError} from "../util/utils";
export let muted: Set<number> = new Set<number>();
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<boolean> {
if (muted.has(id)) return Promise.resolve(false);
muted.add(id);
await saveData();
return Promise.resolve(true);
}
export async function removeMute(id: number): Promise<boolean> {
if (!muted.has(id)) return Promise.resolve(false);
muted.delete(id);
await saveData();
return Promise.resolve(true);
}
export async function readData(): Promise<void> {
try {
jsonFile = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/data.json`).toString());
@@ -53,8 +18,7 @@ export async function readData(): Promise<void> {
admins.unshift(Environment.CREATOR_ID);
Environment.setAdmins(new Set<number>(admins));
muted = new Set<number>(jsonFile.muted || []);
Environment.setMuted(new Set<number>(jsonFile.muted || []));
return Promise.resolve();
} catch (e) {
@@ -69,7 +33,7 @@ export async function saveData(): Promise<void> {
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<void> {
export async function retrieveAnswers(): Promise<void> {
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);
+6 -59
View File
@@ -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);
+9
View File
@@ -0,0 +1,9 @@
export type Answers = {
test: string[]
prefix: string[]
better: string[]
who: string[]
kick: string[]
invite: string[]
day: number[]
}
+147 -6
View File
@@ -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<MessagePart[]> {
export async function loadImagesFromFileIds(maxSizes: PhotoMaxSize[]): Promise<string[] | null> {
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<ArrayBuffer>(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<MessagePart[]> {
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<PhotoMaxSize | null> {
if (!photos) return null;
@@ -927,6 +959,7 @@ export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize |
width: size.width,
height: size.height,
url: await getFileUrl(size.file_id),
file_id: size.file_id,
unique_file_id: size.file_unique_id
};
}
@@ -953,3 +986,111 @@ export function ifTrue(exp?: never): boolean {
export function boolToEmoji(bool: boolean): string {
return bool ? "✅" : "❌";
}
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
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<true>(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<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 = 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");
}