Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active April 7, 2025 03:15
Show Gist options
  • Save weskerty/5311f55462e5b84736bde5b1ac7e2ae3 to your computer and use it in GitHub Desktop.
Save weskerty/5311f55462e5b84736bde5b1ac7e2ae3 to your computer and use it in GitHub Desktop.
Telegram Sticker Downloader
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const { promisify } = require('util');
const { exec: execCallback } = require('child_process');
const { bot, isUrl, sticker, addExif } = require('../lib');
require('dotenv').config();
const exec = promisify(execCallback);
class DownloadQueue {
constructor(maxConcurrent = 2) {
this.queue = [];
this.activeDownloads = 0;
this.maxConcurrent = maxConcurrent;
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.processNext();
});
}
async processNext() {
if (this.activeDownloads >= this.maxConcurrent || this.queue.length === 0) {
return;
}
this.activeDownloads++;
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.activeDownloads--;
this.processNext();
}
}
}
class StickerDownloader {
constructor() {
this.config = {
apiKey: process.env.TGBOTAPIKEY || '',
converterPath: path.join(process.cwd(), 'media', 'bin'),
tgsdPath: path.join(process.cwd(), 'media', 'bin', 'TGSD'),
maxConcurrent: parseInt(process.env.MAXSOLICITUD, 10) || 1,
};
this.downloadQueue = new DownloadQueue(this.config.maxConcurrent);
this.converterBinaries = new Map([
['win32-x64', 'lottie-converter.windows.amd64.exe.zip'],
['linux-x64', 'lottie-converter.linux.amd64.zip'],
['linux-arm64', 'lottie-converter.linux.arm64.zip'],
['default', 'lottie-converter.linux.amd64.zip'],
]);
}
async safeExecute(command, silentError = false) {
try {
const result = await exec(command);
return result;
} catch (error) {
if (!silentError) {
console.error(`Execution failed: ${error.message}`);
}
throw new Error('Execution failed');
}
}
detectConverterBinaryName() {
const platform = os.platform();
const arch = os.arch();
const key = `${platform}-${arch}`;
return this.converterBinaries.get(key) || this.converterBinaries.get('default');
}
async ensureDirectories() {
await Promise.all([
fs.mkdir(this.config.converterPath, { recursive: true }),
fs.mkdir(this.config.tgsdPath, { recursive: true }),
fs.mkdir(path.join(this.config.tgsdPath, 'TGSs'), { recursive: true }),
fs.mkdir(path.join(this.config.tgsdPath, 'converted'), { recursive: true }),
]);
}
async isScriptAvailable() {
const scriptPath = path.join(this.config.tgsdPath, 'DownloaderFlags.sh');
try {
await fs.access(scriptPath);
return true;
} catch {
return false;
}
}
async downloadConverterAndScript(message) {
await this.ensureDirectories();
const fileName = this.detectConverterBinaryName();
const platform = os.platform();
const arch = os.arch();
const key = `${platform}-${arch}`;
let downloadUrl;
if (key === 'win32-x64') {
downloadUrl = 'https://github.com/weskerty/TGStickerDownloader/releases/download/1.1.2/lottie-converter.windows.amd64.exe.zip';
} else if (key === 'linux-x64') {
downloadUrl = 'https://github.com/weskerty/TGStickerDownloader/releases/download/1.1.2/lottie-converter.linux.amd64.zip';
} else if (key === 'linux-arm64' || platform === 'android') {
downloadUrl = 'https://github.com/weskerty/TGStickerDownloader/releases/download/1.1.2/lottie-converter.linux.arm64.zip';
} else {
downloadUrl = 'https://github.com/weskerty/TGStickerDownloader/releases/download/1.1.2/lottie-converter.linux.amd64.zip';
}
const filePath = path.join(this.config.converterPath, fileName);
const fetch = (await import('node-fetch')).default;
const response = await fetch(downloadUrl);
if (!response.ok) throw new Error(`Download failed: ${response.statusText}`);
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(filePath, buffer);
await this.safeExecute(`unzip -o "${filePath}" -d "${this.config.converterPath}"`);
if (platform !== 'win32') {
const extractedPath = path.join(this.config.converterPath, 'lottie-converter');
await fs.chmod(extractedPath, '755').catch(() => {});
}
if (platform !== 'win32') {
await this.safeExecute(`chmod -R 755 "${this.config.tgsdPath}"`).catch(() => {});
}
const scriptPath = path.join(this.config.tgsdPath, 'DownloaderFlags.sh');
const scriptUrl = 'https://raw.githubusercontent.com/weskerty/TGStickerDownloader/main/DownloaderFlags.sh';
const scriptResponse = await fetch(scriptUrl);
if (!scriptResponse.ok) throw new Error(`Script download failed: ${scriptResponse.statusText}`);
const scriptBuffer = Buffer.from(await scriptResponse.arrayBuffer());
await fs.writeFile(scriptPath, scriptBuffer);
if (platform !== 'win32') {
await fs.chmod(scriptPath, '755').catch(() => {});
}
return true;
}
async downloadStickerPack(message, packName) {
return this.downloadQueue.add(async () => {
try {
await this.ensureDirectories();
if (!(await this.isScriptAvailable())) {
await this.downloadConverterAndScript(message);
}
let processedPackName = packName;
if (packName.startsWith('https://t.me/addstickers/')) {
processedPackName = packName.replace('https://t.me/addstickers/', '');
}
const scriptPath = path.join(this.config.tgsdPath, 'DownloaderFlags.sh');
try {
await this.safeExecute(
`cd "${this.config.tgsdPath}" && bash "${scriptPath}" --key "${this.config.apiKey}" --s "${processedPackName}"`
);
} catch (execError) {
console.error(`Script execution error: ${execError.message}`);
await message.send(`Error executing download script.`, { quoted: message.data });
await this.downloadConverterAndScript(message);
await this.safeExecute(
`cd "${this.config.tgsdPath}" && bash "${scriptPath}" --key "${this.config.apiKey}" --s "${processedPackName}"`
);
}
const convertedDir = path.join(this.config.tgsdPath, 'converted');
const files = await fs.readdir(convertedDir);
if (files.length === 0) {
await message.send('No stickers found or conversion failed', { quoted: message.data });
return;
}
await message.send(`Sending ${files.length} stickers!...`, { quoted: message.data });
for (const file of files) {
try {
const filePath = path.join(convertedDir, file);
const fileData = await fs.readFile(filePath);
await message.send(fileData, { isAnimated: true, }, 'sticker');
} catch (error) {
console.error(`Error sending sticker ${file}: ${error.message}`);
}
}
await fs.rm(path.join(this.config.tgsdPath, 'TGSs'), { recursive: true, force: true }).catch(() => {});
await fs.rm(path.join(this.config.tgsdPath, 'converted'), { recursive: true, force: true }).catch(() => {});
await fs.mkdir(path.join(this.config.tgsdPath, 'TGSs'), { recursive: true });
await fs.mkdir(path.join(this.config.tgsdPath, 'converted'), { recursive: true });
} catch (error) {
console.error(`Error downloading sticker pack: ${error.message}`);
await message.send(`Error ${error.message}`, { quoted: message.data });
}
});
}
}
const stickerDownloader = new StickerDownloader();
bot(
{
pattern: 'dtg ?(.*)',
fromMe: true,
desc: 'Telegram Sticker Pack Downloader.',
type: 'download',
},
async (message, match) => {
const input = match.trim() || message.reply_message?.text || '';
if (!input) {
await message.send(
'> 🎭 Download Telegram Sticker Pack:\n`dtg` <pack_name>\n\n' +
'> 🔗 Or with URL:\n`dtg` <https://t.me/addstickers/pack_name>\n\n' +
'> ℹ️ Create a Bot with @BotFather in Telegram and add the APIKEY with `.setvar TGBOTAPIKEY=`',
{ quoted: message.data }
);
return;
}
try {
if (!stickerDownloader.config.apiKey) {
await message.send(
'Error: TGBOTAPIKEY not set. \n Create a Bot with @BotFather in Telegram and add the APIKEY with `.setvar TGBOTAPIKEY=`',
{ quoted: message.data }
);
return;
}
await stickerDownloader.downloadStickerPack(message, input);
} catch (error) {
console.error(`Error in dtg command: ${error.message}`);
await message.send(`Error ${error.message}`, { quoted: message.data });
}
}
);
module.exports = { stickerDownloader };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment