bump libs
migrate to typescript 6 remove ytdl feature
This commit is contained in:
+103
-213
@@ -32,8 +32,6 @@ 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, getYouTubeVideoInfo, isVideoExists} from "./ytdl";
|
||||
import {YouTubeDownload} from "../commands/youtube-download";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {WebSearchResponse} from "../model/web-search-response";
|
||||
import {GeminiChat} from "../commands/gemini-chat";
|
||||
@@ -47,9 +45,6 @@ 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";
|
||||
import {StoredUser} from "../model/stored-user";
|
||||
import {performFFmpeg} from "./ffmpeg";
|
||||
|
||||
@@ -68,7 +63,7 @@ export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const logError = (e: Error | TelegramError | string) => {
|
||||
export const logError = (e: Error | TelegramError | string | unknown) => {
|
||||
console.error(e);
|
||||
};
|
||||
|
||||
@@ -91,7 +86,7 @@ export const isMessageTooLong = (e: Error | TelegramError) => {
|
||||
export function searchChatCommand(
|
||||
commands: Command[],
|
||||
text: string,
|
||||
botUsername: string = botUser.username
|
||||
botUsername: string | undefined = botUser.username
|
||||
): Command | null {
|
||||
for (const command of commands) {
|
||||
const match = command.finalRegexp.exec(text);
|
||||
@@ -129,7 +124,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
let title: string;
|
||||
|
||||
if (isChatCommand) {
|
||||
title = cmd.title;
|
||||
title = cmd.title || "";
|
||||
} else if (isCallbackCommand) {
|
||||
title = cmd.data;
|
||||
} else {
|
||||
@@ -138,7 +133,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 && cb.message && "reply_to_message" in cb.message ? cb.message.reply_to_message.message_id : null) || -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;
|
||||
|
||||
@@ -162,7 +157,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
await replyToMessage({chat_id: chatId, message_id: messageId, text: text});
|
||||
} else if (cb) {
|
||||
await bot.answerCallbackQuery({
|
||||
callback_query_id: cbId,
|
||||
callback_query_id: cbId || "",
|
||||
text: text,
|
||||
cache_time: 0,
|
||||
show_alert: true
|
||||
@@ -182,7 +177,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresChat() && msg.chat.type === "private") {
|
||||
if (reqs.isRequiresChat() && msg?.chat?.type === "private") {
|
||||
console.log(`${title}: chatId is bad`);
|
||||
await notifyUser("Тут Вам не чат.");
|
||||
return false;
|
||||
@@ -215,13 +210,13 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
}
|
||||
|
||||
if (reqs.isRequiresSameUser()) {
|
||||
let originalFromId: number | null;
|
||||
let originalFromId: number | undefined;
|
||||
try {
|
||||
const originalMessage = await MessageStore.get(chatId, messageId);
|
||||
originalFromId = originalMessage?.fromId;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
originalFromId = null;
|
||||
originalFromId = undefined;
|
||||
}
|
||||
|
||||
if (originalFromId && fromId !== originalFromId && fromId !== Environment.CREATOR_ID) {
|
||||
@@ -239,7 +234,7 @@ export async function executeChatCommand(cmd: Command | null, msg: Message, text
|
||||
|
||||
if (!await checkRequirements(cmd, msg)) return false;
|
||||
|
||||
await cmd.execute(msg, cmd.regexp.exec(text));
|
||||
await cmd.execute(msg, cmd.regexp?.exec(text));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -249,7 +244,7 @@ export async function findAndExecuteCallbackCommand(commands: CallbackCommand[],
|
||||
const cmd = searchCallbackCommand(commands, data);
|
||||
if (!cmd) return false;
|
||||
|
||||
if (!await checkRequirements(cmd, null, query)) return false;
|
||||
if (!await checkRequirements(cmd, undefined, query)) return false;
|
||||
|
||||
await cmd.execute(query);
|
||||
await cmd.answerCallbackQuery(query);
|
||||
@@ -281,7 +276,7 @@ export async function editMessageText(options: EditOptions) {
|
||||
link_preview_options: options.link_preview_options,
|
||||
});
|
||||
return Promise.resolve(message);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logError(e);
|
||||
|
||||
if (isMarkupFailed(e)) {
|
||||
@@ -295,6 +290,8 @@ export async function editMessageText(options: EditOptions) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
|
||||
@@ -337,7 +334,7 @@ export async function replyToMessage(options: SendOptions): Promise<Message> {
|
||||
text: options.text,
|
||||
parse_mode: options.parse_mode,
|
||||
reply_parameters: {
|
||||
message_id: "message" in options ? options.message.message_id : options.message_id
|
||||
message_id: <number>("message" in options ? options.message.message_id : options.message_id)
|
||||
},
|
||||
link_preview_options: options.link_preview_options
|
||||
});
|
||||
@@ -1177,12 +1174,13 @@ export function extractTextMessage(msg: Message | StoredMessage | string): strin
|
||||
if (!msg) return null;
|
||||
if (typeof msg === "string") return msg;
|
||||
|
||||
const text = (isStoredMessage(msg) ? msg.text : msg.text || msg.caption || "").trim();
|
||||
if (text.length === 0) return null;
|
||||
const text = (isStoredMessage(msg) ? msg.text : msg.text || msg.caption || "")?.trim();
|
||||
if (!text || !text?.length) return null;
|
||||
return text;
|
||||
}
|
||||
|
||||
export function cutPrefixes(msg: Message | StoredMessage | string): string {
|
||||
export function cutPrefixes(msg: Message | StoredMessage | string | null): string | null {
|
||||
if (!msg) return null;
|
||||
const chatCommands = commands.filter(c => c instanceof ChatCommand);
|
||||
|
||||
const prefixes = [Environment.BOT_PREFIX];
|
||||
@@ -1193,10 +1191,12 @@ export function cutPrefixes(msg: Message | StoredMessage | string): string {
|
||||
|
||||
chatCommands.forEach((cmd) => {
|
||||
const command = cmd.command;
|
||||
if (Array.isArray(command)) {
|
||||
command.forEach(pushPrefix);
|
||||
} else {
|
||||
pushPrefix(command);
|
||||
if (command) {
|
||||
if (Array.isArray(command)) {
|
||||
command.forEach(pushPrefix);
|
||||
} else {
|
||||
pushPrefix(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1216,10 +1216,10 @@ export function cutPrefixes(msg: Message | StoredMessage | string): string {
|
||||
}
|
||||
|
||||
export function isStoredMessage(msg: Message | StoredMessage | null): msg is StoredMessage {
|
||||
return msg && "id" in msg;
|
||||
return !!msg && "id" in msg;
|
||||
}
|
||||
|
||||
export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<string[] | null> {
|
||||
export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<string[] | null | undefined> {
|
||||
if (isStoredMessage(msg)) {
|
||||
return msg.photoMaxSizeFilePath;
|
||||
}
|
||||
@@ -1237,7 +1237,7 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<
|
||||
|
||||
const maxSize = await mapPhotoSizeToMax(getPhotoMaxSize(msg.photo));
|
||||
if (maxSize) {
|
||||
let imageFilePath = path.join(photoDir, maxSize.unique_file_id + ".jpg");
|
||||
let imageFilePath: string | null = path.join(photoDir, maxSize.unique_file_id + ".jpg");
|
||||
if (!fs.existsSync(imageFilePath)) {
|
||||
const res = await axios.get<ArrayBuffer>(maxSize.url, {responseType: "arraybuffer"});
|
||||
const src = Buffer.from(res.data);
|
||||
@@ -1268,7 +1268,7 @@ export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[
|
||||
const promises = sizes.filter(s => !fs.existsSync(photoPathByUniqueId(s.file_unique_id)))
|
||||
.map(s => mapPhotoSizeToMax(s));
|
||||
|
||||
const maxSizes = await Promise.all(promises);
|
||||
const maxSizes = (await Promise.all(promises)).filter(e => !!e);
|
||||
|
||||
const imagePromises = maxSizes.map((size) => {
|
||||
return axios.get<ArrayBuffer>(size.url, {responseType: "arraybuffer"});
|
||||
@@ -1287,37 +1287,40 @@ export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const finalPaths = paths.filter(p => p);
|
||||
finalPaths.unshift(...existing);
|
||||
const finalPaths = existing.concat(...paths.filter(p => !!p).map(p => <string>p));
|
||||
return finalPaths;
|
||||
}
|
||||
|
||||
export async function collectReplyChainText(triggerMsg: Message | StoredMessage, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise<MessagePart[]> {
|
||||
export async function collectReplyChainText(triggerMsg: Message | StoredMessage | null, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise<MessagePart[]> {
|
||||
if (!triggerMsg) return [];
|
||||
|
||||
const parts: MessagePart[] = [];
|
||||
|
||||
const pushPart = async (msg: Message | StoredMessage, textRequired: boolean = false) => {
|
||||
const rawText = extractTextMessage(msg);
|
||||
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
|
||||
const imageNames = await loadImagesIfExists(msg);
|
||||
const pushPart = async (msg: Message | StoredMessage | undefined | null, textRequired: boolean = false) => {
|
||||
if (msg) {
|
||||
const rawText = extractTextMessage(msg);
|
||||
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
|
||||
const imageNames = await loadImagesIfExists(msg);
|
||||
|
||||
if (!cleanText && textRequired) return;
|
||||
if (!cleanText && !imageNames?.length) return;
|
||||
if (!cleanText && textRequired) return;
|
||||
if (!cleanText && !imageNames?.length) return;
|
||||
|
||||
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from.id;
|
||||
const firstName = isStoredMessage(msg) ?
|
||||
(await UserStore.get(msg.fromId))?.firstName : msg.from.first_name;
|
||||
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from?.id;
|
||||
const firstName = isStoredMessage(msg) ?
|
||||
(await UserStore.get(msg.fromId))?.firstName : msg.from?.first_name;
|
||||
|
||||
const images = imageNames ? imageNames.map(n => {
|
||||
const filePath = photoPathByUniqueId(n);
|
||||
return Buffer.from(fs.readFileSync(filePath)).toString("base64");
|
||||
}) : null;
|
||||
const images = imageNames ? imageNames.map(n => {
|
||||
const filePath = photoPathByUniqueId(n);
|
||||
return Buffer.from(fs.readFileSync(filePath)).toString("base64");
|
||||
}) : null;
|
||||
|
||||
parts.push({
|
||||
bot: fromId === botUser.id,
|
||||
content: cleanText ? cleanText : "",
|
||||
name: firstName,
|
||||
images: images ? images : []
|
||||
});
|
||||
parts.push({
|
||||
bot: fromId === botUser.id,
|
||||
content: cleanText ? cleanText : "",
|
||||
name: firstName,
|
||||
images: images ? images : []
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const chatId = isStoredMessage(triggerMsg) ? triggerMsg.chatId as number : triggerMsg.chat.id;
|
||||
@@ -1428,7 +1431,8 @@ export async function waveDistortSharp(
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
export async function downloadTelegramFile(filePath: string): Promise<Buffer> {
|
||||
export async function downloadTelegramFile(filePath?: string | null): Promise<Buffer | null> {
|
||||
if (!filePath) return null;
|
||||
const url = `https://api.telegram.org/file/bot${Environment.BOT_TOKEN}/${filePath}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to download file: ${res.status} ${res.statusText}`);
|
||||
@@ -1606,7 +1610,7 @@ export function startIntervalEditor(params: {
|
||||
try {
|
||||
await params.editFn(next);
|
||||
lastSent = next;
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return;
|
||||
logError("edit failed: " + e);
|
||||
}
|
||||
@@ -1624,7 +1628,7 @@ export function startIntervalEditor(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function boolToInt(bool: boolean): number {
|
||||
export function boolToInt(bool: boolean | undefined): number {
|
||||
return bool ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -1645,7 +1649,7 @@ export function buildExcludedSet<
|
||||
const entries = Object.keys(cols)
|
||||
.filter((key) => !excludeSet.has(key))
|
||||
.map((key) => {
|
||||
const realName = (cols as unknown)[key].name; // actual DB column name
|
||||
const realName = (cols as any)[key].name; // actual DB column name
|
||||
return [key, sql.raw(`excluded.${realName}`)] as const;
|
||||
});
|
||||
|
||||
@@ -1674,7 +1678,7 @@ export function getRuntimeInfo(): RuntimeInfo {
|
||||
|
||||
export type PhotoMaxSize = { width: number, height: number, url: string; file_id: string; unique_file_id: string; };
|
||||
|
||||
export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environment.MAX_PHOTO_SIZE): PhotoSize | null {
|
||||
export function getPhotoMaxSize(photos: PhotoSize[] | undefined, target: number = Environment.MAX_PHOTO_SIZE): PhotoSize | null {
|
||||
if (!photos) return null;
|
||||
|
||||
photos = photos.filter(p => Math.max(p.width, p.height) <= target);
|
||||
@@ -1687,12 +1691,11 @@ export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environmen
|
||||
|
||||
return photos.reduce((prev, cur) => {
|
||||
if (!prev) return cur;
|
||||
|
||||
return cur.width * cur.height > prev.width * prev.height ? cur : prev;
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize | null> {
|
||||
export async function mapPhotoSizeToMax(size: PhotoSize | null): Promise<PhotoMaxSize | null> {
|
||||
if (!size) return null;
|
||||
return {
|
||||
width: size.width,
|
||||
@@ -1733,7 +1736,7 @@ export function boolToEmoji(bool: boolean | undefined): string {
|
||||
|
||||
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
|
||||
|
||||
async function processAlbum(groupId: string): Promise<string[]> {
|
||||
async function processAlbum(groupId: string): Promise<string[] | undefined | null> {
|
||||
const entry = albumCache.get(groupId);
|
||||
if (!entry) return;
|
||||
|
||||
@@ -1741,10 +1744,10 @@ async function processAlbum(groupId: string): Promise<string[]> {
|
||||
.filter(m => m.photo)
|
||||
.map(m => m.photo);
|
||||
|
||||
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
|
||||
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)).filter(s => !!s));
|
||||
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
|
||||
|
||||
console.log(`Received album ${groupId} with ${ids.length} photos.`);
|
||||
console.log(`Received album ${groupId} with ${ids?.length} photos.`);
|
||||
console.log("File IDs:", ids);
|
||||
|
||||
albumCache.delete(groupId);
|
||||
@@ -1755,7 +1758,7 @@ export function photoPathByUniqueId(uniqueId: string): string {
|
||||
return path.join(photoDir, uniqueId + ".jpg");
|
||||
}
|
||||
|
||||
export function getCurrentModel(): string {
|
||||
export function getCurrentModel(): string | undefined {
|
||||
switch (Environment.DEFAULT_AI_PROVIDER) {
|
||||
case AiProvider.OLLAMA:
|
||||
return Environment.OLLAMA_MODEL;
|
||||
@@ -1769,7 +1772,7 @@ export function getCurrentModel(): string {
|
||||
}
|
||||
|
||||
export async function getCurrentModelCapabilities(): Promise<AiModelCapabilities | null> {
|
||||
let promise: Promise<AiModelCapabilities | null> = null;
|
||||
let promise: Promise<AiModelCapabilities | null> | null | undefined = null;
|
||||
switch (Environment.DEFAULT_AI_PROVIDER) {
|
||||
case AiProvider.OLLAMA: {
|
||||
const ollamaGetModel = commands.find(c => c instanceof OllamaGetModel);
|
||||
@@ -1779,13 +1782,14 @@ export async function getCurrentModelCapabilities(): Promise<AiModelCapabilities
|
||||
promise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const defaultModelCapabilities = await ollamaGetModel.getModelCapabilities();
|
||||
const imageModelCapabilities = await ollamaGetModel.loadImageModelInfo();
|
||||
|
||||
const result = {
|
||||
vision: (await ollamaGetModel.loadImageModelInfo()).vision,
|
||||
ocr: null,
|
||||
thinking: (await ollamaGetModel.loadThinkModelInfo()).thinking,
|
||||
tools: defaultModelCapabilities.tools,
|
||||
audio: defaultModelCapabilities.audio
|
||||
vision: imageModelCapabilities?.vision,
|
||||
ocr: imageModelCapabilities?.ocr,
|
||||
thinking: (await ollamaGetModel.loadThinkModelInfo())?.thinking,
|
||||
tools: defaultModelCapabilities?.tools,
|
||||
audio: defaultModelCapabilities?.audio
|
||||
};
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
@@ -1824,6 +1828,7 @@ export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
|
||||
|
||||
export async function processNewMessage(msg: Message): Promise<void> {
|
||||
console.log("New Message", msg);
|
||||
if (!msg.from) return;
|
||||
|
||||
const envFile: string = fs.readFileSync(".env").toString();
|
||||
const env = new Map(
|
||||
@@ -2002,14 +2007,16 @@ export async function processNewMessage(msg: Message): Promise<void> {
|
||||
const photos = await processAlbum(groupId);
|
||||
console.log("processedAlbum", photos);
|
||||
|
||||
storedMsg.photoMaxSizeFilePath = photos;
|
||||
await MessageStore.put(storedMsg).catch(logError);
|
||||
if (storedMsg) {
|
||||
storedMsg.photoMaxSizeFilePath = photos;
|
||||
await MessageStore.put(storedMsg).catch(logError);
|
||||
}
|
||||
resolve(true);
|
||||
}, 1000)
|
||||
});
|
||||
} else {
|
||||
const entry = albumCache.get(groupId);
|
||||
entry.messages.push(msg);
|
||||
entry?.messages?.push(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2044,9 +2051,7 @@ export async function processNewMessage(msg: Message): Promise<void> {
|
||||
|
||||
const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText;
|
||||
|
||||
if (Environment.PROCESS_LINKS && await processYouTubeLink(msg, getFirstLink(msg))) return;
|
||||
|
||||
if (msg.chat.type !== "private" && (!msg.reply_to_message || msg.reply_to_message.from.id !== botUser.id) && !startsWithPrefix && !msg.voice) return;
|
||||
if (msg.chat.type !== "private" && (!msg.reply_to_message || msg.reply_to_message.from?.id !== botUser.id) && !startsWithPrefix && !msg.voice) return;
|
||||
|
||||
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
||||
|
||||
@@ -2060,167 +2065,52 @@ export async function processNewMessage(msg: Message): Promise<void> {
|
||||
const input = path.join(Environment.DATA_PATH, "input.ogg");
|
||||
const output = path.join(Environment.DATA_PATH, "output.wav")
|
||||
|
||||
try {
|
||||
fs.writeFileSync(input, fileBuffer);
|
||||
await performFFmpeg(() =>
|
||||
ffmpeg(input)
|
||||
.toFormat("wav")
|
||||
.save(output)
|
||||
.on("progress", (progress) => {
|
||||
console.log("progress", progress);
|
||||
})
|
||||
);
|
||||
if (fileBuffer) {
|
||||
try {
|
||||
fs.writeFileSync(input, fileBuffer);
|
||||
await performFFmpeg(() =>
|
||||
ffmpeg(input)
|
||||
.toFormat("wav")
|
||||
.save(output)
|
||||
.on("progress", (progress) => {
|
||||
console.log("progress", progress);
|
||||
})
|
||||
);
|
||||
|
||||
fileBuffer = fs.readFileSync(output);
|
||||
voiceB64 = fileBuffer.toString("base64");
|
||||
fs.rmSync(input);
|
||||
fs.rmSync(output);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
fileBuffer = fs.readFileSync(output);
|
||||
voiceB64 = fileBuffer.toString("base64");
|
||||
fs.rmSync(input);
|
||||
fs.rmSync(output);
|
||||
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (Environment.DEFAULT_AI_PROVIDER) {
|
||||
case AiProvider.OLLAMA: {
|
||||
await commands.find(e => e instanceof OllamaChat).executeOllama(msg, textToCheck, false, voiceB64);
|
||||
await commands.find(e => e instanceof OllamaChat)?.executeOllama(msg, textToCheck, false, voiceB64);
|
||||
break;
|
||||
}
|
||||
case AiProvider.GEMINI: {
|
||||
await commands.find(e => e instanceof GeminiChat).executeGemini(msg, textToCheck);
|
||||
await commands.find(e => e instanceof GeminiChat)?.executeGemini(msg, textToCheck);
|
||||
break;
|
||||
}
|
||||
case AiProvider.MISTRAL: {
|
||||
await commands.find(e => e instanceof MistralChat).executeMistral(msg, textToCheck);
|
||||
await commands.find(e => e instanceof MistralChat)?.executeMistral(msg, textToCheck);
|
||||
break;
|
||||
}
|
||||
case AiProvider.OPENAI: {
|
||||
await commands.find(e => e instanceof OpenAIChat).executeOpenAI(msg, textToCheck);
|
||||
await commands.find(e => e instanceof OpenAIChat)?.executeOpenAI(msg, textToCheck);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, id?: string): Promise<boolean> {
|
||||
if (!url && !id) return false;
|
||||
|
||||
let waitMessage: Message | null = msg.from.id === botUser.id ? msg : null;
|
||||
let videoId: string | null = null;
|
||||
|
||||
try {
|
||||
try {
|
||||
videoId = id || getYouTubeVideoId(url);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const yt = commands.find(e => e instanceof YouTubeDownload);
|
||||
|
||||
if (await checkRequirements(yt, msg)) {
|
||||
if (!waitMessage) {
|
||||
waitMessage = await replyToMessage({
|
||||
message: msg,
|
||||
text: "⏳ Ищу информацию о видео..."
|
||||
});
|
||||
} else {
|
||||
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 " + videoId).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);
|
||||
|
||||
await editMessageText({
|
||||
message: waitMessage,
|
||||
text: Environment.errorText,
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
TryAgain.withData("/ytinfo " + videoId).asButton()
|
||||
]]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function processEditedMessage(msg: Message): Promise<void> {
|
||||
console.log("Edited Message", msg);
|
||||
if (!msg.from) return;
|
||||
|
||||
await UserStore.put(msg.from);
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {videoDir, videoTempDir} from "../index";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
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;
|
||||
const match = url.match(regex);
|
||||
if (!match || !match[1]) throw new Error("Invalid YouTube or Shorts URL");
|
||||
return match[1];
|
||||
}
|
||||
|
||||
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
|
||||
}> {
|
||||
const start = Date.now();
|
||||
let buffer: Buffer | null = null;
|
||||
|
||||
try {
|
||||
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));
|
||||
return {
|
||||
time: Date.now() - start,
|
||||
exists: true,
|
||||
buffer: buffer
|
||||
};
|
||||
}
|
||||
|
||||
Platform.shim.eval = async (data: Types.BuildScriptResult, env: Record<string, Types.VMPrimative>) => {
|
||||
const properties = [];
|
||||
if (env.n) properties.push(`n: exportedVars.nFunction("${env.n}")`);
|
||||
if (env.sig) properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
|
||||
|
||||
const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
|
||||
return new Function(code)();
|
||||
};
|
||||
|
||||
const yt = await getYT();
|
||||
|
||||
const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"});
|
||||
console.log("Video info", videoInfo);
|
||||
|
||||
console.log(`Fetching metadata for: ${videoId}...`);
|
||||
|
||||
const targetQuality = "360p";
|
||||
|
||||
const videoFormat = videoInfo.streaming_data?.formats.find(f => f.quality_label.startsWith(targetQuality))
|
||||
|| videoInfo.streaming_data?.adaptive_formats.find(f => f.quality_label.startsWith(targetQuality));
|
||||
|
||||
const audioFormat = videoInfo.chooseFormat({type: "audio", quality: "best", language: "original"});
|
||||
|
||||
console.log("Video format: ", videoFormat);
|
||||
console.log("Audio Format: ", audioFormat);
|
||||
|
||||
if (!videoFormat) {
|
||||
console.log(`Quality ${targetQuality} not found. Falling back to best available.`);
|
||||
}
|
||||
|
||||
const videoWebStream = await videoInfo.download({
|
||||
itag: videoFormat.itag,
|
||||
client: "ANDROID"
|
||||
});
|
||||
|
||||
const audioWebStream = await videoInfo.download({
|
||||
itag: audioFormat.itag,
|
||||
client: "ANDROID"
|
||||
});
|
||||
|
||||
const videoStream = Readable.fromWeb(videoWebStream as any);
|
||||
const audioStream = Readable.fromWeb(audioWebStream as any);
|
||||
|
||||
const videoPath = path.join(videoTempDir, `temp_video_${videoId}.mp4`);
|
||||
const audioPath = path.join(videoTempDir, `temp_audio_${videoId}.mp4`);
|
||||
|
||||
const writeStream = (stream: any, path: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(path);
|
||||
stream.pipe(file);
|
||||
file.on("finish", resolve);
|
||||
file.on("error", reject);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
writeStream(videoStream, videoPath),
|
||||
writeStream(audioStream, audioPath)
|
||||
]);
|
||||
|
||||
await performFFmpeg(() =>
|
||||
ffmpeg()
|
||||
.input(videoPath)
|
||||
.input(audioPath)
|
||||
.videoCodec("copy")
|
||||
.audioCodec("copy")
|
||||
.save(filePath)
|
||||
.on("progress", (progress) => {
|
||||
console.log("progress", progress);
|
||||
})
|
||||
).catch(logError);
|
||||
|
||||
fs.unlinkSync(videoPath);
|
||||
fs.unlinkSync(audioPath);
|
||||
|
||||
buffer = fs.readFileSync(filePath);
|
||||
|
||||
console.log(`✅ Saved to ${videoId}.mp4`);
|
||||
} catch (error) {
|
||||
console.error("❌ Download failed:", error instanceof Error ? error.message : error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const end = Date.now();
|
||||
const diff = end - start;
|
||||
console.log(`Video downloaded.\ntook ${diff}ms`);
|
||||
|
||||
return {
|
||||
time: diff,
|
||||
buffer: buffer,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user