const TelegramBot = require("node-telegram-bot-api"); const fs = require("fs"); const mime = require("mime"); const path = require("path"); const https = require("https"); const config = require("./config"); const { DownloadVideo, YtdlUpdater, downloadFile } = require("./download"); const { MTProtoUser } = require("./mtproto"); async function getImage(url) { return new Promise((resolve, reject) => { https .get(url, (res) => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode} при загрузке изображения`)); return; } const chunks = []; res.on("data", (chunk) => chunks.push(chunk)); res.on("end", () => resolve(Buffer.concat(chunks))); }) .on("error", reject); }); } function removeTemp () { try { fs.rmdirSync(path.join(__dirname, "tmp"), { recursive: true, force: true }, () => {}); } catch (_) {} } removeTemp(); const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m; async function getFileSize(pathname) { const stream = fs.createReadStream(pathname); let size = 0; return await new Promise((resolve, reject) => { stream.on("data", data => size += data.length); stream.on("end", () => resolve(size)); stream.on("error", reject); }); } // Check if download process const tgChatsDownloaded = new Object(); class TgBotSender { constructor(parent, bot, msg, memory = {}) { this.parent = parent; this.bot = bot; this.msg = msg; this.curText = null; this.memory = memory; } async textWithMedia(data, reply_markup) { await this.bot.editMessageMedia(data, { chat_id: this.msg.chat.id, message_id: this.msg.message_id, reply_markup, }); } async text(text, reply_markup) { if (text === this.curText) return; await this.bot.editMessageText(text, { chat_id: this.msg.chat.id, message_id: this.msg.message_id, reply_markup, }); this.curText = text; } async sendVideo (chatId, data, settings = {}, apiOptions = {}) { if (this.parent.mtproto !== null) { const mtprotoMID = await this.parent.mtproto.saveMedia( data, settings.caption ?? "", { thumb: settings.thumb, } ); return await this.bot.copyMessage( chatId, config.mtproto.chatId, mtprotoMID, { caption: settings.caption ?? "", } ); } return await this.bot.sendVideo(chatId, data, settings, apiOptions); } async sendAudio (chatId, data, settings = {}, apiOptions = {}) { if (this.parent.mtproto !== null) { const mtprotoMID = await this.parent.mtproto.saveMedia( data, settings.caption ?? "", { thumb: settings.thumb, filename: apiOptions.filename, } ); return await this.bot.copyMessage( chatId, config.mtproto.chatId, mtprotoMID, { caption: settings.caption ?? "", } ); } return await this.bot.sendAudio(chatId, data, settings, apiOptions); } async download(video, quality = DownloadVideo.qualities.mp4_best) { const { msg } = this.memory; const sent = await this.parent.send(msg, "Start download"); const info = await video.getVideoInfo(); await video.setupQuality(quality, false); let cooldown = new Date(); //const isStream = true; const filename = `${(new Date()).getTime()}.${quality.exc}`; const isStream = path.join(__dirname, `tmp/${filename}`); let uploadFileInterval = null; const finallyHandler = (result) => { clearInterval(uploadFileInterval); //return; if (isStream && isStream !== true) { fs.rm(isStream, { recursive: true, force: true }, () => {}); if (result) fs.rm(result, { recursive: true, force: true }, () => {}); } }; const errHandler = (err) => { console.error(err.stack); //this.bot.sendMessage(msg.chat.id, "Download failed"); sent.text("Download failed"); finallyHandler(); } await video .download(({ percent, downloaded, fine, msg }) => { //console.debug({ percent, fine, downloaded, msg }); const now = new Date(); if (!fine && now - cooldown <= 3500) return; cooldown = new Date(); if (!fine) sent.text(`Downloaded (${percent}%), ${downloaded} bytes`); else sent.text(!msg ? "Download complete. Converting.." : msg); }, isStream) .then(async (downloaded) => { //console.debug("downloaded:", downloaded); try { const videoInfo = await video.getVideoInfo(); if (downloaded.format.type === "video") { const filesize = (typeof isStream !== "string" || this.parent.mtproto !== null) ? 0 : await getFileSize(downloaded.data); //console.debug("filesize:", filesize); if (filesize < 50 * 1024 * 1024) { await this.bot.sendChatAction(msg.chat.id, 'upload_document'); uploadFileInterval = setInterval(() => { this.bot.sendChatAction(msg.chat.id, 'upload_document'); }, 4250); await new Promise(r => setTimeout(r, 3000)); await this.sendVideo( msg.chat.id, downloaded.data, { caption: info.title, thumb: await downloadFile(videoInfo.thumbnail), }, { filename, contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime, //contentType: downloaded.format.mime, }, ); /*await this.bot.sendVideo( msg.chat.id, downloaded.data, { caption: info.title }, { filename, contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime, //contentType: downloaded.format.mime, }, );*/ } else { return await sent.text(`Maximum filesize reached. Try any types (Current file: ${ ((filesize / (1024 * 1024))+"").split(".")[0] } MB)`); } } else if (downloaded.format.type === "audio") { await this.sendAudio( msg.chat.id, downloaded.data, { caption: info.title, thumb: await downloadFile(videoInfo.thumbnail), /*thumbnail: { type: mime.getType(videoInfo.thumbnail), media: videoInfo.thumbnail, }*/ }, { //filename: `downloaded.${downloaded.format.exc}`, filename: `${ videoInfo.title }.mp3`, contentType: downloaded.format.mime, }, ); } } catch (err) { return errHandler(err); } finallyHandler(downloaded.data); }).catch(errHandler); } } class VideosGarbageCollector { constructor () { this.actions = new Object(); this.pinnedVideos = new Object(); this.removeTable = new Object(); setInterval(() => { const tasks = []; for (let registerDate in this.removeTable) { const key = this.removeTable[registerDate]; const savedTo = new Date(registerDate); savedTo.setHours(savedTo.getHours() + 3); if ((new Date()) >= savedTo) { const video = this.pinnedVideos[key]; tasks.push(() => { // Call destructor video.destructor(); // Delete.. delete this.actions[key]; delete this.pinnedVideos[key]; delete this.removeTable[registerDate]; }); } } tasks.forEach(task => task()); if (tasks.length > 0) console.info("GC Clean up", tasks.length, "items(s)"); }, 60000); } register (key, act, pinnedVideo) { this.actions[key] = act; this.pinnedVideos[key] = pinnedVideo; this.removeTable[new Date()] = key; } removeTemp () { if (Object.keys(tgChatsDownloaded).length === 0) removeTemp(); } } class TgBot { constructor() { this.bot = new TelegramBot(config.token, { polling: true, }); this.bot.on("text", (m) => this.newMessageHandler(m)); this.bot.on("callback_query", (m) => this.cbHandler(m)); this.videosGC = new VideosGarbageCollector(); this.updater = new YtdlUpdater(); this.mtproto = null; if (config.mtproto !== null) { this.mtproto = new MTProtoUser( config.mtproto.apiId, config.mtproto.apiHash, config.mtproto.session, ); } } async init () { if (this.mtproto === null) console.warn("API_ID or API_HASH not defined. MTProto API not used"); else await this.mtproto.init(); } async cbHandler(query) { const key = query.from.id + ":" + query.data; //console.debug(query, this.videosGC.actions[key], key); if (!this.videosGC.actions[key]) return this.bot.answerCallbackQuery(query.id, { text: "Resend me video link, please", show_alert: true }); this.videosGC.actions[key](query.id); } async send(msg, txt) { const result = await this.bot.sendMessage(msg.chat.id, txt); return new TgBotSender(this, this.bot, result, { msg }); } async newMessageHandler(msg) { if (msg.chat.id === msg.from.id) { if (config.whitelist[msg.from.id]) { if (ytLinkRegexp.test(msg.text)) { const sent = await this.send(msg, "Wait.."); const errHandler = (err) => { console.error(err.stack); sent.text( "Error. Invalid/Hidden video or Forbidden for download" ); }; try { const video = new DownloadVideo(msg.text); video.setupParent(this); const info = await video.getVideoInfo(); sent.text("Info getted, getting formats..."); //console.debug(info); // Download only video with best quality const qualities = await video.qualities(true); //console.debug(qualities); let keyboard = []; let keyboardM = []; let kbChunk = []; let kbChunkM = []; for (let quality in qualities) { const qualityItem = qualities[quality]; if (qualityItem.type !== null) { if (config.allFormats || ({ mp4: true })[qualityItem.exc]) { if (qualityItem.type === "audio") kbChunkM.push({ text: qualityItem.strictName ?? `🎧 ${qualityItem.exc}/${qualityItem.resolution}`, callback_data: quality + "_" + msg.message_id, }); else if (qualityItem.type === "video") kbChunk.push({ text: qualityItem.strictName ?? `📹 ${qualityItem.exc}/${qualityItem.resolution}`, callback_data: quality + "_" + msg.message_id, }); if (kbChunk.length >= 3) { keyboard.push(kbChunk); kbChunk = []; } if (kbChunkM.length >= 3) { keyboardM.push(kbChunkM); kbChunkM = []; } } } } if (kbChunk.length > 0) keyboard.push(kbChunk); if (kbChunkM.length > 0) keyboardM.push(kbChunkM); keyboard = keyboard.concat(keyboardM); //keyboard = keyboard.slice(-4); //console.debug(keyboard); //console.debug(info.title, info.thumbnail); const thenHandler = () => { for (let quality in qualities) { const qualityItem = qualities[quality]; const actKey = msg.from.id + ":" + quality + "_" + msg.message_id; this.videosGC.register(actKey, async (queryId) => { //console.debug("ACT btn:", actKey); if (tgChatsDownloaded[msg.from.id]) return this.bot.answerCallbackQuery(queryId, { text: 'Wait until download was not finished', show_alert: true }); tgChatsDownloaded[msg.from.id] = true; sent .download(video, qualityItem) .then(() => { delete tgChatsDownloaded[msg.from.id]; this.videosGC.removeTemp(); }) .catch((err) => { errHandler(err); delete tgChatsDownloaded[msg.from.id]; this.videosGC.removeTemp(); }); }, video); /*this.videosGC.actions[actKey] = () => { sent .download(video, qualityItem) .then(() => {}) .catch(errHandler); };)*/ } return Promise.resolve(null); }; await sent .textWithMedia( { type: "photo", media: info.thumbnail, //media: { source: await getImage(info.thumbnail) }, //media: "https://placekitten.com/500/350", caption: info.title, }, { inline_keyboard: keyboard, }, ) .then(thenHandler) .catch(() => { sent .textWithMedia( { type: "photo", //media: info.thumbnail, media: config.emptyImage, caption: info.title, }, { inline_keyboard: keyboard, }, ) .then(thenHandler) .catch(errHandler); }); //this.download(msg, video); //this.send(msg, "Start download"); } catch (e) { errHandler(e); } } else { this.send(msg, "Invalid youtube link"); } } else { this.send(msg, "Permission denied\n" + "But you can create your bot: " + "https://git.fullgream.tech/fullgream/telegram_ytdlp_node.js/"); } } } } const bot = new TgBot(); bot.init();