* environment variable to enable/disable processing links in messages (for downloading videos)
* bot will not automatically download video from youtube or load it from cache. It will fetch video info first, then suggest to download or get video from cache (with retry options) * some rewriting in sending/editing/replying to messages
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import {CallbackCommand} from "../base/callback-command";
|
||||
import {CallbackQuery} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {commands} from "../index";
|
||||
import {YouTubeDownload} from "../commands/youtube-download";
|
||||
|
||||
const downloadText = " 📥 Скачать";
|
||||
const getFromCacheText = "📥 Загрузить из кэша";
|
||||
|
||||
export class DownloadYtVideo extends CallbackCommand {
|
||||
data = "/ytdl";
|
||||
text = " 📥 Скачать";
|
||||
|
||||
requirements = Requirements.Build(Requirement.SAME_USER);
|
||||
|
||||
constructor(text?: string, data?: string) {
|
||||
super();
|
||||
|
||||
this.text = text || this.text;
|
||||
this.data = data || this.data;
|
||||
}
|
||||
|
||||
static withData(inCache?: boolean, data?: string): DownloadYtVideo {
|
||||
return new DownloadYtVideo(inCache ? getFromCacheText : downloadText, data);
|
||||
}
|
||||
|
||||
async execute(query: CallbackQuery): Promise<void> {
|
||||
const videoId = query.data.split(" ")[1];
|
||||
if (!videoId) return;
|
||||
|
||||
const yt = commands.find(c => c instanceof YouTubeDownload);
|
||||
if (!yt) return;
|
||||
await yt.downloadYouTubeVideo(query.message, {videoId: videoId});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {CallbackCommand} from "../base/callback-command";
|
||||
|
||||
export class TryAgain extends CallbackCommand {
|
||||
data = "";
|
||||
text = "🔁 Повторить";
|
||||
|
||||
constructor(text?: string, data?: string) {
|
||||
super();
|
||||
|
||||
this.text = text ?? this.text;
|
||||
this.data = data ?? this.data;
|
||||
}
|
||||
|
||||
static withData(data?: string): TryAgain {
|
||||
return new TryAgain(null, data);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {CallbackCommand} from "../base/callback-command";
|
||||
import {CallbackQuery} from "typescript-telegram-bot-api";
|
||||
import {processYouTubeLink} from "../util/utils";
|
||||
|
||||
export class YtInfo extends CallbackCommand {
|
||||
data = "/ytinfo";
|
||||
text: string;
|
||||
|
||||
async execute(query: CallbackQuery): Promise<void> {
|
||||
const videoUrl = query.data.split(" ")[1];
|
||||
if (!videoUrl) return;
|
||||
|
||||
await processYouTubeLink(query.message, videoUrl);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export class GeminiChat extends ChatCommand {
|
||||
try {
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
text: Environment.waitThinkText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
|
||||
@@ -53,7 +53,7 @@ export class GeminiGenerateImage extends Command {
|
||||
await replyToMessage({
|
||||
message: waitMessage,
|
||||
text: `Произошла ошибка!\n${e.toString()}`,
|
||||
disableLinkPreview: true
|
||||
link_preview_options: {is_disabled: true}
|
||||
}).catch(logError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class MistralChat extends ChatCommand {
|
||||
chat_id: chatId,
|
||||
text: imagesCount ?
|
||||
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
|
||||
: Environment.waitText,
|
||||
: Environment.waitThinkText,
|
||||
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
|
||||
@@ -96,7 +96,7 @@ export class OllamaChat extends ChatCommand {
|
||||
message: msg,
|
||||
text: (!think && imagesCount) ?
|
||||
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
|
||||
: Environment.waitText
|
||||
: Environment.waitThinkText
|
||||
});
|
||||
|
||||
const stream = await ollama.chat({
|
||||
|
||||
@@ -37,7 +37,7 @@ export class OllamaPrompt extends Command {
|
||||
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
text: Environment.waitThinkText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, ollama} from "../index";
|
||||
import {WebSearchResponse} from "../model/web-search-response";
|
||||
import {editMessageText, logError} from "../util/utils";
|
||||
import {oldEditMessageText, logError} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class OllamaSearch extends Command {
|
||||
@@ -23,7 +23,7 @@ export class OllamaSearch extends Command {
|
||||
try {
|
||||
const wait = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
text: Environment.waitThinkText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
@@ -40,7 +40,7 @@ export class OllamaSearch extends Command {
|
||||
message += `${index + 1}. ${r.url}\n`;
|
||||
});
|
||||
|
||||
await editMessageText(chatId, wait.message_id, message);
|
||||
await oldEditMessageText(chatId, wait.message_id, message);
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class OpenAIChat extends ChatCommand {
|
||||
try {
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
text: Environment.waitThinkText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Requirement} from "../base/requirement";
|
||||
import {bot, openAi, photoGenDir} from "../index";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {editMessageText, logError, replyToMessage} from "../util/utils";
|
||||
import {oldEditMessageText, logError, replyToMessage} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
import {APIError} from "openai";
|
||||
|
||||
@@ -102,7 +102,7 @@ export class OpenAIGenImage extends ChatCommand {
|
||||
const text = "❌ Мне запрещено такое генерировать 😠";
|
||||
|
||||
if (waitMessage) {
|
||||
await editMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError);
|
||||
await oldEditMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError);
|
||||
} else {
|
||||
await replyToMessage({message: msg, text: text}).catch(logError);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {bot} from "../index";
|
||||
import {downloadVideoFromYouTube} from "../util/ytdl";
|
||||
import {editMessageText, logError, replyToMessage} from "../util/utils";
|
||||
import {bot, botUser} from "../index";
|
||||
import {DownloadOptions, downloadVideoFromYouTube, getYouTubeVideoId} from "../util/ytdl";
|
||||
import {Environment} from "../common/environment";
|
||||
import {TryAgain} from "../callback_commands/try-again";
|
||||
|
||||
export class YouTubeDownload extends Command {
|
||||
command = ["ytdl", "youtube"];
|
||||
@@ -10,16 +12,22 @@ export class YouTubeDownload extends Command {
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
const url = match?.[3];
|
||||
return this.downloadYouTubeVideo(msg, url);
|
||||
return this.downloadYouTubeVideo(msg, {url: url});
|
||||
}
|
||||
|
||||
async downloadYouTubeVideo(msg: Message, url: string): Promise<void> {
|
||||
let waitMessage: Message | null = null;
|
||||
async downloadYouTubeVideo(msg: Message, options: DownloadOptions): Promise<void> {
|
||||
// TODO: 02.03.2026, Danil Nikolaev: add check for date
|
||||
let waitMessage: Message | null = (msg.from.id === botUser.id) ? msg : null;
|
||||
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
|
||||
|
||||
try {
|
||||
waitMessage = await replyToMessage({message: msg, text: "⏳ Секунду..."});
|
||||
if (!waitMessage) {
|
||||
waitMessage = await replyToMessage({message: msg, text: Environment.waitText});
|
||||
} else {
|
||||
await editMessageText({message: msg, text: Environment.waitText});
|
||||
}
|
||||
|
||||
const {time, exists, buffer} = await downloadVideoFromYouTube(url);
|
||||
const {time, exists, buffer} = await downloadVideoFromYouTube({videoId: videoId});
|
||||
if (buffer) {
|
||||
const start = Date.now();
|
||||
waitMessage = await bot.editMessageMedia({
|
||||
@@ -35,7 +43,7 @@ export class YouTubeDownload extends Command {
|
||||
waitMessage = await bot.editMessageCaption({
|
||||
chat_id: msg.chat.id,
|
||||
message_id: waitMessage.message_id,
|
||||
caption: `✅ [Видео](${url})` + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
|
||||
caption: "✅ [Видео]" + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
|
||||
parse_mode: "MarkdownV2"
|
||||
}) as Message;
|
||||
}
|
||||
@@ -46,7 +54,12 @@ export class YouTubeDownload extends Command {
|
||||
await bot.editMessageText({
|
||||
chat_id: msg.chat.id,
|
||||
message_id: waitMessage.message_id,
|
||||
text: `⚠️ Произошла ошибка.\n${e}`,
|
||||
text: Environment.errorText,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
TryAgain.withData("/ytdl " + videoId).asButton()
|
||||
]]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export class Environment {
|
||||
|
||||
static MAX_PHOTO_SIZE: number;
|
||||
|
||||
static PROCESS_LINKS: boolean;
|
||||
|
||||
static DEFAULT_AI_PROVIDER: AiProvider;
|
||||
|
||||
static SYSTEM_PROMPT?: string;
|
||||
@@ -49,7 +51,9 @@ export class Environment {
|
||||
static OPENAI_MODEL: string;
|
||||
static OPENAI_IMAGE_MODEL: string;
|
||||
|
||||
static waitText = "⏳ Дайте-ка подумать...";
|
||||
static errorText = "⚠️ Произошла ошибка.";
|
||||
static waitText = "⏳ Секунду...";
|
||||
static waitThinkText = "⏳ Дайте-ка подумать...";
|
||||
static analyzingPictureText = "🔍 Внимательно изучаю изображение...";
|
||||
static analyzingPicturesText = "🔍 Внимательно изучаю изображения...";
|
||||
static genImageText = "👨🎨 Генерирую изображение...";
|
||||
@@ -73,6 +77,8 @@ export class Environment {
|
||||
|
||||
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
|
||||
|
||||
Environment.PROCESS_LINKS = ifTrue(process.env.PROCESS_LINKS);
|
||||
|
||||
const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA";
|
||||
if (Object.values(AiProvider).includes(aiProvider as AiProvider)) {
|
||||
Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider;
|
||||
|
||||
+5
-1
@@ -79,6 +79,8 @@ import {OpenAISetModel} from "./commands/openai-set-model";
|
||||
import {Info} from "./commands/info";
|
||||
import {OpenAIGenImage} from "./commands/openai-gen-image";
|
||||
import {clearUpFolderFromOldFiles} from "./util/files";
|
||||
import {DownloadYtVideo} from "./callback_commands/download-yt-video";
|
||||
import {YtInfo} from "./callback_commands/yt-info";
|
||||
|
||||
process.setUncaughtExceptionCaptureCallback(logError);
|
||||
|
||||
@@ -171,7 +173,9 @@ if (Environment.ENABLE_UNSAFE_EVAL) {
|
||||
}
|
||||
|
||||
export const callbackCommands: CallbackCommand[] = [
|
||||
new OllamaCancel()
|
||||
new OllamaCancel(),
|
||||
new DownloadYtVideo(),
|
||||
new YtInfo()
|
||||
];
|
||||
|
||||
if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {InlineKeyboardMarkup, Message, ParseMode} from "typescript-telegram-bot-api";
|
||||
import {LinkPreviewOptions, MessageEntity} from "typescript-telegram-bot-api/dist/types";
|
||||
|
||||
export type EditOptions = ({
|
||||
message: Message
|
||||
} | {
|
||||
chat_id: number;
|
||||
message_id: number;
|
||||
}) & {
|
||||
text: string;
|
||||
parse_mode?: ParseMode;
|
||||
entities?: MessageEntity[];
|
||||
link_preview_options?: LinkPreviewOptions;
|
||||
reply_markup?: InlineKeyboardMarkup;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {InlineKeyboardMarkup, Message, ParseMode} from "typescript-telegram-bot-api";
|
||||
import {
|
||||
ForceReply,
|
||||
LinkPreviewOptions,
|
||||
MessageEntity, ReplyKeyboardMarkup, ReplyKeyboardRemove,
|
||||
ReplyParameters,
|
||||
SuggestedPostParameters
|
||||
} from "typescript-telegram-bot-api/dist/types";
|
||||
|
||||
export type SendOptions = ({
|
||||
message: Message
|
||||
} | {
|
||||
/**
|
||||
* Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
|
||||
*/
|
||||
chat_id: number | string;
|
||||
message_id?: number;
|
||||
}) & {
|
||||
/**
|
||||
* Unique identifier for the target message thread (topic) of the forum; for forum supergroups only
|
||||
*/
|
||||
message_thread_id?: number;
|
||||
/**
|
||||
* Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a
|
||||
* direct messages chat
|
||||
*/
|
||||
direct_messages_topic_id?: number;
|
||||
/**
|
||||
* Text of the message to be sent, 1-4096 characters after entities parsing
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Mode for parsing entities in the message text. See formatting options for more details.
|
||||
*/
|
||||
parse_mode?: ParseMode;
|
||||
/**
|
||||
* A JSON-serialized list of special entities that appear in message text, which can be specified instead of
|
||||
* parse_mode
|
||||
*/
|
||||
entities?: MessageEntity[];
|
||||
/**
|
||||
* Link preview generation options for the message
|
||||
*/
|
||||
link_preview_options?: LinkPreviewOptions;
|
||||
/**
|
||||
* Sends the message silently. Users will receive a notification with no sound.
|
||||
*/
|
||||
disable_notification?: boolean;
|
||||
/**
|
||||
* Protects the contents of the sent message from forwarding and saving
|
||||
*/
|
||||
protect_content?: boolean;
|
||||
/**
|
||||
* Pass True to allow up to 1000 messages per second, ignoring
|
||||
* [broadcasting limits](https://core.telegram.org/bots/faq#how-can-i-message-all-of-my-bot-39s-subscribers-at-once)
|
||||
* for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance
|
||||
*/
|
||||
allow_paid_broadcast?: boolean;
|
||||
/**
|
||||
* Unique identifier of the message effect to be added to the message; for private chats only
|
||||
*/
|
||||
message_effect_id?: string;
|
||||
/**
|
||||
* A JSON-serialized object containing the parameters of the suggested post to send; for direct messages chats only.
|
||||
* If the message is sent as a reply to another suggested post, then that suggested post is automatically declined.
|
||||
*/
|
||||
suggested_post_parameters?: SuggestedPostParameters;
|
||||
/**
|
||||
* Description of the message to reply to
|
||||
*/
|
||||
reply_parameters?: ReplyParameters;
|
||||
/**
|
||||
* Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard,
|
||||
* instructions to remove a reply keyboard or to force a reply from the user
|
||||
*/
|
||||
reply_markup?: InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply;
|
||||
};
|
||||
+167
-78
@@ -11,6 +11,7 @@ import {
|
||||
Message,
|
||||
ParseMode,
|
||||
PhotoSize,
|
||||
TelegramBot,
|
||||
User
|
||||
} from "typescript-telegram-bot-api";
|
||||
import {Environment} from "../common/environment";
|
||||
@@ -30,7 +31,7 @@ import {MessageStore} from "../common/message-store";
|
||||
import {SystemInfo} from "../commands/system-info";
|
||||
import {PrefixResponse} from "../commands/prefix-response";
|
||||
import {OllamaChat} from "../commands/ollama-chat";
|
||||
import {getYouTubeVideoId} from "./ytdl";
|
||||
import {getYouTubeVideoId, getYouTubeVideoInfo, isVideoExists} from "./ytdl";
|
||||
import {YouTubeDownload} from "../commands/youtube-download";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {WebSearchResponse} from "../model/web-search-response";
|
||||
@@ -43,6 +44,11 @@ import {OllamaGetModel} from "../commands/ollama-get-model";
|
||||
import {GeminiGetModel} from "../commands/gemini-get-model";
|
||||
import {MistralGetModel} from "../commands/mistral-get-model";
|
||||
import {OpenAIGetModel} from "../commands/openai-get-model";
|
||||
import {SendOptions} from "../model/send-options";
|
||||
import {EditOptions} from "../model/edit-options";
|
||||
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
|
||||
import {DownloadYtVideo} from "../callback_commands/download-yt-video";
|
||||
import {TryAgain} from "../callback_commands/try-again";
|
||||
|
||||
export const ignore = () => {
|
||||
};
|
||||
@@ -54,7 +60,7 @@ export const ignoreIfNotChanged = (e: Error | TelegramError) => {
|
||||
};
|
||||
|
||||
export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
|
||||
if (!(e instanceof TelegramError && e?.response?.description?.startsWith("Bad Request: can't parse entities"))) {
|
||||
if (!isMarkupFailed(e)) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -67,6 +73,18 @@ export const errorPlaceholder = async (msg: Message) => {
|
||||
await sendErrorPlaceholder(msg).catch(logError);
|
||||
};
|
||||
|
||||
export const isMarkupFailed = (e: Error | TelegramError) => {
|
||||
return TelegramBot.isTelegramError(e) && e?.response?.description?.startsWith("Bad Request: can't parse entities");
|
||||
};
|
||||
|
||||
export const isTooManyRequests = (e: Error | TelegramError) => {
|
||||
return TelegramBot.isTelegramError(e) && e.response.description.includes("Too Many Requests");
|
||||
};
|
||||
|
||||
export const isMessageTooLong = (e: Error | TelegramError) => {
|
||||
return TelegramBot.isTelegramError(e) && e.response.description.includes("MESSAGE_TOO_LONG");
|
||||
};
|
||||
|
||||
export function searchChatCommand(
|
||||
commands: Command[],
|
||||
text: string,
|
||||
@@ -117,7 +135,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
|
||||
const cbId = cb?.id;
|
||||
const chatId = msg?.chat?.id || cb?.message?.chat?.id || -1;
|
||||
const messageId = msg?.message_id || cb?.message?.message_id || -1;
|
||||
const messageId = msg?.message_id || (cb && cb.message && "reply_to_message" in cb.message ? cb.message.reply_to_message.message_id : null) || -1;
|
||||
const fromId = msg?.from?.id || cb?.from?.id || -1;
|
||||
const chatType = msg?.chat?.type || cb?.message?.chat?.type || null;
|
||||
|
||||
@@ -196,11 +214,8 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
if (reqs.isRequiresSameUser()) {
|
||||
let originalFromId: number | null;
|
||||
try {
|
||||
const queryMessage = await MessageStore.get(chatId, messageId);
|
||||
if (queryMessage && queryMessage.replyToMessageId) {
|
||||
const originalMessage = await MessageStore.get(chatId, queryMessage.replyToMessageId);
|
||||
const originalMessage = await MessageStore.get(chatId, messageId);
|
||||
originalFromId = originalMessage?.fromId;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
originalFromId = null;
|
||||
@@ -239,92 +254,87 @@ export async function findAndExecuteCallbackCommand(commands: CallbackCommand[],
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function editMessageText(chatId: number, messageId: number, messageText: string, parseMode?: ParseMode, replyMarkup?: InlineKeyboardMarkup): Promise<void> {
|
||||
if (messageText.trim().length === 0) return Promise.resolve();
|
||||
try {
|
||||
await bot.editMessageText({
|
||||
export async function oldEditMessageText(chatId: number, messageId: number, messageText: string, parseMode?: ParseMode, replyMarkup?: InlineKeyboardMarkup): Promise<boolean | Message> {
|
||||
return editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: messageText,
|
||||
parse_mode: parseMode,
|
||||
link_preview_options: {
|
||||
is_disabled: true
|
||||
},
|
||||
reply_markup: replyMarkup
|
||||
}).catch(ignoreIfMarkupFailed);
|
||||
return Promise.resolve();
|
||||
reply_markup: replyMarkup,
|
||||
link_preview_options: {is_disabled: true}
|
||||
});
|
||||
}
|
||||
|
||||
export async function editMessageText(options: EditOptions) {
|
||||
if (options.text.trim().length === 0) return Promise.resolve(false);
|
||||
|
||||
try {
|
||||
const message = await bot.editMessageText({
|
||||
chat_id: "message" in options ? options.message.chat.id : options.chat_id,
|
||||
message_id: "message" in options ? options.message.message_id : options.message_id,
|
||||
text: options.text,
|
||||
parse_mode: options.parse_mode,
|
||||
reply_markup: options.reply_markup,
|
||||
link_preview_options: options.link_preview_options,
|
||||
});
|
||||
return Promise.resolve(message);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
|
||||
if (e instanceof TelegramError && e.response.description.includes("Too Many Requests")) {
|
||||
if (isMarkupFailed(e)) {
|
||||
return Promise.resolve(true);
|
||||
} else if (isTooManyRequests(e)) {
|
||||
const delay = Number(e.message.split("retry after ")[1]) || 30;
|
||||
setTimeout(() => {
|
||||
return Promise.resolve();
|
||||
}, delay * 1000);
|
||||
} else if (e instanceof TelegramError && e.response.description.includes("MESSAGE_TOO_LONG")) {
|
||||
return Promise.reject(e);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SendOptions = {
|
||||
chat_id?: number;
|
||||
message?: Message,
|
||||
message_id?: number;
|
||||
text: string,
|
||||
parse_mode?: ParseMode,
|
||||
disableLinkPreview?: boolean
|
||||
};
|
||||
|
||||
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: message.chat.id,
|
||||
return sendMessage({
|
||||
message: message,
|
||||
text: text,
|
||||
parse_mode: parseMode
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function sendMessage(options: SendOptions): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: options.chat_id ?? options.message?.chat?.id,
|
||||
chat_id: "message" in options ? options.message.chat.id : options.chat_id,
|
||||
text: options.text,
|
||||
parse_mode: options.parse_mode,
|
||||
link_preview_options: {
|
||||
is_disabled: options.disableLinkPreview
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function replyToMessage(options: SendOptions): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: options.chat_id ?? options.message?.chat?.id,
|
||||
text: options.text,
|
||||
parse_mode: options.parse_mode,
|
||||
reply_parameters: {
|
||||
message_id: options.message_id || options.message?.message_id
|
||||
},
|
||||
link_preview_options: {
|
||||
is_disabled: options.disableLinkPreview
|
||||
}
|
||||
link_preview_options: options.link_preview_options,
|
||||
reply_markup: options.reply_markup,
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function oldReplyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: message.chat.id,
|
||||
return replyToMessage({
|
||||
message: message,
|
||||
text: text,
|
||||
parse_mode: parseMode
|
||||
});
|
||||
}
|
||||
|
||||
export async function replyToMessage(options: SendOptions): Promise<Message> {
|
||||
if (!("message" in options) && !options.message_id) {
|
||||
return Promise.reject("for reply there must be message or message_id");
|
||||
}
|
||||
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: "message" in options ? options.message.chat.id : options.chat_id,
|
||||
text: options.text,
|
||||
parse_mode: options.parse_mode,
|
||||
reply_parameters: {
|
||||
message_id: message.message_id
|
||||
message_id: "message" in options ? options.message.message_id : options.message_id
|
||||
},
|
||||
parse_mode: parseMode,
|
||||
link_preview_options: options.link_preview_options
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
@@ -1200,27 +1210,8 @@ export async function processNewMessage(msg: Message): Promise<void> {
|
||||
}
|
||||
|
||||
const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText;
|
||||
if (msg.entities) {
|
||||
const urlEntities = msg.entities.filter(e => e.type === "url");
|
||||
if (urlEntities.length) {
|
||||
for (const e of urlEntities) {
|
||||
const url = msg.text.substring(e.offset, e.offset + e.length);
|
||||
// TODO: 31/01/2026, Danil Nikolaev: implement proper checking
|
||||
try {
|
||||
getYouTubeVideoId(url);
|
||||
|
||||
const yt = commands.find(e => e instanceof YouTubeDownload);
|
||||
if (await checkRequirements(yt, msg)) {
|
||||
await yt.downloadYouTubeVideo(msg, url);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.PROCESS_LINKS && await processYouTubeLink(msg, getFirstLink(msg))) return;
|
||||
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
||||
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
||||
|
||||
@@ -1244,6 +1235,104 @@ export async function processNewMessage(msg: Message): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstLink(msg: Message): string | null {
|
||||
if (msg.entities) {
|
||||
const urlEntities = msg.entities.filter(e => e.type === "url");
|
||||
if (urlEntities.length) {
|
||||
const e = urlEntities[0];
|
||||
return msg.text.substring(e.offset, e.offset + e.length);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function processYouTubeLink(msg: Message, url: string): Promise<boolean> {
|
||||
if (!url) return false;
|
||||
try {
|
||||
const videoId = getYouTubeVideoId(url);
|
||||
const yt = commands.find(e => e instanceof YouTubeDownload);
|
||||
|
||||
if (await checkRequirements(yt, msg)) {
|
||||
const waitMessage = msg.from.id === botUser.id ? msg : await replyToMessage({
|
||||
message: msg,
|
||||
text: "⏳ Ищу информацию о видео..."
|
||||
});
|
||||
|
||||
if (msg.from.id === botUser.id) {
|
||||
await editMessageText({message: msg, text: "⏳ Ищу информацию о видео..."});
|
||||
}
|
||||
|
||||
let videoInfo: VideoInfo | null = null;
|
||||
let ytError: string = null;
|
||||
|
||||
try {
|
||||
videoInfo = await getYouTubeVideoInfo(videoId);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
|
||||
if ("version" in e) {
|
||||
ytError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("VIDEO_INFO", videoInfo);
|
||||
|
||||
let text: string = null;
|
||||
|
||||
const inCache = isVideoExists({videoId: videoId});
|
||||
|
||||
const duration = videoInfo?.basic_info?.duration || null;
|
||||
const canDownload = inCache || duration && duration <= 300;
|
||||
|
||||
if (videoInfo) {
|
||||
text = "Видео с YouTube\n\n" +
|
||||
`Название: ${videoInfo.basic_info?.title}\n` +
|
||||
`Автор: ${videoInfo.secondary_info?.owner?.author?.name}\n` +
|
||||
`Длительность: ${duration} сек.`;
|
||||
|
||||
if (!canDownload) {
|
||||
text += `\n\nВидео слишком длинное (${duration} сек. > 300 сек.)`;
|
||||
}
|
||||
} else if (!ytError) {
|
||||
text = "Информация о видео не найдена";
|
||||
}
|
||||
|
||||
const errorButInCache = !videoInfo && ytError && inCache;
|
||||
if (errorButInCache) {
|
||||
text = "Я не смогу получить информацию о видео, но нашёл его в кэше.";
|
||||
}
|
||||
|
||||
if (!text && ytError) {
|
||||
await editMessageText({
|
||||
message: waitMessage,
|
||||
text: Environment.errorText,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
TryAgain.withData("/ytinfo " + url).asButton()
|
||||
]]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await editMessageText({
|
||||
message: waitMessage,
|
||||
text: text,
|
||||
reply_markup: canDownload ? {
|
||||
inline_keyboard: [[
|
||||
DownloadYtVideo.withData(inCache, "/ytdl " + videoId).asButton()
|
||||
]]
|
||||
} : {inline_keyboard: []}
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function processEditedMessage(msg: Message): Promise<void> {
|
||||
console.log("Edited Message", msg);
|
||||
|
||||
|
||||
+48
-7
@@ -6,6 +6,21 @@ import Innertube, {Platform, Types} from "youtubei.js";
|
||||
import {Readable} from "node:stream";
|
||||
import {logError} from "./utils";
|
||||
import {performFFmpeg} from "./ffmpeg";
|
||||
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
|
||||
|
||||
let innertube: Innertube | null = null;
|
||||
|
||||
export async function getYT(): Promise<Innertube> {
|
||||
if (innertube) {
|
||||
return innertube;
|
||||
} else {
|
||||
innertube = await Innertube.create({
|
||||
generate_session_locally: true,
|
||||
retrieve_player: true
|
||||
});
|
||||
return innertube;
|
||||
}
|
||||
}
|
||||
|
||||
export function getYouTubeVideoId(url: string): string {
|
||||
const regex = /(?:(?:youtube\.com|music\.youtube\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts|clip)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
|
||||
@@ -14,7 +29,34 @@ export function getYouTubeVideoId(url: string): string {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export async function downloadVideoFromYouTube(url: string): Promise<{
|
||||
export async function getYouTubeVideoInfo(videoId: string): Promise<VideoInfo> {
|
||||
try {
|
||||
return (await getYT()).getInfo(videoId, {client: "ANDROID"});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVideoExists(options: DownloadOptions): boolean {
|
||||
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
|
||||
const filePath = path.join(videoDir, `${videoId}.mp4`);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
export function getVideoFromCache(videoId: string): Buffer | null {
|
||||
if (!isVideoExists({videoId: videoId})) return null;
|
||||
|
||||
const filePath = path.join(videoDir, `${videoId}.mp4`);
|
||||
return Buffer.from(fs.readFileSync(filePath));
|
||||
}
|
||||
|
||||
export type DownloadOptions = {
|
||||
url: string
|
||||
} | {
|
||||
videoId: string;
|
||||
}
|
||||
|
||||
export async function downloadVideoFromYouTube(options: DownloadOptions): Promise<{
|
||||
time: number,
|
||||
exists?: boolean,
|
||||
buffer: Buffer | null
|
||||
@@ -23,7 +65,7 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
|
||||
let buffer: Buffer | null = null;
|
||||
|
||||
try {
|
||||
const videoId = getYouTubeVideoId(url);
|
||||
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
|
||||
const filePath = path.join(videoDir, `${videoId}.mp4`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const buffer = Buffer.from(fs.readFileSync(filePath));
|
||||
@@ -42,12 +84,11 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
|
||||
const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
|
||||
return new Function(code)();
|
||||
};
|
||||
const yt = await Innertube.create({
|
||||
generate_session_locally: true,
|
||||
retrieve_player: true
|
||||
});
|
||||
|
||||
const yt = await getYT();
|
||||
|
||||
const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"});
|
||||
console.log("Video info", videoInfo);
|
||||
|
||||
console.log(`Fetching metadata for: ${videoId}...`);
|
||||
|
||||
@@ -119,7 +160,7 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
|
||||
|
||||
const end = Date.now();
|
||||
const diff = end - start;
|
||||
console.log(`Video downloaded. URL: ${url}\ntook ${diff}ms`);
|
||||
console.log(`Video downloaded.\ntook ${diff}ms`);
|
||||
|
||||
return {
|
||||
time: diff,
|
||||
|
||||
Reference in New Issue
Block a user