Skip to content

Instantly share code, notes, and snippets.

@weskerty
Created January 16, 2026 17:01
Show Gist options
  • Select an option

  • Save weskerty/b66265b7d941c8f3717f3830d669b17d to your computer and use it in GitHub Desktop.

Select an option

Save weskerty/b66265b7d941c8f3717f3830d669b17d to your computer and use it in GitHub Desktop.
TelegramDownLoader Go
const fs = require('fs').promises;
const {createReadStream, existsSync, statSync} = require('fs');
const path = require('path');
const {exec, spawn} = require('child_process');
const {promisify} = require('util');
const {execSync} = require('child_process');
const {bot, logger} = require('../lib');
const eP = promisify(exec);
const LTO = 300000;
const FT = {
video: {
ex: new Set(['mp4', 'mkv', 'avi', 'webm', 'mov', 'flv', 'm4v']),
mt: 'video/mp4',
},
image: {
ex: new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg']),
mt: 'image/jpeg',
},
document: {
ex: new Set(['pdf', 'epub', 'docx', 'txt', 'apk', 'apks', 'zip', 'rar', 'iso', 'ini', 'cbr', 'cbz', 'torrent', 'json', 'xml', 'html', 'css', 'js', 'csv', 'xls', 'xlsx', 'ppt', 'pptx']),
mts: new Map([
['pdf', 'application/pdf'],
['epub', 'application/epub+zip'],
['docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
['txt', 'text/plain'],
['apk', 'application/vnd.android.package-archive'],
['apks', 'application/vnd.android.package-archive'],
['zip', 'application/zip'],
['rar', 'application/x-rar-compressed'],
['iso', 'application/x-iso9660-image'],
['ini', 'text/plain'],
['cbr', 'application/x-cbr'],
['cbz', 'application/x-cbz'],
['torrent', 'application/x-bittorrent'],
['json', 'application/json'],
['xml', 'application/xml'],
['html', 'text/html'],
['css', 'text/css'],
['js', 'application/javascript'],
['csv', 'text/csv'],
['xls', 'application/vnd.ms-excel'],
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
['ppt', 'application/vnd.ms-powerpoint'],
['pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
]),
dmt: 'application/octet-stream',
},
audio: {
ex: new Set(['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma']),
mt: 'audio/mpeg',
},
};
function gFD(fP) {
const e = path.extname(fP).slice(1).toLowerCase();
for (const [ct, tI] of Object.entries(FT)) {
if (tI.ex.has(e)) {
return {
category: ct,
mimetype: ct === 'document'
? tI.mts.get(e) || tI.dmt
: tI.mt,
};
}
}
return {
category: 'document',
mimetype: FT.document.dmt,
};
}
function fFS(bt) {
if (bt < 1024) return bt + ' B';
if (bt < 1048576) return (bt / 1024).toFixed(2) + ' KB';
if (bt < 1073741824) return (bt / 1048576).toFixed(2) + ' MB';
return (bt / 1073741824).toFixed(2) + ' GB';
}
function sWO(cmd, arg) {
return new Promise((res, rej) => {
const ch = spawn(cmd, arg, {
stdio: ['ignore', 'inherit', 'inherit']
});
ch.on('error', (er) => {
logger.error(`[T]Spw: ${er.message}`);
rej(er);
});
ch.on('close', (cd) => {
if (cd === 0) {
res();
} else {
logger.error(`[T]X ${cd}`);
rej(new Error(`Exit ${cd}`));
}
});
});
}
function sLg(cmd, arg) {
return new Promise((res, rej) => {
const ch = spawn(cmd, arg, {
stdio: ['ignore', 'inherit', 'inherit']
});
const to = setTimeout(() => {
logger.info('[T]Tmt');
ch.kill();
rej(new Error('Timeout'));
}, LTO);
ch.on('error', (er) => {
clearTimeout(to);
logger.error(`[T]LogE: ${er.message}`);
rej(er);
});
ch.on('close', (cd) => {
clearTimeout(to);
if (cd === 0) {
res();
} else {
logger.error(`[T]LogF: ${cd}`);
rej(new Error(`LogX ${cd}`));
}
});
});
}
async function sEx(cmd, sE = false) {
try {
return await eP(cmd);
} catch (er) {
if (!sE) {
logger.error(`[T]ExeE: ${er.message}`);
}
throw new Error('ExeF');
}
}
class RQ {
constructor(mC = 1) {
this.q = [];
this.aR = 0;
this.mC = mC;
}
async aRq(tp, tk, m) {
return new Promise((res, rej) => {
this.q.push({tp, tk, m, res, rej});
this.pN();
});
}
async pN() {
if (this.aR >= this.mC || this.q.length === 0) {
return;
}
this.aR++;
const {tk, res, rej} = this.q.shift();
try {
const rt = await tk();
res(rt);
} catch (er) {
rej(er);
} finally {
this.aR--;
this.pN();
}
}
}
class TDLDownloader {
constructor() {
this.cx = null;
this.rQ = null;
this.sTd = null;
this.sR = new Map();
this.fNM = new Map();
this.cDF = new Set();
this.cSD = null;
this.miniSearch = null;
this.fI = false;
}
setContext(cx) {
this.cx = cx;
const jsonBaseDir = cx.AMULEDOWNLOADS ||
cx.TEMP_DOWNLOAD_DIR ||
path.join(process.cwd(), 'tmp');
this.cf = {
tempDir: cx.TEMP_DOWNLOAD_DIR || path.join(process.cwd(), 'tmp'),
jsonDir: path.join(jsonBaseDir, 'tdl_exports'),
indexPath: path.join(jsonBaseDir, 'tdl_exports', 'search_index.json'),
tdlPath: path.join(process.cwd(), 'media', 'bin', 'tdl'),
chatIds: (cx.CHATSTELEGRAMID || '1143692078').split(',').map(id => id.trim()),
shouldDeleteTempFiles: cx.DELETE_TEMP_FILE !== 'false',
maxConcurrent: parseInt(cx.MAXSOLICITUD, 10) || 1
};
this.rQ = new RQ(this.cf.maxConcurrent);
}
async cIF() {
try {
require.resolve('minisearch');
this.fI = true;
logger.info('[T] MiniSearch ya instalado');
return true;
} catch (er) {
try {
logger.info('[T] Instalando MiniSearch...');
await eP('npm install minisearch --force --no-save --ignore-peer-deps', {
cwd: process.cwd()
});
delete require.cache[require.resolve('minisearch')];
this.fI = true;
logger.info('[T] βœ… MiniSearch instalado');
return true;
} catch (iE) {
logger.error('[T]MiniF:', iE.message);
this.fI = false;
return false;
}
}
}
async eD() {
await Promise.all([
fs.mkdir(this.cf.jsonDir, {recursive: true}),
fs.mkdir(this.cf.tempDir, {recursive: true}),
fs.mkdir(path.dirname(this.cf.tdlPath), {recursive: true})
]);
}
async fST() {
try {
await eP('tdl version');
this.sTd = 'tdl';
return true;
} catch {
return false;
}
}
async gTP() {
if (!this.sTd && await this.fST()) {
return this.sTd;
}
if (this.sTd) return this.sTd;
try {
await fs.access(this.cf.tdlPath);
return this.cf.tdlPath;
} catch {
return this.dTd();
}
}
async dTd() {
const dU = 'https://github.com/iyear/tdl/releases/latest/download/tdl_Linux_64bit.tar.gz';
const tD = path.dirname(this.cf.tdlPath);
const tP = path.join(tD, 'tdl_Linux_64bit.tar.gz');
await fs.mkdir(tD, {recursive: true});
try {
await sEx(`curl -L -o "${tP}" "${dU}"`);
} catch {
try {
const fetch = (await import('node-fetch')).default;
const rp = await fetch(dU);
if (!rp.ok) throw new Error(`DlF: ${rp.statusText}`);
const bf = Buffer.from(await rp.arrayBuffer());
await fs.writeFile(tP, bf);
} catch (er) {
throw new Error(`DlE: ${er.message}`);
}
}
await sEx(`tar -xzf "${tP}" -C "${tD}"`);
await fs.chmod(this.cf.tdlPath, '755');
await fs.unlink(tP).catch(() => {});
return this.cf.tdlPath;
}
async lTd() {
const tP = await this.gTP();
await sLg(tP, ['login', '-T', 'qr']);
}
mkD() {
const sI = `tdl_${Date.now()}`;
this.cSD = path.join(this.cf.tempDir, 'tdl', sI);
return this.cSD;
}
async sCl(tg, rt = 3) {
if (!tg) return;
for (let i = 0; i < rt; i++) {
try {
const st = await fs.stat(tg);
if (st.isDirectory()) {
await fs.rm(tg, {recursive: true, force: true});
} else {
await fs.unlink(tg);
}
return;
} catch (er) {
if (er.code === 'ENOENT') {
return;
}
if (i === rt - 1) {
logger.error(`[T]ClF ${tg}: ${er.message}`);
return;
}
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
async cF() {
if (!this.cf.shouldDeleteTempFiles) return;
try {
if (this.cSD) {
await this.sCl(this.cSD);
}
this.cDF.clear();
this.cSD = null;
} catch (er) {
logger.error('[T]Cl:', er);
}
}
async cAF() {
if (!this.cf.shouldDeleteTempFiles) return;
try {
const jF = await fs.readdir(this.cf.jsonDir);
for (const fl of jF) {
if (fl.startsWith('selected_') || fl.startsWith('temp_')) {
await this.sCl(path.join(this.cf.jsonDir, fl));
}
}
} catch (er) {
logger.error('[T]ClAll:', er);
}
}
async dTL(m, url) {
return this.rQ.aRq('download', async () => {
const tP = await this.gTP();
const sD = this.mkD();
try {
await this.eD();
await fs.mkdir(sD, {recursive: true});
await this.cF();
this.cDF.clear();
const ag = ['dl', '--skip-same', '--template', '{{ filenamify .FileName }}', '-d', sD];
for (const u of url) {
ag.push('-u', u);
}
await sWO(tP, ag);
await this.pDF(m, sD);
} catch (er) {
throw new Error(`Err dl: ${er.message}`);
} finally {
if (this.cf.shouldDeleteTempFiles) {
await this.cF();
}
}
}, m);
}
async eAC() {
const tP = await this.gTP();
await this.eD();
const rt = [];
for (const cI of this.cf.chatIds) {
const eF = path.join(this.cf.jsonDir, `export_${cI}.json`);
if (existsSync(eF)) {
rt.push({chatId: cI, file: eF, success: true, skipped: true});
continue;
}
try {
await sWO(tP, ['chat', 'export', '-c', cI, '-o', eF]);
rt.push({chatId: cI, file: eF, success: true, skipped: false});
} catch (er) {
logger.error(`[T]ExpF ${cI}: ${er.message}`);
rt.push({chatId: cI, error: er.message, success: false, skipped: false});
}
}
this.miniSearch = null;
try {
await fs.unlink(this.cf.indexPath);
logger.info('[T] Índice invalidado');
} catch {}
return rt;
}
async cDA() {
try {
const jF = await fs.readdir(this.cf.jsonDir);
for (const fl of jF) {
if (fl.startsWith('export_') && fl.endsWith('.json')) {
const fP = path.join(this.cf.jsonDir, fl);
const st = await fs.stat(fP);
const sM = new Date();
sM.setMonth(sM.getMonth() - 6);
if (st.mtime < sM) return true;
}
}
return false;
} catch {
return false;
}
}
async loadOrCreateIndex() {
try {
if (existsSync(this.cf.indexPath)) {
const indexData = await fs.readFile(this.cf.indexPath, 'utf8');
const MiniSearch = require('minisearch');
this.miniSearch = MiniSearch.loadJSON(indexData, {
fields: ['fileName'],
storeFields: ['id', 'chatId', 'messageId', 'fileName', 'from', 'file_size']
});
logger.info('[T] Índice cargado desde disco');
return;
}
} catch (err) {
logger.warn('[T] Error cargando Γ­ndice, recreando...', err.message);
}
await this.buildAndSaveIndex();
}
async buildAndSaveIndex() {
if (!this.fI) {
throw new Error('MiniSearch no estΓ‘ instalado');
}
const MiniSearch = require('minisearch');
this.miniSearch = new MiniSearch({
fields: ['fileName'],
storeFields: ['id', 'chatId', 'messageId', 'fileName', 'from', 'file_size'],
processTerm: (term) => term
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]/g, ' ')
.trim(),
searchOptions: {
prefix: true,
fuzzy: 0.2,
combineWith: 'AND'
}
});
let totalDocs = 0;
const allFiles = await fs.readdir(this.cf.jsonDir);
const exportFiles = allFiles.filter(f => f.startsWith('export_') && f.endsWith('.json'));
logger.info(`[T] Indexando ${exportFiles.length} archivos JSON...`);
for (const fileName of exportFiles) {
const jF = path.join(this.cf.jsonDir, fileName);
const chatIdMatch = fileName.match(/^export_(.+)\.json$/);
if (!chatIdMatch) continue;
const cI = chatIdMatch[1];
try {
const fC = await fs.readFile(jF, 'utf8');
const jD = JSON.parse(fC);
if (jD?.messages && Array.isArray(jD.messages)) {
const docs = jD.messages
.filter(mg => mg.file_name || mg.file)
.map(mg => ({
id: `${cI}_${mg.id}`,
chatId: cI,
messageId: mg.id,
fileName: mg.file_name || mg.file,
from: mg.from || null,
file_size: mg.file_size
}));
if (docs.length > 0) {
this.miniSearch.addAll(docs);
totalDocs += docs.length;
logger.info(`[T] ${fileName}: ${docs.length} archivos`);
}
}
} catch (pE) {
logger.error(`[T]PaF ${jF}: ${pE.message}`);
}
}
const indexData = JSON.stringify(this.miniSearch);
await fs.writeFile(this.cf.indexPath, indexData);
logger.info(`[T] Índice creado con ${totalDocs} documentos de ${exportFiles.length} chats`);
}
async sIC(m, sQ) {
return this.rQ.aRq('search', async () => {
await this.eD();
this.sR.clear();
this.fNM.clear();
let nE = false;
try {
for (const cI of this.cf.chatIds) {
const eF = path.join(this.cf.jsonDir, `export_${cI}.json`);
if (!existsSync(eF)) {
nE = true;
break;
}
}
if (nE) {
await this.eAC();
}
if (!this.miniSearch) {
await this.loadOrCreateIndex();
}
const iO = await this.cDA();
const results = this.miniSearch.search(sQ, {
prefix: true,
fuzzy: 0.2,
combineWith: 'AND'
});
if (results.length === 0) {
const T1 = iO ?
`DB antigua\nSin res: "${sQ}"` :
`Sin res: "${sQ}"`;
await m.send(T1, {quoted: m.data});
return;
}
const chatResults = new Map();
results.forEach((result, index) => {
const rI = index + 1;
const cI = result.chatId;
if (!chatResults.has(cI)) {
chatResults.set(cI, {
chatId: cI,
name: `Chat ${cI}`,
results: []
});
}
const fM = {
from: result.from,
size: result.file_size ? fFS(result.file_size) : null
};
chatResults.get(cI).results.push({
id: result.messageId,
file: result.fileName,
resultIndex: rI,
metadata: fM
});
this.sR.set(rI, {
chatId: cI,
messageId: result.messageId,
file: result.fileName,
metadata: fM
});
this.fNM.set(`${result.messageId}_${this.sFN(result.fileName)}`, result.fileName);
});
let rM = iO ? "DB antigua\n" : "";
rM += `${results.length} β„›π‘’π“ˆπ“Šπ“π“‰π’Άπ’Ήπ‘œπ“ˆ\n> _Usa tdl dd 1,2,etc_\n`;
for (const cRs of chatResults.values()) {
for (const rs of cRs.results) {
rM += `\`${rs.resultIndex}\` ${rs.file}\n`;
if (rs.metadata) {
const mI = [];
if (rs.metadata.from) mI.push(`*De:* _${rs.metadata.from}_`);
if (rs.metadata.size) mI.push(`*Peso:* _${rs.metadata.size}_`);
if (mI.length > 0) {
rM += `> ${mI.join(', ')}\n`;
}
}
rM += '\n';
}
}
await m.send(rM, {quoted: m.data});
} catch (er) {
throw new Error(`Err bus: ${er.message}`);
}
}, m);
}
sFN(fN) {
return fN.replace(/[<>:"/\\|?*]/g, '_');
}
async cSJ(idx) {
const sI = new Map();
for (const ix of idx) {
const it = this.sR.get(parseInt(ix));
if (it) {
if (!sI.has(it.chatId)) {
sI.set(it.chatId, []);
}
sI.get(it.chatId).push({
id: it.messageId,
type: "message",
file: it.file
});
}
}
const jF = [];
for (const [cI, mg] of sI.entries()) {
const jC = {
id: parseInt(cI),
messages: mg
};
const jP = path.join(this.cf.jsonDir, `selected_${cI}_${Date.now()}.json`);
await fs.writeFile(jP, JSON.stringify(jC));
jF.push(jP);
}
return jF;
}
async dSI(m, idx) {
return this.rQ.aRq('download', async () => {
const tP = await this.gTP();
const sD = this.mkD();
try {
await this.eD();
await fs.mkdir(sD, {recursive: true});
await this.cF();
this.cDF.clear();
if (this.sR.size === 0) {
throw new Error('Sin res');
}
const sI = idx
.map(ix => parseInt(ix.trim()))
.filter(ix => !isNaN(ix) && this.sR.has(ix));
if (sI.length === 0) {
throw new Error('Sin sel');
}
const jF = await this.cSJ(sI);
if (jF.length === 0) {
throw new Error('Err sel');
}
await m.send(`Descargando ${sI.length} archivos...`, {quoted: m.data});
const ag = ['dl', '--skip-same', '--template', '{{ filenamify .FileName }}', '-d', sD];
for (const jFl of jF) {
ag.push('-f', jFl);
}
await sWO(tP, ag);
await this.pDF(m, sD);
} catch (er) {
throw new Error(`Err dl: ${er.message}`);
} finally {
await this.cAF();
if (this.cf.shouldDeleteTempFiles) {
await this.cF();
}
}
}, m);
}
async pDF(m, sD) {
try {
const fl = await fs.readdir(sD);
if (fl.length === 0) {
await m.send("Sin archivos", {quoted: m.data});
return;
}
for (const f of fl) {
const fP = path.join(sD, f);
try {
const st = await fs.stat(fP);
if (st.isDirectory()) {
continue;
}
const {category, mimetype} = gFD(fP);
const fB = await fs.readFile(fP);
const bn = path.basename(fP);
const mt = bn.match(/^(\d+)_(.+)$/);
let fN = bn;
if (mt && mt.length >= 3) {
const mI = parseInt(mt[1]);
for (const [ky, oN] of this.fNM.entries()) {
if (ky.startsWith(`${mI}_`)) {
fN = oN;
break;
}
}
}
await m.send(fB, {
fileName: fN,
mimetype: mimetype,
quoted: m.data
}, category);
} catch (er) {
logger.error(`[T]EnvF ${f}: ${er.message}`);
}
}
} catch (er) {
logger.error(`[T]ProcE: ${er.message}`);
await m.send(`Error: ${er.message}`, {quoted: m.data});
}
}
}
const tdlDownloader = new TDLDownloader();
bot(
{
pattern: 'tdl ?(.*)',
fromMe: true,
desc: 'Buscar y descargar archivos de chats de Telegram usando TDL.',
type: 'download',
},
async (message, match, ctx) => {
tdlDownloader.setContext(ctx);
if (!tdlDownloader.fI) {
try {
await tdlDownloader.cIF();
} catch (error) {
return await message.send(`❌ Error instalando MiniSearch: ${error.message}`, {quoted: message.data});
}
if (!tdlDownloader.fI) {
return await message.send('❌ MiniSearch es requerido pero no se pudo instalar', {quoted: message.data});
}
}
const inp = match.trim() || message.reply_message?.text || '';
if (!inp) {
await message.send(
'> πŸ” Buscar: `tdl` <busqueda>\n' +
'> πŸ“₯ Descargar: `tdl dd` <indices>\n' +
'> πŸ“ Enlace: `tdl` <https://t.me/...>\n' +
'> πŸ”‘ Login: `tdl login`\n' +
'> πŸ’Ύ Exportar: `tdl export`\n' +
'> πŸ†™ Actualizar: `tdl update`',
{quoted: message.data}
);
return;
}
try {
const arg = inp.split(' ');
if (arg[0].toLowerCase() === 'login') {
await message.send('Escanea QR en terminal', {quoted: message.data});
await tdlDownloader.lTd();
await message.send('Login exitoso', {quoted: message.data});
return;
}
if (arg[0].toLowerCase() === 'update') {
const tP = await tdlDownloader.dTd();
await message.send(`TDL actualizado: ${tP}`, {quoted: message.data});
return;
}
if (arg[0].toLowerCase() === 'export') {
await message.send(`Exportando chats...`, {quoted: message.data});
const rt = await tdlDownloader.eAC();
const sC = rt.filter(r => r.success).length;
await message.send(`Exportados ${sC}/${rt.length} chats`, {quoted: message.data});
return;
}
if (arg[0].toLowerCase() === 'dd') {
const idx = arg.slice(1).join('').split(/[,\s]+/);
await tdlDownloader.dSI(message, idx);
return;
}
const tL = arg.filter(a => /^https?:\/\/(t\.me|telegram\.me)\//i.test(a));
if (tL.length > 0) {
await tdlDownloader.dTL(message, tL);
return;
}
await tdlDownloader.sIC(message, inp);
} catch (er) {
await message.send(`Error: ${er.message}`, {quoted: message.data});
}
}
);
module.exports = {tdlDownloader};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment