fix: yt: set cookies for check_size args #35

Merged
GiovaniFZ merged 2 commits from fix_ttk into main 2025-04-15 00:59:12 +00:00
11 changed files with 321 additions and 213 deletions
Showing only changes of commit 32a389a301 - Show all commits

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
node_modules
npm-debug.log
.git
.gitignore
.env
config.env
*.md
!README.md

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM node:20-slim
# Install ffmpeg and other deps
RUN apt-get update && apt-get install -y ffmpeg && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN chmod +x /usr/src/app/src/plugins/yt-dlp/yt-dlp
VOLUME /usr/src/app/config.env
CMD ["npm", "start"]

View file

@ -9,10 +9,10 @@ Kowalski is a a simple Telegram bot made in Node.js.
## Self-host requirements
- Node.js 20 or newer (you can also use Bun)
- Node.js 20 or newer (you can also use [Bun](https://bun.sh))
- A Telegram bot (create one at [@BotFather](https://t.me/botfather))
- Latest version of Node.js
- FFmpeg (only for the `/yt` command)
- Docker and Docker Compose (only required for Docker setup)
## Run it yourself, develop or contribute with Kowalski
@ -36,6 +36,47 @@ After editing the file, save all changes and run the bot with ``npm start``.
> [!TIP]
> To deal with dependencies, just run ``npm install`` or ``npm i`` at any moment to install all of them.
## Running with Docker
> [!IMPORTANT]
> Please complete the above steps to prepare your local copy for building. You do not need to install FFmpeg on your host system.
You can also run Kowalski using Docker, which simplifies the setup process. Make sure you have Docker and Docker Compose installed.
### Using Docker Compose
1. **Make sure to setup your `config.env` file first!**
2. **Run the container**
```bash
docker compose up -d
```
> [!NOTE]
> The `-d` flag causes Kowalski to run in the background. If you're just playing around, you may not want to use this flag.
### Using Docker Run
If you prefer to use Docker directly, you can use these instructions instead.
1. **Make sure to setup your `config.env` file first!**
2. **Build the image**
```bash
docker build -t kowalski .
```
3. **Run the container**
```bash
docker run -d --name kowalski --restart unless-stopped -v $(pwd)/config.env:/usr/src/app/config.env:ro kowalski
```
> [!NOTE]
> The `-d` flag causes Kowalski to run in the background. If you're just playing around, you may not want to use this flag.
## config.env Functions
- **botSource**: Put the link to your bot source code.
@ -48,6 +89,18 @@ After editing the file, save all changes and run the bot with ``npm start``.
- Take care of your ``config.env`` file, as it is so much important and needs to be secret (like your passwords), as anyone can do whatever they want to the bot with this token!
## Troubleshooting
### YouTube Downloading
**Q:** I get a "Permission denied (EACCES)" error in the console when running the `/yt` command
**A:** Make sure `src/plugins/yt-dlp/yt-dlp` is executable. You can do this on Linux like so:
```bash
chmod +x src/plugins/yt-dlp/yt-dlp
```
## About/License
BSD-3-Clause - 2024 Lucas Gabriel (lucmsilva).

9
docker-compose.yml Normal file
View file

@ -0,0 +1,9 @@
services:
kowalski:
build: .
container_name: kowalski
restart: unless-stopped
volumes:
- ./config.env:/usr/src/app/config.env:ro
environment:
- NODE_ENV=production

View file

@ -6,6 +6,12 @@ require('@dotenvx/dotenvx').config({ path: "config.env" });
require('./plugins/ytdlp-wrapper.js');
// require('./plugins/termlogger.js');
// Ensures bot token is set, and not default value
if (!process.env.botToken || process.env.botToken === 'InsertYourBotTokenHere') {
console.error('Bot token is not set. Please set the bot token in the config.env file.')
process.exit(1)
}
const bot = new Telegraf(process.env.botToken);
const maxRetries = process.env.maxRetries || 5;
let restartCount = 0;

View file

@ -62,7 +62,7 @@ async function handleAdminCommand(ctx, action, successMessage, errorMessage) {
reply_to_message_id: ctx.message.message_id
});
} catch (error) {
ctx.reply(errorMessage.replace('{error}', error.message), {
ctx.reply(errorMessage.replace(/{error}/g, error.message), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});
@ -91,12 +91,12 @@ module.exports = (bot) => {
handleAdminCommand(ctx, async () => {
try {
const commitHash = await getGitCommitHash();
await ctx.reply(Strings.gitCurrentCommit.replace('{commitHash}', commitHash), {
await ctx.reply(Strings.gitCurrentCommit.replace(/{commitHash}/g, commitHash), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});
} catch (error) {
ctx.reply(Strings.gitErrRetrievingCommit.replace('{error}', error), {
ctx.reply(Strings.gitErrRetrievingCommit.replace(/{error}/g, error), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});
@ -109,12 +109,12 @@ module.exports = (bot) => {
handleAdminCommand(ctx, async () => {
try {
const result = await updateBot();
await ctx.reply(Strings.botUpdated.replace('{result}', result), {
await ctx.reply(Strings.botUpdated.replace(/{result}/g, result), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});
} catch (error) {
ctx.reply(Strings.errorUpdatingBot.replace('{error}', error), {
ctx.reply(Strings.errorUpdatingBot.replace(/{error}/g, error), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});
@ -127,7 +127,7 @@ module.exports = (bot) => {
const botName = ctx.message.text.split(' ').slice(1).join(' ');
handleAdminCommand(ctx, async () => {
await ctx.telegram.setMyName(botName);
}, Strings.botNameChanged.replace('{botName}', botName), Strings.botNameErr.replace('{error}', error));
}, Strings.botNameChanged.replace(/{botName}/g, botName), Strings.botNameErr.replace(/{error}/g, error));
});
bot.command('setbotdesc', spamwatchMiddleware, async (ctx) => {
@ -135,7 +135,7 @@ module.exports = (bot) => {
const botDesc = ctx.message.text.split(' ').slice(1).join(' ');
handleAdminCommand(ctx, async () => {
await ctx.telegram.setMyDescription(botDesc);
}, Strings.botDescChanged.replace('{botDesc}', botDesc), Strings.botDescErr.replace('{error}', error));
}, Strings.botDescChanged.replace(/{botDesc}/g, botDesc), Strings.botDescErr.replace(/{error}/g, error));
});
bot.command('botkickme', spamwatchMiddleware, async (ctx) => {
@ -159,7 +159,7 @@ module.exports = (bot) => {
caption: botFile
});
} catch (error) {
ctx.reply(Strings.unexpectedErr.replace('{error}', error.message), {
ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
});

View file

@ -6,8 +6,8 @@ async function sendHelpMessage(ctx, isEditing) {
const Strings = getStrings(ctx.from.language_code);
const botInfo = await ctx.telegram.getMe();
const helpText = Strings.botHelp
.replace('{botName}', botInfo.first_name)
.replace("{sourceLink}", process.env.botSource);
.replace(/{botName}/g, botInfo.first_name)
.replace(/{sourceLink}/g, process.env.botSource);
const options = {
parse_mode: 'Markdown',
disable_web_page_preview: true,
@ -35,7 +35,7 @@ module.exports = (bot) => {
bot.command("about", spamwatchMiddleware, async (ctx) => {
const Strings = getStrings(ctx.from.language_code);
const aboutMsg = Strings.botAbout.replace("{sourceLink}", `${process.env.botSource}`);
const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`);
ctx.reply(aboutMsg, {
parse_mode: 'Markdown',

View file

@ -6,7 +6,7 @@ module.exports = (bot) => {
bot.start(spamwatchMiddleware, async (ctx) => {
const Strings = getStrings(ctx.from.language_code);
const botInfo = await ctx.telegram.getMe();
const startMsg = Strings.botWelcome.replace('{botName}', botInfo.first_name);
const startMsg = Strings.botWelcome.replace(/{botName}/g, botInfo.first_name);
ctx.reply(startMsg, {
parse_mode: 'Markdown',

View file

@ -1,198 +1,212 @@
const { getStrings } = require('../plugins/checklang.js');
const { isOnSpamWatch } = require('../plugins/lib-spamwatch/spamwatch.js');
const spamwatchMiddleware = require('../plugins/lib-spamwatch/Middleware.js')(isOnSpamWatch);
const { execFile } = require('child_process');
const os = require('os');
const fs = require('fs');
const path = require('path');
const ytDlpPaths = {
linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'),
win32: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp.exe'),
darwin: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp_macos'),
};
const getYtDlpPath = () => {
const platform = os.platform();
return ytDlpPaths[platform] || ytDlpPaths.linux;
};
const ffmpegPaths = {
linux: '/usr/bin/ffmpeg',
win32: path.resolve(__dirname, '../plugins/ffmpeg/bin/ffmpeg.exe'),
};
const getFfmpegPath = () => {
const platform = os.platform();
return ffmpegPaths[platform] || ffmpegPaths.linux;
};
const downloadFromYoutube = async (command, args) => {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr });
} else {
resolve({ stdout, stderr });
}
});
});
};
const getApproxSize = async (command, videoUrl) => {
let args = [];
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx', '--cookies', path.resolve(__dirname, "../props/cookies.txt")];
} else {
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx'];
}
try {
const { stdout } = await downloadFromYoutube(command, args);
const sizeInBytes = parseInt(stdout.trim(), 10);
if (!isNaN(sizeInBytes)) {
return sizeInBytes / (1024 * 1024);
} else {
return 0;
}
} catch (error) {
throw error;
}
};
module.exports = (bot) => {
bot.command(['yt', 'ytdl', 'sdl', 'video', 'dl'], spamwatchMiddleware, async (ctx) => {
const Strings = getStrings(ctx.from.language_code);
const ytDlpPath = getYtDlpPath();
const userId = ctx.from.id;
const videoUrl = ctx.message.text.split(' ').slice(1).join(' ');
const mp4File = `tmp/${userId}.mp4`;
const tempMp4File = `tmp/${userId}.f137.mp4`;
const tempWebmFile = `tmp/${userId}.f251.webm`;
let cmdArgs = "";
const dlpCommand = ytDlpPath;
const ffmpegPath = getFfmpegPath();
const ffmpegArgs = ['-i', tempMp4File, '-i', tempWebmFile, '-c:v copy -c:a copy -strict -2', mp4File];
if (!videoUrl) {
return ctx.reply(Strings.ytDownload.noLink, {
parse_mode: "Markdown",
disable_web_page_preview: true,
reply_to_message_id: ctx.message.message_id
});
};
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
cmdArgs = "--max-filesize 2G --no-playlist --cookies src/props/cookies.txt --merge-output-format mp4 -o";
} else {
cmdArgs = `--max-filesize 2G --no-playlist --merge-output-format mp4 -o`;
}
try {
const downloadingMessage = await ctx.reply(Strings.ytDownload.checkingSize, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
if (fs.existsSync(ytDlpPath)) {
const approxSizeInMB = await Promise.race([
getApproxSize(ytDlpPath, videoUrl),
]);
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.downloadingVid, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File];
await downloadFromYoutube(dlpCommand, dlpArgs);
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.uploadingVid, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
if (fs.existsSync(tempMp4File)) {
await downloadFromYoutube(ffmpegPath, ffmpegArgs);
}
if (fs.existsSync(mp4File)) {
const message = Strings.ytDownload.msgDesc.replace("{userMention}", `[${ctx.from.first_name}](tg://user?id=${userId})`)
try {
await ctx.replyWithVideo({
source: mp4File
}, {
caption: message,
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
fs.unlinkSync(mp4File);
} catch (error) {
if (toString(error).includes("Request Entity Too Large")) {
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.uploadLimit, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
} else {
const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
errMsg, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
};
fs.unlinkSync(mp4File);
}
} else {
await ctx.reply(mp4File, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
}
} else {
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.libNotFound, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
}
} catch (error) {
const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
errMsg, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
}
});
const { getStrings } = require('../plugins/checklang.js');
const { isOnSpamWatch } = require('../plugins/lib-spamwatch/spamwatch.js');
const spamwatchMiddleware = require('../plugins/lib-spamwatch/Middleware.js')(isOnSpamWatch);
const { execFile } = require('child_process');
const os = require('os');
const fs = require('fs');
const path = require('path');
const ytDlpPaths = {
linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'),
win32: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp.exe'),
darwin: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp_macos'),
};
const getYtDlpPath = () => {
const platform = os.platform();
return ytDlpPaths[platform] || ytDlpPaths.linux;
};
const ffmpegPaths = {
linux: '/usr/bin/ffmpeg',
win32: path.resolve(__dirname, '../plugins/ffmpeg/bin/ffmpeg.exe'),
};
const getFfmpegPath = () => {
const platform = os.platform();
return ffmpegPaths[platform] || ffmpegPaths.linux;
};
const downloadFromYoutube = async (command, args) => {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr });
} else {
resolve({ stdout, stderr });
}
});
});
};
const getApproxSize = async (command, videoUrl) => {
let args = [];
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx', '--cookies', path.resolve(__dirname, "../props/cookies.txt")];
} else {
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx'];
}
try {
const { stdout } = await downloadFromYoutube(command, args);
const sizeInBytes = parseInt(stdout.trim(), 10);
if (!isNaN(sizeInBytes)) {
return sizeInBytes / (1024 * 1024);
} else {
return 0;
}
} catch (error) {
throw error;
}
};
module.exports = (bot) => {
bot.command(['yt', 'ytdl', 'sdl', 'video', 'dl'], spamwatchMiddleware, async (ctx) => {
const Strings = getStrings(ctx.from.language_code);
const ytDlpPath = getYtDlpPath();
const userId = ctx.from.id;
const videoUrl = ctx.message.text.split(' ').slice(1).join(' ');
const mp4File = `tmp/${userId}.mp4`;
const tempMp4File = `tmp/${userId}.f137.mp4`;
const tempWebmFile = `tmp/${userId}.f251.webm`;
let cmdArgs = "";
const dlpCommand = ytDlpPath;
const ffmpegPath = getFfmpegPath();
const ffmpegArgs = ['-i', tempMp4File, '-i', tempWebmFile, '-c:v copy -c:a copy -strict -2', mp4File];
if (!videoUrl) {
return ctx.reply(Strings.ytDownload.noLink, {
parse_mode: "Markdown",
disable_web_page_preview: true,
reply_to_message_id: ctx.message.message_id
});
};
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
cmdArgs = "--max-filesize 2G --no-playlist --cookies src/props/cookies.txt --merge-output-format mp4 -o";
} else {
cmdArgs = `--max-filesize 2G --no-playlist --merge-output-format mp4 -o`;
}
try {
const downloadingMessage = await ctx.reply(Strings.ytDownload.checkingSize, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
if (fs.existsSync(ytDlpPath)) {
const approxSizeInMB = await Promise.race([
getApproxSize(ytDlpPath, videoUrl),
]);
if (approxSizeInMB > 50) {
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.uploadLimit, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
return;
}
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.downloadingVid, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File];
await downloadFromYoutube(dlpCommand, dlpArgs);
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.uploadingVid, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
if (fs.existsSync(tempMp4File)) {
await downloadFromYoutube(ffmpegPath, ffmpegArgs);
}
if (fs.existsSync(mp4File)) {
const message = Strings.ytDownload.msgDesc.replace("{userMention}", `[${ctx.from.first_name}](tg://user?id=${userId})`)
try {
await ctx.replyWithVideo({
source: mp4File
}, {
caption: message,
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
fs.unlinkSync(mp4File);
} catch (error) {
if (error.response.description.includes("Request Entity Too Large")) {
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.uploadLimit, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
} else {
const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
errMsg, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
};
fs.unlinkSync(mp4File);
}
} else {
await ctx.reply(mp4File, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
});
}
} else {
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
Strings.ytDownload.libNotFound, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
}
} catch (error) {
const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
await ctx.telegram.editMessageText(
ctx.chat.id,
downloadingMessage.message_id,
null,
errMsg, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id,
},
);
}
});
};

View file

@ -1,5 +1,5 @@
{
"botWelcome": "*Hello! I am {botName}!*\nI was made with love by some nerds who really love programming!\n\n*Before using, you need to read the privacy policy (/privacy) to understand where your data goes when using this bot.*\n\nAlso, you can use /help to see the bot commands!",
"botWelcome": "*Hello! I'm {botName}!*\nI was made with love by some nerds who really love programming!\n\n*By using {botName}, you affirm that you have read to and agree with the privacy policy (/privacy). This helps you understand where your data goes when using this bot.*\n\nAlso, you can use /help to see the bot commands!",
"botHelp": "*Hey, I'm {botName}, a simple bot made entirely from scratch in Telegraf and Node.js by some nerds who really love programming.*\n\nClick on the buttons below to see which commands you can use!\n",
"botPrivacy": "Check out [this link](https://blog.lucmsilva.com/posts/lynx-privacy-policy) to read the bot's privacy policy.",
"botAbout": "*About the bot*\n\nThe bot base was originally created by [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), now maintained by several people.\n\nThe bot's purpose is to bring fun to your groups here on Telegram in a relaxed and simple way. The bot also features some very useful commands, which you can see using the help command (/help).\n\nSpecial thanks to @givfnz2 for his many contributions to the bot!\n\nSee the source code: [Click here to go to GitHub]({sourceLink})",

View file

@ -1,5 +1,5 @@
{
"botWelcome": "*Olá! Eu sou o {botName}!*\n\n*Antes de usar, você precisa ler a política de privacidade (/privacy) para entender onde seus dados vão ao usar este bot.*\n\nAlém disso, você pode usar /help para ver os meus comandos!",
"botWelcome": "*Olá! Eu sou o {botName}!*\n\n*Ao usar o {botName}, você afirma que leu e concorda com a política de privacidade (/privacy). Isso ajuda você a entender onde seus dados vão ao usar este bot.*\n\nAlém disso, você pode usar /help para ver os meus comandos!",
"botHelp": "*Oi, eu sou o {botName}, um bot simples feito do zero em Telegraf e Node.js por uns nerds que gostam de programação.*\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})\n\nClique nos botões abaixo para ver quais comandos você pode usar!\n",
"botPrivacy": "Acesse [este link](https://blog.lucmsilva.com/posts/lynx-privacy-policy) para ler a política de privacidade do bot.",
"botAbout": "*Sobre o bot*\n\nA base deste bot foi feita originalmente por [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), agora sendo mantido por várias pessoas.\n\nA intenção do bot é trazer diversão para os seus grupos aqui no Telegram de uma maneira bem descontraida e simples. O bot também conta com alguns comandos bem úteis, que você consegue ver com o comando de ajuda (/help).\n\nAgradecimento especial ao @givfnz2 pelas suas várias contribuições ao bot!\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})",