diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 index ba231c9..cfdf0f2 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,14 @@ node_modules +webui/node_modules npm-debug.log .git +webui/.git .gitignore -.env -config.env +webui/.gitignore +.env* +webui/.env* +webui/.next *.md -!README.md \ No newline at end of file +!README.md +ollama/ +db/ \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..db7a321 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# 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 100644 new mode 100755 index b417983..5f0889c --- 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: "daily" + interval: "weekly" diff --git a/.github/workflows/njsscan.yml b/.github/workflows/njsscan.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/update-authors.yml b/.github/workflows/update-authors.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index ba85ba8..dbea724 --- a/.gitignore +++ b/.gitignore @@ -136,11 +136,21 @@ dist lastfm.json sw-blocklist.txt package-lock.json -bun.lock -bun.lockb tmp/ # Executables *.exe yt-dlp -ffmpeg \ No newline at end of file +ffmpeg + +# Bun +bun.lock* + +# Ollama +ollama/ + +# Docker +docker-compose.yml + +# postgres +db/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 index cf3ce05..4a96795 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "src/spamwatch"] - path = src/spamwatch +[submodule "telegram/spamwatch"] + path = telegram/spamwatch url = https://github.com/ABOCN/TelegramBot-SpamWatch diff --git a/AUTHORS b/AUTHORS old mode 100644 new mode 100755 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 473f9b7..f0d7341 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,37 @@ -FROM node:20-slim +FROM oven/bun # Install ffmpeg and other deps -RUN apt-get update && apt-get install -y ffmpeg git && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y \ + ffmpeg \ + git \ + supervisor \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app COPY package*.json ./ +RUN bun install -RUN npm install +COPY webui/package*.json ./webui/ +WORKDIR /usr/src/app/webui +RUN bun install +WORKDIR /usr/src/app COPY . . -RUN chmod +x /usr/src/app/src/plugins/yt-dlp/yt-dlp +WORKDIR /usr/src/app/webui +RUN bun run build -VOLUME /usr/src/app/config.env +RUN chmod +x /usr/src/app/telegram/plugins/yt-dlp/yt-dlp -CMD ["npm", "start"] \ No newline at end of file +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"] diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 71e5cff..36ad1dc --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![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) @@ -14,31 +15,24 @@ 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. -- Node.js 23 or newer (you can also use [Bun](https://bun.sh)) +- [Bun](https://bun.sh) (latest is suggested) - 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 -## Running locally (non-Docker setup) +### AI Requirements -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. +- High-end CPU *or* GPU (~ 6GB vRAM) +- If using CPU, enough RAM to load the models (~6GB w/ defaults) ## 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. @@ -46,9 +40,30 @@ You can also run Kowalski using Docker, which simplifies the setup process. Make ### Using Docker Compose -1. **Make sure to setup your `config.env` file first!** +1. **Copy compose file** -2. **Run the container** + _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** ```bash docker compose up -d @@ -58,31 +73,78 @@ 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 `config.env` file first!** +1. **Make sure to setup your `.env` file first!** -2. **Build the image** + 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** ```bash docker build -t kowalski . ``` -3. **Run the container** +1. **Run the container** ```bash - docker run -d --name kowalski --restart unless-stopped -v $(pwd)/config.env:/usr/src/app/config.env:ro kowalski + docker run -d --name kowalski --restart unless-stopped -v $(pwd)/.env:/usr/src/app/.env:ro kowalski ``` -## config.env Functions +> [!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 + > [!IMPORTANT] -> 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! +> 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 - **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 @@ -90,16 +152,22 @@ If you prefer to use Docker directly, you can use these instructions instead. **Q:** I get a "Permission denied (EACCES)" error in the console when running the `/yt` command -**A:** Make sure `src/plugins/yt-dlp/yt-dlp` is executable. You can do this on Linux like so: +**A:** Make sure `telegram/plugins/yt-dlp/yt-dlp` is executable. You can do this on Linux like so: ```bash -chmod +x src/plugins/yt-dlp/yt-dlp +chmod +x telegram/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). @@ -107,3 +175,5 @@ 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 new file mode 100755 index 0000000..42fd53b --- /dev/null +++ b/TERMS_OF_USE.md @@ -0,0 +1,18 @@ +# 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 deleted file mode 100644 index 452211b..0000000 --- a/config.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# 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 new file mode 100755 index 0000000..6c6ddb0 --- /dev/null +++ b/config/ai.ts @@ -0,0 +1,420 @@ +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 new file mode 100755 index 0000000..1fe94b3 --- /dev/null +++ b/config/settings.ts @@ -0,0 +1,2 @@ +export const seriesPageSize = 4; +export const modelPageSize = 4; \ No newline at end of file diff --git a/database/schema.ts b/database/schema.ts new file mode 100755 index 0000000..ce9a8ed --- /dev/null +++ b/database/schema.ts @@ -0,0 +1,52 @@ +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 deleted file mode 100644 index 981d90a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -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 new file mode 100755 index 0000000..fe467ab --- /dev/null +++ b/docker-compose.yml.ai.example @@ -0,0 +1,30 @@ +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 new file mode 100755 index 0000000..d94e78d --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,24 @@ +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 new file mode 100755 index 0000000..51b8e1d --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +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 100644 new mode 100755 index d7508b0..d9938ac --- a/nodemon.json +++ b/nodemon.json @@ -1,3 +1,6 @@ -{ - "ignore": ["src/props/*.json", "src/props/*.txt"] +{ + "ignore": ["telegram/props/*.json", "telegram/props/*.txt"], + "watch": ["telegram", "database", "config"], + "ext": "ts,js", + "exec": "bun telegram/bot.ts" } \ No newline at end of file diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 379cc96..b5c33cc --- a/package.json +++ b/package.json @@ -1,13 +1,25 @@ { "scripts": { - "start": "nodemon src/bot.js" + "start": "nodemon telegram/bot.ts", + "docs": "bunx typedoc", + "serve:docs": "bun run serve-docs.ts" }, "dependencies": { - "@dotenvx/dotenvx": "^1.28.0", - "axios": "^1.7.9", + "@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", "node-html-parser": "^7.0.1", - "nodemon": "^3.1.7", + "nodemon": "^3.1.10", + "pg": "^8.16.3", "telegraf": "^4.16.3", - "winston": "^3.17.0" + "youtube-url": "^0.5.0" + }, + "devDependencies": { + "@types/pg": "^8.15.4", + "drizzle-kit": "^0.31.4", + "tsx": "^4.20.3" } } diff --git a/src/bot.js b/src/bot.js deleted file mode 100644 index f053de7..0000000 --- a/src/bot.js +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 332e0e3..0000000 --- a/src/commands/animal.js +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index 138ae51..0000000 --- a/src/commands/codename.js +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index e2282ea..0000000 --- a/src/commands/crew.js +++ /dev/null @@ -1,223 +0,0 @@ -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 deleted file mode 100644 index 0c64bbe..0000000 --- a/src/commands/fun.js +++ /dev/null @@ -1,101 +0,0 @@ -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/src/commands/help.js b/src/commands/help.js deleted file mode 100644 index b4cc44b..0000000 --- a/src/commands/help.js +++ /dev/null @@ -1,112 +0,0 @@ -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 deleted file mode 100644 index 1382ad6..0000000 --- a/src/commands/http.js +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index eed4874..0000000 --- a/src/commands/info.js +++ /dev/null @@ -1,63 +0,0 @@ -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/src/commands/main.js b/src/commands/main.js deleted file mode 100644 index 975762d..0000000 --- a/src/commands/main.js +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 4e89370..0000000 --- a/src/commands/modarchive.js +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index e5fbf94..0000000 --- a/src/commands/ponyapi.js +++ /dev/null @@ -1,236 +0,0 @@ -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 deleted file mode 100644 index 1be1e73..0000000 --- a/src/commands/randompony.js +++ /dev/null @@ -1,36 +0,0 @@ -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/src/locales/english.json b/src/locales/english.json deleted file mode 100644 index 26e1244..0000000 --- a/src/locales/english.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "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 `