diff --git a/.dockerignore b/.dockerignore old mode 100755 new mode 100644 index cfdf0f2..ba231c9 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,8 @@ node_modules -webui/node_modules npm-debug.log .git -webui/.git .gitignore -webui/.gitignore -.env* -webui/.env* -webui/.next +.env +config.env *.md -!README.md -ollama/ -db/ \ No newline at end of file +!README.md \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100755 index db7a321..0000000 --- a/.env.example +++ /dev/null @@ -1,23 +0,0 @@ -# links for source and privacy -botPrivacy = "https://github.com/abocn/TelegramBot/blob/main/TERMS_OF_USE.md" -botSource = "https://github.com/ABOCN/TelegramBot" - -# insert token here -botToken = "" - -# ai features -ollamaEnabled = false -# ollamaApi = "http://ollama:11434" -# handlerTimeout = "600_000" # set higher if you expect to download larger models -# flashModel = "gemma3:4b" -# thinkingModel = "qwen3:4b" - -# database -databaseUrl = "postgres://kowalski:kowalski@localhost:5432/kowalski" - -# misc (botAdmins isnt a array here!) -maxRetries = 9999 -botAdmins = 00000000, 00000000, 00000000 -lastKey = "InsertYourLastFmApiKeyHere" -weatherKey = "InsertYourWeatherDotComApiKeyHere" -longerLogs = true \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml old mode 100755 new mode 100644 index 5f0889c..b417983 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,4 @@ updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" diff --git a/.github/workflows/njsscan.yml b/.github/workflows/njsscan.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/update-authors.yml b/.github/workflows/update-authors.yml old mode 100755 new mode 100644 diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index dbea724..ba85ba8 --- a/.gitignore +++ b/.gitignore @@ -136,21 +136,11 @@ dist lastfm.json sw-blocklist.txt package-lock.json +bun.lock +bun.lockb tmp/ # Executables *.exe yt-dlp -ffmpeg - -# Bun -bun.lock* - -# Ollama -ollama/ - -# Docker -docker-compose.yml - -# postgres -db/ \ No newline at end of file +ffmpeg \ No newline at end of file diff --git a/.gitmodules b/.gitmodules old mode 100755 new mode 100644 index 4a96795..cf3ce05 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "telegram/spamwatch"] - path = telegram/spamwatch +[submodule "src/spamwatch"] + path = src/spamwatch url = https://github.com/ABOCN/TelegramBot-SpamWatch diff --git a/AUTHORS b/AUTHORS old mode 100755 new mode 100644 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100755 new mode 100644 diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 index f0d7341..473f9b7 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,18 @@ -FROM oven/bun +FROM node:20-slim # Install ffmpeg and other deps -RUN apt-get update && apt-get install -y \ - ffmpeg \ - git \ - supervisor \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ffmpeg git && apt-get clean && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app COPY package*.json ./ -RUN bun install -COPY webui/package*.json ./webui/ -WORKDIR /usr/src/app/webui -RUN bun install +RUN npm install -WORKDIR /usr/src/app COPY . . -WORKDIR /usr/src/app/webui -RUN bun run build +RUN chmod +x /usr/src/app/src/plugins/yt-dlp/yt-dlp -RUN chmod +x /usr/src/app/telegram/plugins/yt-dlp/yt-dlp +VOLUME /usr/src/app/config.env -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -VOLUME /usr/src/app/.env - -EXPOSE 3000 - -ENV PYTHONUNBUFFERED=1 -ENV BUN_LOG_LEVEL=info - -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +CMD ["npm", "start"] \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 36ad1dc..71e5cff --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![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)](https://github.com/abocn/TelegramBot/blob/main/LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff)](https://www.typescriptlang.org) [![CodeQL](https://github.com/abocn/TelegramBot/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/abocn/TelegramBot/actions/workflows/github-code-scanning/codeql) [![Dependabot Updates](https://github.com/abocn/TelegramBot/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/abocn/TelegramBot/actions/workflows/dependabot/dependabot-updates) @@ -15,24 +14,31 @@ Kowalski is a a simple Telegram bot made in Node.js. > [!IMPORTANT] > You will only need all of them if you are not running it dockerized. Read ["Running with Docker"](#running-with-docker) for more information. -- [Bun](https://bun.sh) (latest is suggested) +- Node.js 23 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) -- Postgres -### AI Requirements +## Running locally (non-Docker setup) -- High-end CPU *or* GPU (~ 6GB vRAM) -- If using CPU, enough RAM to load the models (~6GB w/ defaults) +First, clone the repo with Git: + +```bash +git clone --recurse-submodules https://github.com/ABOCN/TelegramBot +``` + +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``. + +> [!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. ---- - > [!NOTE] > Using the `-d` flag when running causes Kowalski to run in the background. If you're just playing around or testing, you may not want to use this flag. @@ -40,30 +46,9 @@ You can also run Kowalski using Docker, which simplifies the setup process. Make ### Using Docker Compose -1. **Copy compose file** +1. **Make sure to setup your `config.env` file first!** - _Without AI (Ollama)_ - - ```bash - mv docker-compose.yml.example docker-compose.yml - ``` - - _With AI (Ollama)_ - - ```bash - mv docker-compose.yml.ai.example docker-compose.yml - ``` - -1. **Make sure to setup your `.env` file first!** - - In order to successfuly deploy Kowalski, you will need to edit both your `.env` file and enter matching values in `webui/.env`. - - > [!TIP] - > If you intend to setup AI, the defaults for Docker are already included (just uncomment) and don't need to be changed. - > - > Further setup may be needed for GPUs. See the Ollama documentation for more. - -1. **Run the container** +2. **Run the container** ```bash docker compose up -d @@ -73,78 +58,31 @@ You can also run Kowalski using Docker, which simplifies the setup process. Make If you prefer to use Docker directly, you can use these instructions instead. -1. **Make sure to setup your `.env` file first!** +1. **Make sure to setup your `config.env` file first!** - In order to successfuly deploy Kowalski, you will need to edit both your `.env` file and enter matching values in `webui/.env`. - -1. **Build the image** +2. **Build the image** ```bash docker build -t kowalski . ``` -1. **Run the container** +3. **Run the container** ```bash - docker run -d --name kowalski --restart unless-stopped -v $(pwd)/.env:/usr/src/app/.env:ro kowalski + docker run -d --name kowalski --restart unless-stopped -v $(pwd)/config.env:/usr/src/app/config.env:ro kowalski ``` -> [!NOTE] -> You must setup Ollama on your own if you would like to use AI features. - -## Running locally (non-Docker/development setup) - -First, clone the repo with Git: - -```bash -git clone --recurse-submodules https://github.com/ABOCN/TelegramBot -``` - -Next, inside the repository directory, create an `.env` file with some content, which you can see the [example .env file](.env.example) to fill info with. To see the meaning of each one, see [the Functions section](#env-functions). - -After editing the file, save all changes and run the bot with ``bun start``. - -> [!TIP] -> To deal with dependencies, just run ``bun install`` or ``bun i`` at any moment to install all of them. - -### Efficant Local (w/ Docker) Development - -If you want to develop a component of Kowalski, without dealing with the headache of several terminals, we suggest you follow these guidelines: - -1. If you are working on one component, run it with Bun, and Dockerize the other components. -1. Minimize the amount of non-Dockerized components to reduce headaches. -1. You will have to change your `.env` a lot. This is a common source of issues. Make sure the hostname and port are correct. - -## .env Functions - +## config.env Functions > [!IMPORTANT] -> Take care of your ``.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! - -### Bot +> 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! - **botSource**: Put the link to your bot source code. - **botPrivacy**: Put the link to your bot privacy policy. - **maxRetries**: Maximum number of retries for a failing command on Kowalski. Default is 5. If the limit is hit, the bot will crash past this number. - **botToken**: Put your bot token that you created at [@BotFather](https://t.me/botfather). -- **ollamaEnabled** (optional): Enables/disables AI features -- **ollamaApi** (optional): Ollama API endpoint for various AI features, will be disabled if not set -- **handlerTimeout** (optional): How long handlers will wait before timing out. Set this high if using large AI models. -- **flashModel** (optional): Which model will be used for /ask -- **thinkingModel** (optional): Which model will be used for /think -- **updateEveryChars** (optional): The amount of chars until message update triggers (for streaming response) -- **databaseUrl**: Database server configuration (see `.env.example`) - **botAdmins**: Put the ID of the people responsible for managing the bot. They can use some administrative + exclusive commands on any group. - **lastKey**: Last.fm API key, for use on `lastfm.js` functions, like see who is listening to what song and etc. - **weatherKey**: Weather.com API key, used for the `/weather` command. -- **longerLogs**: Set to `true` to enable verbose logging whenever possible. - -> [!NOTE] -> Further, advanced fine-tuning and configuration can be done in TypeScript with the files in the `/config` folder. - -### WebUI - -- **botApiUrl**: Likely will stay the same, but changes the API that the bot exposes -- **databaseUrl**: Database server configuration (see `.env.example`) ## Troubleshooting @@ -152,22 +90,16 @@ If you want to develop a component of Kowalski, without dealing with the headach **Q:** I get a "Permission denied (EACCES)" error in the console when running the `/yt` command -**A:** Make sure `telegram/plugins/yt-dlp/yt-dlp` is executable. You can do this on Linux like so: +**A:** Make sure `src/plugins/yt-dlp/yt-dlp` is executable. You can do this on Linux like so: ```bash -chmod +x telegram/plugins/yt-dlp/yt-dlp +chmod +x src/plugins/yt-dlp/yt-dlp ``` -### AI - -**Q:** How can I disable AI features? - -**A:** AI features are disabled by default, unless you have set `ollamaEnabled` to `true` in your `.env` file. Set it back to `false` to disable. - ## Contributors - Profile pictures of Kowalski contributors + Made with [contrib.rocks](https://contrib.rocks). @@ -175,5 +107,3 @@ Made with [contrib.rocks](https://contrib.rocks). ## About/License BSD-3-Clause - 2024 Lucas Gabriel (lucmsilva). - -With some components under Unlicense. diff --git a/TERMS_OF_USE.md b/TERMS_OF_USE.md deleted file mode 100755 index 42fd53b..0000000 --- a/TERMS_OF_USE.md +++ /dev/null @@ -1,18 +0,0 @@ -# Terms of Use - -By using Kowalski ([@KowalskiNodeBot](https://t.me/KowalskiNodeBot), you agree to the terms outlined below. If you do not agree with any of these terms, please discontinue use of the bot immediately. - -## 1. Blocklist System - -We reserve the right to block users from accessing the bot based on their behavior. Users who generate inappropriate content or misuse the bot will be permanently blocked by user ID. Attempts to circumvent a block by using alternative or secondary accounts will also result in those accounts being blocked. - -Additionally, Kowalski integrates with the SpamWatch API to automatically deny access to users banned by that system. If you are listed in SpamWatch, you will not be able to use the bot. - -## 2. Source Code - -The bot's source code is publicly available. You can review it at the Kowalski GitHub Repository: -[https://github.com/abocn/TelegramBot](https://github.com/abocn/TelegramBot) - -## 3. Changes to These Terms - -We may modify or update these Terms of Use at any time, with or without prior notice. Continued use of the bot constitutes acceptance of the latest version of the terms. diff --git a/config.env.example b/config.env.example new file mode 100644 index 0000000..452211b --- /dev/null +++ b/config.env.example @@ -0,0 +1,12 @@ +# links for source and privacy +botPrivacy = "https://blog.lucmsilva.com/posts/lynx-privacy-policy" +botSource = "https://github.com/ABOCN/TelegramBot" + +# insert token here +botToken = "" + +# misc (botAdmins isnt a array here!) +maxRetries = 9999 +botAdmins = 00000000, 00000000, 00000000 +lastKey = "InsertYourLastFmApiKeyHere" +weatherKey = "InsertYourWeatherDotComApiKeyHere" \ No newline at end of file diff --git a/config/ai.ts b/config/ai.ts deleted file mode 100755 index 6c6ddb0..0000000 --- a/config/ai.ts +++ /dev/null @@ -1,420 +0,0 @@ -export interface ModelInfo { - name: string; - label: string; - descriptionEn: string; - descriptionPt: string; - models: Array<{ - name: string; - label: string; - parameterSize: string; - thinking: boolean; - uncensored: boolean; - }>; -} - -export const defaultFlashModel = "gemma3:4b" -export const defaultThinkingModel = "qwen3:4b" -export const unloadModelAfterB = 12 // how many billion params until model is auto-unloaded -export const maxUserQueueSize = 3 - -export const models: ModelInfo[] = [ - { - name: 'gemma3n', - label: 'gemma3n', - descriptionEn: 'Gemma3n is a family of open, light on-device models for general tasks.', - descriptionPt: 'Gemma3n é uma família de modelos abertos, leves e para dispositivos locais, para tarefas gerais.', - models: [ - { - name: 'gemma3n:e2b', - label: 'Gemma3n e2b', - parameterSize: '2B', - thinking: false, - uncensored: false - }, - { - name: 'gemma3n:e4b', - label: 'Gemma3n e4b', - parameterSize: '4B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'gemma3', - label: 'gemma3 [ & Uncensored ]', - descriptionEn: 'Gemma3-abliterated is a family of open, uncensored models for general tasks.', - descriptionPt: 'Gemma3-abliterated é uma família de modelos abertos, não censurados, para tarefas gerais.', - models: [ - { - name: 'huihui_ai/gemma3-abliterated:1b', - label: 'Gemma3 Uncensored 1B', - parameterSize: '1B', - thinking: false, - uncensored: true - }, - { - name: 'huihui_ai/gemma3-abliterated:4b', - label: 'Gemma3 Uncensored 4B', - parameterSize: '4B', - thinking: false, - uncensored: true - }, - { - name: 'gemma3:1b', - label: 'Gemma3 1B', - parameterSize: '1B', - thinking: false, - uncensored: false - }, - { - name: 'gemma3:4b', - label: 'Gemma3 4B', - parameterSize: '4B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'qwen3', - label: 'Qwen3', - descriptionEn: 'Qwen3 is a multilingual reasoning model series.', - descriptionPt: 'Qwen3 é uma série de modelos multilingues.', - models: [ - { - name: 'qwen3:0.6b', - label: 'Qwen3 0.6B', - parameterSize: '0.6B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:1.7b', - label: 'Qwen3 1.7B', - parameterSize: '1.7B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:4b', - label: 'Qwen3 4B', - parameterSize: '4B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:8b', - label: 'Qwen3 8B', - parameterSize: '8B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:14b', - label: 'Qwen3 14B', - parameterSize: '14B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:30b', - label: 'Qwen3 30B', - parameterSize: '30B', - thinking: true, - uncensored: false - }, - { - name: 'qwen3:32b', - label: 'Qwen3 32B', - parameterSize: '32B', - thinking: true, - uncensored: false - }, - ] - }, - { - name: 'qwen3-abliterated', - label: 'Qwen3 [ Uncensored ]', - descriptionEn: 'Qwen3-abliterated is a multilingual reasoning model series.', - descriptionPt: 'Qwen3-abliterated é uma série de modelos multilingues.', - models: [ - { - name: 'huihui_ai/qwen3-abliterated:0.6b', - label: 'Qwen3 Uncensored 0.6B', - parameterSize: '0.6B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:1.7b', - label: 'Qwen3 Uncensored 1.7B', - parameterSize: '1.7B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:4b', - label: 'Qwen3 Uncensored 4B', - parameterSize: '4B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:8b', - label: 'Qwen3 Uncensored 8B', - parameterSize: '8B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:14b', - label: 'Qwen3 Uncensored 14B', - parameterSize: '14B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:30b', - label: 'Qwen3 Uncensored 30B', - parameterSize: '30B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/qwen3-abliterated:32b', - label: 'Qwen3 Uncensored 32B', - parameterSize: '32B', - thinking: true, - uncensored: true - }, - ] - }, - { - name: 'qwq', - label: 'QwQ', - descriptionEn: 'QwQ is the reasoning model of the Qwen series.', - descriptionPt: 'QwQ é o modelo de raciocínio da série Qwen.', - models: [ - { - name: 'qwq:32b', - label: 'QwQ 32B', - parameterSize: '32B', - thinking: true, - uncensored: false - }, - { - name: 'huihui_ai/qwq-abliterated:32b', - label: 'QwQ Uncensored 32B', - parameterSize: '32B', - thinking: true, - uncensored: true - }, - ] - }, - { - name: 'llama4', - label: 'Llama4', - descriptionEn: 'The latest collection of multimodal models from Meta.', - descriptionPt: 'A coleção mais recente de modelos multimodais da Meta.', - models: [ - { - name: 'llama4:scout', - label: 'Llama4 109B A17B', - parameterSize: '109B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'deepseek', - label: 'DeepSeek [ & Uncensored ]', - descriptionEn: 'DeepSeek is a research model for reasoning tasks.', - descriptionPt: 'DeepSeek é um modelo de pesquisa para tarefas de raciocínio.', - models: [ - { - name: 'deepseek-r1:1.5b', - label: 'DeepSeek 1.5B', - parameterSize: '1.5B', - thinking: true, - uncensored: false - }, - { - name: 'deepseek-r1:7b', - label: 'DeepSeek 7B', - parameterSize: '7B', - thinking: true, - uncensored: false - }, - { - name: 'deepseek-r1:8b', - label: 'DeepSeek 8B', - parameterSize: '8B', - thinking: true, - uncensored: false - }, - { - name: 'huihui_ai/deepseek-r1-abliterated:1.5b', - label: 'DeepSeek Uncensored 1.5B', - parameterSize: '1.5B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/deepseek-r1-abliterated:7b', - label: 'DeepSeek Uncensored 7B', - parameterSize: '7B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/deepseek-r1-abliterated:8b', - label: 'DeepSeek Uncensored 8B', - parameterSize: '8B', - thinking: true, - uncensored: true - }, - { - name: 'huihui_ai/deepseek-r1-abliterated:14b', - label: 'DeepSeek Uncensored 14B', - parameterSize: '14B', - thinking: true, - uncensored: true - }, - ] - }, - { - name: 'hermes3', - label: 'Hermes3', - descriptionEn: 'Hermes 3 is the latest version of the flagship Hermes series of LLMs by Nous Research.', - descriptionPt: 'Hermes 3 é a versão mais recente da série Hermes de LLMs da Nous Research.', - models: [ - { - name: 'hermes3:3b', - label: 'Hermes3 3B', - parameterSize: '3B', - thinking: false, - uncensored: false - }, - { - name: 'hermes3:8b', - label: 'Hermes3 8B', - parameterSize: '8B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'mistral', - label: 'Mistral', - descriptionEn: 'The 7B model released by Mistral AI, updated to version 0.3.', - descriptionPt: 'O modelo 7B lançado pela Mistral AI, atualizado para a versão 0.3.', - models: [ - { - name: 'mistral:7b', - label: 'Mistral 7B', - parameterSize: '7B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'phi4 [ & Uncensored ]', - label: 'Phi4', - descriptionEn: 'Phi-4 is a 14B parameter, state-of-the-art open model from Microsoft. ', - descriptionPt: 'Phi-4 é um modelo de 14B de última geração, aberto pela Microsoft.', - models: [ - { - name: 'phi4:14b', - label: 'Phi4 14B', - parameterSize: '14B', - thinking: false, - uncensored: false - }, - { - name: 'huihui_ai/phi4-abliterated:14b', - label: 'Phi4 Uncensored 14B', - parameterSize: '14B', - thinking: false, - uncensored: true - }, - ] - }, - { - name: 'phi3', - label: 'Phi3', - descriptionEn: 'Phi-3 is a family of lightweight 3B (Mini) and 14B (Medium) state-of-the-art open models by Microsoft.', - descriptionPt: 'Phi-3 é uma família de modelos leves de 3B (Mini) e 14B (Médio) de última geração, abertos pela Microsoft.', - models: [ - { - name: 'phi3:3.8b', - label: 'Phi3 3.8B', - parameterSize: '3.8B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'llama3', - label: 'Llama4', - descriptionEn: 'Llama 3, a lightweight model from Meta.', - descriptionPt: 'Llama 3, um modelo leve da Meta.', - models: [ - { - name: 'llama3:8b', - label: 'Llama3 8B', - parameterSize: '8B', - thinking: false, - uncensored: false - }, - ] - }, - { - name: 'llama3.1 [ Uncensored ]', - label: 'Llama3.1', - descriptionEn: 'Ablitered v3 llama-3.1 8b with uncensored prompt ', - descriptionPt: 'Llama3.1 é um modelo aberto, leve e para dispositivos locais, com prompt não censurado.', - models: [ - { - name: 'mannix/llama3.1-8b-abliterated:latest', - label: 'Llama3.1 8B', - parameterSize: '8B', - thinking: false, - uncensored: true - }, - ] - }, - { - name: 'llama3.2 [ & Uncensored ]', - label: 'Llama3.2', - descriptionEn: 'Llama3.2 is a family of open, lightweight models for general tasks.', - descriptionPt: 'Llama3.2 é uma família de modelos abertos, leves e para dispositivos locais, para tarefas gerais.', - models: [ - { - name: 'llama3.2:1b', - label: 'Llama3.2 1B', - parameterSize: '1B', - thinking: false, - uncensored: false - }, - { - name: 'llama3.2:3b', - label: 'Llama3.2 3B', - parameterSize: '3B', - thinking: false, - uncensored: false - }, - { - name: 'socialnetwooky/llama3.2-abliterated:3b_q8_0', - label: 'Llama3.2 Uncensored 3B', - parameterSize: '3B', - thinking: false, - uncensored: true - }, - ] - }, -]; \ No newline at end of file diff --git a/config/settings.ts b/config/settings.ts deleted file mode 100755 index 1fe94b3..0000000 --- a/config/settings.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const seriesPageSize = 4; -export const modelPageSize = 4; \ No newline at end of file diff --git a/database/schema.ts b/database/schema.ts deleted file mode 100755 index ce9a8ed..0000000 --- a/database/schema.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - integer, - pgTable, - varchar, - timestamp, - boolean, - real, - index -} from "drizzle-orm/pg-core"; - -export const usersTable = pgTable("users", { - telegramId: varchar({ length: 255 }).notNull().primaryKey(), - username: varchar({ length: 255 }).notNull(), - firstName: varchar({ length: 255 }).notNull(), - lastName: varchar({ length: 255 }).notNull(), - aiEnabled: boolean().notNull().default(false), - showThinking: boolean().notNull().default(false), - customAiModel: varchar({ length: 255 }).notNull().default("deepseek-r1:1.5b"), - aiTemperature: real().notNull().default(0.9), - aiRequests: integer().notNull().default(0), - aiCharacters: integer().notNull().default(0), - disabledCommands: varchar({ length: 255 }).array().notNull().default([]), - languageCode: varchar({ length: 255 }).notNull(), - aiTimeoutUntil: timestamp(), - aiMaxExecutionTime: integer().default(0), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), -}); - -export const twoFactorTable = pgTable("two_factor", { - userId: varchar({ length: 255 }).notNull().references(() => usersTable.telegramId).primaryKey(), - currentCode: varchar({ length: 255 }).notNull(), - codeExpiresAt: timestamp().notNull(), - codeAttempts: integer().notNull().default(0), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), -}, (table) => [ - index("idx_two_factor_user_id").on(table.userId), - index("idx_two_factor_code_expires_at").on(table.codeExpiresAt), -]); - -export const sessionsTable = pgTable("sessions", { - id: varchar({ length: 255 }).notNull().primaryKey(), - userId: varchar({ length: 255 }).notNull().references(() => usersTable.telegramId), - sessionToken: varchar({ length: 255 }).notNull().unique(), - expiresAt: timestamp().notNull(), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), -}, (table) => [ - index("idx_sessions_user_id").on(table.userId), - index("idx_sessions_expires_at").on(table.expiresAt), -]); 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/docker-compose.yml.ai.example b/docker-compose.yml.ai.example deleted file mode 100755 index fe467ab..0000000 --- a/docker-compose.yml.ai.example +++ /dev/null @@ -1,30 +0,0 @@ -services: - kowalski: - build: . - container_name: kowalski - ports: - - "3000:3000" - volumes: - - ./.env:/usr/src/app/.env:ro - - ./telegram/props/lastfm.json:/usr/src/app/telegram/props/lastfm.json - environment: - - NODE_ENV=production - env_file: - - .env - depends_on: - - postgres - - ollama - ollama: - image: ollama/ollama - container_name: kowalski-ollama - volumes: - - ./ollama:/root/.ollama - postgres: - image: postgres:17 - container_name: kowalski-postgres - volumes: - - ./db:/var/lib/postgresql/data - environment: - - POSTGRES_USER=kowalski - - POSTGRES_PASSWORD=kowalski - - POSTGRES_DB=kowalski \ No newline at end of file diff --git a/docker-compose.yml.example b/docker-compose.yml.example deleted file mode 100755 index d94e78d..0000000 --- a/docker-compose.yml.example +++ /dev/null @@ -1,24 +0,0 @@ -services: - kowalski: - build: . - container_name: kowalski - ports: - - "3000:3000" - volumes: - - ./.env:/usr/src/app/.env:ro - - ./telegram/props/lastfm.json:/usr/src/app/telegram/props/lastfm.json - environment: - - NODE_ENV=production - env_file: - - .env - depends_on: - - postgres - postgres: - image: postgres:17 - container_name: kowalski-postgres - volumes: - - ./db:/var/lib/postgresql/data - environment: - - POSTGRES_USER=kowalski - - POSTGRES_PASSWORD=kowalski - - POSTGRES_DB=kowalski \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100755 index 51b8e1d..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'dotenv/config'; -import { defineConfig } from 'drizzle-kit'; - -export default defineConfig({ - out: './drizzle', - schema: './database/schema.ts', - dialect: 'postgresql', - dbCredentials: { - url: process.env.databaseUrl!, - }, -}); diff --git a/nodemon.json b/nodemon.json old mode 100755 new mode 100644 index d9938ac..d7508b0 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,3 @@ -{ - "ignore": ["telegram/props/*.json", "telegram/props/*.txt"], - "watch": ["telegram", "database", "config"], - "ext": "ts,js", - "exec": "bun telegram/bot.ts" +{ + "ignore": ["src/props/*.json", "src/props/*.txt"] } \ No newline at end of file diff --git a/package.json b/package.json old mode 100755 new mode 100644 index b5c33cc..379cc96 --- a/package.json +++ b/package.json @@ -1,25 +1,13 @@ { "scripts": { - "start": "nodemon telegram/bot.ts", - "docs": "bunx typedoc", - "serve:docs": "bun run serve-docs.ts" + "start": "nodemon src/bot.js" }, "dependencies": { - "@dotenvx/dotenvx": "^1.45.1", - "@types/bun": "^1.2.17", - "axios": "^1.10.0", - "dotenv": "^17.0.0", - "drizzle-orm": "^0.44.2", - "express": "^5.1.0", + "@dotenvx/dotenvx": "^1.28.0", + "axios": "^1.7.9", "node-html-parser": "^7.0.1", - "nodemon": "^3.1.10", - "pg": "^8.16.3", + "nodemon": "^3.1.7", "telegraf": "^4.16.3", - "youtube-url": "^0.5.0" - }, - "devDependencies": { - "@types/pg": "^8.15.4", - "drizzle-kit": "^0.31.4", - "tsx": "^4.20.3" + "winston": "^3.17.0" } } diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..f053de7 --- /dev/null +++ b/src/bot.js @@ -0,0 +1,76 @@ +const { Telegraf } = require('telegraf'); +const path = require('path'); +const fs = require('fs'); +const { isOnSpamWatch } = require('./spamwatch/spamwatch.js'); +require('@dotenvx/dotenvx').config({ path: "config.env" }); +require('./plugins/ytDlpWrapper.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; + +const loadCommands = () => { + const commandsPath = path.join(__dirname, 'commands'); + + try { + const files = fs.readdirSync(commandsPath); + files.forEach((file) => { + try { + const command = require(path.join(commandsPath, file)); + if (typeof command === 'function') { + command(bot, isOnSpamWatch); + } + } catch (error) { + console.error(`Failed to load command file ${file}: ${error.message}`); + } + }); + } catch (error) { + console.error(`Failed to read commands directory: ${error.message}`); + } +}; + +const startBot = async () => { + const botInfo = await bot.telegram.getMe(); + console.log(`${botInfo.first_name} is running...`); + try { + await bot.launch(); + restartCount = 0; + } catch (error) { + console.error('Failed to start bot:', error.message); + if (restartCount < maxRetries) { + restartCount++; + console.log(`Retrying to start bot... Attempt ${restartCount}`); + setTimeout(startBot, 5000); + } else { + console.error('Maximum retry attempts reached. Exiting.'); + process.exit(1); + } + } +}; + +const handleShutdown = (signal) => { + console.log(`Received ${signal}. Stopping bot...`); + bot.stop(signal); + process.exit(0); +}; + +process.once('SIGINT', () => handleShutdown('SIGINT')); +process.once('SIGTERM', () => handleShutdown('SIGTERM')); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception:', error.message); + console.error(error.stack); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); + +loadCommands(); +startBot(); diff --git a/src/commands/animal.js b/src/commands/animal.js new file mode 100644 index 0000000..332e0e3 --- /dev/null +++ b/src/commands/animal.js @@ -0,0 +1,122 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const axios = require("axios"); + +module.exports = (bot) => { + bot.command("duck", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + try { + const response = await axios(Resources.duckApi); + ctx.replyWithPhoto(response.data.url, { + caption: "🦆", + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + const message = Strings.duckApiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + return; + } + }); + + bot.command("fox", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + try { + const response = await axios(Resources.foxApi); + ctx.replyWithPhoto(response.data.image, { + caption: "🦊", + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + const message = Strings.foxApiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + return; + } + }); + + bot.command("dog", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + try { + const response = await axios(Resources.dogApi); + ctx.replyWithPhoto(response.data.message, { + caption: "🐶", + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + const message = Strings.foxApiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + return; + } + }); + + bot.command("cat", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const apiUrl = `${Resources.catApi}?json=true`; + const response = await axios.get(apiUrl); + const data = response.data; + const imageUrl = `${data.url}`; + + try { + await ctx.replyWithPhoto(imageUrl, { + caption: `🐱`, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + ctx.reply(Strings.catImgErr, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + }); + + bot.command(['soggy', 'soggycat'], spamwatchMiddleware, async (ctx) => { + const userInput = ctx.message.text.split(' ')[1]; + + switch (true) { + case (userInput === "2" || userInput === "thumb"): + ctx.replyWithPhoto( + Resources.soggyCat2, { + caption: Resources.soggyCat2, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + break; + + case (userInput === "3" || userInput === "sticker"): + ctx.replyWithSticker( + Resources.soggyCatSticker, { + reply_to_message_id: ctx.message.message_id + }); + break; + + case (userInput === "4" || userInput === "alt"): + ctx.replyWithPhoto( + Resources.soggyCatAlt, { + caption: Resources.soggyCatAlt, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + break; + + default: + ctx.replyWithPhoto( + Resources.soggyCat, { + caption: Resources.soggyCat, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + break; + }; + }); +} \ No newline at end of file diff --git a/src/commands/codename.js b/src/commands/codename.js new file mode 100644 index 0000000..138ae51 --- /dev/null +++ b/src/commands/codename.js @@ -0,0 +1,56 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const axios = require('axios'); +const { verifyInput } = require('../plugins/verifyInput.js'); + +async function getDeviceList() { + try { + const response = await axios.get(Resources.codenameApi); + return response.data + } catch (error) { + const message = Strings.codenameCheck.apiErr + .replace('{error}', error.message); + + return ctx.reply(message, { + parse_mode: "Markdown", + reply_to_message_id: ctx.message.message_id + }); + } +} + +module.exports = (bot) => { + bot.command(['codename', 'whatis'], spamwatchMiddleware, async (ctx) => { + const userInput = ctx.message.text.split(" ").slice(1).join(" "); + const Strings = getStrings(ctx.from.language_code); + const { noCodename } = Strings.codenameCheck + + if(verifyInput(ctx, userInput, noCodename)){ + return; + } + + const jsonRes = await getDeviceList() + const phoneSearch = Object.keys(jsonRes).find((codename) => codename === userInput); + + if (!phoneSearch) { + return ctx.reply(Strings.codenameCheck.notFound, { + parse_mode: "Markdown", + reply_to_message_id: ctx.message.message_id + }); + } + + const deviceDetails = jsonRes[phoneSearch]; + const device = deviceDetails.find((item) => item.brand) || deviceDetails[0]; + const message = Strings.codenameCheck.resultMsg + .replace('{brand}', device.brand) + .replace('{codename}', userInput) + .replace('{model}', device.model) + .replace('{name}', device.name); + + return ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }) +} \ No newline at end of file diff --git a/src/commands/crew.js b/src/commands/crew.js new file mode 100644 index 0000000..e2282ea --- /dev/null +++ b/src/commands/crew.js @@ -0,0 +1,223 @@ +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const os = require('os'); +const { exec } = require('child_process'); +const { error } = require('console'); + +function getGitCommitHash() { + return new Promise((resolve, reject) => { + exec('git rev-parse --short HEAD', (error, stdout, stderr) => { + if (error) { + reject(`Error: ${stderr}`); + } else { + resolve(stdout.trim()); + } + }); + }); +} + +function updateBot() { + return new Promise((resolve, reject) => { + exec('git pull && echo "A" >> restart.txt', (error, stdout, stderr) => { + if (error) { + reject(`Error: ${stderr}`); + } else { + resolve(stdout.trim()); + } + }); + }); +} + +function formatUptime(uptime) { + const hours = Math.floor(uptime / 3600); + const minutes = Math.floor((uptime % 3600) / 60); + const seconds = Math.floor(uptime % 60); + return `${hours}h ${minutes}m ${seconds}s`; +} + +function getSystemInfo() { + const { platform, release, arch, cpus, totalmem, freemem, loadavg, uptime } = os; + const [cpu] = cpus(); + return `*Server Stats*\n\n` + + `*OS:* \`${platform()} ${release()}\`\n` + + `*Arch:* \`${arch()}\`\n` + + `*Node.js Version:* \`${process.version}\`\n` + + `*CPU:* \`${cpu.model}\`\n` + + `*CPU Cores:* \`${cpus().length} cores\`\n` + + `*RAM:* \`${(freemem() / (1024 ** 3)).toFixed(2)} GB / ${(totalmem() / (1024 ** 3)).toFixed(2)} GB\`\n` + + `*Load Average:* \`${loadavg().map(avg => avg.toFixed(2)).join(', ')}\`\n` + + `*Uptime:* \`${formatUptime(uptime())}\`\n\n`; +} + +async function handleAdminCommand(ctx, action, successMessage, errorMessage) { + const Strings = getStrings(ctx.from.language_code); + const userId = ctx.from.id; + const adminArray = JSON.parse("[" + process.env.botAdmins + "]"); + if (adminArray.includes(userId)) { + try { + await action(); + ctx.reply(successMessage, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + ctx.reply(errorMessage.replace(/{error}/g, error.message), { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + } else { + ctx.reply(Strings.noPermission, { + reply_to_message_id: ctx.message.message_id + }); + } +} + +module.exports = (bot) => { + bot.command('getbotstats', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + handleAdminCommand(ctx, async () => { + const stats = getSystemInfo(); + await ctx.reply(stats, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }, '', Strings.errorRetrievingStats); + }); + + bot.command('getbotcommit', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + handleAdminCommand(ctx, async () => { + try { + const commitHash = await getGitCommitHash(); + 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}/g, error), { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + }, '', Strings.gitErrRetrievingCommit); + }); + + bot.command('updatebot', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + handleAdminCommand(ctx, async () => { + try { + const result = await updateBot(); + 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}/g, error), { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + }, '', Strings.errorUpdatingBot); + }); + + bot.command('setbotname', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const botName = ctx.message.text.split(' ').slice(1).join(' '); + handleAdminCommand(ctx, async () => { + await ctx.telegram.setMyName(botName); + }, Strings.botNameChanged.replace(/{botName}/g, botName), Strings.botNameErr.replace(/{error}/g, error)); + }); + + bot.command('setbotdesc', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const botDesc = ctx.message.text.split(' ').slice(1).join(' '); + handleAdminCommand(ctx, async () => { + await ctx.telegram.setMyDescription(botDesc); + }, Strings.botDescChanged.replace(/{botDesc}/g, botDesc), Strings.botDescErr.replace(/{error}/g, error)); + }); + + bot.command('botkickme', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + handleAdminCommand(ctx, async () => { + ctx.reply(Strings.kickingMyself, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + await ctx.telegram.leaveChat(ctx.chat.id); + }, '', Strings.kickingMyselfErr); + }); + + bot.command('getfile', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const botFile = ctx.message.text.split(' ').slice(1).join(' '); + handleAdminCommand(ctx, async () => { + try { + await ctx.replyWithDocument({ + source: botFile, + caption: botFile + }); + } catch (error) { + ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + }, '', Strings.unexpectedErr); + }); + + bot.command('run', spamwatchMiddleware, async (ctx) => { + const command = ctx.message.text.split(' ').slice(1).join(' '); + handleAdminCommand(ctx, async () => { + if (!command) { + return ctx.reply('Por favor, forneça um comando para executar.'); + } + + exec(command, (error, stdout, stderr) => { + if (error) { + return ctx.reply(`\`${error.message}\``, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + if (stderr) { + return ctx.reply(`\`${stderr}\``, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + ctx.reply(`\`${stdout}\``, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }); + }, '', "Nope!"); + }); + + bot.command('eval', spamwatchMiddleware, async (ctx) => { + const code = ctx.message.text.split(' ').slice(1).join(' '); + if (!code) { + return ctx.reply('Por favor, forneça um código para avaliar.'); + } + + try { + const result = eval(code); + ctx.reply(`Result: ${result}`, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + ctx.reply(`Error: ${error.message}`, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + }); + + bot.command('crash', spamwatchMiddleware, async (ctx) => { + handleAdminCommand(ctx, async () => { + ctx.reply(null); + }, '', "Nope!"); + }); +}; diff --git a/src/commands/fun.js b/src/commands/fun.js new file mode 100644 index 0000000..0c64bbe --- /dev/null +++ b/src/commands/fun.js @@ -0,0 +1,101 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); + +function sendRandomReply(ctx, gifUrl, textKey) { + const Strings = getStrings(ctx.from.language_code); + const randomNumber = Math.floor(Math.random() * 100); + const shouldSendGif = randomNumber > 50; + + const caption = Strings[textKey].replace('{randomNum}', randomNumber) + + if (shouldSendGif) { + ctx.replyWithAnimation(gifUrl, { + caption, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }).catch(err => { + gifErr = gifErr.replace('{err}', err); + ctx.reply(Strings.gifErr, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }); + } else { + ctx.reply(caption, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } +} + + +async function handleDiceCommand(ctx, emoji, delay) { + const Strings = getStrings(ctx.from.language_code); + + const result = await ctx.sendDice({ emoji, reply_to_message_id: ctx.message.message_id }); + const botResponse = Strings.funEmojiResult + .replace('{emoji}', result.dice.emoji) + .replace('{value}', result.dice.value); + + setTimeout(() => { + ctx.reply(botResponse, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }, delay); +} + +function getRandomInt(max) { + return Math.floor(Math.random() * (max + 1)); +} + +module.exports = (bot) => { + bot.command('random', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const randomValue = getRandomInt(11); + const randomVStr = Strings.randomNum.replace('{number}', randomValue); + + ctx.reply( + randomVStr, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }); + + bot.command('dice', spamwatchMiddleware, async (ctx) => { + await handleDiceCommand(ctx, undefined, 4000); + }); + + bot.command('slot', spamwatchMiddleware, async (ctx) => { + await handleDiceCommand(ctx, '🎰', 3000); + }); + + bot.command('ball', spamwatchMiddleware, async (ctx) => { + await handleDiceCommand(ctx, '⚽', 3000); + }); + + bot.command('dart', spamwatchMiddleware, async (ctx) => { + await handleDiceCommand(ctx, '🎯', 3000); + }); + + bot.command('bowling', spamwatchMiddleware, async (ctx) => { + await handleDiceCommand(ctx, '🎳', 3000); + }); + + bot.command('idice', spamwatchMiddleware, async (ctx) => { + ctx.replyWithSticker( + Resources.infiniteDice, { + reply_to_message_id: ctx.message.message_id + }); + }); + + bot.command('furry', spamwatchMiddleware, async (ctx) => { + sendRandomReply(ctx, Resources.furryGif, 'furryAmount'); + }); + + bot.command('gay', spamwatchMiddleware, async (ctx) => { + sendRandomReply(ctx, Resources.gayFlag, 'gayAmount'); + }); +}; \ No newline at end of file diff --git a/telegram/commands/gsmarena.ts b/src/commands/gsmarena.js old mode 100755 new mode 100644 similarity index 51% rename from telegram/commands/gsmarena.ts rename to src/commands/gsmarena.js index b345a00..23478c3 --- a/telegram/commands/gsmarena.ts +++ b/src/commands/gsmarena.js @@ -4,27 +4,18 @@ // With some help from GPT (I don't really like AI but whatever) // If this were a kang, I would not be giving credits to him! -import { isOnSpamWatch } from '../spamwatch/spamwatch'; -import spamwatchMiddlewareModule from '../spamwatch/Middleware'; -import axios from 'axios'; -import { parse } from 'node-html-parser'; -import { getDeviceByCodename } from './codename'; -import { getStrings } from '../plugins/checklang'; -import { languageCode } from '../utils/language-code'; -import { isCommandDisabled } from '../utils/check-command-disabled'; +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); -const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch); +const axios = require('axios'); +const { parse } = require('node-html-parser'); -interface PhoneSearchResult { - name: string; - url: string; -} - -interface PhoneDetails { - specs: Record>; - name?: string; - url?: string; - picture?: string; +class PhoneSearchResult { + constructor(name, url) { + this.name = name; + this.url = url; + Object.freeze(this); + } } const HEADERS = { @@ -41,7 +32,7 @@ function getDataFromSpecs(specsData, category, attributes) { .join("\n"); } -function parseSpecs(specsData: PhoneDetails): PhoneDetails { +function parseSpecs(specsData) { const categories = { "status": ["Launch", ["Status"]], "network": ["Network", ["Technology"]], @@ -78,7 +69,7 @@ function parseSpecs(specsData: PhoneDetails): PhoneDetails { const [cat, attrs] = categories[key]; acc[key] = getDataFromSpecs(specsData, cat, attrs) || ""; return acc; - }, { specs: {} } as PhoneDetails); + }, {}); parsedData["name"] = specsData.name || ""; parsedData["url"] = specsData.url || ""; @@ -86,7 +77,7 @@ function parseSpecs(specsData: PhoneDetails): PhoneDetails { return parsedData; } -function formatPhone(phone: PhoneDetails) { +function formatPhone(phone) { const formattedPhone = parseSpecs(phone); const attributesDict = { "Status": "status", @@ -131,7 +122,7 @@ function formatPhone(phone: PhoneDetails) { return `\n\nName: ${formattedPhone.name}\n\n${attributes}\n\n${deviceImage}\n\n${deviceUrl}`; } -async function fetchHtml(url: string) { +async function fetchHtml(url) { try { const response = await axios.get(url, { headers: HEADERS }); return response.data; @@ -141,7 +132,7 @@ async function fetchHtml(url: string) { } } -async function searchPhone(phone: string): Promise { +async function searchPhone(phone) { try { const searchUrl = `https://m.gsmarena.com/results.php3?sQuickSearch=yes&sName=${encodeURIComponent(phone)}`; const htmlContent = await fetchHtml(searchUrl); @@ -151,7 +142,7 @@ async function searchPhone(phone: string): Promise { return foundPhones.map((phoneTag) => { const name = phoneTag.querySelector('img')?.getAttribute('title') || ""; const url = phoneTag.querySelector('a')?.getAttribute('href') || ""; - return { name, url }; + return new PhoneSearchResult(name, url); }); } catch (error) { console.error("Error searching for phone:", error); @@ -173,7 +164,7 @@ async function checkPhoneDetails(url) { return { ...specsData, name, picture, url: `https://www.gsmarena.com/${url}` }; } catch (error) { console.error("Error fetching phone details:", error); - return { specs: {}, name: "", url: "", picture: "" }; + return {}; } } @@ -210,132 +201,54 @@ function getUsername(ctx){ return userName; } -const deviceSelectionCache: Record = {}; -const lastSelectionMessageId: Record = {}; - -export default (bot, db) => { +module.exports = (bot) => { bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => { - if (await isCommandDisabled(ctx, db, 'device-specs')) return; - const userId = ctx.from.id; const userName = getUsername(ctx); - const Strings = getStrings(languageCode(ctx)); const phone = ctx.message.text.split(" ").slice(1).join(" "); if (!phone) { - return ctx.reply(Strings.gsmarenaProvidePhoneName || "[TODO: Add gsmarenaProvidePhoneName to locales] Please provide the phone name.", { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) }); + return ctx.reply("Please provide the phone name.", { reply_to_message_id: ctx.message.message_id }); } - console.log("[GSMArena] Searching for", phone); - const statusMsg = await ctx.reply((Strings.gsmarenaSearchingFor || "[TODO: Add gsmarenaSearchingFor to locales] Searching for {phone}...").replace('{phone}', phone), { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}), parse_mode: 'Markdown' }); - - let results = await searchPhone(phone); + const results = await searchPhone(phone); if (results.length === 0) { - const codenameResults = await getDeviceByCodename(phone.split(" ")[0]); - if (!codenameResults) { - await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaNoPhonesFound || "[TODO: Add gsmarenaNoPhonesFound to locales] No phones found for {phone}.").replace('{phone}', phone), { parse_mode: 'Markdown' }); - return; - } - - await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaSearchingFor || "[TODO: Add gsmarenaSearchingFor to locales] Searching for {phone}...").replace('{phone}', codenameResults.name), { parse_mode: 'Markdown' }); - const nameResults = await searchPhone(codenameResults.name); - if (nameResults.length === 0) { - await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaNoPhonesFoundBoth || "[TODO: Add gsmarenaNoPhonesFoundBoth to locales] No phones found for {name} and {phone}.").replace('{name}', codenameResults.name).replace('{phone}', phone), { parse_mode: 'Markdown' }); - return; - } - results = nameResults; + return ctx.reply("No phones found.", { reply_to_message_id: ctx.message.message_id }); } - if (deviceSelectionCache[userId]?.timeout) { - clearTimeout(deviceSelectionCache[userId].timeout); - } - deviceSelectionCache[userId] = { - results, - timeout: setTimeout(() => { delete deviceSelectionCache[userId]; }, 5 * 60 * 1000) + const testUser = `${userName}, please select your device:`; + const options = { + parse_mode: 'HTML', + reply_to_message_id: ctx.message.message_id, + disable_web_page_preview: true, + reply_markup: { + inline_keyboard: results.map(result => [{ text: result.name, callback_data: `details:${result.url}:${ctx.from.id}` }]) + } }; + ctx.reply(testUser, options); - if (lastSelectionMessageId[userId]) { - try { - await ctx.telegram.editMessageText( - ctx.chat.id, - lastSelectionMessageId[userId], - undefined, - Strings.gsmarenaSelectDevice || "[TODO: Add gsmarenaSelectDevice to locales] Please select your device:", - { - parse_mode: 'HTML', - reply_to_message_id: ctx.message.message_id, - disable_web_page_preview: true, - reply_markup: { - inline_keyboard: results.map((result, idx) => { - const callbackData = `gsmadetails:${idx}:${ctx.from.id}`; - return [{ text: result.name, callback_data: callbackData }]; - }) - } - } - ); - } catch (e) { - const testUser = `${userName}, ${Strings.gsmarenaSelectDevice || "[TODO: Add gsmarenaSelectDevice to locales] please select your device:"}`; - const options = { - parse_mode: 'HTML', - reply_to_message_id: ctx.message.message_id, - disable_web_page_preview: true, - reply_markup: { - inline_keyboard: results.map((result, idx) => { - const callbackData = `gsmadetails:${idx}:${ctx.from.id}`; - return [{ text: result.name, callback_data: callbackData }]; - }) - } - }; - const selectionMsg = await ctx.reply(testUser, options); - lastSelectionMessageId[userId] = selectionMsg.message_id; - } - } else { - const testUser = `${userName}, ${Strings.gsmarenaSelectDevice || "[TODO: Add gsmarenaSelectDevice to locales] please select your device:"}`; - const inlineKeyboard = results.map((result, idx) => { - const callbackData = `gsmadetails:${idx}:${ctx.from.id}`; - return [{ text: result.name, callback_data: callbackData }]; - }); - const options = { - parse_mode: 'HTML', - reply_to_message_id: ctx.message.message_id, - disable_web_page_preview: true, - reply_markup: { - inline_keyboard: inlineKeyboard - } - }; - const selectionMsg = await ctx.reply(testUser, options); - lastSelectionMessageId[userId] = selectionMsg.message_id; - } - await ctx.telegram.deleteMessage(ctx.chat.id, statusMsg.message_id).catch(() => {}); }); - bot.action(/gsmadetails:(\d+):(\d+)/, async (ctx) => { - const idx = parseInt(ctx.match[1]); + bot.action(/details:(.+):(.+)/, async (ctx) => { + const url = ctx.match[1]; const userId = parseInt(ctx.match[2]); const userName = getUsername(ctx); - const Strings = getStrings(languageCode(ctx)); const callbackQueryUserId = ctx.update.callback_query.from.id; if (userId !== callbackQueryUserId) { - return ctx.answerCbQuery(`${userName}, ${Strings.gsmarenaNotAllowed || "[TODO: Add gsmarenaNotAllowed to locales] you are not allowed to interact with this."}`); + return ctx.answerCbQuery(`${userName}, you are not allowed to interact with this.`); } ctx.answerCbQuery(); - const cache = deviceSelectionCache[userId]; - if (!cache || !cache.results[idx]) { - return ctx.reply(Strings.gsmarenaInvalidOrExpired || "[TODO: Add gsmarenaInvalidOrExpired to locales] Whoops, invalid or expired option. Please try again.", { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) }); - } - const url = cache.results[idx].url; - const phoneDetails = await checkPhoneDetails(url); if (phoneDetails.name) { const message = formatPhone(phoneDetails); - ctx.editMessageText(`${userName}, ${Strings.gsmarenaDeviceDetails || "[TODO: Add gsmarenaDeviceDetails to locales] these are the details of your device:"}` + message, { parse_mode: 'HTML', disable_web_page_preview: false }); + ctx.editMessageText(`${userName}, these are the details of your device:` + message, { parse_mode: 'HTML', disable_web_page_preview: false }); } else { - ctx.reply(Strings.gsmarenaErrorFetchingDetails || "[TODO: Add gsmarenaErrorFetchingDetails to locales] Error fetching phone details.", { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) }); + ctx.reply("Error fetching phone details.", { reply_to_message_id: ctx.message.message_id }); } }); }; diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 0000000..b4cc44b --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,112 @@ +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); + +async function sendHelpMessage(ctx, isEditing) { + const Strings = getStrings(ctx.from.language_code); + const botInfo = await ctx.telegram.getMe(); + const helpText = Strings.botHelp + .replace(/{botName}/g, botInfo.first_name) + .replace(/{sourceLink}/g, process.env.botSource); + function getMessageId(ctx) { + return ctx.message?.message_id || ctx.callbackQuery?.message?.message_id; + }; + const createOptions = (ctx, includeReplyTo = false) => { + const options = { + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_markup: { + inline_keyboard: [ + [{ text: Strings.mainCommands, callback_data: 'helpMain' }, { text: Strings.usefulCommands, callback_data: 'helpUseful' }], + [{ text: Strings.interactiveEmojis, callback_data: 'helpInteractive' }, { text: Strings.funnyCommands, callback_data: 'helpFunny' }], + [{ text: Strings.lastFm.helpEntry, callback_data: 'helpLast' }, { text: Strings.animalCommands, callback_data: 'helpAnimals' }], + [{ text: Strings.ytDownload.helpEntry, callback_data: 'helpYouTube' }, { text: Strings.ponyApi.helpEntry, callback_data: 'helpMLP' }] + ] + } + }; + if (includeReplyTo) { + const messageId = getMessageId(ctx); + if (messageId) { + options.reply_to_message_id = messageId; + }; + }; + return options; + }; + if (isEditing) { + await ctx.editMessageText(helpText, createOptions(ctx)); + } else { + await ctx.reply(helpText, createOptions(ctx, true)); + }; +} + +module.exports = (bot) => { + bot.help(spamwatchMiddleware, async (ctx) => { + await sendHelpMessage(ctx); + }); + + bot.command("about", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`); + ctx.reply(aboutMsg, { + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + }) + + bot.on('callback_query', async (ctx) => { + const callbackData = ctx.callbackQuery.data; + const Strings = getStrings(ctx.from.language_code); + const options = { + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_markup: JSON.stringify({ + inline_keyboard: [ + [{ text: Strings.varStrings.varBack, callback_data: 'helpBack' }], + ] + }) + }; + + switch (callbackData) { + case 'helpMain': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.mainCommandsDesc, options); + break; + case 'helpUseful': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.usefulCommandsDesc, options); + break; + case 'helpInteractive': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.interactiveEmojisDesc, options); + break; + case 'helpFunny': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.funnyCommandsDesc, options); + break; + case 'helpLast': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.lastFm.helpDesc, options); + break; + case 'helpYouTube': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.ytDownload.helpDesc, options); + break; + case 'helpAnimals': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.animalCommandsDesc, options); + break; + case 'helpMLP': + await ctx.answerCbQuery(); + await ctx.editMessageText(Strings.ponyApi.helpDesc, options); + break; + case 'helpBack': + await ctx.answerCbQuery(); + await sendHelpMessage(ctx, true); + break; + default: + await ctx.answerCbQuery(Strings.errInvalidOption); + break; + } + }); +} diff --git a/src/commands/http.js b/src/commands/http.js new file mode 100644 index 0000000..1382ad6 --- /dev/null +++ b/src/commands/http.js @@ -0,0 +1,73 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const axios = require('axios'); +const { verifyInput } = require('../plugins/verifyInput.js'); + +module.exports = (bot) => { + bot.command("http", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const userInput = ctx.message.text.split(' ')[1]; + const apiUrl = Resources.httpApi; + const { invalidCode } = Strings.httpCodes + + if (verifyInput(ctx, userInput, invalidCode, true)) { + return; + } + + try { + const response = await axios.get(apiUrl); + const data = response.data; + const codesArray = Array.isArray(data) ? data : Object.values(data); + const codeInfo = codesArray.find(item => item.code === parseInt(userInput)); + + if (codeInfo) { + const message = Strings.httpCodes.resultMsg + .replace("{code}", codeInfo.code) + .replace("{message}", codeInfo.message) + .replace("{description}", codeInfo.description); + await ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } else { + await ctx.reply(Strings.httpCodes.notFound, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + } catch (error) { + const message = Strings.httpCodes.fetchErr.replace("{error}", error); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + }); + + bot.command("httpcat", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(/\s+/g, ''); + const { invalidCode } = Strings.httpCodes + + if (verifyInput(ctx, userInput, invalidCode, true)) { + return; + } + + const apiUrl = `${Resources.httpCatApi}${userInput}`; + + try { + await ctx.replyWithPhoto(apiUrl, { + caption: `🐱 ${apiUrl}`, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + ctx.reply(Strings.catImgErr, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } + }); +}; diff --git a/src/commands/info.js b/src/commands/info.js new file mode 100644 index 0000000..eed4874 --- /dev/null +++ b/src/commands/info.js @@ -0,0 +1,63 @@ +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); + +async function getUserInfo(ctx) { + const Strings = getStrings(ctx.from.language_code); + let lastName = ctx.from.last_name; + if (lastName === undefined) { + lastName = " "; + } + + userInfo = Strings.userInfo + .replace('{userName}', `${ctx.from.first_name} ${lastName}` || Strings.varStrings.varUnknown) + .replace('{userId}', ctx.from.id || Strings.varStrings.varUnknown) + .replace('{userHandle}', ctx.from.username ? `@${ctx.from.username}` : Strings.varStrings.varNone) + .replace('{userPremium}', ctx.from.is_premium ? Strings.varStrings.varYes : Strings.varStrings.varNo) + .replace('{userLang}', ctx.from.language_code || Strings.varStrings.varUnknown); + + return userInfo; +} + +async function getChatInfo(ctx) { + const Strings = getStrings(ctx.from.language_code); + if (ctx.chat.type === 'group' || ctx.chat.type === 'supergroup') { + chatInfo = Strings.chatInfo + .replace('{chatId}', ctx.chat.id || Strings.varStrings.varUnknown) + .replace('{chatName}', ctx.chat.title || Strings.varStrings.varUnknown) + .replace('{chatHandle}', ctx.chat.username ? `@${ctx.chat.username}` : Strings.varStrings.varNone) + .replace('{chatMembersCount}', await ctx.getChatMembersCount(ctx.chat.id || Strings.varStrings.varUnknown)) + .replace('{chatType}', ctx.chat.type || Strings.varStrings.varUnknown) + .replace('{isForum}', ctx.chat.is_forum ? Strings.varStrings.varYes : Strings.varStrings.varNo); + + return chatInfo; + } else { + return ctx.reply( + Strings.groupOnly, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } +} + +module.exports = (bot) => { + bot.command('chatinfo', spamwatchMiddleware, async (ctx) => { + const chatInfo = await getChatInfo(ctx); + ctx.reply( + chatInfo, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + } + ); + }); + + bot.command('userinfo', spamwatchMiddleware, async (ctx) => { + const userInfo = await getUserInfo(ctx); + ctx.reply( + userInfo, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + } + ); + }); +}; diff --git a/telegram/commands/lastfm.ts b/src/commands/lastfm.js old mode 100755 new mode 100644 similarity index 78% rename from telegram/commands/lastfm.ts rename to src/commands/lastfm.js index 2ccecbf..1b1976f --- a/telegram/commands/lastfm.ts +++ b/src/commands/lastfm.js @@ -1,17 +1,14 @@ -import Resources from '../props/resources.json'; -import fs from 'fs'; -import axios from 'axios'; -import { getStrings } from '../plugins/checklang'; -import { isOnSpamWatch } from '../spamwatch/spamwatch'; -import spamwatchMiddlewareModule from '../spamwatch/Middleware'; -import { isCommandDisabled } from '../utils/check-command-disabled'; - -const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch); +const Resources = require('../props/resources.json'); +const fs = require('fs'); +const axios = require('axios'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); const scrobbler_url = Resources.lastFmApi; const api_key = process.env.lastKey; -const dbFile = 'telegram/props/lastfm.json'; +const dbFile = 'src/props/lastfm.json'; let users = {}; function loadUsers() { @@ -38,7 +35,7 @@ function saveUsers() { } } -async function getFromMusicBrainz(mbid: string) { +async function getFromMusicBrainz(mbid) { try { const response = await axios.get(`${Resources.musicBrainzApi}${mbid}`); const imgObjLarge = response.data.images[0]?.thumbnails?.['1200']; @@ -61,12 +58,10 @@ function getFromLast(track) { return imageUrl; } -export default (bot, db) => { +module.exports = (bot) => { loadUsers(); - bot.command('setuser', async (ctx) => { - if (await isCommandDisabled(ctx, db, 'lastfm')) return; - + bot.command('setuser', (ctx) => { const userId = ctx.from.id; const Strings = getStrings(ctx.from.language_code); const lastUser = ctx.message.text.split(' ')[1]; @@ -75,7 +70,7 @@ export default (bot, db) => { return ctx.reply(Strings.lastFm.noUser, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; @@ -87,24 +82,22 @@ export default (bot, db) => { ctx.reply(message, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }); bot.command(['lt', 'lmu', 'last', 'lfm'], spamwatchMiddleware, async (ctx) => { - if (await isCommandDisabled(ctx, db, 'lastfm')) return; - const userId = ctx.from.id; const Strings = getStrings(ctx.from.language_code); const lastfmUser = users[userId]; const genericImg = Resources.lastFmGenericImg; const botInfo = await ctx.telegram.getMe(); - + if (!lastfmUser) { return ctx.reply(Strings.lastFm.noUserSet, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; @@ -129,7 +122,7 @@ export default (bot, db) => { return ctx.reply(noRecent, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; @@ -142,8 +135,8 @@ export default (bot, db) => { if (albumMbid) { imageUrl = await getFromMusicBrainz(albumMbid); - } - + } + if (!imageUrl) { imageUrl = getFromLast(track); } @@ -156,7 +149,7 @@ export default (bot, db) => { const artistUrl = `https://www.last.fm/music/${encodeURIComponent(artistName)}`; const userUrl = `https://www.last.fm/user/${encodeURIComponent(lastfmUser)}`; - let num_plays = 0; + let num_plays = ''; try { const response_plays = await axios.get(scrobbler_url, { params: { @@ -171,8 +164,11 @@ export default (bot, db) => { 'User-Agent': `@${botInfo.username}-node-telegram-bot` } }); - num_plays = response_plays.data.track.userplaycount; + + if (!num_plays || num_plays === undefined) { + num_plays = 0; + }; } catch (err) { console.log(err) const message = Strings.lastFm.apiErr @@ -181,7 +177,7 @@ export default (bot, db) => { ctx.reply(message, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; @@ -205,13 +201,13 @@ export default (bot, db) => { caption: message, parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); } else { ctx.reply(message, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; } catch (err) { @@ -222,7 +218,7 @@ export default (bot, db) => { ctx.reply(message, { parse_mode: "Markdown", disable_web_page_preview: true, - ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) + reply_to_message_id: ctx.message.message_id }); }; }); diff --git a/src/commands/main.js b/src/commands/main.js new file mode 100644 index 0000000..975762d --- /dev/null +++ b/src/commands/main.js @@ -0,0 +1,27 @@ +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); + +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}/g, botInfo.first_name); + + ctx.reply(startMsg, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }); + + bot.command('privacy', spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const message = Strings.botPrivacy.replace("{botPrivacy}", process.env.botPrivacy); + + ctx.reply(message, { + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + }); +}; \ No newline at end of file diff --git a/src/commands/modarchive.js b/src/commands/modarchive.js new file mode 100644 index 0000000..4e89370 --- /dev/null +++ b/src/commands/modarchive.js @@ -0,0 +1,72 @@ +const Resources = require('../props/resources.json'); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); + +async function downloadModule(moduleId) { + try { + const downloadUrl = `${Resources.modArchiveApi}${moduleId}`; + const response = await axios({ + url: downloadUrl, + method: 'GET', + responseType: 'stream', + }); + + const disposition = response.headers['content-disposition']; + let fileName = moduleId; + + if (disposition && disposition.includes('filename=')) { + fileName = disposition + .split('filename=')[1] + .split(';')[0] + .replace(/['"]/g, ''); + } + + const filePath = path.resolve(__dirname, fileName); + + const writer = fs.createWriteStream(filePath); + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', () => resolve({ filePath, fileName })); + writer.on('error', reject); + }); + } catch (error) { + return null; + } +} + +module.exports = (bot) => { + bot.command(['modarchive', 'tma'], spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const moduleId = ctx.message.text.split(' ')[1]; + + if (moduleId == NaN || null) { + return ctx.reply(Strings.maInvalidModule, { + parse_mode: "Markdown", + reply_to_message_id: ctx.message.message_id + }); + } + + const result = await downloadModule(moduleId); + + if (result) { + const { filePath, fileName } = result; + + await ctx.replyWithDocument({ source: filePath }, { + caption: fileName, + reply_to_message_id: ctx.message.message_id + }); + + fs.unlinkSync(filePath); + } else { + ctx.reply(Strings.maDownloadError, { + parse_mode: "Markdown", + reply_to_message_id: ctx.message.message_id + }); + } + }); +}; diff --git a/src/commands/ponyapi.js b/src/commands/ponyapi.js new file mode 100644 index 0000000..e5fbf94 --- /dev/null +++ b/src/commands/ponyapi.js @@ -0,0 +1,236 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const axios = require("axios"); +const { verifyInput } = require('../plugins/verifyInput.js'); + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +module.exports = (bot) => { + bot.command("mlp", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + + ctx.reply(Strings.ponyApi.helpDesc, { + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + }); + + bot.command("mlpchar", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+"); + const { noCharName } = Strings.ponyApi + + if (verifyInput(ctx, userInput, noCharName)) { + return; + } + + const capitalizedInput = capitalizeFirstLetter(userInput); + const apiUrl = `${Resources.ponyApi}/character/${capitalizedInput}`; + + try { + const response = await axios(apiUrl); + const charactersArray = []; + + if (Array.isArray(response.data.data)) { + response.data.data.forEach(character => { + let aliases = []; + if (character.alias) { + if (typeof character.alias === 'string') { + aliases.push(character.alias); + } else if (Array.isArray(character.alias)) { + aliases = aliases.concat(character.alias); + } + } + + charactersArray.push({ + id: character.id, + name: character.name, + alias: aliases.length > 0 ? aliases.join(', ') : Strings.varStrings.varNone, + url: character.url, + sex: character.sex, + residence: character.residence ? character.residence.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + occupation: character.occupation ? character.occupation.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + kind: character.kind ? character.kind.join(', ') : Strings.varStrings.varNone, + image: character.image + }); + }); + }; + + if (charactersArray.length > 0) { + const result = Strings.ponyApi.charRes + .replace("{id}", charactersArray[0].id) + .replace("{name}", charactersArray[0].name) + .replace("{alias}", charactersArray[0].alias) + .replace("{url}", charactersArray[0].url) + .replace("{sex}", charactersArray[0].sex) + .replace("{residence}", charactersArray[0].residence) + .replace("{occupation}", charactersArray[0].occupation) + .replace("{kind}", charactersArray[0].kind); + + ctx.replyWithPhoto(charactersArray[0].image[0], { + caption: `${result}`, + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + } else { + ctx.reply(Strings.ponyApi.noCharFound, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + } catch (error) { + const message = Strings.ponyApi.apiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + }); + + bot.command("mlpep", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+"); + + const { noEpisodeNum } = Strings.ponyApi + + if (verifyInput(ctx, userInput, noEpisodeNum, true)) { + return; + } + + const apiUrl = `${Resources.ponyApi}/episode/by-overall/${userInput}`; + + try { + const response = await axios(apiUrl); + const episodeArray = []; + + if (Array.isArray(response.data.data)) { + response.data.data.forEach(episode => { + episodeArray.push({ + id: episode.id, + name: episode.name, + image: episode.image, + url: episode.url, + season: episode.season, + episode: episode.episode, + overall: episode.overall, + airdate: episode.airdate, + storyby: episode.storyby ? episode.storyby.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + writtenby: episode.writtenby ? episode.writtenby.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + storyboard: episode.storyboard ? episode.storyboard.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + }); + }); + }; + + if (episodeArray.length > 0) { + const result = Strings.ponyApi.epRes + .replace("{id}", episodeArray[0].id) + .replace("{name}", episodeArray[0].name) + .replace("{url}", episodeArray[0].url) + .replace("{season}", episodeArray[0].season) + .replace("{episode}", episodeArray[0].episode) + .replace("{overall}", episodeArray[0].overall) + .replace("{airdate}", episodeArray[0].airdate) + .replace("{storyby}", episodeArray[0].storyby) + .replace("{writtenby}", episodeArray[0].writtenby) + .replace("{storyboard}", episodeArray[0].storyboard); + + ctx.replyWithPhoto(episodeArray[0].image, { + caption: `${result}`, + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + } else { + ctx.reply(Strings.ponyApi.noEpisodeFound, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + } catch (error) { + const message = Strings.ponyApi.apiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + }); + + bot.command("mlpcomic", spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+"); + + const { noComicName } = Strings.ponyApi + + if (verifyInput(ctx, userInput, noComicName)) { + return; + }; + + const apiUrl = `${Resources.ponyApi}/comics-story/${userInput}`; + + try { + const response = await axios(apiUrl); + const comicArray = []; + if (Array.isArray(response.data.data)) { + response.data.data.forEach(comic => { + let letterers = []; + if (comic.letterer) { + if (typeof comic.letterer === 'string') { + letterers.push(comic.letterer); + } else if (Array.isArray(comic.letterer)) { + letterers = aliases.concat(comic.letterer); + } + } + comicArray.push({ + id: comic.id, + name: comic.name, + series: comic.series, + image: comic.image, + url: comic.url, + writer: comic.writer ? comic.writer.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + artist: comic.artist ? comic.artist.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + colorist: comic.colorist ? comic.colorist.replace(/\n/g, ' / ') : Strings.varStrings.varNone, + letterer: letterers.length > 0 ? letterers.join(', ') : Strings.varStrings.varNone, + editor: comic.editor + }); + }); + }; + + if (comicArray.length > 0) { + const result = Strings.ponyApi.comicRes + .replace("{id}", comicArray[0].id) + .replace("{name}", comicArray[0].name) + .replace("{series}", comicArray[0].series) + .replace("{url}", comicArray[0].url) + .replace("{writer}", comicArray[0].writer) + .replace("{artist}", comicArray[0].artist) + .replace("{colorist}", comicArray[0].colorist) + .replace("{letterer}", comicArray[0].letterer) + .replace("{editor}", comicArray[0].editor); + + ctx.replyWithPhoto(comicArray[0].image, { + caption: `${result}`, + parse_mode: 'Markdown', + disable_web_page_preview: true, + reply_to_message_id: ctx.message.message_id + }); + } else { + ctx.reply(Strings.ponyApi.noComicFound, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + } catch (error) { + const message = Strings.ponyApi.apiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + }; + }); +}; diff --git a/src/commands/randompony.js b/src/commands/randompony.js new file mode 100644 index 0000000..1be1e73 --- /dev/null +++ b/src/commands/randompony.js @@ -0,0 +1,36 @@ +const Resources = require('../props/resources.json'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const axios = require("axios"); + +module.exports = (bot) => { + bot.command(["rpony", "randompony", "mlpart"], spamwatchMiddleware, async (ctx) => { + const Strings = getStrings(ctx.from.language_code); + try { + const response = await axios(Resources.randomPonyApi); + let tags = []; + + if (response.data.pony.tags) { + if (typeof response.data.pony.tags === 'string') { + tags.push(response.data.pony.tags); + } else if (Array.isArray(response.data.pony.tags)) { + tags = tags.concat(response.data.pony.tags); + } + } + + ctx.replyWithPhoto(response.data.pony.representations.full, { + caption: `${response.data.pony.sourceURL}\n\n${tags.length > 0 ? tags.join(', ') : ''}`, + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + } catch (error) { + const message = Strings.ponyApi.apiErr.replace('{error}', error.message); + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id + }); + return; + } + }); +} \ No newline at end of file diff --git a/telegram/commands/weather.ts b/src/commands/weather.js old mode 100755 new mode 100644 similarity index 69% rename from telegram/commands/weather.ts rename to src/commands/weather.js index 26a1b04..be1ebe9 --- a/telegram/commands/weather.ts +++ b/src/commands/weather.js @@ -2,16 +2,12 @@ // Copyright (c) 2024 BubbalooTeam. (https://github.com/BubbalooTeam) // Minor code changes by lucmsilva (https://github.com/lucmsilva651) -import Resources from '../props/resources.json'; -import axios from 'axios'; -import { getStrings } from '../plugins/checklang'; -import { isOnSpamWatch } from '../spamwatch/spamwatch'; -import spamwatchMiddlewareModule from '../spamwatch/Middleware'; -import verifyInput from '../plugins/verifyInput'; -import { Context, Telegraf } from 'telegraf'; -import { isCommandDisabled } from '../utils/check-command-disabled'; - -const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch); +const Resources = require('../props/resources.json'); +const axios = require('axios'); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch); +const { verifyInput } = require('../plugins/verifyInput.js'); const statusEmojis = { 0: '⛈', 1: '⛈', 2: '⛈', 3: '⛈', 4: '⛈', 5: '🌨', 6: '🌨', 7: '🌨', @@ -23,10 +19,10 @@ const statusEmojis = { 43: '❄️', 44: 'n/a', 45: '🌧', 46: '🌨', 47: '🌩' }; -const getStatusEmoji = (statusCode: number) => statusEmojis[statusCode] || 'n/a'; +const getStatusEmoji = (statusCode) => statusEmojis[statusCode] || 'n/a'; -function getLocaleUnit(countryCode: string) { - const fahrenheitCountries: string[] = ['US', 'BS', 'BZ', 'KY', 'LR']; +function getLocaleUnit(countryCode) { + const fahrenheitCountries = ['US', 'BS', 'BZ', 'KY', 'LR']; if (fahrenheitCountries.includes(countryCode)) { return { temperatureUnit: 'F', speedUnit: 'mph', apiUnit: 'e' }; @@ -35,12 +31,9 @@ function getLocaleUnit(countryCode: string) { } } -export default (bot: Telegraf, db: any) => { - bot.command(['weather', 'clima'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => { - if (await isCommandDisabled(ctx, db, 'weather')) return; - - const reply_to_message_id = ctx.message.message_id; - const userLang = ctx.from?.language_code || "en-US"; +module.exports = (bot) => { + bot.command(['clima', 'weather'], spamwatchMiddleware, async (ctx) => { + const userLang = ctx.from.language_code || "en-US"; const Strings = getStrings(userLang); const userInput = ctx.message.text.split(' ').slice(1).join(' '); const { provideLocation } = Strings.weatherStatus @@ -49,18 +42,10 @@ export default (bot: Telegraf, db: any) => { return; } - const location: string = userInput; - const apiKey: string = process.env.weatherKey || ''; - - if (!apiKey || apiKey === "InsertYourWeatherDotComApiKeyHere") { - return ctx.reply(Strings.weatherStatus.apiKeyErr, { - parse_mode: "Markdown", - ...({ reply_to_message_id }) - }); - } + const location = userInput; + const apiKey = process.env.weatherKey; try { - // TODO: this also needs to be sanitized and validated const locationResponse = await axios.get(`${Resources.weatherApi}/location/search`, { params: { apiKey: apiKey, @@ -74,7 +59,7 @@ export default (bot: Telegraf, db: any) => { if (!locationData || !locationData.address) { return ctx.reply(Strings.weatherStatus.invalidLocation, { parse_mode: "Markdown", - ...({ reply_to_message_id }) + reply_to_message_id: ctx.message.message_id }); } @@ -111,13 +96,13 @@ export default (bot: Telegraf, db: any) => { ctx.reply(weatherMessage, { parse_mode: "Markdown", - ...({ reply_to_message_id }) + reply_to_message_id: ctx.message.message_id }); } catch (error) { const message = Strings.weatherStatus.apiErr.replace('{error}', error.message); ctx.reply(message, { parse_mode: "Markdown", - ...({ reply_to_message_id }) + reply_to_message_id: ctx.message.message_id }); } }); diff --git a/telegram/commands/youtube.ts b/src/commands/youtube.js old mode 100755 new mode 100644 similarity index 64% rename from telegram/commands/youtube.ts rename to src/commands/youtube.js index 5b20029..d3fa755 --- a/telegram/commands/youtube.ts +++ b/src/commands/youtube.js @@ -1,14 +1,10 @@ -import { getStrings } from '../plugins/checklang'; -import { isOnSpamWatch } from '../spamwatch/spamwatch'; -import spamwatchMiddlewareModule from '../spamwatch/Middleware'; -import { execFile } from 'child_process'; -import { isCommandDisabled } from '../utils/check-command-disabled'; -import os from 'os'; -import fs from 'fs'; -import path from 'path'; -import * as ytUrl from 'youtube-url'; - -const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch); +const { getStrings } = require('../plugins/checkLang.js'); +const { isOnSpamWatch } = require('../spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../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'), @@ -32,7 +28,7 @@ const getFfmpegPath = () => { return ffmpegPaths[platform] || ffmpegPaths.linux; }; -const downloadFromYoutube = async (command: string, args: string[]): Promise<{ stdout: string; stderr: string }> => { +const downloadFromYoutube = async (command, args) => { return new Promise((resolve, reject) => { execFile(command, args, (error, stdout, stderr) => { if (error) { @@ -44,8 +40,8 @@ const downloadFromYoutube = async (command: string, args: string[]): Promise<{ s }); }; -const getApproxSize = async (command: string, videoUrl: string): Promise => { - let args: string[] = []; +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 { @@ -64,59 +60,30 @@ const getApproxSize = async (command: string, videoUrl: string): Promise } }; -const isValidUrl = (url: string): boolean => { - try { - new URL(url); - return true; - } catch { - return false; - } -}; - -export default (bot, db) => { +module.exports = (bot) => { bot.command(['yt', 'ytdl', 'sdl', 'video', 'dl'], spamwatchMiddleware, async (ctx) => { - if (await isCommandDisabled(ctx, db, 'youtube-download')) return; - const Strings = getStrings(ctx.from.language_code); const ytDlpPath = getYtDlpPath(); - const userId: number = ctx.from.id; - const videoUrl: string = ctx.message.text.split(' ').slice(1).join(' '); - const videoIsYoutube: boolean = ytUrl.valid(videoUrl); - const randId: string = Math.random().toString(36).substring(2, 15); - const mp4File: string = `tmp/${userId}-${randId}.mp4`; - const tempMp4File: string = `tmp/${userId}-${randId}.f137.mp4`; - const tempWebmFile: string = `tmp/${userId}-${randId}.f251.webm`; - let cmdArgs: string = ""; - const dlpCommand: string = ytDlpPath; - const ffmpegPath: string = getFfmpegPath(); - const ffmpegArgs: string[] = ['-i', tempMp4File, '-i', tempWebmFile, '-c:v copy -c:a copy -strict -2', mp4File]; + 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]; - /* - for now, no checking is done for the video url - yt-dlp should handle the validation, though it supports too many sites to hard-code - */ if (!videoUrl) { return ctx.reply(Strings.ytDownload.noLink, { parse_mode: "Markdown", disable_web_page_preview: true, reply_to_message_id: ctx.message.message_id }); - } - - // make sure its a valid url - if (!isValidUrl(videoUrl)) { - console.log("[!] Invalid URL:", videoUrl) - return ctx.reply(Strings.ytDownload.noLink, { - parse_mode: "Markdown", - disable_web_page_preview: true, - reply_to_message_id: ctx.message.message_id - }); - } - - console.log(`\nDownload Request:\nURL: ${videoUrl}\nYOUTUBE: ${videoIsYoutube}\n`) + }; if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) { - cmdArgs = "--max-filesize 2G --no-playlist --cookies telegram/props/cookies.txt --merge-output-format mp4 -o"; + 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`; } @@ -133,7 +100,6 @@ export default (bot, db) => { ]); if (approxSizeInMB > 50) { - console.log("[!] Video size exceeds 50MB:", approxSizeInMB) await ctx.telegram.editMessageText( ctx.chat.id, downloadingMessage.message_id, @@ -147,7 +113,6 @@ export default (bot, db) => { return; } - console.log("[i] Downloading video...") await ctx.telegram.editMessageText( ctx.chat.id, downloadingMessage.message_id, @@ -161,7 +126,6 @@ export default (bot, db) => { const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File]; await downloadFromYoutube(dlpCommand, dlpArgs); - console.log("[i] Uploading video...") await ctx.telegram.editMessageText( ctx.chat.id, downloadingMessage.message_id, @@ -232,25 +196,17 @@ export default (bot, db) => { }, ); } - console.log("[i] Request completed\n") } catch (error) { - let errMsg = Strings.ytDownload.uploadErr - - if (error.stderr.includes("--cookies-from-browser")) { - console.log("[!] Ratelimited by video provider:", error.stderr) - errMsg = Strings.ytDownload.botDetection - if (error.stderr.includes("youtube")) { - errMsg = Strings.ytDownload.botDetection.replace("video provider", "YouTube") - } - } else { - console.log("[!]", error.stderr) - } - - // will no longer edit the message as the message context is not outside the try block - await ctx.reply(errMsg, { + 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, - }); + }, + ); } }); }; \ No newline at end of file diff --git a/src/locales/english.json b/src/locales/english.json new file mode 100644 index 0000000..26e1244 --- /dev/null +++ b/src/locales/english.json @@ -0,0 +1,114 @@ +{ + "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\nCheck out the source code: [Click here to go to GitHub]({sourceLink})\n\nClick on the buttons below to see which commands you can use!\n", + "botPrivacy": "Check out [this link]({botPrivacy}) 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})", + "aboutBot": "About the bot", + "varStrings": { + "varYes": "Yes", + "varNo": "No", + "varTo": "to", + "varIs": "is", + "varWas": "was", + "varNone": "None", + "varUnknown": "Unknown", + "varBack": "Back" + }, + "unexpectedErr": "Some unexpected error occurred during a bot action. Please report it to the developers.\n\n{error}", + "errInvalidOption": "Whoops! Invalid option!", + "kickingMyself": "*Since you don't need me, I'll leave.*", + "kickingMyselfErr": "Error leaving the chat.", + "noPermission": "You don't have permission to run this command.", + "privateOnly": "This command should only be used in private chats, not in groups.", + "groupOnly": "This command should only be used in groups, not in private chats.", + "botNameChanged": "*Bot name changed to* `{botName}`.", + "botNameErr": "*Error changing bot name:*\n`{tgErr}`", + "botDescChanged": "*Bot description changed to* `{botDesc}`.", + "botDescErr": "*Error changing bot description:*\n`{tgErr}`", + "gayAmount": "You are *{randomNum}%* gay!", + "furryAmount": "You are *{randomNum}%* furry!", + "randomNum": "*Generated number (0-10):* `{number}`.", + "userInfo": "*User info*\n\n*Name:* `{userName}`\n*Username:* `{userHandle}`\n*User ID:* `{userId}`\n*Language:* `{userLang}`\n*Premium user:* `{userPremium}`", + "chatInfo": "*Chat info*\n\n*Name:* `{chatName}`\n*Chat ID:* `{chatId}`\n*Handle:* `{chatHandle}`\n*Type:* `{chatType}`\n*Members:* `{chatMembersCount}`\n*Is a forum:* `{isForum}`", + "funEmojiResult": "*You rolled {emoji} and got* `{value}`*!*\nYou don't know what that means? Me neither!", + "gifErr": "*Something went wrong while sending the GIF. Please try again later.*\n\n{err}", + "lastFm": { + "helpEntry": "Last.fm", + "helpDesc": "*Last.fm*\n\n- /lt | /lmu | /last | /lfm: Shows the last song from your Last.fm profile + the number of plays.\n- /setuser ``: Sets the user for the command above.", + "noUser": "*Please provide a Last.fm username.*\nExample: `/setuser `", + "noUserSet": "*You haven't set your Last.fm username yet.*\nUse the command /setuser to set.\n\nExample: `/setuser `", + "noRecentTracks": "*No recent tracks found for Last.fm user* `{lastfmUser}`*.*", + "userHasBeenSet": "*Your Last.fm username has been set to:* `{lastUser}`.", + "listeningTo": "{lastfmUser} *{nowPlaying} listening {playCount}*:\n\n{trackName} by {artistName}", + "playCount": "to, for the {plays}th time", + "apiErr": "*Error retrieving data for Last.fm user* {lastfmUser}.\n\n`{err}`" + }, + "gitCurrentCommit": "*Current commit:* `{commitHash}`", + "gitErrRetrievingCommit": "*Error retrieving commit:* {error}", + "weatherStatus": { + "provideLocation": "*Please provide a location.*", + "invalidLocation": "*Invalid location. Try again.*", + "resultMsg": "*Weather in {addressFirst}:*\n\n*Status:* `{getStatusEmoji(iconCode)} {wxPhraseLong}`\n*Temperature:* `{temperature} °{temperatureUnit}`\n*Feels like:* `{temperatureFeelsLike} °{temperatureUnit2}`\n*Humidity:* `{relativeHumidity}%`\n*Wind speed:* `{windSpeed} {speedUnit}`", + "apiErr": "*An error occurred while retrieving the weather. Please try again later.*\n\n`{error}`" + }, + "mainCommands": "Main commands", + "mainCommandsDesc": "*Main commands*\n\n- /help: Show bot's help\n- /start: Start the bot\n- /privacy: Read the bot's Privacy Policy", + "usefulCommands": "Useful commands", + "usefulCommandsDesc": "*Useful commands*\n\n- /chatinfo: Send information about the group\n- /userinfo: Send information about yourself\n- /d | /device ``: Search for a device on GSMArena and show its specs.\n/codename | /whatis ``: Shows what device is based on the codename. Example: `/codename begonia`\n- /weather | /clima ``: See weather status for a specific location.\n- /modarchive | /tma ``: Download a module from The Mod Archive.\n- /http ``: Send details about a specific HTTP code. Example: `/http 404`", + "funnyCommands": "Funny commands", + "funnyCommandsDesc": "*Funny commands*\n\n- /gay: Check if you are gay\n- /furry: Check if you are a furry\n- /random: Pick a random number between 0-10", + "interactiveEmojis": "Interactive emojis", + "interactiveEmojisDesc": "*Interactive emojis*\n\n- /dice: Roll a dice\n- /idice: Infinitely roll a colored dice\n- /slot: Try to combine the figures!\n- /ball: Try to kick the ball into the goal!\n- /bowling: Try to hit the pins!\n- /dart: Try to hit the target!", + "animalCommands": "Animals", + "animalCommandsDesc": "*Animals*\n\n- /soggy | /soggycat `<1 | 2 | 3 | 4 | orig | thumb | sticker | alt>`: Sends the [Soggy cat meme](https://knowyourmeme.com/memes/soggy-cat)\n- /cat: Sends a random picture of a cat.\n- /fox: Sends a random picture of a fox.\n- /duck: Sends a random picture of a duck.\n- /dog: Sends a random picture of a dog.\n- /httpcat ``: Send cat memes from http.cat with your specified HTTP code. Example: `/httpcat 404`", + "maInvalidModule": "Please provide a valid module ID from The Mod Archive.\nExample: `/modarchive 81574`", + "maDownloadError": "Error downloading the file. Check the module ID and try again.", + "ytDownload": { + "helpEntry": "Video download", + "helpDesc": "*Video download*\n\n- /yt | /ytdl | /sdl | /dl | /video `