Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83af496361 |
Generated
+25
-1341
File diff suppressed because it is too large
Load Diff
+2
-6
@@ -17,18 +17,14 @@
|
|||||||
"drizzle-orm": "^1.0.0-beta.12-5845444",
|
"drizzle-orm": "^1.0.0-beta.12-5845444",
|
||||||
"emoji-regex": "^10.6.0",
|
"emoji-regex": "^10.6.0",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"puppeteer": "^24.36.1",
|
|
||||||
"puppeteer-extra": "^3.3.6",
|
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"systeminformation": "^5.30.6",
|
"systeminformation": "^5.30.6",
|
||||||
"twemoji": "^14.0.2",
|
"twemoji": "^14.0.2",
|
||||||
"typescript-telegram-bot-api": "^0.11.0",
|
"typescript-telegram-bot-api": "^0.11.0"
|
||||||
"youtubei.js": "^16.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.8",
|
"@types/bun": "^1.3.7",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
oldReplyToMessage,
|
oldReplyToMessage,
|
||||||
startIntervalEditor
|
startIntervalEditor
|
||||||
} from "../util/utils";
|
} from "../util/utils";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
export class GeminiChat extends ChatCommand {
|
export class GeminiChat extends ChatCommand {
|
||||||
command = "gemini";
|
command = "gemini";
|
||||||
@@ -65,9 +66,10 @@ export class GeminiChat extends ChatCommand {
|
|||||||
const images = messageParts[0].images;
|
const images = messageParts[0].images;
|
||||||
|
|
||||||
images.forEach(image => {
|
images.forEach(image => {
|
||||||
|
const base64Image = Buffer.from(fs.readFileSync(image)).toString("base64");
|
||||||
input.push({
|
input.push({
|
||||||
type: "image",
|
type: "image",
|
||||||
data: image,
|
data: base64Image,
|
||||||
mime_type: "image/png"
|
mime_type: "image/png"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
escapeMarkdownV2Text,
|
escapeMarkdownV2Text,
|
||||||
logError,
|
logError,
|
||||||
oldReplyToMessage,
|
oldReplyToMessage,
|
||||||
|
photoPathByUniqueId,
|
||||||
startIntervalEditor
|
startIntervalEditor
|
||||||
} from "../util/utils";
|
} from "../util/utils";
|
||||||
import {Environment} from "../common/environment";
|
import {Environment} from "../common/environment";
|
||||||
import {bot, mistralAi} from "../index";
|
import {bot, mistralAi} from "../index";
|
||||||
import {MessageStore} from "../common/message-store";
|
import {MessageStore} from "../common/message-store";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
export class MistralChat extends ChatCommand {
|
export class MistralChat extends ChatCommand {
|
||||||
command = "mistral";
|
command = "mistral";
|
||||||
@@ -44,9 +46,10 @@ export class MistralChat extends ChatCommand {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const image of part.images) {
|
for (const image of part.images) {
|
||||||
|
const base64Image = Buffer.from(fs.readFileSync(photoPathByUniqueId(image))).toString("base64");
|
||||||
content.push({
|
content.push({
|
||||||
type: "image_url",
|
type: "image_url",
|
||||||
imageUrl: "data:image/jpeg;base64," + image
|
imageUrl: "data:image/jpeg;base64," + base64Image
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +84,6 @@ export class MistralChat extends ChatCommand {
|
|||||||
|
|
||||||
const stream = await mistralAi.chat.stream({
|
const stream = await mistralAi.chat.stream({
|
||||||
model: Environment.MISTRAL_MODEL,
|
model: Environment.MISTRAL_MODEL,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
messages: chatMessages as any
|
messages: chatMessages as any
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {OllamaCancel} from "../callback_commands/ollama-cancel";
|
|||||||
import {OllamaGetModel} from "./ollama-get-model";
|
import {OllamaGetModel} from "./ollama-get-model";
|
||||||
|
|
||||||
export class OllamaChat extends ChatCommand {
|
export class OllamaChat extends ChatCommand {
|
||||||
command = ["ollama", "ollamathink"];
|
command = "ollama";
|
||||||
argsMode = "required" as const;
|
argsMode = "required" as const;
|
||||||
|
|
||||||
title = "/ollama";
|
title = "/ollama";
|
||||||
@@ -24,10 +24,10 @@ export class OllamaChat extends ChatCommand {
|
|||||||
|
|
||||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||||
console.log("match", match);
|
console.log("match", match);
|
||||||
return this.executeOllama(msg, match?.[3], match?.[1]?.toLowerCase()?.startsWith("ollamathink"));
|
return this.executeOllama(msg, match?.[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeOllama(msg: Message, text: string, think: boolean = false): Promise<void> {
|
async executeOllama(msg: Message, text: string): Promise<void> {
|
||||||
if (!text || text.trim().length === 0) return;
|
if (!text || text.trim().length === 0) return;
|
||||||
|
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
@@ -55,9 +55,9 @@ export class OllamaChat extends ChatCommand {
|
|||||||
return total + (curr.images?.length ?? 0);
|
return total + (curr.images?.length ?? 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
if (!think && imagesCount) {
|
if (imagesCount) {
|
||||||
try {
|
try {
|
||||||
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadImageModelInfo();
|
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadModelInfo();
|
||||||
if (modelInfo) {
|
if (modelInfo) {
|
||||||
const caps = modelInfo.capabilities || [];
|
const caps = modelInfo.capabilities || [];
|
||||||
if (!caps.includes("vision")) {
|
if (!caps.includes("vision")) {
|
||||||
@@ -73,38 +73,20 @@ export class OllamaChat extends ChatCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (think) {
|
|
||||||
try {
|
|
||||||
const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo();
|
|
||||||
if (modelInfo) {
|
|
||||||
const caps = modelInfo.capabilities || [];
|
|
||||||
if (!caps.includes("thinking")) {
|
|
||||||
await replyToMessage({
|
|
||||||
message: msg,
|
|
||||||
text: "Моя текущая модель не умеет размышлять 🥹"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuid = crypto.randomUUID();
|
const uuid = crypto.randomUUID();
|
||||||
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
|
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
|
||||||
|
|
||||||
waitMessage = await replyToMessage({
|
waitMessage = await replyToMessage({
|
||||||
message: msg,
|
message: msg,
|
||||||
text: (!think && imagesCount) ?
|
text: imagesCount ?
|
||||||
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
|
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
|
||||||
: Environment.waitText
|
: Environment.waitText
|
||||||
});
|
});
|
||||||
|
|
||||||
const stream = await ollama.chat({
|
const stream = await ollama.chat({
|
||||||
model: think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL,
|
model: Environment.OLLAMA_MODEL,
|
||||||
stream: true,
|
stream: true,
|
||||||
think: think,
|
think: false,
|
||||||
messages: chatMessages,
|
messages: chatMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,7 +153,6 @@ export class OllamaChat extends ChatCommand {
|
|||||||
message_id: waitMessage.message_id,
|
message_id: waitMessage.message_id,
|
||||||
text: "🤔 Размышляю...",
|
text: "🤔 Размышляю...",
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
reply_markup: cancelMarkup
|
|
||||||
}).catch(logError);
|
}).catch(logError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,75 +11,25 @@ export class OllamaGetModel extends ChatCommand {
|
|||||||
|
|
||||||
async execute(msg: Message): Promise<void> {
|
async execute(msg: Message): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const model = Environment.OLLAMA_MODEL;
|
const showResponse = await this.loadModelInfo();
|
||||||
const imageModel = Environment.OLLAMA_IMAGE_MODEL;
|
|
||||||
const thinkModel = Environment.OLLAMA_THINK_MODEL;
|
|
||||||
|
|
||||||
const promises: (Promise<ShowResponse | null> | null)[] = [this.loadModelInfo()];
|
const caps = showResponse.capabilities;
|
||||||
|
|
||||||
if (imageModel && imageModel !== model) {
|
const text = "```Ollama\n" +
|
||||||
promises.push(this.loadImageModelInfo());
|
`model: ${Environment.OLLAMA_MODEL}\n\n` +
|
||||||
} else {
|
`vision: ${boolToEmoji(caps.includes("vision"))}\n` +
|
||||||
promises.push(null);
|
`thinking: ${boolToEmoji(caps.includes("thinking"))}\n` +
|
||||||
}
|
`tools: ${boolToEmoji(caps.includes("tools"))}`
|
||||||
|
+ "```";
|
||||||
if (thinkModel && thinkModel !== model) {
|
|
||||||
promises.push(this.loadThinkModelInfo());
|
|
||||||
} else {
|
|
||||||
promises.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const infos = await Promise.all(promises);
|
|
||||||
|
|
||||||
let modelInfo = infos[0];
|
|
||||||
const modelText = "```Text\n" + this.getModelText(model, modelInfo) + "```";
|
|
||||||
|
|
||||||
modelInfo = infos[1];
|
|
||||||
const imageModelText = modelInfo ?
|
|
||||||
"```Image\n" + this.getModelText(imageModel, modelInfo) + "```" : null;
|
|
||||||
|
|
||||||
modelInfo = infos[2];
|
|
||||||
const thinkModelText = modelInfo ?
|
|
||||||
"```Think\n" + this.getModelText(thinkModel, modelInfo) + "```" : null;
|
|
||||||
|
|
||||||
const modelInfos = [modelText];
|
|
||||||
if (imageModelText) {
|
|
||||||
modelInfos.push(imageModelText);
|
|
||||||
}
|
|
||||||
if (thinkModelText) {
|
|
||||||
modelInfos.push(thinkModelText);
|
|
||||||
}
|
|
||||||
|
|
||||||
await replyToMessage({
|
|
||||||
message: msg,
|
|
||||||
text: modelInfos.join("\n\n"),
|
|
||||||
parse_mode: "Markdown"
|
|
||||||
}).catch(logError);
|
|
||||||
|
|
||||||
|
await replyToMessage({message: msg, text: text, parse_mode: "Markdown"}).catch(logError);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e);
|
logError(e);
|
||||||
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
|
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModelText(model: string, info: ShowResponse): string {
|
|
||||||
const caps = info.capabilities;
|
|
||||||
|
|
||||||
return `model: ${model}\n\n` +
|
|
||||||
`vision: ${boolToEmoji(caps.includes("vision"))}\n` +
|
|
||||||
`thinking: ${boolToEmoji(caps.includes("thinking"))}\n` +
|
|
||||||
`tools: ${boolToEmoji(caps.includes("tools"))}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadModelInfo(): Promise<ShowResponse | null> {
|
async loadModelInfo(): Promise<ShowResponse | null> {
|
||||||
return ollama.show({model: Environment.OLLAMA_MODEL});
|
return ollama.show({model: Environment.OLLAMA_MODEL});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadImageModelInfo(): Promise<ShowResponse | null> {
|
|
||||||
return ollama.show({model: Environment.OLLAMA_IMAGE_MODEL});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadThinkModelInfo(): Promise<ShowResponse | null> {
|
|
||||||
return ollama.show({model: Environment.OLLAMA_THINK_MODEL});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import {ChatCommand} from "../base/chat-command";
|
|
||||||
import {Message} from "typescript-telegram-bot-api";
|
|
||||||
import {logError, replyToMessage} from "../util/utils";
|
|
||||||
import {bot} from "../index";
|
|
||||||
import {downloadVideoFromYouTube} from "../util/ytdl";
|
|
||||||
|
|
||||||
export class YouTubeDownload extends ChatCommand {
|
|
||||||
command = ["ytdl", "youtube"];
|
|
||||||
argsMode = "required" as const;
|
|
||||||
|
|
||||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
|
||||||
const url = match?.[3];
|
|
||||||
return this.downloadYouTubeVideo(msg, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadYouTubeVideo(msg: Message, url: string): Promise<void> {
|
|
||||||
let waitMessage: Message | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
waitMessage = await replyToMessage({message: msg, text: "⏳ Секунду..."});
|
|
||||||
|
|
||||||
const {time, exists, buffer} = await downloadVideoFromYouTube(url);
|
|
||||||
if (buffer) {
|
|
||||||
const start = Date.now();
|
|
||||||
waitMessage = await bot.editMessageMedia({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_id: waitMessage.message_id,
|
|
||||||
media: {
|
|
||||||
type: "video",
|
|
||||||
media: buffer
|
|
||||||
}
|
|
||||||
}) as Message;
|
|
||||||
|
|
||||||
const diff = Date.now() - start;
|
|
||||||
waitMessage = await bot.editMessageCaption({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_id: waitMessage.message_id,
|
|
||||||
caption: `✅ [Видео](${url})` + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
|
|
||||||
parse_mode: "MarkdownV2"
|
|
||||||
}) as Message;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
|
|
||||||
if (waitMessage && "text" in waitMessage) {
|
|
||||||
await bot.editMessageText({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_id: waitMessage.message_id,
|
|
||||||
text: `⚠️ Произошла ошибка.\n${e}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {saveData} from "../db/database";
|
import {saveData} from "../db/database";
|
||||||
import {Answers} from "../model/answers";
|
import {Answers} from "../model/answers";
|
||||||
import {ifTrue} from "../util/utils";
|
|
||||||
|
|
||||||
export class Environment {
|
export class Environment {
|
||||||
static BOT_TOKEN: string;
|
static BOT_TOKEN: string;
|
||||||
@@ -28,8 +27,6 @@ export class Environment {
|
|||||||
|
|
||||||
static OLLAMA_ADDRESS?: string;
|
static OLLAMA_ADDRESS?: string;
|
||||||
static OLLAMA_MODEL?: string;
|
static OLLAMA_MODEL?: string;
|
||||||
static OLLAMA_IMAGE_MODEL?: string;
|
|
||||||
static OLLAMA_THINK_MODEL?: string;
|
|
||||||
static OLLAMA_API_KEY?: string;
|
static OLLAMA_API_KEY?: string;
|
||||||
|
|
||||||
static GEMINI_API_KEY?: string;
|
static GEMINI_API_KEY?: string;
|
||||||
@@ -47,26 +44,24 @@ export class Environment {
|
|||||||
|
|
||||||
static load() {
|
static load() {
|
||||||
Environment.BOT_TOKEN = process.env.BOT_TOKEN;
|
Environment.BOT_TOKEN = process.env.BOT_TOKEN;
|
||||||
Environment.TEST_ENVIRONMENT = ifTrue(process.env.TEST_ENVIRONMENT);
|
Environment.TEST_ENVIRONMENT = process.env.TEST_ENVIRONMENT === "true";
|
||||||
Environment.CHAT_IDS_WHITELIST = new Set(process.env.CHAT_IDS_WHITELIST?.split(",")?.map(e => parseInt(e.trim(), 10)) || []);
|
Environment.CHAT_IDS_WHITELIST = new Set(process.env.CHAT_IDS_WHITELIST?.split(",")?.map(e => parseInt(e.trim(), 10)) || []);
|
||||||
Environment.BOT_PREFIX = process.env.BOT_PREFIX || "";
|
Environment.BOT_PREFIX = process.env.BOT_PREFIX || "";
|
||||||
Environment.CREATOR_ID = parseInt(process.env.CREATOR_ID || "");
|
Environment.CREATOR_ID = parseInt(process.env.CREATOR_ID || "");
|
||||||
Environment.IS_DOCKER = ifTrue(process.env.IS_DOCKER);
|
Environment.IS_DOCKER = process.env.IS_DOCKER == "true";
|
||||||
Environment.DATA_PATH = Environment.IS_DOCKER ? "/" + path.join("config", "data") : "data";
|
Environment.DATA_PATH = Environment.IS_DOCKER ? "/" + path.join("config", "data") : "data";
|
||||||
Environment.DB_PATH = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME);
|
Environment.DB_PATH = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME);
|
||||||
|
|
||||||
Environment.ONLY_FOR_CREATOR_MODE = ifTrue(process.env.ONLY_FOR_CREATOR_MODE);
|
Environment.ONLY_FOR_CREATOR_MODE = process.env.ONLY_FOR_CREATOR_MODE == "true";
|
||||||
|
|
||||||
Environment.USE_NAMES_IN_PROMPT = ifTrue(process.env.USE_NAMES_IN_PROMPT);
|
Environment.USE_NAMES_IN_PROMPT = process.env.USE_NAMES_IN_PROMPT == "true";
|
||||||
|
|
||||||
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
|
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
|
||||||
|
|
||||||
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
|
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
|
||||||
|
|
||||||
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
|
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
|
||||||
Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:4b";
|
Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL;
|
||||||
Environment.OLLAMA_IMAGE_MODEL = process.env.OLLAMA_IMAGE_MODEL || Environment.OLLAMA_MODEL;
|
|
||||||
Environment.OLLAMA_THINK_MODEL = process.env.OLLAMA_THINK_MODEL || Environment.OLLAMA_MODEL;
|
|
||||||
Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
|
Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
|
||||||
|
|
||||||
Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {StoredMessage} from "../model/stored-message";
|
import {StoredMessage} from "../model/stored-message";
|
||||||
import {Message} from "typescript-telegram-bot-api";
|
import {Message} from "typescript-telegram-bot-api";
|
||||||
import {extractTextMessage, getPhotoMaxSize, isStoredMessage} from "../util/utils";
|
import {extractTextMessage, isStoredMessage} from "../util/utils";
|
||||||
import {messageDao} from "../index";
|
import {messageDao} from "../index";
|
||||||
|
|
||||||
export class MessageStore {
|
export class MessageStore {
|
||||||
@@ -22,7 +22,6 @@ export class MessageStore {
|
|||||||
fromId: m.from.id,
|
fromId: m.from.id,
|
||||||
text: extractTextMessage(m),
|
text: extractTextMessage(m),
|
||||||
date: m.date ?? 0,
|
date: m.date ?? 0,
|
||||||
photoMaxSizeFilePath: m.photo ? [getPhotoMaxSize(m.photo).file_unique_id] : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.map.set(this.key(msg.chatId, msg.id), msg);
|
this.map.set(this.key(msg.chatId, msg.id), msg);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class UserStore {
|
|||||||
return this.map;
|
return this.map;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async put(u: User): Promise<StoredUser> {
|
static async put(u: User) {
|
||||||
const user: StoredUser = {
|
const user: StoredUser = {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
isBot: u.is_bot,
|
isBot: u.is_bot,
|
||||||
@@ -22,7 +22,6 @@ export class UserStore {
|
|||||||
this.map.set(u.id, user);
|
this.map.set(u.id, user);
|
||||||
|
|
||||||
await userDao.insert(userDao.mapTo([u]));
|
await userDao.insert(userDao.mapTo([u]));
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async get(id: number): Promise<StoredUser | null> {
|
static async get(id: number): Promise<StoredUser | null> {
|
||||||
|
|||||||
+1
-36
@@ -3,10 +3,8 @@ import {Environment} from "./common/environment";
|
|||||||
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
|
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
|
||||||
import {ChatCommand} from "./base/chat-command";
|
import {ChatCommand} from "./base/chat-command";
|
||||||
import {
|
import {
|
||||||
delay,
|
|
||||||
extractTextMessage,
|
extractTextMessage,
|
||||||
findAndExecuteCallbackCommand,
|
findAndExecuteCallbackCommand,
|
||||||
ignore,
|
|
||||||
initSystemSpecs,
|
initSystemSpecs,
|
||||||
logError,
|
logError,
|
||||||
processNewMessage
|
processNewMessage
|
||||||
@@ -68,11 +66,6 @@ import {GeminiGetModel} from "./commands/gemini-get-model";
|
|||||||
import {GeminiSetModel} from "./commands/gemini-set-model";
|
import {GeminiSetModel} from "./commands/gemini-set-model";
|
||||||
import {Debug} from "./commands/debug";
|
import {Debug} from "./commands/debug";
|
||||||
import {GeminiGenerateImage} from "./commands/gemini-generate-image";
|
import {GeminiGenerateImage} from "./commands/gemini-generate-image";
|
||||||
import {YouTubeDownload} from "./commands/youtube-download";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import {setInterval} from "node:timers";
|
|
||||||
import {clearUpVideoFolder} from "./util/files";
|
|
||||||
|
|
||||||
process.setUncaughtExceptionCaptureCallback(logError);
|
process.setUncaughtExceptionCaptureCallback(logError);
|
||||||
|
|
||||||
@@ -155,8 +148,6 @@ export const chatCommands: ChatCommand[] = [
|
|||||||
|
|
||||||
new Shutdown(),
|
new Shutdown(),
|
||||||
new Leave(),
|
new Leave(),
|
||||||
|
|
||||||
new YouTubeDownload()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const callbackCommands: CallbackCommand[] = [
|
export const callbackCommands: CallbackCommand[] = [
|
||||||
@@ -196,12 +187,7 @@ if (Environment.MISTRAL_API_KEY) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const photoDir = path.join(Environment.DATA_PATH, "photo");
|
|
||||||
export const videoDir = path.join(Environment.DATA_PATH, "video");
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
|
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
|
||||||
`DATA_PATH: ${Environment.DATA_PATH}\n` +
|
`DATA_PATH: ${Environment.DATA_PATH}\n` +
|
||||||
@@ -209,25 +195,6 @@ async function main() {
|
|||||||
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}`
|
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}`
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.mkdir(photoDir, ignore);
|
|
||||||
fs.mkdir(videoDir, ignore);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const midnight = new Date();
|
|
||||||
midnight.setHours(0, 0, 0, 0);
|
|
||||||
midnight.setDate(now.getDate() + 1);
|
|
||||||
|
|
||||||
const diff = midnight.getTime() - now.getTime();
|
|
||||||
console.log("Clearing up videos will be started in " + diff + "ms");
|
|
||||||
|
|
||||||
delay(diff).then(() => {
|
|
||||||
setInterval(() => {
|
|
||||||
console.log("Started clearing up videos");
|
|
||||||
clearUpVideoFolder();
|
|
||||||
}, 1000 * 60 * 60 * 24);
|
|
||||||
});
|
|
||||||
|
|
||||||
const commands = chatCommands.filter(cmd => {
|
const commands = chatCommands.filter(cmd => {
|
||||||
return cmd.title && cmd.title.startsWith("/") && cmd.title.split(" ").length === 1 && cmd.description;
|
return cmd.title && cmd.title.startsWith("/") && cmd.title.split(" ").length === 1 && cmd.description;
|
||||||
}).map(cmd => {
|
}).map(cmd => {
|
||||||
@@ -249,9 +216,7 @@ async function main() {
|
|||||||
await UserStore.put(botUser);
|
await UserStore.put(botUser);
|
||||||
await bot.startPolling();
|
await bot.startPolling();
|
||||||
|
|
||||||
const end = Date.now();
|
console.log("Bot started!");
|
||||||
const diff = Math.abs(end - start);
|
|
||||||
console.log(`Bot started in ${diff}ms!`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
logError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export type OllamaRequest = {
|
export type OllamaRequest = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
stream: any;
|
stream: any;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
fromId: number;
|
fromId: number;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import {logError} from "./utils";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import {videoDir} from "../index";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export function clearUpVideoFolder() {
|
|
||||||
fs.readdir(videoDir, (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
logError(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filenamesToDelete: string[] = [];
|
|
||||||
|
|
||||||
files.forEach((filename, index) => {
|
|
||||||
fs.stat(path.join(videoDir, filename), (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
logError(err);
|
|
||||||
} else {
|
|
||||||
const then = stats.mtime.getTime() / 1000;
|
|
||||||
const now = Date.now() / 1000;
|
|
||||||
const diff = Math.abs(now - then);
|
|
||||||
const moreThanOneDay = diff >= 60 * 60 * 24;
|
|
||||||
if (moreThanOneDay) {
|
|
||||||
filenamesToDelete.push(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === files.length - 1) {
|
|
||||||
console.log("filenamesToDelete", filenamesToDelete);
|
|
||||||
if (filenamesToDelete.length) {
|
|
||||||
filenamesToDelete.forEach((filename) => {
|
|
||||||
const fullPath = path.join(videoDir, filename);
|
|
||||||
fs.rm(fullPath, logError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+41
-103
@@ -27,8 +27,6 @@ import {MessageStore} from "../common/message-store";
|
|||||||
import {SystemInfo} from "../commands/system-info";
|
import {SystemInfo} from "../commands/system-info";
|
||||||
import {PrefixResponse} from "../commands/prefix-response";
|
import {PrefixResponse} from "../commands/prefix-response";
|
||||||
import {OllamaChat} from "../commands/ollama-chat";
|
import {OllamaChat} from "../commands/ollama-chat";
|
||||||
import {getYouTubeVideoId} from "./ytdl";
|
|
||||||
import {YouTubeDownload} from "../commands/youtube-download";
|
|
||||||
|
|
||||||
export const ignore = () => {
|
export const ignore = () => {
|
||||||
};
|
};
|
||||||
@@ -471,14 +469,10 @@ export function extractTextMessage(msg: Message | StoredMessage | string): strin
|
|||||||
export function cutPrefixes(msg: Message | StoredMessage | string): string {
|
export function cutPrefixes(msg: Message | StoredMessage | string): string {
|
||||||
const prefixes = [
|
const prefixes = [
|
||||||
Environment.BOT_PREFIX,
|
Environment.BOT_PREFIX,
|
||||||
`/ollamathink@${botUser.username}`,
|
|
||||||
"/ollamathink",
|
|
||||||
`/ollama@${botUser.username}`,
|
|
||||||
"/ollama",
|
|
||||||
`/gemini@${botUser.username}`,
|
`/gemini@${botUser.username}`,
|
||||||
"/gemini",
|
"/gemini",
|
||||||
`/mistral@${botUser.username}`,
|
`/mistral@${botUser.username}`,
|
||||||
"/mistral",
|
"/mistral"
|
||||||
];
|
];
|
||||||
|
|
||||||
const text = extractTextMessage(msg);
|
const text = extractTextMessage(msg);
|
||||||
@@ -503,20 +497,11 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<
|
|||||||
return msg.photoMaxSizeFilePath;
|
return msg.photoMaxSizeFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!msg.photo?.length) return;
|
|
||||||
|
|
||||||
const imageFilePaths: string[] = [];
|
const imageFilePaths: string[] = [];
|
||||||
|
|
||||||
for (const size of msg.photo) {
|
const maxSize = await getPhotoMaxSize(msg.photo);
|
||||||
const exists = fs.existsSync(photoPathByUniqueId(size.file_unique_id));
|
|
||||||
if (exists) {
|
|
||||||
return [size.file_unique_id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxSize = await mapPhotoSizeToMax(getPhotoMaxSize(msg.photo));
|
|
||||||
if (maxSize) {
|
if (maxSize) {
|
||||||
const imagePath = path.join(Environment.DATA_PATH, "photo");
|
const imagePath = path.join(Environment.DATA_PATH, "temp");
|
||||||
if (!fs.existsSync(imagePath)) {
|
if (!fs.existsSync(imagePath)) {
|
||||||
fs.mkdirSync(imagePath);
|
fs.mkdirSync(imagePath);
|
||||||
}
|
}
|
||||||
@@ -542,23 +527,14 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<
|
|||||||
return imageFilePaths;
|
return imageFilePaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[] | null> {
|
export async function loadImagesFromFileIds(maxSizes: PhotoMaxSize[]): Promise<string[] | null> {
|
||||||
if (!sizes?.length) return null;
|
if (!maxSizes?.length) return null;
|
||||||
|
|
||||||
const dataPath = path.join(Environment.DATA_PATH, "photo");
|
const dataPath = path.join(Environment.DATA_PATH, "temp");
|
||||||
if (!fs.existsSync(dataPath)) {
|
if (!fs.existsSync(dataPath)) {
|
||||||
fs.mkdirSync(dataPath);
|
fs.mkdirSync(dataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing =
|
|
||||||
sizes.filter(s => fs.existsSync(photoPathByUniqueId(s.file_unique_id)))
|
|
||||||
.map(s => s.file_unique_id);
|
|
||||||
|
|
||||||
const promises = sizes.filter(s => !fs.existsSync(photoPathByUniqueId(s.file_unique_id)))
|
|
||||||
.map(s => mapPhotoSizeToMax(s));
|
|
||||||
|
|
||||||
const maxSizes = await Promise.all(promises);
|
|
||||||
|
|
||||||
const imagePromises = maxSizes.map((size) => {
|
const imagePromises = maxSizes.map((size) => {
|
||||||
return axios.get<ArrayBuffer>(size.url, {responseType: "arraybuffer"});
|
return axios.get<ArrayBuffer>(size.url, {responseType: "arraybuffer"});
|
||||||
});
|
});
|
||||||
@@ -566,19 +542,17 @@ export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[
|
|||||||
const responses = await Promise.all(imagePromises);
|
const responses = await Promise.all(imagePromises);
|
||||||
const paths = responses.map((res, index) => {
|
const paths = responses.map((res, index) => {
|
||||||
try {
|
try {
|
||||||
const uniqueFileId = maxSizes[index].unique_file_id;
|
const imageFilePath = path.join(dataPath, maxSizes[index].unique_file_id + ".jpg");
|
||||||
const imageFilePath = path.join(dataPath, uniqueFileId + ".jpg");
|
|
||||||
const src = Buffer.from(res.data);
|
const src = Buffer.from(res.data);
|
||||||
fs.writeFileSync(imageFilePath, src);
|
fs.writeFileSync(imageFilePath, src);
|
||||||
return uniqueFileId;
|
return imageFilePath;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e);
|
logError(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const finalPaths = paths.filter(p => p);
|
|
||||||
finalPaths.unshift(...existing);
|
return paths.filter(p => 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, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise<MessagePart[]> {
|
||||||
@@ -587,20 +561,15 @@ export async function collectReplyChainText(triggerMsg: Message | StoredMessage,
|
|||||||
const pushPart = async (msg: Message | StoredMessage, textRequired: boolean = false) => {
|
const pushPart = async (msg: Message | StoredMessage, textRequired: boolean = false) => {
|
||||||
const rawText = extractTextMessage(msg);
|
const rawText = extractTextMessage(msg);
|
||||||
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
|
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
|
||||||
const imageNames = await loadImagesIfExists(msg);
|
const images = await loadImagesIfExists(msg);
|
||||||
|
|
||||||
if (!cleanText && textRequired) return;
|
if (!cleanText && textRequired) return;
|
||||||
if (!cleanText && !imageNames?.length) return;
|
if (!cleanText && !images?.length) return;
|
||||||
|
|
||||||
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from.id;
|
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from.id;
|
||||||
const firstName = isStoredMessage(msg) ?
|
const firstName = isStoredMessage(msg) ?
|
||||||
(await UserStore.get(msg.fromId))?.firstName : msg.from.first_name;
|
(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;
|
|
||||||
|
|
||||||
parts.push({
|
parts.push({
|
||||||
bot: fromId === botUser.id,
|
bot: fromId === botUser.id,
|
||||||
content: cleanText ? cleanText : "",
|
content: cleanText ? cleanText : "",
|
||||||
@@ -963,7 +932,7 @@ export function getRuntimeInfo(): RuntimeInfo {
|
|||||||
|
|
||||||
export type PhotoMaxSize = { width: number, height: number, url: string; file_id: string; unique_file_id: string; };
|
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 async function getPhotoMaxSize(photos: PhotoSize[], target: number = Environment.MAX_PHOTO_SIZE): Promise<PhotoMaxSize | null> {
|
||||||
if (!photos) return null;
|
if (!photos) return null;
|
||||||
|
|
||||||
photos = photos.filter(p => Math.max(p.width, p.height) <= target);
|
photos = photos.filter(p => Math.max(p.width, p.height) <= target);
|
||||||
@@ -971,7 +940,7 @@ export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environmen
|
|||||||
if (photos.length === 0) return null;
|
if (photos.length === 0) return null;
|
||||||
|
|
||||||
if (photos.length === 1) {
|
if (photos.length === 1) {
|
||||||
return photos[0];
|
return mapPhotoSizeToMax(photos[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const max = photos.reduce((prev, cur) => {
|
const max = photos.reduce((prev, cur) => {
|
||||||
@@ -980,7 +949,8 @@ export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environmen
|
|||||||
return cur.width * cur.height > prev.width * prev.height ? cur : prev;
|
return cur.width * cur.height > prev.width * prev.height ? cur : prev;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
return max;
|
if (!max) return null;
|
||||||
|
return mapPhotoSizeToMax(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize | null> {
|
export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize | null> {
|
||||||
@@ -994,25 +964,20 @@ export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize |
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function imageToBase64(filePath: string, withMimeType: boolean = false): Promise<string | null> {
|
export async function imageToBase64(filePath: string): Promise<string> {
|
||||||
if (!fs.existsSync(filePath)) return null;
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
try {
|
if (err) {
|
||||||
const file = fs.readFileSync(filePath);
|
return reject(err);
|
||||||
const base64 = Buffer.from(file).toString("base64");
|
}
|
||||||
if (withMimeType) {
|
const base64Image = Buffer.from(data).toString("base64");
|
||||||
return `data:image/jpeg;base64,${base64}`;
|
const dataUrl = `data:image/jpeg;base64,${base64Image}`;
|
||||||
}
|
resolve(dataUrl);
|
||||||
|
});
|
||||||
return base64;
|
});
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export function ifTrue(exp?: never): boolean {
|
||||||
export function ifTrue(exp?: any): boolean {
|
|
||||||
if (!exp) return false;
|
if (!exp) return false;
|
||||||
|
|
||||||
return ["true", "t", "y", 1, "1"].includes(exp);
|
return ["true", "t", "y", 1, "1"].includes(exp);
|
||||||
@@ -1028,21 +993,14 @@ export async function processNewMessage(msg: Message) {
|
|||||||
console.log("message", msg);
|
console.log("message", msg);
|
||||||
|
|
||||||
let storedMsg: StoredMessage | null = null;
|
let storedMsg: StoredMessage | null = null;
|
||||||
|
Promise.all([
|
||||||
try {
|
MessageStore.put(msg).then(r => {
|
||||||
const results = await Promise.all([
|
storedMsg = r;
|
||||||
MessageStore.put(msg),
|
console.log("storedMsg", storedMsg);
|
||||||
UserStore.put(msg.from)
|
}),
|
||||||
]
|
UserStore.put(msg.from)
|
||||||
);
|
]
|
||||||
|
).catch(logError);
|
||||||
storedMsg = results[0];
|
|
||||||
if (!msg.media_group_id && storedMsg.photoMaxSizeFilePath) {
|
|
||||||
await loadImagesIfExists(msg);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((msg.new_chat_members?.length || 0 > 0)) {
|
if ((msg.new_chat_members?.length || 0 > 0)) {
|
||||||
await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(Environment.ANSWERS.invite)}).catch(logError);
|
await bot.sendMessage({chat_id: msg.chat.id, text: randomValue(Environment.ANSWERS.invite)}).catch(logError);
|
||||||
@@ -1104,34 +1062,12 @@ export async function processNewMessage(msg: Message) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = chatCommands.find(e => e instanceof YouTubeDownload);
|
|
||||||
if (await checkRequirements(yt, msg)) {
|
|
||||||
await yt.downloadYouTubeVideo(msg, url);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
||||||
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
||||||
|
|
||||||
const chat = chatCommands.find(e => e instanceof OllamaChat);
|
const chat = chatCommands.find(e => e instanceof OllamaChat);
|
||||||
if (await checkRequirements(chat, msg)) {
|
if (await checkRequirements(chat, msg)) {
|
||||||
await chat.executeOllama(msg, textToCheck);
|
await chat.executeOllama(msg, startsWithPrefix ? messageWithoutPrefix : cmdText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1080,9 @@ async function processAlbum(groupId: string): Promise<string[]> {
|
|||||||
.map(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)));
|
||||||
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
|
const ids = allPhotoMaxSizes.map(p => p.unique_file_id);
|
||||||
|
|
||||||
|
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);
|
console.log("File IDs:", ids);
|
||||||
@@ -1154,5 +1092,5 @@ async function processAlbum(groupId: string): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function photoPathByUniqueId(uniqueId: string): string {
|
export function photoPathByUniqueId(uniqueId: string): string {
|
||||||
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
|
return path.join(Environment.DATA_PATH, "temp", uniqueId + ".jpg");
|
||||||
}
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import Innertube, {Platform, Types, Utils} from "youtubei.js";
|
|
||||||
import fs, {createWriteStream} from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import {Environment} from "../common/environment";
|
|
||||||
|
|
||||||
export function getYouTubeVideoId(url: string): string {
|
|
||||||
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts)\/|.*[?&]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 downloadVideoFromYouTube(url: string, targetQuality: string = "720p"): Promise<{
|
|
||||||
time: number,
|
|
||||||
exists?: boolean,
|
|
||||||
buffer: Buffer | null
|
|
||||||
}> {
|
|
||||||
const start = Date.now();
|
|
||||||
let buffer: Buffer | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const videoId = getYouTubeVideoId(url);
|
|
||||||
const videoFolder = path.join(Environment.DATA_PATH, "video");
|
|
||||||
if (!fs.existsSync(videoFolder)) {
|
|
||||||
fs.mkdirSync(videoFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(videoFolder, `${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 Innertube.create({
|
|
||||||
generate_session_locally: true,
|
|
||||||
retrieve_player: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = await yt.getInfo(videoId);
|
|
||||||
|
|
||||||
console.log(`Fetching metadata for: ${videoId}...`);
|
|
||||||
|
|
||||||
const format = info.streaming_data?.formats.find(f => f.quality_label === targetQuality)
|
|
||||||
|| info.streaming_data?.adaptive_formats.find(f => f.quality_label === targetQuality);
|
|
||||||
|
|
||||||
if (!format) {
|
|
||||||
console.log(`Quality ${targetQuality} not found. Falling back to best available.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = await yt.download(videoId, {
|
|
||||||
type: "video+audio",
|
|
||||||
quality: "best",
|
|
||||||
format: "mp4"
|
|
||||||
});
|
|
||||||
|
|
||||||
const file = createWriteStream(filePath);
|
|
||||||
|
|
||||||
console.log("Downloading...");
|
|
||||||
|
|
||||||
for await (const chunk of Utils.streamToIterable(stream)) {
|
|
||||||
file.write(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
file.end();
|
|
||||||
|
|
||||||
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. URL: ${url}\ntook ${diff}ms`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
time: diff,
|
|
||||||
buffer: buffer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+1
-3
@@ -9,9 +9,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"allowJs": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
Reference in New Issue
Block a user