Files
tg-chat-bot/src/callback_commands/ai-cancel.ts
T
2026-05-13 10:18:54 +03:00

143 lines
6.2 KiB
TypeScript

import {CallbackCommand} from "../base/callback-command";
import {CallbackQuery, InlineKeyboardMarkup, Message} from "typescript-telegram-bot-api";
import {abortAiRequest, getAiCancelRequest} from "../ai/cancel-registry";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {MessageStore} from "../common/message-store";
import {bot} from "../index";
import {buildCancelledGenerationText, logError} from "../util/utils";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
import {buildAiRegenerateCallbackData} from "../ai/regenerate-callback";
import {isAiProviderConfigured, resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
const TELEGRAM_TEXT_LIMIT = 4096;
const TELEGRAM_CAPTION_LIMIT = 1024;
export class AiCancel extends CallbackCommand {
data = "/cancel_ai";
text = Environment.aiCancelCallbackText;
requirements = Requirements.Build(Requirement.SAME_USER);
async execute(query: CallbackQuery): Promise<void> {
if (!query.message || !query.data) return;
const parsed = this.parseCallbackData(query.data);
if (!parsed) return;
const request = getAiCancelRequest(parsed.requestId);
if (!request) {
await this.markMessageAsCancelled(query, parsed.provider);
return;
}
if (request.fromId !== query.from.id && query.from.id !== Environment.CREATOR_ID) return;
const cancelled = await abortAiRequest(parsed.requestId);
if (!cancelled) return;
}
private parseCallbackData(data: string): { requestId: string; provider?: AiProvider } | null {
const [, requestId, provider] = data.split(/\s+/);
if (!requestId) return null;
return {
requestId,
provider: Object.values(AiProvider).includes(provider as AiProvider) ? provider as AiProvider : undefined,
};
}
private async markMessageAsCancelled(query: CallbackQuery, providerFromCallback?: AiProvider): Promise<void> {
const callbackMessage = query.message;
if (!callbackMessage || callbackMessage.date === 0) return;
const message = callbackMessage as Message;
const stored = await MessageStore.get(message.chat.id, message.message_id).catch(e => {
logError(e);
return null;
});
const sourceFromId = await this.resolveSourceFromId(message, stored).catch(e => {
logError(e);
return undefined;
});
const regenerateProvider = providerFromCallback && isAiProviderConfigured(providerFromCallback)
? providerFromCallback
: await resolveEffectiveAiProviderForUser(sourceFromId ?? query.from.id);
const providerName = (providerFromCallback ?? regenerateProvider).toLowerCase();
const isCaption = this.isCaptionMessage(message);
const limit = isCaption ? TELEGRAM_CAPTION_LIMIT : TELEGRAM_TEXT_LIMIT;
const baseText = stored?.text ?? message.text ?? message.caption ?? "";
const cancelledText = buildCancelledGenerationText(baseText, providerName, limit);
const replyMarkup = this.regenerateKeyboard(regenerateProvider);
const formatted = prepareTelegramMarkdownV2(cancelledText, {mode: "final"});
try {
const result = isCaption
? await enqueueTelegramApiCall(
() => bot.editMessageCaption({
chat_id: message.chat.id,
message_id: message.message_id,
caption: formatted,
parse_mode: "MarkdownV2",
reply_markup: replyMarkup,
}),
{method: "editMessageCaption", chatId: message.chat.id, chatType: message.chat.type}
)
: await enqueueTelegramApiCall(
() => bot.editMessageText({
chat_id: message.chat.id,
message_id: message.message_id,
text: formatted,
parse_mode: "MarkdownV2",
reply_markup: replyMarkup,
}),
{method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type}
);
if (result) {
await MessageStore.put({...(result as object), text: cancelledText} as Message);
} else {
await MessageStore.put({
chatId: message.chat.id,
id: message.message_id,
replyToMessageId: stored?.replyToMessageId ?? this.replyToMessageId(message),
fromId: message.from?.id ?? stored?.fromId ?? 0,
text: cancelledText,
date: message.date ?? stored?.date ?? Math.floor(Date.now() / 1000),
photoMaxSizeFilePath: stored?.photoMaxSizeFilePath,
attachments: stored?.attachments,
});
}
} catch (e) {
logError(e);
}
}
private regenerateKeyboard(provider: AiProvider): InlineKeyboardMarkup {
return {
inline_keyboard: [[{
text: Environment.regenerateText,
callback_data: buildAiRegenerateCallbackData(provider),
}]],
};
}
private async resolveSourceFromId(message: Message, stored: Awaited<ReturnType<typeof MessageStore.get>>): Promise<number | undefined> {
const reply = "reply_to_message" in message ? message.reply_to_message : undefined;
if (reply?.from?.id) return reply.from.id;
const source = await MessageStore.get(message.chat.id, stored?.replyToMessageId);
return source?.fromId;
}
private replyToMessageId(message: Message): number | undefined {
return "reply_to_message" in message ? message.reply_to_message?.message_id : undefined;
}
private isCaptionMessage(message: Message): boolean {
return message.caption !== undefined;
}
}