diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ba231c9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +config.env +*.md +!README.md \ No newline at end of file diff --git a/.github/workflows/update-authors.yml b/.github/workflows/update-authors.yml new file mode 100644 index 0000000..ae7f058 --- /dev/null +++ b/.github/workflows/update-authors.yml @@ -0,0 +1,37 @@ +name: Update AUTHORS File + +on: + push: + branches: + - main + +jobs: + update-authors: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Generate AUTHORS file (Name format) + run: | + git log --format='%aN <%aE>' | sort -u > AUTHORS + + - name: Check if AUTHORS file changed + run: | + if git diff --quiet AUTHORS; then + echo "No changes in AUTHORS file." + exit 0 + fi + + - name: Commit and push changes + uses: EndBug/add-and-commit@v9.1.4 + with: + push: true + add: "AUTHORS" + default_author: github_actions + message: "Update AUTHORS file automatically" diff --git a/.gitignore b/.gitignore index d1151eb..ba85ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -139,7 +139,6 @@ package-lock.json bun.lock bun.lockb tmp/ -plugins/ # Executables *.exe diff --git a/AUTHORS b/AUTHORS index 1348bed..6f138dd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,14 +1,16 @@ +A Bunch of Computer Nerds A Bunch of Computer Nerds A Bunch of Computer Nerds -A Bunch of Computer Nerds +Aidan DaviDev <97841570+DaviisDev@users.noreply.github.com> -dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -fossabot Giovani Finazzi <53719063+GiovaniFZ@users.noreply.github.com> GiovaniFZ Lucas Gabriel <90426410+lucmsilva651@users.noreply.github.com> Lucas Gabriel -lucmsilva651 Luquinhas Luquinhas +dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +fossabot +github-actions <41898282+github-actions[bot]@users.noreply.github.com> +lucmsilva651 mthlma <156229140+mthlma@users.noreply.github.com> diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56b5b32 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md index 25ffa5b..a154166 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,84 @@ # Kowalski (Node.js Telegram Bot) + [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) ![GitHub License](https://img.shields.io/github/license/ABOCN/TelegramBot) Kowalski is a a simple Telegram bot made in Node.js. - - You can find Kowalski at [@KowalskiNodeBot](https://t.me/KowalskiNodeBot) on Telegram. + +- You can find Kowalski at [@KowalskiNodeBot](https://t.me/KowalskiNodeBot) on Telegram. ## Self-host requirements - - Node.js 20 or newer (you can also use Bun) - - A Telegram bot (create one at [@BotFather](https://t.me/botfather)) - - Latest version of Node.js - - FFmpeg (only for the /yt command) + +- Node.js 20 or newer (you can also use [Bun](https://bun.sh)) +- A Telegram bot (create one at [@BotFather](https://t.me/botfather)) +- FFmpeg (only for the `/yt` command) +- Docker and Docker Compose (only required for Docker setup) ## Run it yourself, develop or contribute with Kowalski + First, clone the repo with Git: -``` + +```bash git clone https://github.com/ABOCN/TelegramBot ``` + And now, init the submodules with these commands (this is very important): -``` + +```bash cd TelegramBot git submodule update --init --recursive ``` + Next, inside the repository directory, create a `config.env` file with some content, which you can see the [example .env file](config.env.example) to fill info with. To see the meaning of each one, see [the Functions section](#configenv-functions). After editing the file, save all changes and run the bot with ``npm start``. -- To deal with dependencies, just run ``npm install`` or ``npm i`` at any moment to install all of them. + +> [!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. - **botToken**: Put your bot token that you created at [@BotFather](https://t.me/botfather). - **botAdmins**: Put the ID of the people responsible for managing the bot. They can use some administrative + exclusive commands on any group. @@ -34,7 +86,21 @@ After editing the file, save all changes and run the bot with ``npm start``. - **weatherKey**: Weather.com API key, used for the `/weather` command. ## Note + - 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). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..981d90a --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/src/bot.js b/src/bot.js index ad909db..472bc10 100644 --- a/src/bot.js +++ b/src/bot.js @@ -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; diff --git a/src/commands/crew.js b/src/commands/crew.js index 18b0eb9..52a3703 100644 --- a/src/commands/crew.js +++ b/src/commands/crew.js @@ -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 }); diff --git a/src/commands/gsmarena.js b/src/commands/gsmarena.js index f6fce9c..db9ce08 100644 --- a/src/commands/gsmarena.js +++ b/src/commands/gsmarena.js @@ -196,7 +196,11 @@ function extractMetaData(meta, key) { module.exports = (bot) => { bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => { const userId = ctx.from.id; - const userName = ctx.from.first_name; + let userName = String(ctx.from.first_name); + + if(userName.includes("<") && userName.includes(">")) { + userName = userName.replace("<", "").replace(">", ""); + } const phone = ctx.message.text.split(" ").slice(1).join(" "); if (!phone) { diff --git a/src/commands/help.js b/src/commands/help.js index 58dfa86..ab9e601 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -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', diff --git a/src/commands/main.js b/src/commands/main.js index c111603..6333296 100644 --- a/src/commands/main.js +++ b/src/commands/main.js @@ -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', diff --git a/src/commands/youtube.js b/src/commands/youtube.js index 76b0d29..2442f95 100644 --- a/src/commands/youtube.js +++ b/src/commands/youtube.js @@ -41,7 +41,12 @@ const downloadFromYoutube = async (command, args) => { }; const getApproxSize = async (command, videoUrl) => { - const args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx']; + 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); @@ -94,14 +99,28 @@ module.exports = (bot) => { 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, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File]; @@ -112,12 +131,12 @@ module.exports = (bot) => { downloadingMessage.message_id, null, Strings.ytDownload.uploadingVid, { - parse_mode: 'Markdown', - reply_to_message_id: ctx.message.message_id, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); - if(fs.existsSync(tempMp4File)){ + if (fs.existsSync(tempMp4File)) { await downloadFromYoutube(ffmpegPath, ffmpegArgs); } @@ -126,23 +145,24 @@ module.exports = (bot) => { try { await ctx.replyWithVideo({ - source: mp4File }, { + 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")) { + 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, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); } else { const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error) @@ -151,9 +171,9 @@ module.exports = (bot) => { downloadingMessage.message_id, null, errMsg, { - parse_mode: 'Markdown', - reply_to_message_id: ctx.message.message_id, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); }; @@ -171,22 +191,21 @@ module.exports = (bot) => { downloadingMessage.message_id, null, Strings.ytDownload.libNotFound, { - parse_mode: 'Markdown', - reply_to_message_id: ctx.message.message_id, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); } } catch (error) { - console.error(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, - }, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }, ); } }); diff --git a/src/locales/english.json b/src/locales/english.json index 0a4291b..b2fb10a 100644 --- a/src/locales/english.json +++ b/src/locales/english.json @@ -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})", diff --git a/src/locales/portuguese.json b/src/locales/portuguese.json index 570f308..6b492f2 100644 --- a/src/locales/portuguese.json +++ b/src/locales/portuguese.json @@ -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})",