Compare commits

..

No commits in common. "main" and "old-javascript" have entirely different histories.

128 changed files with 1625 additions and 11680 deletions

12
.dockerignore Executable file → Normal file
View file

@ -1,14 +1,8 @@
node_modules node_modules
webui/node_modules
npm-debug.log npm-debug.log
.git .git
webui/.git
.gitignore .gitignore
webui/.gitignore .env
.env* config.env
webui/.env*
webui/.next
*.md *.md
!README.md !README.md
ollama/
db/

View file

@ -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

2
.github/dependabot.yml vendored Executable file → Normal file
View file

@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm" # See documentation for possible values - package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "daily"

0
.github/workflows/njsscan.yml vendored Executable file → Normal file
View file

0
.github/workflows/stale.yml vendored Executable file → Normal file
View file

0
.github/workflows/update-authors.yml vendored Executable file → Normal file
View file

16
.gitignore vendored Executable file → Normal file
View file

@ -136,21 +136,11 @@ dist
lastfm.json lastfm.json
sw-blocklist.txt sw-blocklist.txt
package-lock.json package-lock.json
bun.lock
bun.lockb
tmp/ tmp/
# Executables # Executables
*.exe *.exe
yt-dlp yt-dlp
ffmpeg ffmpeg
# Bun
bun.lock*
# Ollama
ollama/
# Docker
docker-compose.yml
# postgres
db/

4
.gitmodules vendored Executable file → Normal file
View file

@ -1,3 +1,3 @@
[submodule "telegram/spamwatch"] [submodule "src/spamwatch"]
path = telegram/spamwatch path = src/spamwatch
url = https://github.com/ABOCN/TelegramBot-SpamWatch url = https://github.com/ABOCN/TelegramBot-SpamWatch

0
AUTHORS Executable file → Normal file
View file

0
CODE_OF_CONDUCT.md Executable file → Normal file
View file

31
Dockerfile Executable file → Normal file
View file

@ -1,37 +1,18 @@
FROM oven/bun FROM node:20-slim
# Install ffmpeg and other deps # Install ffmpeg and other deps
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y ffmpeg git && apt-get clean && rm -rf /var/lib/apt/lists/*
ffmpeg \
git \
supervisor \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json ./ COPY package*.json ./
RUN bun install
COPY webui/package*.json ./webui/ RUN npm install
WORKDIR /usr/src/app/webui
RUN bun install
WORKDIR /usr/src/app
COPY . . COPY . .
WORKDIR /usr/src/app/webui RUN chmod +x /usr/src/app/src/plugins/yt-dlp/yt-dlp
RUN bun run build
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 CMD ["npm", "start"]
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"]

0
LICENSE Executable file → Normal file
View file

120
README.md Executable file → Normal file
View file

@ -2,7 +2,6 @@
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![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) [![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) [![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) [![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] > [!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. > 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)) - A Telegram bot (create one at [@BotFather](https://t.me/botfather))
- FFmpeg (only for the `/yt` command) - FFmpeg (only for the `/yt` command)
- Docker and Docker Compose (only required for Docker setup) - Docker and Docker Compose (only required for Docker setup)
- Postgres
### AI Requirements ## Running locally (non-Docker setup)
- High-end CPU *or* GPU (~ 6GB vRAM) First, clone the repo with Git:
- If using CPU, enough RAM to load the models (~6GB w/ defaults)
```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 ## Running with Docker
> [!IMPORTANT] > [!IMPORTANT]
> Please complete the above steps to prepare your local copy for building. You do not need to install FFmpeg on your host system. > Please complete the above steps to prepare your local copy for building. You do not need to install FFmpeg on your host system.
---
> [!NOTE] > [!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. > 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 ### Using Docker Compose
1. **Copy compose file** 1. **Make sure to setup your `config.env` file first!**
_Without AI (Ollama)_ 2. **Run the container**
```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 ```bash
docker compose up -d 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. 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`. 2. **Build the image**
1. **Build the image**
```bash ```bash
docker build -t kowalski . docker build -t kowalski .
``` ```
1. **Run the container** 3. **Run the container**
```bash ```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] ## config.env Functions
> 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] > [!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! > 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!
### Bot
- **botSource**: Put the link to your bot source code. - **botSource**: Put the link to your bot source code.
- **botPrivacy**: Put the link to your bot privacy policy. - **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. - **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). - **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. - **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. - **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. - **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 ## 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 **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 ```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 ## Contributors
<a href="https://github.com/abocn/TelegramBot/graphs/contributors"> <a href="https://github.com/abocn/TelegramBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=abocn/TelegramBot" alt="Profile pictures of Kowalski contributors" /> <img src="https://contrib.rocks/image?repo=abocn/TelegramBot" />
</a> </a>
Made with [contrib.rocks](https://contrib.rocks). Made with [contrib.rocks](https://contrib.rocks).
@ -175,5 +107,3 @@ Made with [contrib.rocks](https://contrib.rocks).
## About/License ## About/License
BSD-3-Clause - 2024 Lucas Gabriel (lucmsilva). BSD-3-Clause - 2024 Lucas Gabriel (lucmsilva).
With some components under Unlicense.

View file

@ -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.

12
config.env.example Normal file
View file

@ -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"

View file

@ -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
},
]
},
];

View file

@ -1,2 +0,0 @@
export const seriesPageSize = 4;
export const modelPageSize = 4;

View file

@ -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),
]);

9
docker-compose.yml Normal file
View file

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

View file

@ -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

View file

@ -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

View file

@ -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!,
},
});

7
nodemon.json Executable file → Normal file
View file

@ -1,6 +1,3 @@
{ {
"ignore": ["telegram/props/*.json", "telegram/props/*.txt"], "ignore": ["src/props/*.json", "src/props/*.txt"]
"watch": ["telegram", "database", "config"],
"ext": "ts,js",
"exec": "bun telegram/bot.ts"
} }

22
package.json Executable file → Normal file
View file

@ -1,25 +1,13 @@
{ {
"scripts": { "scripts": {
"start": "nodemon telegram/bot.ts", "start": "nodemon src/bot.js"
"docs": "bunx typedoc",
"serve:docs": "bun run serve-docs.ts"
}, },
"dependencies": { "dependencies": {
"@dotenvx/dotenvx": "^1.45.1", "@dotenvx/dotenvx": "^1.28.0",
"@types/bun": "^1.2.17", "axios": "^1.7.9",
"axios": "^1.10.0",
"dotenv": "^17.0.0",
"drizzle-orm": "^0.44.2",
"express": "^5.1.0",
"node-html-parser": "^7.0.1", "node-html-parser": "^7.0.1",
"nodemon": "^3.1.10", "nodemon": "^3.1.7",
"pg": "^8.16.3",
"telegraf": "^4.16.3", "telegraf": "^4.16.3",
"youtube-url": "^0.5.0" "winston": "^3.17.0"
},
"devDependencies": {
"@types/pg": "^8.15.4",
"drizzle-kit": "^0.31.4",
"tsx": "^4.20.3"
} }
} }

76
src/bot.js Normal file
View file

@ -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();

122
src/commands/animal.js Normal file
View file

@ -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;
};
});
}

56
src/commands/codename.js Normal file
View file

@ -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
});
})
}

223
src/commands/crew.js Normal file
View file

@ -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!");
});
};

101
src/commands/fun.js Normal file
View file

@ -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');
});
};

View file

@ -4,27 +4,18 @@
// With some help from GPT (I don't really like AI but whatever) // 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! // If this were a kang, I would not be giving credits to him!
import { isOnSpamWatch } from '../spamwatch/spamwatch'; const { isOnSpamWatch } = require('../spamwatch/spamwatch.js');
import spamwatchMiddlewareModule from '../spamwatch/Middleware'; const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch);
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 spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch); const axios = require('axios');
const { parse } = require('node-html-parser');
interface PhoneSearchResult { class PhoneSearchResult {
name: string; constructor(name, url) {
url: string; this.name = name;
} this.url = url;
Object.freeze(this);
interface PhoneDetails { }
specs: Record<string, Record<string, string>>;
name?: string;
url?: string;
picture?: string;
} }
const HEADERS = { const HEADERS = {
@ -41,7 +32,7 @@ function getDataFromSpecs(specsData, category, attributes) {
.join("\n"); .join("\n");
} }
function parseSpecs(specsData: PhoneDetails): PhoneDetails { function parseSpecs(specsData) {
const categories = { const categories = {
"status": ["Launch", ["Status"]], "status": ["Launch", ["Status"]],
"network": ["Network", ["Technology"]], "network": ["Network", ["Technology"]],
@ -78,7 +69,7 @@ function parseSpecs(specsData: PhoneDetails): PhoneDetails {
const [cat, attrs] = categories[key]; const [cat, attrs] = categories[key];
acc[key] = getDataFromSpecs(specsData, cat, attrs) || ""; acc[key] = getDataFromSpecs(specsData, cat, attrs) || "";
return acc; return acc;
}, { specs: {} } as PhoneDetails); }, {});
parsedData["name"] = specsData.name || ""; parsedData["name"] = specsData.name || "";
parsedData["url"] = specsData.url || ""; parsedData["url"] = specsData.url || "";
@ -86,7 +77,7 @@ function parseSpecs(specsData: PhoneDetails): PhoneDetails {
return parsedData; return parsedData;
} }
function formatPhone(phone: PhoneDetails) { function formatPhone(phone) {
const formattedPhone = parseSpecs(phone); const formattedPhone = parseSpecs(phone);
const attributesDict = { const attributesDict = {
"Status": "status", "Status": "status",
@ -131,7 +122,7 @@ function formatPhone(phone: PhoneDetails) {
return `<b>\n\nName: </b><code>${formattedPhone.name}</code>\n\n${attributes}\n\n${deviceImage}\n\n${deviceUrl}`; return `<b>\n\nName: </b><code>${formattedPhone.name}</code>\n\n${attributes}\n\n${deviceImage}\n\n${deviceUrl}`;
} }
async function fetchHtml(url: string) { async function fetchHtml(url) {
try { try {
const response = await axios.get(url, { headers: HEADERS }); const response = await axios.get(url, { headers: HEADERS });
return response.data; return response.data;
@ -141,7 +132,7 @@ async function fetchHtml(url: string) {
} }
} }
async function searchPhone(phone: string): Promise<PhoneSearchResult[]> { async function searchPhone(phone) {
try { try {
const searchUrl = `https://m.gsmarena.com/results.php3?sQuickSearch=yes&sName=${encodeURIComponent(phone)}`; const searchUrl = `https://m.gsmarena.com/results.php3?sQuickSearch=yes&sName=${encodeURIComponent(phone)}`;
const htmlContent = await fetchHtml(searchUrl); const htmlContent = await fetchHtml(searchUrl);
@ -151,7 +142,7 @@ async function searchPhone(phone: string): Promise<PhoneSearchResult[]> {
return foundPhones.map((phoneTag) => { return foundPhones.map((phoneTag) => {
const name = phoneTag.querySelector('img')?.getAttribute('title') || ""; const name = phoneTag.querySelector('img')?.getAttribute('title') || "";
const url = phoneTag.querySelector('a')?.getAttribute('href') || ""; const url = phoneTag.querySelector('a')?.getAttribute('href') || "";
return { name, url }; return new PhoneSearchResult(name, url);
}); });
} catch (error) { } catch (error) {
console.error("Error searching for phone:", 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}` }; return { ...specsData, name, picture, url: `https://www.gsmarena.com/${url}` };
} catch (error) { } catch (error) {
console.error("Error fetching phone details:", error); console.error("Error fetching phone details:", error);
return { specs: {}, name: "", url: "", picture: "" }; return {};
} }
} }
@ -210,132 +201,54 @@ function getUsername(ctx){
return userName; return userName;
} }
const deviceSelectionCache: Record<number, { results: PhoneSearchResult[], timeout: NodeJS.Timeout }> = {}; module.exports = (bot) => {
const lastSelectionMessageId: Record<number, number> = {};
export default (bot, db) => {
bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => { bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'device-specs')) return;
const userId = ctx.from.id; const userId = ctx.from.id;
const userName = getUsername(ctx); const userName = getUsername(ctx);
const Strings = getStrings(languageCode(ctx));
const phone = ctx.message.text.split(" ").slice(1).join(" "); const phone = ctx.message.text.split(" ").slice(1).join(" ");
if (!phone) { 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 results = await searchPhone(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);
if (results.length === 0) { if (results.length === 0) {
const codenameResults = await getDeviceByCodename(phone.split(" ")[0]); return ctx.reply("No phones found.", { reply_to_message_id: ctx.message.message_id });
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;
} }
if (deviceSelectionCache[userId]?.timeout) { const testUser = `<a href="tg://user?id=${userId}">${userName}</a>, please select your device:`;
clearTimeout(deviceSelectionCache[userId].timeout); const options = {
} parse_mode: 'HTML',
deviceSelectionCache[userId] = { reply_to_message_id: ctx.message.message_id,
results, disable_web_page_preview: true,
timeout: setTimeout(() => { delete deviceSelectionCache[userId]; }, 5 * 60 * 1000) 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 = `<a href=\"tg://user?id=${userId}\">${userName}</a>, ${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 = `<a href=\"tg://user?id=${userId}\">${userName}</a>, ${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) => { bot.action(/details:(.+):(.+)/, async (ctx) => {
const idx = parseInt(ctx.match[1]); const url = ctx.match[1];
const userId = parseInt(ctx.match[2]); const userId = parseInt(ctx.match[2]);
const userName = getUsername(ctx); const userName = getUsername(ctx);
const Strings = getStrings(languageCode(ctx));
const callbackQueryUserId = ctx.update.callback_query.from.id; const callbackQueryUserId = ctx.update.callback_query.from.id;
if (userId !== callbackQueryUserId) { 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(); 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); const phoneDetails = await checkPhoneDetails(url);
if (phoneDetails.name) { if (phoneDetails.name) {
const message = formatPhone(phoneDetails); const message = formatPhone(phoneDetails);
ctx.editMessageText(`<b><a href=\"tg://user?id=${userId}\">${userName}</a>, ${Strings.gsmarenaDeviceDetails || "[TODO: Add gsmarenaDeviceDetails to locales] these are the details of your device:"}</b>` + message, { parse_mode: 'HTML', disable_web_page_preview: false }); ctx.editMessageText(`<b><a href="tg://user?id=${userId}">${userName}</a>, these are the details of your device:</b>` + message, { parse_mode: 'HTML', disable_web_page_preview: false });
} else { } 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 });
} }
}); });
}; };

112
src/commands/help.js Normal file
View file

@ -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;
}
});
}

73
src/commands/http.js Normal file
View file

@ -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
});
}
});
};

63
src/commands/info.js Normal file
View file

@ -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,
}
);
});
};

56
telegram/commands/lastfm.ts → src/commands/lastfm.js Executable file → Normal file
View file

@ -1,17 +1,14 @@
import Resources from '../props/resources.json'; const Resources = require('../props/resources.json');
import fs from 'fs'; const fs = require('fs');
import axios from 'axios'; const axios = require('axios');
import { getStrings } from '../plugins/checklang'; const { getStrings } = require('../plugins/checkLang.js');
import { isOnSpamWatch } from '../spamwatch/spamwatch'; const { isOnSpamWatch } = require('../spamwatch/spamwatch.js');
import spamwatchMiddlewareModule from '../spamwatch/Middleware'; const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch);
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
const scrobbler_url = Resources.lastFmApi; const scrobbler_url = Resources.lastFmApi;
const api_key = process.env.lastKey; const api_key = process.env.lastKey;
const dbFile = 'telegram/props/lastfm.json'; const dbFile = 'src/props/lastfm.json';
let users = {}; let users = {};
function loadUsers() { function loadUsers() {
@ -38,7 +35,7 @@ function saveUsers() {
} }
} }
async function getFromMusicBrainz(mbid: string) { async function getFromMusicBrainz(mbid) {
try { try {
const response = await axios.get(`${Resources.musicBrainzApi}${mbid}`); const response = await axios.get(`${Resources.musicBrainzApi}${mbid}`);
const imgObjLarge = response.data.images[0]?.thumbnails?.['1200']; const imgObjLarge = response.data.images[0]?.thumbnails?.['1200'];
@ -61,12 +58,10 @@ function getFromLast(track) {
return imageUrl; return imageUrl;
} }
export default (bot, db) => { module.exports = (bot) => {
loadUsers(); loadUsers();
bot.command('setuser', async (ctx) => { bot.command('setuser', (ctx) => {
if (await isCommandDisabled(ctx, db, 'lastfm')) return;
const userId = ctx.from.id; const userId = ctx.from.id;
const Strings = getStrings(ctx.from.language_code); const Strings = getStrings(ctx.from.language_code);
const lastUser = ctx.message.text.split(' ')[1]; const lastUser = ctx.message.text.split(' ')[1];
@ -75,7 +70,7 @@ export default (bot, db) => {
return ctx.reply(Strings.lastFm.noUser, { return ctx.reply(Strings.lastFm.noUser, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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, { ctx.reply(message, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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) => { bot.command(['lt', 'lmu', 'last', 'lfm'], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'lastfm')) return;
const userId = ctx.from.id; const userId = ctx.from.id;
const Strings = getStrings(ctx.from.language_code); const Strings = getStrings(ctx.from.language_code);
const lastfmUser = users[userId]; const lastfmUser = users[userId];
const genericImg = Resources.lastFmGenericImg; const genericImg = Resources.lastFmGenericImg;
const botInfo = await ctx.telegram.getMe(); const botInfo = await ctx.telegram.getMe();
if (!lastfmUser) { if (!lastfmUser) {
return ctx.reply(Strings.lastFm.noUserSet, { return ctx.reply(Strings.lastFm.noUserSet, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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, { return ctx.reply(noRecent, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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) { if (albumMbid) {
imageUrl = await getFromMusicBrainz(albumMbid); imageUrl = await getFromMusicBrainz(albumMbid);
} }
if (!imageUrl) { if (!imageUrl) {
imageUrl = getFromLast(track); imageUrl = getFromLast(track);
} }
@ -156,7 +149,7 @@ export default (bot, db) => {
const artistUrl = `https://www.last.fm/music/${encodeURIComponent(artistName)}`; const artistUrl = `https://www.last.fm/music/${encodeURIComponent(artistName)}`;
const userUrl = `https://www.last.fm/user/${encodeURIComponent(lastfmUser)}`; const userUrl = `https://www.last.fm/user/${encodeURIComponent(lastfmUser)}`;
let num_plays = 0; let num_plays = '';
try { try {
const response_plays = await axios.get(scrobbler_url, { const response_plays = await axios.get(scrobbler_url, {
params: { params: {
@ -171,8 +164,11 @@ export default (bot, db) => {
'User-Agent': `@${botInfo.username}-node-telegram-bot` 'User-Agent': `@${botInfo.username}-node-telegram-bot`
} }
}); });
num_plays = response_plays.data.track.userplaycount; num_plays = response_plays.data.track.userplaycount;
if (!num_plays || num_plays === undefined) {
num_plays = 0;
};
} catch (err) { } catch (err) {
console.log(err) console.log(err)
const message = Strings.lastFm.apiErr const message = Strings.lastFm.apiErr
@ -181,7 +177,7 @@ export default (bot, db) => {
ctx.reply(message, { ctx.reply(message, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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, caption: message,
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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 { } else {
ctx.reply(message, { ctx.reply(message, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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) { } catch (err) {
@ -222,7 +218,7 @@ export default (bot, db) => {
ctx.reply(message, { ctx.reply(message, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, 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
}); });
}; };
}); });

27
src/commands/main.js Normal file
View file

@ -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
});
});
};

View file

@ -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
});
}
});
};

236
src/commands/ponyapi.js Normal file
View file

@ -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
});
};
});
};

View file

@ -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;
}
});
}

View file

@ -2,16 +2,12 @@
// Copyright (c) 2024 BubbalooTeam. (https://github.com/BubbalooTeam) // Copyright (c) 2024 BubbalooTeam. (https://github.com/BubbalooTeam)
// Minor code changes by lucmsilva (https://github.com/lucmsilva651) // Minor code changes by lucmsilva (https://github.com/lucmsilva651)
import Resources from '../props/resources.json'; const Resources = require('../props/resources.json');
import axios from 'axios'; const axios = require('axios');
import { getStrings } from '../plugins/checklang'; const { getStrings } = require('../plugins/checkLang.js');
import { isOnSpamWatch } from '../spamwatch/spamwatch'; const { isOnSpamWatch } = require('../spamwatch/spamwatch.js');
import spamwatchMiddlewareModule from '../spamwatch/Middleware'; const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch);
import verifyInput from '../plugins/verifyInput'; const { verifyInput } = require('../plugins/verifyInput.js');
import { Context, Telegraf } from 'telegraf';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
const statusEmojis = { const statusEmojis = {
0: '⛈', 1: '⛈', 2: '⛈', 3: '⛈', 4: '⛈', 5: '🌨', 6: '🌨', 7: '🌨', 0: '⛈', 1: '⛈', 2: '⛈', 3: '⛈', 4: '⛈', 5: '🌨', 6: '🌨', 7: '🌨',
@ -23,10 +19,10 @@ const statusEmojis = {
43: '❄️', 44: 'n/a', 45: '🌧', 46: '🌨', 47: '🌩' 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) { function getLocaleUnit(countryCode) {
const fahrenheitCountries: string[] = ['US', 'BS', 'BZ', 'KY', 'LR']; const fahrenheitCountries = ['US', 'BS', 'BZ', 'KY', 'LR'];
if (fahrenheitCountries.includes(countryCode)) { if (fahrenheitCountries.includes(countryCode)) {
return { temperatureUnit: 'F', speedUnit: 'mph', apiUnit: 'e' }; return { temperatureUnit: 'F', speedUnit: 'mph', apiUnit: 'e' };
@ -35,12 +31,9 @@ function getLocaleUnit(countryCode: string) {
} }
} }
export default (bot: Telegraf<Context>, db: any) => { module.exports = (bot) => {
bot.command(['weather', 'clima'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => { bot.command(['clima', 'weather'], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'weather')) return; const userLang = ctx.from.language_code || "en-US";
const reply_to_message_id = ctx.message.message_id;
const userLang = ctx.from?.language_code || "en-US";
const Strings = getStrings(userLang); const Strings = getStrings(userLang);
const userInput = ctx.message.text.split(' ').slice(1).join(' '); const userInput = ctx.message.text.split(' ').slice(1).join(' ');
const { provideLocation } = Strings.weatherStatus const { provideLocation } = Strings.weatherStatus
@ -49,18 +42,10 @@ export default (bot: Telegraf<Context>, db: any) => {
return; return;
} }
const location: string = userInput; const location = userInput;
const apiKey: string = process.env.weatherKey || ''; const apiKey = process.env.weatherKey;
if (!apiKey || apiKey === "InsertYourWeatherDotComApiKeyHere") {
return ctx.reply(Strings.weatherStatus.apiKeyErr, {
parse_mode: "Markdown",
...({ reply_to_message_id })
});
}
try { try {
// TODO: this also needs to be sanitized and validated
const locationResponse = await axios.get(`${Resources.weatherApi}/location/search`, { const locationResponse = await axios.get(`${Resources.weatherApi}/location/search`, {
params: { params: {
apiKey: apiKey, apiKey: apiKey,
@ -74,7 +59,7 @@ export default (bot: Telegraf<Context>, db: any) => {
if (!locationData || !locationData.address) { if (!locationData || !locationData.address) {
return ctx.reply(Strings.weatherStatus.invalidLocation, { return ctx.reply(Strings.weatherStatus.invalidLocation, {
parse_mode: "Markdown", parse_mode: "Markdown",
...({ reply_to_message_id }) reply_to_message_id: ctx.message.message_id
}); });
} }
@ -111,13 +96,13 @@ export default (bot: Telegraf<Context>, db: any) => {
ctx.reply(weatherMessage, { ctx.reply(weatherMessage, {
parse_mode: "Markdown", parse_mode: "Markdown",
...({ reply_to_message_id }) reply_to_message_id: ctx.message.message_id
}); });
} catch (error) { } catch (error) {
const message = Strings.weatherStatus.apiErr.replace('{error}', error.message); const message = Strings.weatherStatus.apiErr.replace('{error}', error.message);
ctx.reply(message, { ctx.reply(message, {
parse_mode: "Markdown", parse_mode: "Markdown",
...({ reply_to_message_id }) reply_to_message_id: ctx.message.message_id
}); });
} }
}); });

104
telegram/commands/youtube.ts → src/commands/youtube.js Executable file → Normal file
View file

@ -1,14 +1,10 @@
import { getStrings } from '../plugins/checklang'; const { getStrings } = require('../plugins/checkLang.js');
import { isOnSpamWatch } from '../spamwatch/spamwatch'; const { isOnSpamWatch } = require('../spamwatch/spamwatch.js');
import spamwatchMiddlewareModule from '../spamwatch/Middleware'; const spamwatchMiddleware = require('../spamwatch/Middleware.js')(isOnSpamWatch);
import { execFile } from 'child_process'; const { execFile } = require('child_process');
import { isCommandDisabled } from '../utils/check-command-disabled'; const os = require('os');
import os from 'os'; const fs = require('fs');
import fs from 'fs'; const path = require('path');
import path from 'path';
import * as ytUrl from 'youtube-url';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
const ytDlpPaths = { const ytDlpPaths = {
linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'), linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'),
@ -32,7 +28,7 @@ const getFfmpegPath = () => {
return ffmpegPaths[platform] || ffmpegPaths.linux; 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) => { return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => { execFile(command, args, (error, stdout, stderr) => {
if (error) { if (error) {
@ -44,8 +40,8 @@ const downloadFromYoutube = async (command: string, args: string[]): Promise<{ s
}); });
}; };
const getApproxSize = async (command: string, videoUrl: string): Promise<number> => { const getApproxSize = async (command, videoUrl) => {
let args: string[] = []; let args = [];
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) { 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")]; args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx', '--cookies', path.resolve(__dirname, "../props/cookies.txt")];
} else { } else {
@ -64,59 +60,30 @@ const getApproxSize = async (command: string, videoUrl: string): Promise<number>
} }
}; };
const isValidUrl = (url: string): boolean => { module.exports = (bot) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
export default (bot, db) => {
bot.command(['yt', 'ytdl', 'sdl', 'video', 'dl'], spamwatchMiddleware, async (ctx) => { 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 Strings = getStrings(ctx.from.language_code);
const ytDlpPath = getYtDlpPath(); const ytDlpPath = getYtDlpPath();
const userId: number = ctx.from.id; const userId = ctx.from.id;
const videoUrl: string = ctx.message.text.split(' ').slice(1).join(' '); const videoUrl = ctx.message.text.split(' ').slice(1).join(' ');
const videoIsYoutube: boolean = ytUrl.valid(videoUrl); const mp4File = `tmp/${userId}.mp4`;
const randId: string = Math.random().toString(36).substring(2, 15); const tempMp4File = `tmp/${userId}.f137.mp4`;
const mp4File: string = `tmp/${userId}-${randId}.mp4`; const tempWebmFile = `tmp/${userId}.f251.webm`;
const tempMp4File: string = `tmp/${userId}-${randId}.f137.mp4`; let cmdArgs = "";
const tempWebmFile: string = `tmp/${userId}-${randId}.f251.webm`; const dlpCommand = ytDlpPath;
let cmdArgs: string = ""; const ffmpegPath = getFfmpegPath();
const dlpCommand: string = ytDlpPath; const ffmpegArgs = ['-i', tempMp4File, '-i', tempWebmFile, '-c:v copy -c:a copy -strict -2', mp4File];
const ffmpegPath: string = getFfmpegPath();
const ffmpegArgs: string[] = ['-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) { if (!videoUrl) {
return ctx.reply(Strings.ytDownload.noLink, { return ctx.reply(Strings.ytDownload.noLink, {
parse_mode: "Markdown", parse_mode: "Markdown",
disable_web_page_preview: true, disable_web_page_preview: true,
reply_to_message_id: ctx.message.message_id 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"))) { 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 { } else {
cmdArgs = `--max-filesize 2G --no-playlist --merge-output-format mp4 -o`; cmdArgs = `--max-filesize 2G --no-playlist --merge-output-format mp4 -o`;
} }
@ -133,7 +100,6 @@ export default (bot, db) => {
]); ]);
if (approxSizeInMB > 50) { if (approxSizeInMB > 50) {
console.log("[!] Video size exceeds 50MB:", approxSizeInMB)
await ctx.telegram.editMessageText( await ctx.telegram.editMessageText(
ctx.chat.id, ctx.chat.id,
downloadingMessage.message_id, downloadingMessage.message_id,
@ -147,7 +113,6 @@ export default (bot, db) => {
return; return;
} }
console.log("[i] Downloading video...")
await ctx.telegram.editMessageText( await ctx.telegram.editMessageText(
ctx.chat.id, ctx.chat.id,
downloadingMessage.message_id, downloadingMessage.message_id,
@ -161,7 +126,6 @@ export default (bot, db) => {
const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File]; const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File];
await downloadFromYoutube(dlpCommand, dlpArgs); await downloadFromYoutube(dlpCommand, dlpArgs);
console.log("[i] Uploading video...")
await ctx.telegram.editMessageText( await ctx.telegram.editMessageText(
ctx.chat.id, ctx.chat.id,
downloadingMessage.message_id, downloadingMessage.message_id,
@ -232,25 +196,17 @@ export default (bot, db) => {
}, },
); );
} }
console.log("[i] Request completed\n")
} catch (error) { } catch (error) {
let errMsg = Strings.ytDownload.uploadErr const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
await ctx.telegram.editMessageText(
if (error.stderr.includes("--cookies-from-browser")) { ctx.chat.id,
console.log("[!] Ratelimited by video provider:", error.stderr) downloadingMessage.message_id,
errMsg = Strings.ytDownload.botDetection null,
if (error.stderr.includes("youtube")) { errMsg, {
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, {
parse_mode: 'Markdown', parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id, reply_to_message_id: ctx.message.message_id,
}); },
);
} }
}); });
}; };

114
src/locales/english.json Normal file
View file

@ -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 `<user>`: Sets the user for the command above.",
"noUser": "*Please provide a Last.fm username.*\nExample: `/setuser <username>`",
"noUserSet": "*You haven't set your Last.fm username yet.*\nUse the command /setuser to set.\n\nExample: `/setuser <username>`",
"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 `<model>`: Search for a device on GSMArena and show its specs.\n/codename | /whatis `<device codename>`: Shows what device is based on the codename. Example: `/codename begonia`\n- /weather | /clima `<city>`: See weather status for a specific location.\n- /modarchive | /tma `<module id>`: Download a module from The Mod Archive.\n- /http `<HTTP code>`: 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 `<http code>`: 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 `<video link>`: Download a video from some platforms (e.g. YouTube, Instagram, Facebook, etc.).\n\n See [this link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) for more information and which services are supported.\n\n*Note: Telegram is currently limiting bot uploads to 50MB, which means that if the video you want to download is larger than 50MB, the quality will be reduced to try to upload it anyway. We're trying our best to work around or fix this problem.*",
"downloadingVid": "*Downloading video...*",
"libNotFound": "*It seems that the yt-dlp executable does not exist on our server...\n\nIn that case, the problem is on our end! Please wait until we have noticed and solved the problem.*",
"checkingSize": "*Checking if the video exceeds the 50MB limit...*",
"uploadingVid": "*Uploading video...*",
"msgDesc": "{userMention}*, there is your downloaded video.*",
"downloadErr": "*Error during YT video download:*\n\n`{err}`",
"uploadErr": "Error uploading file. Please try again later.\n\n{error}",
"uploadLimit": "*This video exceeds the 50 MB upload limit imposed by Telegram on our bot. Please try another video. We're doing our best to increase this limit.*",
"sizeLimitWarn": "*This video had its quality reduced because it exceeded the 50MB limit for uploads imposed by Telegram.*",
"noLink": "Please provide a link to a video to download."
},
"botUpdated": "Bot updated with success.\n\n```{result}```",
"errorUpdatingBot": "Error updating bot\n\n{error}",
"catImgErr": "Sorry, but I couldn't get the cat photo you wanted.",
"catGifErr": "Sorry, but I couldn't get the cat GIF you wanted.",
"dogImgErr": "Sorry, but I couldn't get the dog photo you wanted.",
"foxApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
"duckApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
"httpCodes": {
"invalidCode": "Please enter a valid HTTP code.",
"fetchErr": "An error occurred while fetching the HTTP code.",
"notFound": "HTTP code not found.",
"resultMsg": "*HTTP Code*: {code}\n*Name*: `{message}`\n*Description*: {description}"
},
"ponyApi": {
"helpEntry": "My Little Pony",
"helpDesc": "*My Little Pony*\n\n- /mlp: Displays this help message.\n- /mlpchar `<character name>`: Shows specific information about a My Little Pony character. Example: `/mlpchar Twilight Sparkle`\n- /mlpep: Shows specific information about a My Little Pony episode. Example: `/mlpep 136`\n- /mlpcomic `<comic name>`: Shows specific information about a My Little Pony comic. Example: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Sends a random artwork made by the My Little Pony community.",
"charRes": "*{name} (ID: {id})*\n\n*Alias:* `{alias}`\n*Sex:* `{sex}`\n*Residence:* `{residence}`\n*Occupation:* `{occupation}`\n*Kind:* `{kind}`\n\n*Fandom URL:*\n[{url}]({url})",
"epRes": "*{name} (ID: {id})*\n\n*Season:* `{season}`\n*Episode:* `{episode}`\n*Overall Ep.:* `{overall}`\n*Release date:* `{airdate}`\n*Story by:* `{storyby}`\n*Written by:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*Fandom URL:*\n[{url}]({url})",
"comicRes": "*{name} (ID: {id})*\n\n*Series:* `{series}`\n*Writer:* `{writer}`\n*Artist:* `{artist}`\n*Colorist:* `{colorist}`\n*Letterer:* `{letterer}`\n*Editor:* `{editor}`\n\n*Fandom URL:*\n[{url}]({url})",
"noCharName": "Please provide the character's name.",
"noCharFound": "No character found.",
"noEpisodeNum": "Please provide the episode's number.",
"noEpisodeFound": "No episode found.",
"noComicName": "Please provide the comic's name.",
"noComicFound": "No comic found.",
"apiErr": "An error occurred while fetching data from the API.\n\n`{error}`"
},
"codenameCheck": {
"noCodename": "Please provide a codename to search.",
"invalidCodename": "Invalid codename.",
"notFound": "Phone not found.",
"resultMsg": "*Name:* `{name}`\n*Brand:* `{brand}`\n*Model:* `{model}`\n*Codename:* `{codename}`",
"apiErr": "An error occurred while fetching data from the API.\n\n`{err}`"
}
}

114
src/locales/portuguese.json Normal file
View file

@ -0,0 +1,114 @@
{
"botWelcome": "*Olá! Eu sou o {botName}!*\nEu fui feito com amor por uns nerds que amam programação!\n\n*Ao usar o {botName}, você afirma que leu e concorda com a política de privacidade (/privacy). Isso ajuda você a entender onde seus dados vão ao usar este bot.*\n\nAlém disso, você pode usar /help para ver os meus comandos!",
"botHelp": "*Oi, eu sou o {botName}, um bot simples feito do zero em Telegraf e Node.js por uns nerds que gostam de programação.*\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})\n\nClique nos botões abaixo para ver quais comandos você pode usar!\n",
"botPrivacy": "Acesse [este link]({botPrivacy}) para ler a política de privacidade do bot.",
"botAbout": "*Sobre o bot*\n\nA base deste bot foi feita originalmente por [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), agora sendo mantido por várias pessoas.\n\nA intenção do bot é trazer diversão para os seus grupos aqui no Telegram de uma maneira bem descontraida e simples. O bot também conta com alguns comandos bem úteis, que você consegue ver com o comando de ajuda (/help).\n\nAgradecimento especial ao @givfnz2 pelas suas várias contribuições ao bot!\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})",
"aboutBot": "Sobre o bot",
"varStrings": {
"varYes": "Sim",
"varNo": "Não",
"varTo": "",
"varIs": "está",
"varWas": "estava",
"varNone": "Nenhum",
"varUnknown": "Desconhecido",
"varBack": "Voltar"
},
"unexpectedErr": "Algum erro inesperado ocorreu durante uma ação do bot. Por favor, reporte aos desenvolvedores.\n\n{error}",
"errInvalidOption": "Ops! Opção inválida!",
"kickingMyself": "*Já que você não precisa de mim, vou sair daqui.*",
"kickingMyselfErr": "Erro ao sair do chat.",
"noPermission": "Você não tem permissão para executar este comando.",
"privateOnly": "Este comando deve ser usado apenas em chats privados, não em grupos.",
"groupOnly": "Este comando deve ser usado apenas em grupos, não em chats privados.",
"botNameChanged": "*Nome do bot alterado para* `{botName}`.",
"botNameErr": "*Erro ao alterar o nome do bot:*\n`{tgErr}`",
"botDescChanged": "*Descrição do bot alterada para* `{botDesc}`.",
"botDescErr": "*Erro ao alterar a descrição do bot:*\n`{tgErr}`",
"gayAmount": "Você é *{randomNum}%* gay!",
"furryAmount": "Você é *{randomNum}%* furry!",
"randomNum": "*Número gerado (0-10):* `{number}`.",
"userInfo": "*Informações do usuário*\n\n*Nome:* `{userName}`\n*Usuário:* `{userHandle}`\n*ID:* `{userId}`\n*Idioma:* `{userLang}`\n*Usuário Premium:* `{userPremium}`",
"chatInfo": "*Informações do chat*\n\n*Nome:* `{chatName}`\n*ID do chat:* `{chatId}`\n*Identificador:* `{chatHandle}`\n*Tipo:* `{chatType}`\n*Membros:* `{chatMembersCount}`\n*É um fórum:* `{isForum}`",
"funEmojiResult": "*Você lançou {emoji} e obteve *`{value}`*!*\nVocê não sabe o que isso significa? Nem eu!",
"gifErr": "*Algo deu errado ao enviar o GIF. Tente novamente mais tarde.*\n\n{err}",
"lastFm": {
"helpEntry": "Last.fm",
"helpDesc": "*Last.fm*\n\n- /lt | /lmu | /last | /lfm: Mostra a última música do seu perfil no Last.fm + o número de reproduções.\n- /setuser `<usuário>`: Define o usuário para o comando acima.",
"noUser": "*Por favor, forneça um nome de usuário do Last.fm.*\nExemplo: `/setuser <username>`",
"noUserSet": "*Você ainda não definiu seu nome de usuário do Last.fm.*\nUse o comando /setuser para definir.\n\nExemplo: `/setuser <username>`",
"noRecentTracks": "*Nenhuma faixa recente encontrada para o usuário do Last.fm* `{lastfmUser}`*.*",
"userHasBeenSet": "*Seu nome de usuário do Last.fm foi definido como:* `{lastUser}`.",
"listeningTo": "{lastfmUser} *{nowPlaying} ouvindo{playCount}*:\n\n{trackName} por {artistName}",
"playCount": " pela {plays}ª vez",
"apiErr": "*Erro ao recuperar dados para o usuário do Last.fm* {lastfmUser}.\n\n`{err}`"
},
"gitCurrentCommit": "*Commit atual:* `{commitHash}`",
"gitErrRetrievingCommit": "*Erro ao obter o commit:* {error}",
"weatherStatus": {
"provideLocation": "*Por favor, forneça uma localização.*",
"invalidLocation": "*Localização inválida. Tente novamente.*",
"resultMsg": "*Clima em {addressFirst}:*\n\n*Estado:* `{getStatusEmoji(iconCode)} {wxPhraseLong}`\n*Temperatura:* `{temperature} °{temperatureUnit}`\n*Sensação térmica:* `{temperatureFeelsLike} °{temperatureUnit2}`\n*Umidade:* `{relativeHumidity}%`\n*Velocidade do vento:* `{windSpeed} {speedUnit}`",
"apiErr": "*Ocorreu um erro ao obter o clima. Tente novamente mais tarde.*\n\n`{error}`"
},
"mainCommands": "Comandos principais",
"mainCommandsDesc": "*Comandos principais*\n\n- /help: Exibe a ajuda do bot\n- /start: Inicia o bot\n- /privacy: Leia a política de privacidade do bot",
"usefulCommands": "Comandos úteis",
"usefulCommandsDesc": "*Comandos úteis*\n\n- /chatinfo: Envia informações sobre o grupo\n- /userinfo: Envia informações sobre você\n- /d | /device `<modelo>`: Pesquisa um dispositivo no GSMArena e mostra suas especificações.\n- /weather | /clima `<cidade>`: Veja o status do clima para uma localização específica\n- /modarchive | /tma `<id do módulo>`: Baixa um módulo do The Mod Archive.\n- /http `<código HTTP>`: Envia detalhes sobre um código HTTP específico. Exemplo: `/http 404`",
"funnyCommands": "Comandos engraçados",
"funnyCommandsDesc": "*Comandos engraçados*\n\n- /gay: Verifique se você é gay\n- /furry: Verifique se você é furry\n- /random: Escolhe um número aleatório entre 0-10",
"interactiveEmojis": "Emojis interativos",
"interactiveEmojisDesc": "*Emojis interativos*\n\n- /dice: Jogue um dado\n- /idice: Role infinitamente um dado colorido\n- /slot: Tente combinar as figuras!\n- /ball: Tente chutar a bola no gol!\n- /bowling: Tente derrubar os pinos!\n- /dart: Tente acertar o alvo!",
"animalCommands": "Animais",
"animalCommandsDesc": "*Animais*\n\n- /soggy | /soggycat `<1 | 2 | 3 | 4 | orig | thumb | sticker | alt>`: Envia o [meme do gato encharcado](https://knowyourmeme.com/memes/soggy-cat)\n- /cat - Envia uma foto aleatória de um gato.\n- /fox - Envia uma foto aleatória de uma raposa.\n- /duck - Envia uma foto aleatória de um pato.\n- /dog - Envia uma imagem aleatória de um cachorro.\n- /httpcat `<código http>`: Envia memes de gato do http.cat com o código HTTP especificado. Exemplo: `/httpcat 404`",
"maInvalidModule": "Por favor, forneça um ID de módulo válido do The Mod Archive.\nExemplo: `/modarchive 81574`",
"maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.",
"ytDownload": {
"helpEntry": "Download de vídeos",
"helpDesc": "*Download de vídeos*\n\n- /yt | /ytdl | /sdl | /dl | /video `<link do vídeo>`: Baixa um vídeo de algumas plataformas (ex: YouTube, Instagram, Facebook, etc.).\n\nConsulte [este link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) para obter mais informações e saber quais serviços são compatíveis.\n\n*Nota: O Telegram está atualmente limitando os uploads de bots a 50MB, o que significa que se o vídeo que você deseja baixar for maior que 50MB, a qualidade será reduzida para tentar carregá-lo de qualquer maneira. Estamos fazendo o possível para contornar ou corrigir esse problema.*",
"downloadingVid": "*Baixando vídeo...*",
"libNotFound": "*Parece que o executável do yt-dlp não existe no nosso servidor...\n\nNesse caso, o problema está no nosso lado! Aguarde até que tenhamos notado e resolvido o problema.*",
"checkingSize": "Verificando se o vídeo excede o limite de 50 MB...",
"uploadingVid": "*Enviando vídeo...*",
"msgDesc": "{userMention}*, aqui está o seu vídeo baixado.*",
"downloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`",
"uploadErr": "Erro ao enviar o arquivo. Tente novamente mais tarde.\n\n{error}",
"uploadLimit": "*Este vídeo excede o limite de carregamento de 50 MB imposto pelo Telegram ao nosso bot. Por favor, tente outro vídeo. Estamos fazendo o possível para aumentar esse limite.*",
"sizeLimitWarn": "*Esse vídeo teve a qualidade reduzida por estar excedendo o limite de 50MB para uploads imposto pelo Telegram.*",
"noLink": "*Por favor, forneça um link de um vídeo para download.*"
},
"botUpdated": "Bot atualizado com sucesso.\n\n```{result}```",
"errorUpdatingBot": "Erro ao atualizar o bot\n\n{error}",
"catImgErr": "Desculpe, mas não consegui obter a foto do gato que você queria.",
"catGifErr": "Desculpe, mas não consegui obter o GIF do gato que você queria.",
"dogImgErr": "Desculpe, mas não consegui obter a foto do cacbhorro que você queria.",
"foxApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
"duckApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
"httpCodes": {
"invalidCode": "Por favor, insira um código HTTP válido.",
"fetchErr": "Ocorreu um erro ao buscar o código HTTP.",
"notFound": "Código HTTP não encontrado.",
"resultMsg": "*Código HTTP*: `{code}`\n*Nome*: `{message}`\n*Descrição*: `{description}`"
},
"ponyApi": {
"helpEntry": "My Little Pony",
"helpDesc": "*My Little Pony*\n\n- /mlp: Exibe esta mensagem de ajuda.\n- /mlpchar `<nome do personagem>`: Mostra informações específicas sobre um personagem de My Little Pony em inglês. Exemplo: `/mlpchar twilight`\n- /mlpep: Mostra informações específicas sobre um episódio de My Little Pony em inglês. Exemplo: `/mlpep 136`\n- /mlpcomic `<nome da comic>`: Mostra informações específicas sobre uma comic de My Little Pony em inglês. Exemplo: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Envia uma arte aleatória feita pela comunidade de My Little Pony.",
"charRes": "*{name} (ID: {id})*\n\n*Apelido:* `{alias}`\n*Sexo:* `{sex}`\n*Residência:* `{residence}`\n*Ocupação:* `{occupation}`\n*Tipo:* `{kind}`\n\n*URL no Fandom:*\n[{url}]({url})",
"epRes": "*{name} (ID: {id})*\n\n*Temporada:* `{season}`\n*Episódio:* `{episode}`\n*Número do Episódio:* `{overall}`\n*Data de lançamento:* `{airdate}`\n*História por:* `{storyby}`\n*Escrito por:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*URL no Fandom:*\n[{url}]({url})",
"comicRes": "*{name} (ID: {id})*\n\n*Série:* `{series}`\n*Roteirista:* `{writer}`\n*Artista:* `{artist}`\n*Colorista:* `{colorist}`\n*Letrista:* `{letterer}`\n*Editor:* `{editor}`\n\n*URL no Fandom:*\n[{url}]({url})",
"noCharName": "Por favor, forneça o nome do personagem.",
"noCharFound": "Nenhum personagem encontrado.",
"noEpisodeNum": "Por favor, forneça o número do episódio.",
"noEpisodeFound": "Nenhum episódio encontrado.",
"noComicName": "Por favor, forneça o nome da comic.",
"noComicFound": "Nenhuma comic foi encontrada.",
"apiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`"
},
"codenameCheck": {
"noCodename": "Por favor, forneça um codinome para pesquisar.",
"invalidCodename": "Codinome inválido.",
"notFound": "Celular não encontrado.",
"resultMsg": "*Nome:* `{name}`\n*Marca:* `{brand}`\n*Modelo:* `{model}`\n*Codinome:* `{codename}`",
"apiErr": "Ocorreu um erro ao buscar os dados da API.\n\n`{err}`"
}
}

View file

@ -7,11 +7,8 @@ const languageFiles = {
'en-gb': '../locales/english.json' 'en-gb': '../locales/english.json'
}; };
function getStrings(languageCode?: string) { function getStrings(languageCode) {
if (!languageCode) { const filePath = languageFiles[languageCode] || languageFiles['en'];
return require(languageFiles['en']);
}
const filePath: string = languageFiles[languageCode] || languageFiles['en'];
try { try {
return require(filePath); return require(filePath);
} catch (error) { } catch (error) {
@ -20,4 +17,6 @@ function getStrings(languageCode?: string) {
} }
} }
export { getStrings }; module.exports = {
getStrings
};

View file

@ -0,0 +1,14 @@
function verifyInput(ctx, userInput, message, verifyNaN = false) {
if (!userInput || (verifyNaN && isNaN(userInput))) {
ctx.reply(message, {
parse_mode: "Markdown",
reply_to_message_id: ctx.message.message_id
});
return true;
}
return false;
}
module.exports = {
verifyInput
};

View file

@ -1,7 +1,7 @@
import axios from 'axios'; const axios = require('axios');
import fs from 'fs'; const fs = require('fs');
import path from 'path'; const path = require('path');
import os from 'os'; const os = require('os');
const downloadDir = path.resolve(__dirname, 'yt-dlp'); const downloadDir = path.resolve(__dirname, 'yt-dlp');

View file

1
src/spamwatch Submodule

@ -0,0 +1 @@
Subproject commit 2f532fdd0d83566c27d22ad7ca2dfbcf4680786d

View file

@ -1,19 +0,0 @@
#!/bin/bash
echo "Starting BOT..."
cd /usr/src/app
bun start 2>&1 | sed "s/^/[BOT] /" &
BOT_PID=$!
echo "BOT started with PID $BOT_PID"
echo "Starting WEBUI..."
cd /usr/src/app/webui
bun run start 2>&1 | sed "s/^/[WEBUI] /" &
WEBUI_PID=$!
echo "WEBUI started with PID $WEBUI_PID"
echo "Services started:"
echo " Bot PID: $BOT_PID"
echo " WebUI PID: $WEBUI_PID"
wait $BOT_PID $WEBUI_PID

View file

@ -1,31 +0,0 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel=info
[program:telegram-bot]
command=bun start
directory=/usr/src/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_logfile_backups=0
stderr_logfile_backups=0
[program:webui]
command=bun run start
directory=/usr/src/app/webui
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_logfile_backups=0
stderr_logfile_backups=0

View file

@ -1,102 +0,0 @@
import express from "express";
import { drizzle } from "drizzle-orm/node-postgres";
import { Client } from "pg";
import * as schema from "../../database/schema";
import { eq } from "drizzle-orm";
import { twoFactorTable, usersTable } from "../../database/schema";
import { Telegraf } from "telegraf";
import { getStrings } from "../plugins/checklang";
const client = new Client({ connectionString: process.env.databaseUrl });
const db = drizzle(client, { schema });
const bot = new Telegraf(process.env.botToken!);
const botName = bot.botInfo?.first_name && bot.botInfo?.last_name ? `${bot.botInfo.first_name} ${bot.botInfo.last_name}` : "Kowalski"
function shouldLogLonger() {
return process.env.longerLogs === 'true';
}
export async function startServer() {
await client.connect();
const app = express();
app.use(express.json());
app.get("/health", (res) => {
res.send("OK");
});
app.post("/2fa/get", async (req, res) => {
try {
const { userId } = req.body;
if (!userId) {
console.log("[🌐 API] Missing userId in request");
return res.status(400).json({ generated: false, error: "User ID is required" });
}
if (shouldLogLonger()) {
console.log("[🌐 API] Looking up user:", userId);
}
const user = await db.query.usersTable.findFirst({
where: eq(usersTable.telegramId, userId),
columns: {
languageCode: true,
},
});
if (!user) {
console.log("[🌐 API] User not found:", userId);
return res.status(404).json({ generated: false, error: "User not found" });
}
const code = Math.floor(100000 + Math.random() * 900000).toString();
console.log("[🌐 API] Inserting 2FA record");
await db.insert(twoFactorTable).values({
userId,
currentCode: code,
codeAttempts: 0,
codeExpiresAt: new Date(Date.now() + 1000 * 60 * 5),
}).onConflictDoUpdate({
target: twoFactorTable.userId,
set: {
currentCode: code,
codeAttempts: 0,
codeExpiresAt: new Date(Date.now() + 1000 * 60 * 5),
}
});
if (shouldLogLonger()) {
console.log("[🌐 API] Sending 2FA message");
}
try {
const Strings = getStrings(user.languageCode);
const message = Strings.twoFactor.codeMessage
.replace("{botName}", botName)
.replace("{code}", code);
await bot.telegram.sendMessage(userId, message, { parse_mode: "MarkdownV2" });
if (shouldLogLonger()) {
console.log("[🌐 API] Message sent successfully");
}
} catch (error) {
console.error("[🌐 API] Error sending 2FA code to user", error);
return res.status(500).json({ generated: false, error: "Error sending 2FA message" });
}
res.json({ generated: true });
} catch (error) {
console.error("[🌐 API] Unexpected error in 2FA endpoint:", error);
return res.status(500).json({ generated: false, error: "Internal server error" });
}
});
app.listen(3030, () => {
console.log("[🌐 API] Running on port 3030\n");
});
}

View file

@ -1,131 +0,0 @@
import { Telegraf } from 'telegraf';
import path from 'path';
import fs from 'fs';
import { isSpamwatchConnected } from './spamwatch/spamwatch';
import '@dotenvx/dotenvx';
import 'dotenv/config';
import './plugins/ytDlpWrapper';
import { preChecks } from './commands/ai';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Client } from 'pg';
import * as schema from '../database/schema';
import { ensureUserInDb } from './utils/ensure-user';
import { getSpamwatchBlockedCount } from './spamwatch/spamwatch';
import { startServer } from './api/server';
(async function main() {
const { botToken, handlerTimeout, maxRetries, databaseUrl, ollamaEnabled } = process.env;
if (!botToken || botToken === 'InsertYourBotTokenHere') {
console.error('Bot token is not set. Please set the bot token in the .env file.');
process.exit(1);
}
if (ollamaEnabled === "true") {
if (!(await preChecks())) {
process.exit(1);
}
}
const client = new Client({ connectionString: databaseUrl });
await client.connect();
const db = drizzle(client, { schema });
const bot = new Telegraf(
botToken,
{ handlerTimeout: Number(handlerTimeout) || 600_000 }
);
const maxRetriesNum = Number(maxRetries) || 5;
let restartCount = 0;
bot.use(async (ctx, next) => {
await ensureUserInDb(ctx, db);
return next();
});
function loadCommands() {
const commandsPath = path.join(__dirname, 'commands');
let loadedCount = 0;
try {
const files = fs.readdirSync(commandsPath)
.filter(file => file.endsWith('.ts'));
files.forEach((file) => {
try {
const commandPath = path.join(commandsPath, file);
const command = require(commandPath).default || require(commandPath);
if (typeof command === 'function') {
command(bot, db);
loadedCount++;
}
} catch (error) {
console.error(`Failed to load command file ${file}: ${error.message}`);
}
});
console.log(`[🤖 BOT] Loaded ${loadedCount} commands.`);
} catch (error) {
console.error(`Failed to read commands directory: ${error.message}`);
}
}
async function startBot() {
try {
const botInfo = await bot.telegram.getMe();
console.log(`${botInfo.first_name} is running...`);
await bot.launch();
restartCount = 0;
} catch (error) {
console.error('Failed to start bot:', error.message);
if (restartCount < maxRetriesNum) {
restartCount++;
console.log(`Retrying to start bot... Attempt ${restartCount}`);
setTimeout(startBot, 5000);
} else {
console.error('Maximum retry attempts reached. Exiting.');
process.exit(1);
}
}
}
function handleShutdown(signal: string) {
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);
});
async function testDbConnection() {
try {
await db.query.usersTable.findMany({ limit: 1 });
const users = await db.query.usersTable.findMany({});
const userCount = users.length;
console.log(`[💽 DB] Connected [${userCount} users]`);
} catch (err) {
console.error('[💽 DB] Failed to connect:', err);
process.exit(1);
}
}
await testDbConnection();
if (isSpamwatchConnected()) {
const blockedCount = getSpamwatchBlockedCount();
// the 3 spaces are intentional
console.log(`[🛡️ SW] Connected [${blockedCount} blocked]`);
} else {
console.log('[🛡️ SW] Not connected or blocklist empty');
}
loadCommands();
startServer();
startBot();
})();

File diff suppressed because it is too large Load diff

View file

@ -1,159 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import axios from 'axios';
import { Context, Telegraf } from 'telegraf';
import { replyToMessageId } from '../utils/reply-to-message-id';
import { languageCode } from '../utils/language-code';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
export const duckHandler = async (ctx: Context & { message: { text: string } }) => {
const reply_to_message_id = replyToMessageId(ctx);
try {
const response = await axios(Resources.duckApi);
ctx.replyWithPhoto(response.data.url, {
caption: "🦆",
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
const Strings = getStrings(languageCode(ctx));
const message = Strings.duckApiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
};
export const foxHandler = async (ctx: Context & { message: { text: string } }) => {
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = replyToMessageId(ctx);
try {
const response = await axios(Resources.foxApi);
ctx.replyWithPhoto(response.data.image, {
caption: "🦊",
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
const message = Strings.foxApiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
};
export const dogHandler = async (ctx: Context & { message: { text: string } }) => {
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = replyToMessageId(ctx);
try {
const response = await axios(Resources.dogApi);
ctx.replyWithPhoto(response.data.message, {
caption: "🐶",
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
const message = Strings.dogApiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
};
export const catHandler = async (ctx: Context & { message: { text: string } }) => {
const Strings = getStrings(languageCode(ctx));
const apiUrl = `${Resources.catApi}?json=true`;
const reply_to_message_id = replyToMessageId(ctx);
try {
const response = await axios.get(apiUrl);
const data = response.data;
const imageUrl = `${data.url}`;
await ctx.replyWithPhoto(imageUrl, {
caption: `🐱`,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
const message = Strings.catImgErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
};
export const soggyHandler = async (ctx: Context & { message: { text: string } }) => {
const userInput = ctx.message.text.split(' ')[1];
const reply_to_message_id = replyToMessageId(ctx);
switch (true) {
case (userInput === "2" || userInput === "thumb"):
ctx.replyWithPhoto(
Resources.soggyCat2, {
caption: Resources.soggyCat2,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
break;
case (userInput === "3" || userInput === "sticker"):
ctx.replyWithSticker(
Resources.soggyCatSticker,
reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : undefined
);
break;
case (userInput === "4" || userInput === "alt"):
ctx.replyWithPhoto(
Resources.soggyCatAlt, {
caption: Resources.soggyCatAlt,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
break;
default:
ctx.replyWithPhoto(
Resources.soggyCat, {
caption: Resources.soggyCat,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
break;
};
};
export default (bot: Telegraf<Context>, db: any) => {
bot.command("duck", spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
await duckHandler(ctx);
});
bot.command("fox", spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
await foxHandler(ctx);
});
bot.command("dog", spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
await dogHandler(ctx);
});
bot.command("cat", spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
await catHandler(ctx);
});
bot.command(['soggy', 'soggycat'], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'soggy-cat')) return;
await soggyHandler(ctx);
});
}

View file

@ -1,88 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import axios from 'axios';
import verifyInput from '../plugins/verifyInput';
import { Context, Telegraf } from 'telegraf';
import { replyToMessageId } from '../utils/reply-to-message-id';
import * as schema from '../../database/schema';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
interface Device {
brand: string;
codename: string;
model: string;
name: string;
}
export async function getDeviceByCodename(codename: string): Promise<Device | null> {
try {
const response = await axios.get(Resources.codenameApi);
const jsonRes = response.data;
const deviceDetails = jsonRes[codename];
if (!deviceDetails) return null;
return deviceDetails.find((item: Device) => item.brand) || deviceDetails[0];
} catch (error) {
return null;
}
}
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
if (from.language_code && languageCode === 'en') {
languageCode = from.language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
export default (bot: Telegraf<Context>, db) => {
bot.command(['codename', 'whatis'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'codename-lookup')) return;
const userInput = ctx.message.text.split(" ").slice(1).join(" ");
const { Strings } = await getUserAndStrings(ctx, db);
const { noCodename } = Strings.codenameCheck;
const reply_to_message_id = replyToMessageId(ctx);
if (verifyInput(ctx, userInput, noCodename)) {
return;
}
const device = await getDeviceByCodename(userInput);
if (!device) {
return ctx.reply(Strings.codenameCheck.notFound, {
parse_mode: "Markdown",
...({ reply_to_message_id })
});
}
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 })
});
})
}

View file

@ -1,271 +0,0 @@
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import os from 'os';
import { exec } from 'child_process';
import { error } from 'console';
import { Context, Telegraf } from 'telegraf';
import * as schema from '../../database/schema';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
if (from.language_code && languageCode === 'en') {
languageCode = from.language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
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: number) {
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: Context & { message: { text: string } }, action: () => Promise<void>, successMessage: string, errorMessage: string) {
const { Strings } = await getUserAndStrings(ctx);
const userId = ctx.from?.id;
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
if (userId && adminArray.includes(userId)) {
try {
await action();
if (successMessage) {
ctx.reply(successMessage, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
} catch (error) {
ctx.reply(errorMessage.replace(/{error}/g, error.message), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
} else {
ctx.reply(Strings.noPermission, {
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
}
export default (bot: Telegraf<Context>, db) => {
bot.command('getbotstats', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
handleAdminCommand(ctx, async () => {
const stats = getSystemInfo();
await ctx.reply(stats, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}, '', Strings.errorRetrievingStats);
});
bot.command('getbotcommit', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
handleAdminCommand(ctx, async () => {
try {
const commitHash = await getGitCommitHash();
await ctx.reply(Strings.gitCurrentCommit.replace(/{commitHash}/g, commitHash), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
} catch (error) {
ctx.reply(Strings.gitErrRetrievingCommit.replace(/{error}/g, error), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
}, '', Strings.gitErrRetrievingCommit);
});
bot.command('updatebot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
handleAdminCommand(ctx, async () => {
try {
const result = await updateBot();
await ctx.reply(Strings.botUpdated.replace(/{result}/g, result), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
} catch (error) {
ctx.reply(Strings.errorUpdatingBot.replace(/{error}/g, error), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
}, '', Strings.errorUpdatingBot);
});
bot.command('setbotname', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
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: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
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: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
handleAdminCommand(ctx, async () => {
if (!ctx.chat) {
ctx.reply(Strings.chatNotFound, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
return;
}
ctx.reply(Strings.kickingMyself, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
await ctx.telegram.leaveChat(ctx.chat.id);
}, '', Strings.kickingMyselfErr);
});
bot.command('getfile', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const { Strings } = await getUserAndStrings(ctx, db);
const botFile = ctx.message.text.split(' ').slice(1).join(' ');
if (!botFile) {
ctx.reply(Strings.noFileProvided, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
return;
}
handleAdminCommand(ctx, async () => {
try {
await ctx.replyWithDocument({
// @ts-ignore
source: botFile,
caption: botFile
}, {
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
} catch (error) {
ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
}, '', Strings.unexpectedErr);
});
bot.command('run', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
const command = ctx.message.text.split(' ').slice(1).join(' ');
handleAdminCommand(ctx, async () => {
if (!command) {
ctx.reply('Por favor, forneça um comando para executar.');
return;
}
exec(command, (error, stdout, stderr) => {
if (error) {
return ctx.reply(`\`${error.message}\``, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
if (stderr) {
return ctx.reply(`\`${stderr}\``, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
ctx.reply(`\`${stdout}\``, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
});
}, '', "Nope!");
});
bot.command('eval', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
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',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
} catch (error) {
ctx.reply(`Error: ${error.message}`, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
});
bot.command('crash', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
handleAdminCommand(ctx, async () => {
ctx.reply('Crashed!');
}, '', "Nope!");
});
};

View file

@ -1,140 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import { Context, Telegraf } from 'telegraf';
import * as schema from '../../database/schema';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
if (from.language_code && languageCode === 'en') {
languageCode = from.language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
function sendRandomReply(ctx: Context & { message: { text: string } }, gifUrl: string, textKey: string, db: any) {
getUserAndStrings(ctx, db).then(({ Strings }) => {
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',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
}).catch(err => {
const gifErr = Strings.gifErr.replace('{err}', err);
ctx.reply(gifErr, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
});
} else {
ctx.reply(caption, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}
});
}
async function handleDiceCommand(ctx: Context & { message: { text: string } }, emoji: string, delay: number, db: any) {
const { Strings } = await getUserAndStrings(ctx, db);
// @ts-ignore
const result = await ctx.sendDice({ emoji, ...(ctx.message?.message_id ? { reply_parameters: { 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',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
}, delay);
}
function getRandomInt(max: number) {
return Math.floor(Math.random() * (max + 1));
}
export default (bot: Telegraf<Context>, db) => {
bot.command('random', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
const { Strings } = await getUserAndStrings(ctx, db);
const randomValue = getRandomInt(10);
const randomVStr = Strings.randomNum.replace('{number}', randomValue);
ctx.reply(
randomVStr, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
});
// TODO: maybe send custom stickers to match result of the roll? i think there are pre-existing ones
bot.command('dice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
await handleDiceCommand(ctx, '🎲', 4000, db);
});
bot.command('slot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
await handleDiceCommand(ctx, '🎰', 3000, db);
});
bot.command('ball', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
await handleDiceCommand(ctx, '⚽', 3000, db);
});
bot.command('dart', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
await handleDiceCommand(ctx, '🎯', 3000, db);
});
bot.command('bowling', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
await handleDiceCommand(ctx, '🎳', 3000, db);
});
bot.command('idice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'infinite-dice')) return;
const { Strings } = await getUserAndStrings(ctx, db);
ctx.replyWithSticker(
Resources.infiniteDice, {
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
});
bot.command('furry', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
sendRandomReply(ctx, Resources.furryGif, 'furryAmount', db);
});
bot.command('gay', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
sendRandomReply(ctx, Resources.gayFlag, 'gayAmount', db);
});
};

View file

@ -1,154 +0,0 @@
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import type { Context } from 'telegraf';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db?: any): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
function isAdmin(ctx: Context): boolean {
const userId = ctx.from?.id;
if (!userId) return false;
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
return adminArray.includes(userId);
}
interface MessageOptions {
parse_mode: string;
disable_web_page_preview: boolean;
reply_markup: {
inline_keyboard: { text: string; callback_data: string; }[][];
};
reply_to_message_id?: number;
}
async function sendHelpMessage(ctx, isEditing, db) {
const { Strings } = await getUserAndStrings(ctx, db);
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): MessageOptions => {
const options: MessageOptions = {
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' }],
[{ text: Strings.ai.helpEntry, callback_data: 'helpAi' }]
]
}
};
if (includeReplyTo) {
const messageId = getMessageId(ctx);
if (messageId) {
(options as any).reply_parameters = { message_id: messageId };
};
};
return options;
};
if (isEditing) {
await ctx.editMessageText(helpText, createOptions(ctx));
} else {
await ctx.reply(helpText, createOptions(ctx, true));
};
}
export default (bot, db) => {
bot.help(spamwatchMiddleware, async (ctx) => {
await sendHelpMessage(ctx, false, db);
});
bot.command("about", spamwatchMiddleware, async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`);
ctx.reply(aboutMsg, {
parse_mode: 'Markdown',
disable_web_page_preview: true,
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
});
});
const options = (Strings) => ({
parse_mode: 'Markdown',
disable_web_page_preview: true,
reply_markup: JSON.stringify({
inline_keyboard: [
[{ text: Strings.varStrings.varBack, callback_data: 'helpBack' }],
]
})
});
bot.action('helpMain', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.mainCommandsDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpUseful', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.usefulCommandsDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpInteractive', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.interactiveEmojisDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpFunny', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.funnyCommandsDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpLast', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.lastFm.helpDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpYouTube', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.ytDownload.helpDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpAnimals', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.animalCommandsDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpMLP', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
await ctx.editMessageText(Strings.ponyApi.helpDesc, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpAi', async (ctx) => {
const { Strings } = await getUserAndStrings(ctx, db);
const helpText = isAdmin(ctx) ? Strings.ai.helpDescAdmin : Strings.ai.helpDesc;
await ctx.editMessageText(helpText, options(Strings));
await ctx.answerCbQuery();
});
bot.action('helpBack', async (ctx) => {
await sendHelpMessage(ctx, true, db);
await ctx.answerCbQuery();
});
}

View file

@ -1,114 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import axios from 'axios';
import verifyInput from '../plugins/verifyInput';
import { Context, Telegraf } from 'telegraf';
import * as schema from '../../database/schema';
import { languageCode } from '../utils/language-code';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
if (from.language_code && languageCode === 'en') {
languageCode = from.language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
export default (bot: Telegraf<Context>, db) => {
bot.command("http", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'http-status')) return;
const reply_to_message_id = ctx.message.message_id;
const { Strings } = await getUserAndStrings(ctx, db);
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 ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} else {
await ctx.reply(Strings.httpCodes.notFound, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
} catch (error) {
const message = Strings.httpCodes.fetchErr.replace('{error}', error);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
});
bot.command("httpcat", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = ctx.message.message_id;
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(/\s+/g, '');
const { invalidCode } = Strings.httpCodes
if (verifyInput(ctx, userInput, invalidCode, true)) {
return;
}
if (userInput.length !== 3) {
ctx.reply(Strings.httpCodes.invalidCode, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
})
return
}
const apiUrl = `${Resources.httpCatApi}${userInput}`;
try {
await ctx.replyWithPhoto(apiUrl, {
caption: `🐱 ${apiUrl}`,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
ctx.reply(Strings.catImgErr, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
}
});
};

View file

@ -1,88 +0,0 @@
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import { Context, Telegraf } from 'telegraf';
import * as schema from '../../database/schema';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
const from = ctx.from;
if (db && from.id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
if (dbUser.length > 0) {
languageCode = dbUser[0].languageCode;
}
}
if (from.language_code && languageCode === 'en') {
languageCode = from.language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
}
const Strings = getStrings(languageCode);
return { Strings, languageCode };
}
async function getUserInfo(ctx: Context & { message: { text: string } }, db: any) {
const { Strings } = await getUserAndStrings(ctx, db);
let lastName = ctx.from?.last_name;
if (lastName === undefined) {
lastName = " ";
}
const 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: Context & { message: { text: string } }, db: any) {
const { Strings } = await getUserAndStrings(ctx, db);
if ((ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup')) {
const chat = ctx.chat as (typeof ctx.chat & { username?: string; is_forum?: boolean });
const chatInfo = Strings.chatInfo
.replace('{chatId}', chat?.id || Strings.varStrings.varUnknown)
.replace('{chatName}', chat?.title || Strings.varStrings.varUnknown)
.replace('{chatHandle}', chat?.username ? `@${chat.username}` : Strings.varStrings.varNone)
.replace('{chatMembersCount}', await ctx.getChatMembersCount())
.replace('{chatType}', chat?.type || Strings.varStrings.varUnknown)
.replace('{isForum}', chat?.is_forum ? Strings.varStrings.varYes : Strings.varStrings.varNo);
return chatInfo;
} else {
return Strings.groupOnly;
}
}
export default (bot: Telegraf<Context>, db) => {
bot.command('chatinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'info-commands')) return;
const chatInfo = await getChatInfo(ctx, db);
ctx.reply(
chatInfo, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
}
);
});
bot.command('userinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'info-commands')) return;
const userInfo = await getUserInfo(ctx, db);
ctx.reply(
userInfo, {
parse_mode: 'Markdown',
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
}
);
});
};

View file

@ -1,556 +0,0 @@
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import { Context, Telegraf } from 'telegraf';
import { replyToMessageId } from '../utils/reply-to-message-id';
import * as schema from '../../database/schema';
import { eq } from 'drizzle-orm';
import { ensureUserInDb } from '../utils/ensure-user';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { getModelLabelByName } from './ai';
import { models } from '../../config/ai';
import { langs } from '../locales/config';
import { modelPageSize, seriesPageSize } from '../../config/settings';
type UserRow = typeof schema.usersTable.$inferSelect;
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
async function getUserAndStrings(ctx: Context, db: NodePgDatabase<typeof schema>): Promise<{ user: UserRow | null, Strings: any, languageCode: string }> {
let user: UserRow | null = null;
let languageCode = 'en';
if (!ctx.from) {
const Strings = getStrings(languageCode);
return { user, Strings, languageCode };
}
const { id, language_code } = ctx.from;
if (id) {
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(id)), limit: 1 });
if (dbUser.length === 0) {
await ensureUserInDb(ctx, db);
const newUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(id)), limit: 1 });
if (newUser.length > 0) {
user = newUser[0];
languageCode = user.languageCode;
}
} else {
user = dbUser[0];
languageCode = user.languageCode;
}
}
if (!user && language_code) {
languageCode = language_code;
console.warn('[WARN !] Falling back to Telegram language_code for user', id);
}
const Strings = getStrings(languageCode);
return { user, Strings, languageCode };
}
type SettingsMenu = { text: string, reply_markup: any };
function getSettingsMenu(user: UserRow, Strings: any): SettingsMenu {
const langObj = langs.find(l => l.code === user.languageCode);
const langLabel = langObj ? langObj.label : user.languageCode;
const userId = user.telegramId;
return {
text: `*${Strings.settings.selectSetting}*`,
reply_markup: {
inline_keyboard: [
[
{ text: `${Strings.settings.ai.aiEnabled}: ${user.aiEnabled ? Strings.settings.enabled : Strings.settings.disabled}`, callback_data: `settings_aiEnabled_${userId}` },
{ text: `🧠 ${Strings.settings.ai.aiModel}: ${getModelLabelByName(user.customAiModel)}`, callback_data: `settings_aiModel_0_${userId}` }
],
[
{ text: `🌡️ ${Strings.settings.ai.aiTemperature}: ${user.aiTemperature}`, callback_data: `settings_aiTemperature_${userId}` },
{ text: `🌐 ${langLabel}`, callback_data: `settings_language_${userId}` }
],
[
{ text: `🧠 ${Strings.settings.ai.showThinking}: ${user.showThinking ? Strings.settings.enabled : Strings.settings.disabled}`, callback_data: `settings_showThinking_${userId}` }
]
]
}
};
}
function extractUserIdFromCallback(data: string): string | null {
const match = data.match(/_(\d+)$/);
return match ? match[1] : null;
}
function getNotAllowedMessage(Strings: any) {
return Strings.gsmarenaNotAllowed;
}
function logSettingsAccess(action: string, ctx: Context, allowed: boolean, expectedUserId: string | null) {
if (process.env.longerLogs === 'true') {
const actualUserId = ctx.from?.id;
const username = ctx.from?.username || ctx.from?.first_name || 'unknown';
console.log(`[Settings] Action: ${action}, Callback from: ${username} (${actualUserId}), Expected: ${expectedUserId}, Allowed: ${allowed}`);
}
}
function handleTelegramError(err: any, context: string) {
const description = err?.response?.description || '';
const ignoredErrors = [
'query is too old',
'query ID is invalid',
'message is not modified',
'message to edit not found',
];
const isIgnored = ignoredErrors.some(errorString => description.includes(errorString));
if (!isIgnored) {
console.error(`[${context}] Unexpected Telegram error:`, err);
}
}
export default (bot: Telegraf<Context>, db: NodePgDatabase<typeof schema>) => {
bot.start(spamwatchMiddleware, async (ctx: Context) => {
const { user, Strings } = await getUserAndStrings(ctx, db);
const botInfo = await ctx.telegram.getMe();
const reply_to_message_id = replyToMessageId(ctx);
const startMsg = Strings.botWelcome.replace(/{botName}/g, botInfo.first_name);
if (!user) return;
ctx.reply(
startMsg.replace(
/{aiEnabled}/g,
user.aiEnabled ? Strings.settings.enabled : Strings.settings.disabled
).replace(
/{aiModel}/g,
getModelLabelByName(user.customAiModel)
).replace(
/{aiTemperature}/g,
user.aiTemperature.toString()
).replace(
/{aiRequests}/g,
user.aiRequests.toString()
).replace(
/{aiCharacters}/g,
user.aiCharacters.toString()
).replace(
/{languageCode}/g,
user.languageCode
), {
parse_mode: 'Markdown',
...({ reply_to_message_id })
}
);
});
bot.command(["settings"], spamwatchMiddleware, async (ctx: Context) => {
const reply_to_message_id = replyToMessageId(ctx);
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const menu = getSettingsMenu(user, Strings);
await ctx.reply(
menu.text,
{
reply_markup: menu.reply_markup,
parse_mode: 'Markdown',
...({ reply_to_message_id })
}
);
});
const updateSettingsKeyboard = async (ctx: Context, user: UserRow, Strings: any) => {
const menu = getSettingsMenu(user, Strings);
await ctx.editMessageReplyMarkup(menu.reply_markup);
};
bot.action(/^settings_aiEnabled_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_aiEnabled', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
await db.update(schema.usersTable)
.set({ aiEnabled: !user.aiEnabled })
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
await updateSettingsKeyboard(ctx, updatedUser, Strings);
});
bot.action(/^settings_showThinking_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_showThinking', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
await db.update(schema.usersTable)
.set({ showThinking: !user.showThinking })
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
await updateSettingsKeyboard(ctx, updatedUser, Strings);
});
bot.action(/^settings_aiModel_(\d+)_(\d+)$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_aiModel', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const match = data.match(/^settings_aiModel_(\d+)_/);
if (!match) return;
const page = parseInt(match[1], 10);
const pageSize = 4;
const start = page * pageSize;
const end = start + pageSize;
const paginatedModels = models.slice(start, end);
const buttons = paginatedModels.map((series, idx) => {
const originalIndex = start + idx;
const isSelected = series.models.some(m => m.name === user.customAiModel);
const label = isSelected ? `${series.label}` : series.label;
return { text: label, callback_data: `selectseries_${originalIndex}_0_${user.telegramId}` };
});
const navigationButtons: any[] = [];
if (page > 0) {
navigationButtons.push({ text: Strings.varStrings.varLess, callback_data: `settings_aiModel_${page - 1}_${user.telegramId}` });
}
if (end < models.length) {
navigationButtons.push({ text: Strings.varStrings.varMore, callback_data: `settings_aiModel_${page + 1}_${user.telegramId}` });
}
const keyboard: any[][] = [];
for (const button of buttons) {
keyboard.push([button]);
}
if (navigationButtons.length > 0) {
keyboard.push(navigationButtons);
}
keyboard.push([{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` }]);
try {
await ctx.editMessageText(
`${Strings.settings.ai.selectSeries}`,
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: keyboard
}
}
);
} catch (err) {
handleTelegramError(err, 'settings_aiModel');
}
});
bot.action(/^selectseries_\d+_\d+_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('selectseries', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const match = data.match(/^selectseries_(\d+)_(\d+)_(\d+)$/);
if (!match) return;
const seriesIdx = parseInt(match[1], 10);
const modelPage = parseInt(match[2], 10);
const series = models[seriesIdx];
if (!series) return;
const seriesPage = Math.floor(seriesIdx / seriesPageSize);
const start = modelPage * modelPageSize;
const end = start + modelPageSize;
const paginatedSeriesModels = series.models.slice(start, end);
const modelButtons = paginatedSeriesModels.map((m, idx) => {
const originalModelIndex = start + idx;
const isSelected = m.name === user.customAiModel;
const label = isSelected ? `${m.label}` : m.label;
return [{ text: `${label} (${m.parameterSize})`, callback_data: `setmodel_${seriesIdx}_${originalModelIndex}_${user.telegramId}` }];
});
const navigationButtons: any[] = [];
if (modelPage > 0) {
navigationButtons.push({ text: Strings.varStrings.varLess, callback_data: `selectseries_${seriesIdx}_${modelPage - 1}_${user.telegramId}` });
}
if (end < series.models.length) {
navigationButtons.push({ text: Strings.varStrings.varMore, callback_data: `selectseries_${seriesIdx}_${modelPage + 1}_${user.telegramId}` });
}
const keyboard: any[][] = [...modelButtons];
if (navigationButtons.length > 0) {
keyboard.push(navigationButtons);
}
keyboard.push([{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_aiModel_${seriesPage}_${user.telegramId}` }]);
const desc = user.languageCode === 'pt' ? series.descriptionPt : series.descriptionEn;
try {
await ctx.editMessageText(
`${Strings.settings.ai.seriesDescription.replace('{seriesDescription}', desc)}\n\n${Strings.settings.ai.selectParameterSize.replace('{seriesLabel}', series.label).replace(' [ & Uncensored ]', '')}\n\n${Strings.settings.ai.parameterSizeExplanation}`,
{
reply_markup: {
inline_keyboard: keyboard
}
}
);
} catch (err) {
handleTelegramError(err, 'selectseries');
}
});
bot.action(/^setmodel_\d+_\d+_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('setmodel', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const match = data.match(/^setmodel_(\d+)_(\d+)_\d+$/);
if (!match) return;
const seriesIdx = parseInt(match[1], 10);
const modelIdx = parseInt(match[2], 10);
const series = models[seriesIdx];
const model = series?.models[modelIdx];
if (!series || !model) return;
await db.update(schema.usersTable)
.set({ customAiModel: model.name })
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
const menu = getSettingsMenu(updatedUser, Strings);
try {
if (ctx.callbackQuery.message) {
await ctx.editMessageText(
menu.text,
{
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
}
);
} else {
await ctx.reply(menu.text, {
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
});
}
} catch (err) {
handleTelegramError(err, 'setmodel');
}
});
bot.action(/^settings_aiTemperature_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_aiTemperature', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const temps = [0.2, 0.5, 0.7, 0.9, 1.2];
try {
await ctx.editMessageText(
`${Strings.settings.ai.temperatureExplanation}\n\n${Strings.settings.ai.selectTemperature}`,
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: temps.map(t => [{ text: t.toString(), callback_data: `settemp_${t}_${user.telegramId}` }])
.concat([
[{ text: Strings.varStrings.varMore, callback_data: `show_more_temps_${user.telegramId}` }],
[
{ text: Strings.varStrings.varBack, callback_data: `settings_back_${user.telegramId}` }
]
])
}
}
);
} catch (err) {
handleTelegramError(err, 'settings_aiTemperature');
}
});
bot.action(/^show_more_temps_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('show_more_temps', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const moreTemps = [1.4, 1.6, 1.8, 2.0];
try {
await ctx.editMessageReplyMarkup({
inline_keyboard: moreTemps.map(t => [{ text: `🔥 ${t}`, callback_data: `settemp_${t}_${user.telegramId}` }])
.concat([
[{ text: Strings.varStrings.varLess, callback_data: `settings_aiTemperature_${user.telegramId}` }],
[{ text: Strings.varStrings.varBack, callback_data: `settings_back_${user.telegramId}` }]
])
});
} catch (err) {
handleTelegramError(err, 'show_more_temps');
}
});
bot.action(/^settemp_.+_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settemp', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const temp = parseFloat(data.replace(/^settemp_/, '').replace(/_\d+$/, ''));
await db.update(schema.usersTable)
.set({ aiTemperature: temp })
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
await updateSettingsKeyboard(ctx, updatedUser, Strings);
});
bot.action(/^settings_language_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_language', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
try {
await ctx.editMessageText(
Strings.settings.selectLanguage,
{
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: langs.map(l => [{ text: l.label, callback_data: `setlang_${l.code}_${user.telegramId}` }]).concat([[{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` }]])
}
}
);
} catch (err) {
handleTelegramError(err, 'settings_language');
}
});
bot.action(/^settings_back_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('settings_back', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user, Strings } = await getUserAndStrings(ctx, db);
if (!user) return;
const menu = getSettingsMenu(user, Strings);
try {
if (ctx.callbackQuery.message) {
await ctx.editMessageText(
menu.text,
{
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
}
);
} else {
await ctx.reply(menu.text, {
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
});
}
} catch (err) {
handleTelegramError(err, 'settings_back');
}
});
bot.action(/^setlang_.+_\d+$/, async (ctx) => {
const data = (ctx.callbackQuery as any).data;
const userId = extractUserIdFromCallback(data);
const allowed = !!userId && String(ctx.from.id) === userId;
logSettingsAccess('setlang', ctx, allowed, userId);
if (!allowed) {
const { Strings } = await getUserAndStrings(ctx, db);
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
}
await ctx.answerCbQuery();
const { user } = await getUserAndStrings(ctx, db);
if (!user) {
console.log('[Settings] No user found');
return;
}
const lang = data.replace(/^setlang_/, '').replace(/_\d+$/, '');
await db.update(schema.usersTable)
.set({ languageCode: lang })
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
const updatedStrings = getStrings(updatedUser.languageCode);
const menu = getSettingsMenu(updatedUser, updatedStrings);
try {
if (ctx.callbackQuery.message) {
await ctx.editMessageText(
menu.text,
{
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
}
);
} else {
await ctx.reply(menu.text, {
reply_markup: menu.reply_markup,
parse_mode: 'Markdown'
});
}
} catch (err) {
handleTelegramError(err, 'setlang');
}
});
bot.command('privacy', spamwatchMiddleware, async (ctx: Context) => {
const { Strings } = await getUserAndStrings(ctx, db);
if (!ctx.from || !ctx.message) return;
const message = Strings.botPrivacy.replace("{botPrivacy}", process.env.botPrivacy ?? "");
ctx.reply(message, {
parse_mode: 'Markdown',
reply_to_message_id: ctx.message.message_id
} as any);
});
};

View file

@ -1,88 +0,0 @@
import Resources from '../props/resources.json';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import { languageCode } from '../utils/language-code';
import { Context, Telegraf } from 'telegraf';
import { replyToMessageId } from '../utils/reply-to-message-id';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
interface ModuleResult {
filePath: string;
fileName: string;
}
async function downloadModule(moduleId: string): Promise<ModuleResult | null> {
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.join(__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;
}
}
export const modarchiveHandler = async (ctx: Context) => {
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = replyToMessageId(ctx);
const moduleId = ctx.message && 'text' in ctx.message && typeof ctx.message.text === 'string'
? ctx.message.text.split(' ')[1]?.trim()
: undefined;
if (!moduleId || !/^\d+$/.test(moduleId)) {
return ctx.reply(Strings.maInvalidModule, {
parse_mode: "Markdown",
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
}
const result = await downloadModule(moduleId);
if (result) {
const { filePath, fileName } = result;
const regexExtension = /\.\w+$/i;
const hasExtension = regexExtension.test(fileName);
if (hasExtension) {
try {
await ctx.replyWithDocument({ source: filePath }, {
caption: fileName,
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} finally {
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
}
return;
}
}
return ctx.reply(Strings.maInvalidModule, {
parse_mode: "Markdown",
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
export default (bot: Telegraf<Context>, db) => {
bot.command(['modarchive', 'tma'], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'modarchive')) return;
await modarchiveHandler(ctx);
});
};

View file

@ -1,286 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import axios from 'axios';
import verifyInput from '../plugins/verifyInput';
import { Telegraf, Context } from 'telegraf';
import { languageCode } from '../utils/language-code';
import { replyToMessageId } from '../utils/reply-to-message-id';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
interface Character {
id: string;
name: string;
alias: string;
url: string;
sex: string;
residence: string;
occupation: string;
kind: string;
image: string[];
}
interface Episode {
id: string;
name: string;
image: string;
url: string;
season: string;
episode: string;
overall: string;
airdate: string;
storyby: string;
writtenby: string;
storyboard: string;
}
interface Comic {
id: string;
name: string;
series: string;
image: string;
url: string;
writer: string;
artist: string;
colorist: string;
letterer: string;
editor: string;
}
function capitalizeFirstLetter(letter: string) {
return letter.charAt(0).toUpperCase() + letter.slice(1);
}
function sendReply(ctx: Context, text: string, reply_to_message_id?: number) {
return ctx.reply(text, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
}
function sendPhoto(ctx: Context, photo: string, caption: string, reply_to_message_id?: number) {
return ctx.replyWithPhoto(photo, {
caption,
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
}
export default (bot: Telegraf<Context>, db) => {
bot.command("mlp", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = replyToMessageId(ctx);
sendReply(ctx, Strings.ponyApi.helpDesc, reply_to_message_id);
});
bot.command("mlpchar", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
const { message } = ctx;
const reply_to_message_id = replyToMessageId(ctx);
const Strings = getStrings(languageCode(ctx) || 'en');
const userInput = message.text.split(' ').slice(1).join(' ').trim().replace(/\s+/g, '+');
const { noCharName } = Strings.ponyApi;
if (verifyInput(ctx, userInput, noCharName)) return;
if (!userInput || /[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
return sendReply(ctx, Strings.mlpInvalidCharacter, reply_to_message_id);
}
const capitalizedInput = capitalizeFirstLetter(userInput);
const apiUrl = `${Resources.ponyApi}/character/${capitalizedInput}`;
try {
const response = await axios(apiUrl);
const data = response.data.data;
if (Array.isArray(data) && data.length > 0) {
const character = data[0];
const aliases = Array.isArray(character.alias)
? character.alias.join(', ')
: character.alias || Strings.varStrings.varNone;
const result = Strings.ponyApi.charRes
.replace("{id}", character.id)
.replace("{name}", character.name)
.replace("{alias}", aliases)
.replace("{url}", character.url)
.replace("{sex}", character.sex)
.replace("{residence}", character.residence ? character.residence.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
.replace("{occupation}", character.occupation ? character.occupation.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
.replace("{kind}", Array.isArray(character.kind) ? character.kind.join(', ') : Strings.varStrings.varNone);
sendPhoto(ctx, character.image[0], result, reply_to_message_id);
} else {
sendReply(ctx, Strings.ponyApi.noCharFound, reply_to_message_id);
}
} catch (error: any) {
const message = Strings.ponyApi.apiErr.replace('{error}', error.message || 'Unknown error');
sendReply(ctx, message, reply_to_message_id);
}
});
bot.command("mlpep", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
const Strings = getStrings(languageCode(ctx) || 'en');
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+");
const reply_to_message_id = replyToMessageId(ctx);
const { noEpisodeNum } = Strings.ponyApi
if (verifyInput(ctx, userInput, noEpisodeNum, true)) {
return;
}
if (Number(userInput) > 10000) {
ctx.reply(Strings.mlpInvalidEpisode, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
const apiUrl = `${Resources.ponyApi}/episode/by-overall/${userInput}`;
try {
const response = await axios(apiUrl);
const episodeArray: Episode[] = [];
if (Array.isArray(response.data.data)) {
response.data.data.forEach((episode: 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',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} else {
ctx.reply(Strings.ponyApi.noEpisodeFound, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
} catch (error) {
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
});
bot.command("mlpcomic", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
const Strings = getStrings(languageCode(ctx) || 'en');
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+");
const reply_to_message_id = replyToMessageId(ctx);
const { noComicName } = Strings.ponyApi
if (verifyInput(ctx, userInput, noComicName)) {
return;
};
// if special characters or numbers (max 30 characters)
if (/[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
ctx.reply(Strings.mlpInvalidCharacter, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
const apiUrl = `${Resources.ponyApi}/comics-story/${userInput}`;
try {
const response = await axios(apiUrl);
const comicArray: Comic[] = [];
if (Array.isArray(response.data.data)) {
response.data.data.forEach(comic => {
let letterers: string[] = [];
if (comic.letterer) {
if (typeof comic.letterer === 'string') {
letterers.push(comic.letterer);
} else if (Array.isArray(comic.letterer)) {
letterers = letterers.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',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} else {
ctx.reply(Strings.ponyApi.noComicFound, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
} catch (error) {
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
};
});
};

View file

@ -1,32 +0,0 @@
/*
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import escape from 'markdown-escape';
import axios from 'axios';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
export default (bot) => {
bot.command("quote", spamwatchMiddleware, async (ctx) => {
const Strings = getStrings(ctx.from.language_code);
try {
const response = await axios.get(Resources.quoteApi);
const data = response.data;
ctx.reply(escape(`${escape(Strings.quoteResult)}\n> *${escape(data.quote)}*\n_${escape(data.author)}_`), {
reply_to_message_id: ctx.message.message_id,
parse_mode: 'Markdown'
});
} catch (error) {
console.error(error);
ctx.reply(Strings.quoteErr, {
reply_to_message_id: ctx.message.id,
parse_mode: 'MarkdownV2'
});
};
});
};
*/

View file

@ -1,52 +0,0 @@
import Resources from '../props/resources.json';
import { getStrings } from '../plugins/checklang';
import { isOnSpamWatch } from '../spamwatch/spamwatch';
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
import axios from 'axios';
import { Telegraf, Context } from 'telegraf';
import { languageCode } from '../utils/language-code';
import { replyToMessageId } from '../utils/reply-to-message-id';
import { isCommandDisabled } from '../utils/check-command-disabled';
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
export const randomponyHandler = async (ctx: Context & { message: { text: string } }) => {
const Strings = getStrings(languageCode(ctx));
const reply_to_message_id = replyToMessageId(ctx);
ctx.reply(Strings.ponyApi.searching, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
try {
const response = await axios(Resources.randomPonyApi);
let tags: string[] = [];
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 ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
} catch (error) {
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
ctx.reply(message, {
parse_mode: 'Markdown',
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
});
return;
}
};
export default (bot: Telegraf<Context>, db) => {
bot.command(["rpony", "randompony", "mlpart"], spamwatchMiddleware, async (ctx) => {
if (await isCommandDisabled(ctx, db, 'random-pony')) return;
await randomponyHandler(ctx);
});
}

View file

@ -1,41 +0,0 @@
/*
import axios from "axios";
import { Context, Telegraf } from "telegraf";
import { replyToMessageId } from "../utils/reply-to-message-id";
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function mediaWikiToMarkdown(input: string) {
input = input.replace(/===(.*?)===/g, '*$1*');
input = input.replace(/==(.*?)==/g, '*$1*');
input = input.replace(/=(.*?)=/g, '*$1*');
input = input.replace(/'''(.*?)'''/g, '**$1**');
input = input.replace(/''(.*?)''/g, '_$1_');
input = input.replace(/^\*\s/gm, '- ');
input = input.replace(/^\#\s/gm, '1. ');
input = input.replace(/{{Quote(.*?)}}/g, "```\n$1```\n");
input = input.replace(/\[\[(.*?)\|?(.*?)\]\]/g, (_, link, text) => {
const sanitizedLink = link.replace(/ /g, '_');
return text ? `[${text}](${sanitizedLink})` : `[${sanitizedLink}](${sanitizedLink})`;
});
input = input.replace(/\[\[File:(.*?)\|.*?\]\]/g, '![$1](https://en.wikipedia.org/wiki/File:$1)');
return input;
}
export default (bot: Telegraf<Context>) => {
bot.command("wiki", async (ctx) => {
const userInput = capitalizeFirstLetter(ctx.message.text.split(' ')[1]);
const apiUrl = `https://en.wikipedia.org/w/index.php?title=${userInput}&action=raw`;
const response = await axios(apiUrl, { headers: { 'Accept': "text/plain" } });
const convertedResponse = response.data.replace(/<\/?div>/g, "").replace(/{{Infobox.*?}}/s, "");
const result = mediaWikiToMarkdown(convertedResponse).slice(0, 2048);
const reply_to_message_id = replyToMessageId(ctx);
ctx.reply(result, { parse_mode: 'Markdown', ...({ reply_to_message_id, disable_web_page_preview: true }) });
});
};
*/

View file

@ -1,4 +0,0 @@
export const langs = [
{ code: 'en', label: 'English' },
{ code: 'pt', label: 'Português' }
];

View file

@ -1,240 +0,0 @@
{
"userNotFound": "User not found.",
"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",
"varMore": "➡️ More",
"varLess": " Less"
},
"unexpectedErr": "An unexpected error occurred: {error}",
"errInvalidOption": "Whoops! Invalid option!",
"commandDisabled": "🚫 This command is currently disabled for your account.\n\nYou can enable it in the web interface: {frontUrl}",
"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 `<user>`: Sets the user for the command above.",
"noUser": "*Please provide a Last.fm username.*\nExample: `/setuser <username>`",
"noUserSet": "*You haven't set your Last.fm username yet.*\nUse the command /setuser to set.\n\nExample: `/setuser <username>`",
"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}`",
"apiKeyErr": "*An API key was not set by the bot owner. Please try again later.*"
},
"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\n- /settings: Show your user settings",
"usefulCommands": "🛠️ Useful Commands",
"usefulCommandsDesc": "🛠️ *Useful commands*\n\n- /chatinfo: Send information about the group\n- /userinfo: Send information about yourself\n- /d | /device `<model>`: Search for a device on GSMArena and show its specs.\n/codename | /whatis `<device codename>`: Shows what device is based on the codename. Example: `/codename begonia`\n- /weather | /clima `<city>`: See weather status for a specific location.\n- /modarchive | /tma `<module id>`: Download a module from The Mod Archive.\n- /http `<HTTP code>`: 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 `<http code>`: Send cat memes from http.cat with your specified HTTP code. Example: `/httpcat 404`",
"ai": {
"helpEntry": "✨ AI Commands",
"helpDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI model\n- /think `<prompt>`: Ask a thinking model about a question\n- /ai `<prompt>`: Ask your custom-set AI model a question\n- /aistop: Stop your current AI request\n- /aistats: Show your AI usage stats",
"helpDescAdmin": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI model\n- /think `<prompt>`: Ask a thinking model about a question\n- /ai `<prompt>`: Ask your custom-set AI model a question\n- /aistop: Stop your current AI request\n- /aistats: Show your AI usage stats\n\n*Admin Commands:*\n- /queue: List current AI queue\n- /qdel `<user_id>`: Clear queue items for a user\n- /qlimit `<user_id>` `<duration>`: Timeout user from AI commands\n- /setexec `<user_id>` `<duration>`: Set max execution time for user\n- /rlimit `<user_id>`: Remove all AI limits for user\n- /limits: List all current AI limits",
"disabled": "✨ AI features are currently disabled globally.",
"disabledForUser": "✨ AI features are disabled for your account. You can enable them with the /settings command.",
"pulling": "🔄 Model {model} not found locally, pulling...",
"askGenerating": "✨ Generating response with {model}...",
"askNoMessage": "✨ You need to ask me a question!",
"languageCode": "Language",
"thinking": "`🧠 Thinking...`",
"finishedThinking": "`🧠 Done thinking.`",
"urlWarning": "\n\n⚠ Note: The model cannot access or visit links!",
"inQueue": " You are {position} in the queue.",
"queueFull": "🚫 You already have too many requests in the queue. Please wait for them to finish.",
"startingProcessing": "✨ Starting to process your request...",
"systemPrompt": "You are a friendly assistant called {botName}.\nCurrent Date/Time (UTC): {date}\n\n---\n\nUser message:\n{message}",
"statusWaitingRender": "⏳ Streaming...",
"statusRendering": "🖼️ Rendering...",
"statusComplete": "✅ Complete!",
"modelHeader": "🤖 *{model}* 🌡️ *{temperature}* {status}",
"noChatFound": "No chat found",
"pulled": "✅ Pulled {model} successfully, please retry the command.",
"selectTemperature": "*Please select a temperature:*",
"temperatureExplanation": "Temperature controls the randomness of the AI's responses. Lower values (e.g., 0.2) make the model more focused and deterministic, while higher values (e.g., 1.2 or above) make it more creative and random.",
"queueEmpty": "✅ The AI queue is currently empty.",
"queueList": "📋 *AI Queue Status*\n\n{queueItems}\n\n*Total items:* {totalItems}",
"queueItem": "• User: {username} ({userId})\n Model: {model}\n Status: {status}\n",
"queueCleared": "✅ Cleared {count} queue items for user {userId}.",
"queueClearError": "❌ Error clearing queue for user {userId}: {error}",
"noQueueItems": " No queue items found for user {userId}.",
"userTimedOut": "⏱️ User {userId} has been timed out from AI commands until {timeoutEnd}.",
"userTimeoutRemoved": "✅ AI timeout removed for user {userId}.",
"userTimeoutError": "❌ Error setting timeout for user {userId}: {error}",
"invalidDuration": "❌ Invalid duration format. Use: 1m, 1h, 1d, 1w, etc.",
"userExecTimeSet": "⏱️ Max execution time set to {duration} for user {userId}.",
"userExecTimeRemoved": "✅ Max execution time limit removed for user {userId}.",
"userExecTimeError": "❌ Error setting execution time for user {userId}: {error}",
"invalidUserId": "❌ Invalid user ID. Please provide a valid Telegram user ID.",
"userNotFound": "❌ User {userId} not found in database.",
"userTimedOutFromAI": "⏱️ You are currently timed out from AI commands until {timeoutEnd}.",
"requestTooLong": "⏱️ Your request is taking too long. It has been cancelled to prevent system overload.",
"userLimitsRemoved": "✅ All AI limits removed for user {userId}.",
"userLimitRemoveError": "❌ Error removing limits for user {userId}: {error}",
"limitsHeader": "📋 *Current AI Limits*",
"noLimitsSet": "✅ No AI limits are currently set.",
"timeoutLimitsHeader": "*🔒 Users with AI Timeouts:*",
"timeoutLimitItem": "• {displayName} ({userId}) - Until: {timeoutEnd}",
"execLimitsHeader": "*⏱️ Users with Execution Time Limits:*",
"execLimitItem": "• {displayName} ({userId}) - Max: {execTime}",
"limitsListError": "❌ Error retrieving limits: {error}",
"requestStopped": "🛑 Your AI request has been stopped.",
"requestRemovedFromQueue": "🛑 Your AI request has been removed from the queue.",
"noActiveRequest": " You don't have any active AI requests to stop.",
"executionTimeoutReached": "\n\n⏱ Max execution time limit reached!",
"stoppedCurrentAndCleared": "🛑 Stopped current request and cleared {count} queued item(s) for user {userId}.",
"stoppedCurrentRequestOnly": "🛑 Stopped current request for user {userId} (no queued items found).",
"stoppedCurrentAndClearedQueue": "🛑 Stopped current request and cleared all queued items for user {userId}."
},
"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 `<video link>`: Download a video from some platforms (e.g. YouTube, Instagram, Facebook, etc.).\n\n See [this link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) for more information and which services are supported.\n\n*Note: Telegram is currently limiting bot uploads to 50MB, which means that if the video you want to download is larger than 50MB, the quality will be reduced to try to upload it anyway. We're trying our best to work around or fix this problem.*",
"downloadingVid": "⬇️ *Downloading video...*",
"libNotFound": "*It seems that the yt-dlp executable does not exist on our server...\n\nIn that case, the problem is on our end! Please wait until we have noticed and solved the problem.*",
"checkingSize": "🔎 *Checking if the video exceeds the 50MB limit...*",
"uploadingVid": "⬆️ *Uploading video...*",
"msgDesc": "{userMention}*, there is your downloaded video.*",
"downloadErr": "*Error during YT video download:*\n\n`{err}`",
"uploadErr": "Error uploading file. Please try again later.",
"uploadLimit": "*This video exceeds the 50 MB upload limit imposed by Telegram on our bot. Please try another video. We're doing our best to increase this limit.*",
"sizeLimitWarn": "*This video had its quality reduced because it exceeded the 50MB limit for uploads imposed by Telegram.*",
"noLink": "Please provide a link to a video to download.",
"botDetection": "My server is being rate limited by the video provider! Please try again later, or ask the bot owner to add their cookies/account."
},
"settings": {
"helpEntry": "🔧 Settings",
"helpDesc": "🔧 *Settings*\n\n- /settings: Show your settings",
"mainSettings": "🔧 *Settings*\n\n- AI Enabled: {aiEnabled}\n- /ai Custom Model: {aiModel}\n- AI Temperature: {aiTemperature}\n- Total AI Requests: {aiRequests}\n- Total AI Characters Sent/Recieved: {aiCharacters}\n- Language: {languageCode}",
"enabled": "Enabled",
"disabled": "Disabled",
"selectSetting": "Please select a setting to modify or view.",
"ai": {
"aiEnabled": "AI Enabled",
"aiModel": "AI Model",
"aiTemperature": "AI Temperature",
"aiRequests": "Total AI Requests",
"aiCharacters": "Total AI Characters Sent/Recieved",
"languageCode": "Language",
"aiEnabledSetTo": "AI Enabled set to {aiEnabled}",
"aiModelSetTo": "AI Model set to {aiModel}",
"aiTemperatureSetTo": "AI Temperature set to {aiTemperature}",
"selectSeries": "*Please select a model series.*\n\nThis will be set as the default model for the /ai command.",
"seriesDescription": "{seriesDescription}",
"selectParameterSize": "*Please select a parameter size for {seriesLabel}*.",
"parameterSizeExplanation": "Parameter size (e.g. 2B, 4B) refers to the number of parameters in the model. Larger models may be more capable but require more resources.",
"modelSetTo": "Model set to {aiModel} ({parameterSize})",
"selectTemperature": "*Please select a temperature:*",
"temperatureExplanation": "Temperature controls the randomness of the AI's responses. Lower values (e.g., 0.2) make the model more focused and deterministic, while higher values (e.g., 1.2 or above) make it more creative and random.",
"showThinking": "Show Model Thinking"
},
"selectLanguage": "*Please select a language:*",
"languageCodeSetTo": "Language set to {languageCode}",
"unknownAction": "Unknown action."
},
"botUpdated": "Bot updated with success.\n\n```{result}```",
"errorUpdatingBot": "Error updating bot\n\n{error}",
"catImgErr": "Sorry, but I couldn't get the cat photo you wanted.",
"catGifErr": "Sorry, but I couldn't get the cat GIF you wanted.",
"dogImgErr": "Sorry, but I couldn't get the dog photo you wanted.",
"mlpInvalidCharacter": "Please provide a valid character name.",
"mlpInvalidEpisode": "Please provide a valid episode number.",
"foxApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
"duckApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
"httpCodes": {
"invalidCode": "Please enter a valid HTTP code.",
"fetchErr": "An error occurred while fetching the HTTP code.",
"notFound": "HTTP code not found.",
"resultMsg": "*HTTP Code*: {code}\n*Name*: `{message}`\n*Description*: {description}"
},
"ponyApi": {
"helpEntry": "🐴 My Little Pony",
"helpDesc": "🐴 *My Little Pony*\n\n- /mlp: Displays this help message.\n- /mlpchar `<character name>`: Shows specific information about a My Little Pony character. Example: `/mlpchar Twilight Sparkle`\n- /mlpep: Shows specific information about a My Little Pony episode. Example: `/mlpep 136`\n- /mlpcomic `<comic name>`: Shows specific information about a My Little Pony comic. Example: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Sends a random artwork made by the My Little Pony community.",
"charRes": "*{name} (ID: {id})*\n\n*Alias:* `{alias}`\n*Sex:* `{sex}`\n*Residence:* `{residence}`\n*Occupation:* `{occupation}`\n*Kind:* `{kind}`\n\n*Fandom URL:*\n[{url}]({url})",
"epRes": "*{name} (ID: {id})*\n\n*Season:* `{season}`\n*Episode:* `{episode}`\n*Overall Ep.:* `{overall}`\n*Release date:* `{airdate}`\n*Story by:* `{storyby}`\n*Written by:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*Fandom URL:*\n[{url}]({url})",
"comicRes": "*{name} (ID: {id})*\n\n*Series:* `{series}`\n*Writer:* `{writer}`\n*Artist:* `{artist}`\n*Colorist:* `{colorist}`\n*Letterer:* `{letterer}`\n*Editor:* `{editor}`\n\n*Fandom URL:*\n[{url}]({url})",
"noCharName": "Please provide the character's name.",
"noCharFound": "No character found.",
"noEpisodeNum": "Please provide the episode's number.",
"noEpisodeFound": "No episode found.",
"noComicName": "Please provide the comic's name.",
"noComicFound": "No comic found.",
"searching": "Searching for a character…",
"apiErr": "An error occurred while fetching data from the API.\n\n`{error}`"
},
"codenameCheck": {
"noCodename": "Please provide a codename to search.",
"invalidCodename": "Invalid codename.",
"notFound": "Phone not found.",
"resultMsg": "*Name:* `{name}`\n*Brand:* `{brand}`\n*Model:* `{model}`\n*Codename:* `{codename}`",
"apiErr": "An error occurred while fetching data from the API.\n\n`{err}`"
},
"chatNotFound": "Chat not found.",
"noFileProvided": "Please provide a file to send.",
"gsmarenaProvidePhoneName": "Please provide the phone name.",
"gsmarenaSearchingFor": "Searching for `{phone}`...",
"gsmarenaNoPhonesFound": "No phones found for `{phone}`.",
"gsmarenaNoPhonesFoundBoth": "No phones found for `{name}` and `{phone}`.",
"gsmarenaSelectDevice": "Please select your device:",
"gsmarenaNotAllowed": "you are not allowed to interact with this.",
"gsmarenaInvalidOrExpired": "Whoops, invalid or expired option. Please try again.",
"gsmarenaDeviceDetails": "these are the details of your device:",
"gsmarenaErrorFetchingDetails": "Error fetching phone details.",
"info": {
"ping": "Pong!",
"pinging": "Pinging...",
"pong": "Pong in {ms}ms.",
"botInfo": "Kowalski is a multipurpose bot with a variety of features, including AI, moderation, and more.",
"credits": "Kowalski was created by ihatenodejs/Aidan, with contributions from the open-source community. It is licensed under the Unlicense license."
},
"aiStats": {
"header": "✨ *Your AI Usage Stats*",
"requests": "*Total AI Requests:* {aiRequests}",
"characters": "*Total AI Characters:* {aiCharacters}\n_That's around {bookCount} books of text!_"
},
"twoFactor": {
"helpEntry": "🔒 2FA",
"helpDesc": "🔒 *2FA*\n\n- /2fa: Show your 2FA settings",
"codeMessage": "🔒 *{botName} 2FA*\n\nYour 2FA code is: `{code}`"
}
}

View file

@ -1,235 +0,0 @@
{
"botWelcome": "*Olá! Eu sou o {botName}!*\nEu fui feito com amor por uns nerds que amam programação!\n\n*Ao usar o {botName}, você afirma que leu e concorda com a política de privacidade (/privacy). Isso ajuda você a entender onde seus dados vão ao usar este bot.*\n\nAlém disso, você pode usar /help para ver os meus comandos!",
"botHelp": "*Oi, eu sou o {botName}, um bot simples feito do zero em Telegraf e Node.js por uns nerds que gostam de programação.*\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})\n\nClique nos botões abaixo para ver quais comandos você pode usar!\n",
"botPrivacy": "Acesse [este link]({botPrivacy}) para ler a política de privacidade do bot.",
"botAbout": "*Sobre o bot*\n\nA base deste bot foi feita originalmente por [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), agora sendo mantido por várias pessoas.\n\nA intenção do bot é trazer diversão para os seus grupos aqui no Telegram de uma maneira bem descontraida e simples. O bot também conta com alguns comandos bem úteis, que você consegue ver com o comando de ajuda (/help).\n\nAgradecimento especial ao @givfnz2 pelas suas várias contribuições ao bot!\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})",
"aboutBot": "Sobre o bot",
"varStrings": {
"varYes": "Sim",
"varNo": "Não",
"varTo": "",
"varIs": "está",
"varWas": "estava",
"varNone": "Nenhum",
"varUnknown": "Desconhecido",
"varBack": "⬅️ Voltar",
"varMore": "➡️ Mais",
"varLess": " Menos"
},
"unexpectedErr": "Ocorreu um erro inesperado: {error}",
"errInvalidOption": "Ops! Opção inválida!",
"commandDisabled": "🚫 Este comando está atualmente desativado para sua conta.\n\nVocê pode habilitá-lo na interface web: {frontUrl}",
"kickingMyself": "*Já que você não precisa de mim, vou sair daqui.*",
"kickingMyselfErr": "Erro ao sair do chat.",
"noPermission": "Você não tem permissão para executar este comando.",
"privateOnly": "Este comando deve ser usado apenas em chats privados, não em grupos.",
"groupOnly": "Este comando deve ser usado apenas em grupos, não em chats privados.",
"botNameChanged": "*Nome do bot alterado para* `{botName}`.",
"botNameErr": "*Erro ao alterar o nome do bot:*\n`{tgErr}`",
"botDescChanged": "*Descrição do bot alterada para* `{botDesc}`.",
"botDescErr": "*Erro ao alterar a descrição do bot:*\n`{tgErr}`",
"gayAmount": "Você é *{randomNum}%* gay!",
"furryAmount": "Você é *{randomNum}%* furry!",
"randomNum": "*Número gerado (0-10):* `{number}`.",
"userInfo": "*Informações do usuário*\n\n*Nome:* `{userName}`\n*Usuário:* `{userHandle}`\n*ID:* `{userId}`\n*Idioma:* `{userLang}`\n*Usuário Premium:* `{userPremium}`",
"chatInfo": "*Informações do chat*\n\n*Nome:* `{chatName}`\n*ID do chat:* `{chatId}`\n*Identificador:* `{chatHandle}`\n*Tipo:* `{chatType}`\n*Membros:* `{chatMembersCount}`\n*É um fórum:* `{isForum}`",
"funEmojiResult": "*Você lançou {emoji} e obteve *`{value}`*!*\nVocê não sabe o que isso significa? Nem eu!",
"gifErr": "*Algo deu errado ao enviar o GIF. Tente novamente mais tarde.*\n\n{err}",
"lastFm": {
"helpEntry": "🎵 Last.fm",
"helpDesc": "🎵 *Last.fm*\n\n- /lt | /lmu | /last | /lfm: Mostra a última música do seu perfil no Last.fm + o número de reproduções.\n- /setuser `<usuário>`: Define o usuário para o comando acima.",
"noUser": "*Por favor, forneça um nome de usuário do Last.fm.*\nExemplo: `/setuser <username>`",
"noUserSet": "*Você ainda não definiu seu nome de usuário do Last.fm.*\nUse o comando /setuser para definir.\n\nExemplo: `/setuser <username>`",
"noRecentTracks": "*Nenhuma faixa recente encontrada para o usuário do Last.fm* `{lastfmUser}`*.*",
"userHasBeenSet": "*Seu nome de usuário do Last.fm foi definido como:* `{lastUser}`.",
"listeningTo": "{lastfmUser} *{nowPlaying} ouvindo{playCount}*:\n\n{trackName} por {artistName}",
"playCount": " pela {plays}ª vez",
"apiErr": "*Erro ao recuperar dados para o usuário do Last.fm* {lastfmUser}.\n\n`{err}`"
},
"gitCurrentCommit": "*Commit atual:* `{commitHash}`",
"gitErrRetrievingCommit": "*Erro ao obter o commit:* {error}",
"weatherStatus": {
"provideLocation": "*Por favor, forneça uma localização.*",
"invalidLocation": "*Localização inválida. Tente novamente.*",
"resultMsg": "*Clima em {addressFirst}:*\n\n*Estado:* `{getStatusEmoji(iconCode)} {wxPhraseLong}`\n*Temperatura:* `{temperature} °{temperatureUnit}`\n*Sensação térmica:* `{temperatureFeelsLike} °{temperatureUnit2}`\n*Umidade:* `{relativeHumidity}%`\n*Velocidade do vento:* `{windSpeed} {speedUnit}`",
"apiErr": "*Ocorreu um erro ao obter o clima. Tente novamente mais tarde.*\n\n`{error}`",
"apiKeyErr": "*Uma chave de API não foi definida pelo proprietário do bot. Tente novamente mais tarde.*"
},
"mainCommands": " Comandos principais",
"mainCommandsDesc": " *Comandos principais*\n\n- /help: Exibe a ajuda do bot\n- /start: Inicia o bot\n- /privacy: Leia a política de privacidade do bot\n- /settings: Exibe suas configurações",
"usefulCommands": "🛠️ Comandos úteis",
"usefulCommandsDesc": "🛠️ *Comandos úteis*\n\n- /chatinfo: Envia informações sobre o grupo\n- /userinfo: Envia informações sobre você\n- /d | /device `<modelo>`: Pesquisa um dispositivo no GSMArena e mostra suas especificações.\n- /weather | /clima `<cidade>`: Veja o status do clima para uma localização específica\n- /modarchive | /tma `<id do módulo>`: Baixa um módulo do The Mod Archive.\n- /http `<código HTTP>`: Envia detalhes sobre um código HTTP específico. Exemplo: `/http 404`",
"funnyCommands": "😂 Comandos engraçados",
"funnyCommandsDesc": "*Comandos engraçados*\n\n- /gay: Verifique se você é gay\n- /furry: Verifique se você é furry\n- /random: Escolhe um número aleatório entre 0-10",
"interactiveEmojis": "🎲 Emojis interativos",
"interactiveEmojisDesc": "🎲 *Emojis interativos*\n\n- /dice: Jogue um dado\n- /idice: Role infinitamente um dado colorido\n- /slot: Tente combinar as figuras!\n- /ball: Tente chutar a bola no gol!\n- /bowling: Tente derrubar os pinos!\n- /dart: Tente acertar o alvo!",
"animalCommands": "🐱 Animais",
"animalCommandsDesc": "🐱 *Animais*\n\n- /soggy | /soggycat `<1 | 2 | 3 | 4 | orig | thumb | sticker | alt>`: Envia o [meme do gato encharcado](https://knowyourmeme.com/memes/soggy-cat)\n- /cat - Envia uma foto aleatória de um gato.\n- /fox - Envia uma foto aleatória de uma raposa.\n- /duck - Envia uma foto aleatória de um pato.\n- /dog - Envia uma imagem aleatória de um cachorro.\n- /httpcat `<código http>`: Envia memes de gato do http.cat com o código HTTP especificado. Exemplo: `/httpcat 404`",
"ai": {
"helpEntry": "✨ Comandos de IA",
"helpDesc": "✨ *Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA\n- /think `<prompt>`: Fazer uma pergunta a um modelo de pensamento\n- /ai `<prompt>`: Fazer uma pergunta a um modelo de IA personalizado\n- /aistop: Parar sua solicitação de IA atual\n- /aistats: Mostra suas estatísticas de uso de IA",
"helpDescAdmin": "✨ *Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA\n- /think `<prompt>`: Fazer uma pergunta a um modelo de pensamento\n- /ai `<prompt>`: Fazer uma pergunta a um modelo de IA personalizado\n- /aistop: Parar sua solicitação de IA atual\n- /aistats: Mostra suas estatísticas de uso de IA\n\n*Comandos de Admin:*\n- /queue: Listar fila atual de IA\n- /qdel `<user_id>`: Limpar itens da fila para um usuário\n- /qlimit `<user_id>` `<duration>`: Timeout de usuário dos comandos de IA\n- /setexec `<user_id>` `<duration>`: Definir tempo máximo de execução para usuário\n- /rlimit `<user_id>`: Remover todos os limites de IA para usuário\n- /limits: Listar todos os limites atuais de IA",
"disabled": "A AIApi foi desativada\\.",
"disabledForUser": "As funções de IA estão desativadas para a sua conta. Você pode ativá-las com o comando /settings.",
"pulling": "🔄 Modelo {model} não encontrado localmente, baixando...",
"askGenerating": "✨ Gerando resposta com {model}...",
"askNoMessage": "⚠️ Você precisa fazer uma pergunta.",
"thinking": "`🧠 Pensando...`",
"finishedThinking": "`🧠 Pensamento concluido.`",
"urlWarning": "\n\n⚠ Nota: O modelo de IA não pode acessar ou visitar links!",
"inQueue": " Você é o {position} na fila.",
"queueFull": "🚫 Você já tem muitas solicitações na fila. Por favor, espere que elas terminem.",
"startingProcessing": "✨ Começando a processar o seu pedido...",
"aiEnabled": "IA",
"aiModel": "Modelo de IA",
"aiTemperature": "Temperatura",
"selectSeries": "*Por favor, selecione uma série de modelos de IA.*",
"seriesDescription": "{seriesDescription}",
"selectParameterSize": "Por favor, selecione um tamanho de parâmetro para {seriesLabel}.",
"parameterSizeExplanation": "O tamanho do parâmetro (ex: 2B, 4B) refere-se ao número de parâmetros do modelo. Modelos maiores podem ser mais capazes, mas exigem mais recursos.",
"systemPrompt": "Você é um assistente de Telegram chamado {botName}.\nData/Hora atual (UTC): {date}\n\n---\n\nMensagem do usuário:\n{message}",
"statusWaitingRender": "⏳ Transmitindo...",
"statusRendering": "🖼️ Renderizando...",
"statusComplete": "✅ Completo!",
"modelHeader": "🤖 *{model}* 🌡️ *{temperature}* {status}",
"noChatFound": "Nenhum chat encontrado",
"pulled": "✅ {model} baixado com sucesso, por favor tente o comando novamente.",
"queueEmpty": "✅ A fila de IA está atualmente vazia.",
"queueList": "📋 *Status da Fila de IA*\n\n{queueItems}\n\n*Total de itens:* {totalItems}",
"queueItem": "• Usuário: {username} ({userId})\n Modelo: {model}\n Status: {status}\n",
"queueCleared": "✅ Limpos {count} itens da fila para o usuário {userId}.",
"queueClearError": "❌ Erro ao limpar fila para o usuário {userId}: {error}",
"noQueueItems": " Nenhum item da fila encontrado para o usuário {userId}.",
"userTimedOut": "⏱️ Usuário {userId} foi suspenso dos comandos de IA até {timeoutEnd}.",
"userTimeoutRemoved": "✅ Timeout de IA removido para o usuário {userId}.",
"userTimeoutError": "❌ Erro ao definir timeout para o usuário {userId}: {error}",
"invalidDuration": "❌ Formato de duração inválido. Use: 1m, 1h, 1d, 1w, etc.",
"userExecTimeSet": "⏱️ Tempo máximo de execução definido para {duration} para o usuário {userId}.",
"userExecTimeRemoved": "✅ Limite de tempo máximo de execução removido para o usuário {userId}.",
"userExecTimeError": "❌ Erro ao definir tempo de execução para o usuário {userId}: {error}",
"invalidUserId": "❌ ID de usuário inválido. Por favor, forneça um ID de usuário válido do Telegram.",
"userNotFound": "❌ Usuário {userId} não encontrado na base de dados.",
"userTimedOutFromAI": "⏱️ Você está atualmente suspenso dos comandos de IA até {timeoutEnd}.",
"requestTooLong": "⏱️ Sua solicitação está demorando muito. Foi cancelada para evitar sobrecarga do sistema.",
"userLimitsRemoved": "✅ Todos os limites de IA removidos para o usuário {userId}.",
"userLimitRemoveError": "❌ Erro ao remover limites para o usuário {userId}: {error}",
"limitsHeader": "📋 *Limites Atuais de IA*",
"noLimitsSet": "✅ Nenhum limite de IA está atualmente definido.",
"timeoutLimitsHeader": "*🔒 Usuários com Timeouts de IA:*",
"timeoutLimitItem": "• {displayName} ({userId}) - Até: {timeoutEnd}",
"execLimitsHeader": "*⏱️ Usuários com Limites de Tempo de Execução:*",
"execLimitItem": "• {displayName} ({userId}) - Máx: {execTime}",
"limitsListError": "❌ Erro ao recuperar limites: {error}",
"requestStopped": "🛑 Sua solicitação de IA foi interrompida.",
"requestRemovedFromQueue": "🛑 Sua solicitação de IA foi removida da fila.",
"noActiveRequest": " Você não tem nenhuma solicitação ativa de IA para parar.",
"executionTimeoutReached": "\n\n⏱ Limite máximo de tempo de execução atingido!",
"stoppedCurrentAndCleared": "🛑 Parou solicitação atual e limpou {count} item(s) da fila para o usuário {userId}.",
"stoppedCurrentRequestOnly": "🛑 Parou solicitação atual para o usuário {userId} (nenhum item na fila encontrado).",
"stoppedCurrentAndClearedQueue": "🛑 Parou solicitação atual e limpou todos os itens da fila para o usuário {userId}."
},
"maInvalidModule": "Por favor, forneça um ID de módulo válido do The Mod Archive.\nExemplo: `/modarchive 81574`",
"maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.",
"ytDownload": {
"helpEntry": "📺 Download de vídeos",
"helpDesc": "📺 *Download de vídeos*\n\n- /yt | /ytdl | /sdl | /dl | /video `<link do vídeo>`: Baixa um vídeo de algumas plataformas (ex: YouTube, Instagram, Facebook, etc.).\n\nConsulte [este link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) para obter mais informações e saber quais serviços são compatíveis.\n\n*Nota: O Telegram está atualmente limitando os uploads de bots a 50MB, o que significa que se o vídeo que você deseja baixar for maior que 50MB, a qualidade será reduzida para tentar carregá-lo de qualquer maneira. Estamos fazendo o possível para contornar ou corrigir esse problema.*",
"downloadingVid": "⬇️ *Baixando vídeo...*",
"libNotFound": "*Parece que o executável do yt-dlp não existe no nosso servidor...\n\nNesse caso, o problema está no nosso lado! Aguarde até que tenhamos notado e resolvido o problema.*",
"checkingSize": "🔎 *Verificando se o vídeo excede o limite de 50 MB...*",
"uploadingVid": "⬆️ *Enviando vídeo...*",
"msgDesc": "{userMention}*, aqui está o seu vídeo baixado.*",
"downloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`",
"uploadErr": "Erro ao enviar o arquivo. Tente novamente mais tarde.",
"uploadLimit": "*Este vídeo excede o limite de carregamento de 50 MB imposto pelo Telegram ao nosso bot. Por favor, tente outro vídeo. Estamos fazendo o possível para aumentar esse limite.*",
"sizeLimitWarn": "*Esse vídeo teve a qualidade reduzida por estar excedendo o limite de 50MB para uploads imposto pelo Telegram.*",
"noLink": "*Por favor, forneça um link de um vídeo para download.*",
"botDetection": "Meu servidor está com a taxa limitada pelo provedor de vídeo! Tente novamente mais tarde ou peça ao proprietário do bot para adicionar seus cookies/conta."
},
"settings": {
"helpEntry": "🔧 Configurações",
"helpDesc": "🔧 *Configurações*\n\n- /settings: Mostrar suas configurações",
"mainSettings": "🔧 *Configurações*\n\n- Inteligência Artificial Ativado: {aiEnabled}\n- /ai Modelo personalizado: {aiModel}\n- Inteligência Artificial Temperatura: {aiTemperature}\n- Total de Requests: {aiRequests}\n- Total de Caracteres Enviados/Recebidos: {aiCharacters}\n- Idioma: {languageCode}",
"enabled": "Ativado",
"disabled": "Desativado",
"selectSetting": "Por favor, selecione uma configuração para modificar ou visualizar.",
"ai": {
"aiEnabled": "IA",
"aiModel": "Modelo",
"aiTemperature": "Temperatura",
"aiRequests": "Total de Requests",
"aiCharacters": "Total de Caracteres Enviados/Recebidos",
"languageCode": "Idioma",
"aiEnabledSetTo": "Inteligência Artificial definido para {aiEnabled}",
"aiModelSetTo": "Modelo personalizado definido para {aiModel}",
"aiTemperatureSetTo": "Temperatura definida para {aiTemperature}",
"selectSeries": "*Por favor, selecione uma série de modelos.*\n\nIsso será definido como o modelo padrão para o comando /ai.",
"seriesDescription": "{seriesDescription}",
"selectParameterSize": "Por favor, selecione um tamanho de parâmetro para {seriesLabel}.",
"parameterSizeExplanation": "O tamanho do parâmetro (ex: 2B, 4B) refere-se ao número de parâmetros do modelo. Modelos maiores podem ser mais capazes, mas exigem mais recursos.",
"modelSetTo": "Modelo definido para {aiModel} ({parameterSize})",
"selectTemperature": "*Por favor, selecione uma temperatura:*",
"temperatureExplanation": "A temperatura controla a aleatoriedade das respostas da IA. Valores mais baixos (ex: 0.2) tornam o modelo mais focado e determinístico, enquanto valores mais altos (ex: 1.2 ou mais) tornam as respostas mais criativas e aleatórias.",
"showThinking": "Mostrar Pensamento do Modelo"
},
"selectLanguage": "*Por favor, selecione um idioma:*",
"languageCodeSetTo": "Idioma definido para {languageCode}",
"unknownAction": "Ação desconhecida."
},
"botUpdated": "Bot atualizado com sucesso.\n\n```{result}```",
"errorUpdatingBot": "Erro ao atualizar o bot\n\n{error}",
"catImgErr": "Desculpe, mas não consegui obter a foto do gato que você queria.",
"catGifErr": "Desculpe, mas não consegui obter o GIF do gato que você queria.",
"dogImgErr": "Desculpe, mas não consegui obter a foto do cacbhorro que você queria.",
"mlpInvalidCharacter": "Por favor, forneça um nome de personagem válido.",
"mlpInvalidEpisode": "Por favor, forneça um número de episódio válido.",
"foxApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
"duckApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
"httpCodes": {
"invalidCode": "Por favor, insira um código HTTP válido.",
"fetchErr": "Ocorreu um erro ao buscar o código HTTP.",
"notFound": "Código HTTP não encontrado.",
"resultMsg": "*Código HTTP*: `{code}`\n*Nome*: `{message}`\n*Descrição*: `{description}`"
},
"ponyApi": {
"helpEntry": "🐴 My Little Pony",
"helpDesc": "🐴 *My Little Pony*\n\n- /mlp: Exibe esta mensagem de ajuda.\n- /mlpchar `<nome do personagem>`: Mostra informações específicas sobre um personagem de My Little Pony em inglês. Exemplo: `/mlpchar twilight`\n- /mlpep: Mostra informações específicas sobre um episódio de My Little Pony em inglês. Exemplo: `/mlpep 136`\n- /mlpcomic `<nome da comic>`: Mostra informações específicas sobre uma comic de My Little Pony em inglês. Exemplo: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Envia uma arte aleatória feita pela comunidade de My Little Pony.",
"charRes": "*{name} (ID: {id})*\n\n*Apelido:* `{alias}`\n*Sexo:* `{sex}`\n*Residência:* `{residence}`\n*Ocupação:* `{occupation}`\n*Tipo:* `{kind}`\n\n*URL no Fandom:*\n[{url}]({url})",
"epRes": "*{name} (ID: {id})*\n\n*Temporada:* `{season}`\n*Episódio:* `{episode}`\n*Número do Episódio:* `{overall}`\n*Data de lançamento:* `{airdate}`\n*História por:* `{storyby}`\n*Escrito por:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*URL no Fandom:*\n[{url}]({url})",
"comicRes": "*{name} (ID: {id})*\n\n*Série:* `{series}`\n*Roteirista:* `{writer}`\n*Artista:* `{artist}`\n*Colorista:* `{colorist}`\n*Letrista:* `{letterer}`\n*Editor:* `{editor}`\n\n*URL no Fandom:*\n[{url}]({url})",
"noCharName": "Por favor, forneça o nome do personagem.",
"noCharFound": "Nenhum personagem encontrado.",
"noEpisodeNum": "Por favor, forneça o número do episódio.",
"noEpisodeFound": "Nenhum episódio encontrado.",
"noComicName": "Por favor, forneça o nome da comic.",
"noComicFound": "Nenhuma comic foi encontrada.",
"searching": "Procurando por um personagem…",
"apiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`"
},
"codenameCheck": {
"noCodename": "Por favor, forneça um codinome para pesquisar.",
"invalidCodename": "Codinome inválido.",
"notFound": "Celular não encontrado.",
"resultMsg": "*Nome:* `{name}`\n*Marca:* `{brand}`\n*Modelo:* `{model}`\n*Codinome:* `{codename}`",
"apiErr": "Ocorreu um erro ao buscar os dados da API.\n\n`{err}`"
},
"noFileProvided": "Por favor, forneça um arquivo para envio.",
"gsmarenaProvidePhoneName": "Por favor, forneça o nome do celular.",
"gsmarenaSearchingFor": "Procurando por `{phone}`...",
"gsmarenaNoPhonesFound": "Nenhum celular encontrado para `{phone}`.",
"gsmarenaNoPhonesFoundBoth": "Nenhum celular encontrado para `{name}` e `{phone}`.",
"gsmarenaSelectDevice": "Por favor, selecione seu dispositivo:",
"gsmarenaNotAllowed": "você não tem permissão para interagir com isso.",
"gsmarenaInvalidOrExpired": "Ops! Opção inválida ou expirada. Por favor, tente novamente.",
"gsmarenaDeviceDetails": "estes são os detalhes do seu dispositivo:",
"gsmarenaErrorFetchingDetails": "Erro ao buscar detalhes do celular.",
"aiStats": {
"header": "✨ *Suas estatísticas de uso de IA*",
"requests": "*Total de requisições de IA:* {aiRequests}",
"characters": "*Total de caracteres de IA:* {aiCharacters}\n_Isso é cerca de {bookCount} livros de texto!_"
},
"twoFactor": {
"helpEntry": "🔒 2FA",
"helpDesc": "🔒 *2FA*\n\n- /2fa: Mostra suas configurações de 2FA",
"codeMessage": "🔒 *{botName} 2FA*\n\nSeu código de 2FA é: `{code}`"
}
}

View file

@ -1,14 +0,0 @@
import { Context } from "telegraf";
import { replyToMessageId } from "../utils/reply-to-message-id";
export default function verifyInput(ctx: Context, userInput: string, message: string, verifyNaN = false) {
const reply_to_message_id = replyToMessageId(ctx);
if (!userInput || (verifyNaN && isNaN(Number(userInput)))) {
ctx.reply(message, {
parse_mode: "Markdown",
...({ reply_to_message_id })
});
return true;
}
return false;
}

View file

@ -1,72 +0,0 @@
// CHECK-COMMAND-DISABLED.TS
// by ihatenodejs/Aidan
//
// -----------------------------------------------------------------------
//
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org/>
import { Context } from 'telegraf';
import { getStrings } from '../plugins/checklang';
import { replyToMessageId } from './reply-to-message-id';
export async function isCommandDisabled(ctx: Context, db: any, commandId: string): Promise<boolean> {
if (!ctx.from) return false;
const telegramId = String(ctx.from.id);
try {
const user = await db.query.usersTable.findFirst({
where: (fields, { eq }) => eq(fields.telegramId, telegramId),
columns: {
disabledCommands: true,
languageCode: true,
},
});
if (!user) return false;
const isDisabled = user.disabledCommands?.includes(commandId) || false;
if (isDisabled) {
const Strings = getStrings(user.languageCode);
const frontUrl = process.env.frontUrl || 'https://kowalski.social';
const reply_to_message_id = replyToMessageId(ctx);
await ctx.reply(
Strings.commandDisabled.replace('{frontUrl}', frontUrl),
{
parse_mode: 'Markdown',
...(reply_to_message_id && { reply_parameters: { message_id: reply_to_message_id } })
}
);
}
return isDisabled;
} catch (error) {
console.error('[💽 DB] Error checking disabled commands:', error);
return false;
}
}

View file

@ -1,67 +0,0 @@
// ENSURE-USER.TS
// by ihatenodejs/Aidan
//
// -----------------------------------------------------------------------
//
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org/>
import { usersTable } from '../../database/schema';
export async function ensureUserInDb(ctx, db) {
if (!ctx.from) return;
const telegramId = String(ctx.from.id);
const username = ctx.from.username || '';
const firstName = ctx.from.first_name || ' ';
const lastName = ctx.from.last_name || ' ';
const languageCode = ctx.from.language_code || 'en';
const existing = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, telegramId), limit: 1 });
if (existing.length === 0) {
const userToInsert = {
telegramId,
username,
firstName,
lastName,
languageCode,
aiEnabled: false,
showThinking: false,
customAiModel: "deepseek-r1:1.5b",
aiTemperature: 0.9,
aiRequests: 0,
aiCharacters: 0,
disabledCommands: [],
aiTimeoutUntil: null,
aiMaxExecutionTime: 0,
};
try {
await db.insert(usersTable).values(userToInsert);
console.log(`[💽 DB] Added new user: ${username || firstName} (${telegramId})`);
} catch (err) {
console.error('[💽 DB] Error inserting user:', err);
throw err;
}
}
}

View file

@ -1,9 +0,0 @@
import { Context } from "telegraf";
export const languageCode = (ctx: Context) => {
if(ctx.from) {
return ctx.from.language_code
} else {
return 'en'
}
}

View file

@ -1,92 +0,0 @@
// LOG.TS
// by ihatenodejs/Aidan
//
// -----------------------------------------------------------------------
//
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org/>
import { flash_model, thinking_model } from "../commands/ai"
class Logger {
private static instance: Logger
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
logCmdStart(user: string, command: string, model: string): void {
console.log(`\n[✨ AI | START] Received /${command} for model ${model} (from ${user})`)
}
logThinking(chatId: number, messageId: number, thinking: boolean): void {
if (thinking) {
console.log(`[✨ AI | THINKING | ${chatId}:${messageId}] Model started thinking`)
} else {
console.log(`[✨ AI | THINKING | ${chatId}:${messageId}] Model stopped thinking`)
}
}
logChunk(chatId: number, messageId: number, text: string, isOverflow: boolean = false): void {
if (process.env.longerLogs === 'true') {
const prefix = isOverflow ? "[✨ AI | OVERFLOW]" : "[✨ AI | CHUNK]"
console.log(`${prefix} [${chatId}:${messageId}] ${text.length} chars pushed to Telegram`)
}
}
logPrompt(prompt: string): void {
if (process.env.longerLogs === 'true') {
console.log(`[✨ AI | PROMPT] ${prompt}`)
}
}
logError(error: unknown): void {
if (typeof error === 'object' && error !== null && 'response' in error) {
const err = error as { response?: { error_code?: number, parameters?: { retry_after?: number }, description?: string }, on?: { method?: string } };
if (err.response?.error_code === 429) {
const retryAfter = err.response.parameters?.retry_after || 1;
console.error(`[✨ AI | RATE_LIMIT] Too Many Requests - retry after ${retryAfter}s`);
} else if (err.response?.error_code === 400 && err.response?.description?.includes("can't parse entities")) {
console.error("[✨ AI | PARSE_ERROR] Markdown parsing failed, retrying with plain text");
} else {
const errorDetails = {
code: err.response?.error_code,
description: err.response?.description,
method: err.on?.method
};
console.error("[✨ AI | ERROR]", JSON.stringify(errorDetails, null, 2));
}
} else {
console.error("[✨ AI | ERROR]", error);
}
}
}
export const logger = Logger.getInstance()

View file

@ -1,254 +0,0 @@
// RATE-LIMITER.TS
// by ihatenodejs/Aidan
//
// -----------------------------------------------------------------------
//
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org/>
import { Context } from 'telegraf'
import { logger } from './log'
class RateLimiter {
private lastEditTimes: Map<string, number> = new Map()
private readonly minInterval: number = 5000
private pendingUpdates: Map<string, string> = new Map()
private updateQueue: Map<string, NodeJS.Timeout> = new Map()
private readonly max_msg_length: number = 3500
private overflowMessages: Map<string, number> = new Map()
private isRateLimited: boolean = false
private rateLimitEndTime: number = 0
private getMessageKey(chatId: number, messageId: number): string {
return `${chatId}:${messageId}`
}
private async waitForRateLimit(chatId: number, messageId: number): Promise<void> {
if (!this.isRateLimited) return
console.log(`[✨ AI | RATELIMIT] [${chatId}:${messageId}] Ratelimited, waiting for end of ${this.rateLimitEndTime - Date.now()}ms`)
const now = Date.now()
if (now < this.rateLimitEndTime) {
await new Promise(resolve => setTimeout(resolve, this.rateLimitEndTime - now))
}
this.isRateLimited = false
}
private chunkText(text: string): string[] {
const chunks: string[] = []
let currentChunk = ''
let currentLength = 0
const lines = text.split('\n')
for (const line of lines) {
if (currentLength + line.length + 1 > this.max_msg_length) {
if (currentChunk) {
chunks.push(currentChunk)
currentChunk = ''
currentLength = 0
}
if (line.length > this.max_msg_length) {
for (let i = 0; i < line.length; i += this.max_msg_length) {
chunks.push(line.substring(i, i + this.max_msg_length))
}
} else {
currentChunk = line
currentLength = line.length
}
} else {
if (currentChunk) {
currentChunk += '\n'
currentLength++
}
currentChunk += line
currentLength += line.length
}
}
if (currentChunk) {
chunks.push(currentChunk)
}
return chunks
}
private handleTelegramError(
error: unknown,
messageKey: string,
options: Record<string, unknown>,
ctx: Context,
chatId: number,
messageId: number
): boolean {
if (!isTelegramError(error)) return false
if (error.response.error_code === 429) {
const retryAfter = error.response.parameters?.retry_after || 1
this.isRateLimited = true
this.rateLimitEndTime = Date.now() + (retryAfter * 1000)
const existingTimeout = this.updateQueue.get(messageKey)
if (existingTimeout) clearTimeout(existingTimeout)
const timeout = setTimeout(() => {
this.processUpdate(ctx, chatId, messageId, options)
}, retryAfter * 1000)
this.updateQueue.set(messageKey, timeout)
return true
}
if (error.response.error_code === 400) {
if (error.response.description?.includes("can't parse entities") || error.response.description?.includes("MESSAGE_TOO_LONG")) {
const plainOptions = { ...options, parse_mode: undefined }
this.processUpdate(ctx, chatId, messageId, plainOptions)
return true
}
if (error.response.description?.includes("message is not modified")) {
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
return true
}
logger.logError(error)
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
return true
}
logger.logError(error)
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
return true
}
private async processUpdate(
ctx: Context,
chatId: number,
messageId: number,
options: Record<string, unknown>
): Promise<void> {
const messageKey = this.getMessageKey(chatId, messageId)
const latestText = this.pendingUpdates.get(messageKey)
if (!latestText) return
const now = Date.now()
const lastEditTime = this.lastEditTimes.get(messageKey) || 0
const timeSinceLastEdit = now - lastEditTime
await this.waitForRateLimit(chatId, messageId)
if (timeSinceLastEdit < this.minInterval) {
const existingTimeout = this.updateQueue.get(messageKey)
if (existingTimeout) clearTimeout(existingTimeout)
const timeout = setTimeout(() => {
this.processUpdate(ctx, chatId, messageId, options)
}, this.minInterval - timeSinceLastEdit)
this.updateQueue.set(messageKey, timeout)
return
}
try {
if (latestText.length > this.max_msg_length) {
const chunks = this.chunkText(latestText)
const firstChunk = chunks[0]
logger.logChunk(chatId, messageId, firstChunk)
try {
await ctx.telegram.editMessageText(chatId, messageId, undefined, firstChunk, options)
} catch (error: unknown) {
if (
isTelegramError(error) &&
!error.response.description?.includes("message is not modified")
) {
throw error
}
}
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i]
const overflowMessageId = this.overflowMessages.get(messageKey)
if (overflowMessageId) {
try {
await ctx.telegram.editMessageText(chatId, overflowMessageId, undefined, chunk, options)
logger.logChunk(chatId, overflowMessageId, chunk, true)
} catch (error: unknown) {
if (
isTelegramError(error) &&
!error.response.description?.includes("message is not modified")
) {
throw error
}
}
} else {
const newMessage = await ctx.telegram.sendMessage(chatId, chunk, {
...options,
reply_to_message_id: messageId
} as any)
logger.logChunk(chatId, newMessage.message_id, chunk, true)
this.overflowMessages.set(messageKey, newMessage.message_id)
}
}
this.pendingUpdates.set(messageKey, firstChunk)
if (chunks.length > 1) {
this.pendingUpdates.set(
this.getMessageKey(chatId, this.overflowMessages.get(messageKey)!),
chunks[chunks.length - 1]
)
}
} else {
logger.logChunk(chatId, messageId, latestText)
try {
await ctx.telegram.editMessageText(chatId, messageId, undefined, latestText, options)
} catch (error: unknown) {
if (
isTelegramError(error) &&
!error.response.description?.includes("message is not modified")
) {
throw error
}
}
this.pendingUpdates.delete(messageKey)
}
this.lastEditTimes.set(messageKey, Date.now())
this.updateQueue.delete(messageKey)
} catch (error: unknown) {
if (!this.handleTelegramError(error, messageKey, options, ctx, chatId, messageId)) {
logger.logError(error)
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
}
}
}
async editMessageWithRetry(
ctx: Context,
chatId: number,
messageId: number,
text: string,
options: Record<string, unknown>
): Promise<void> {
const messageKey = this.getMessageKey(chatId, messageId)
this.pendingUpdates.set(messageKey, text)
await this.processUpdate(ctx, chatId, messageId, options)
}
}
export const rateLimiter = new RateLimiter()
function isTelegramError(error: unknown): error is { response: { description?: string, error_code?: number, parameters?: { retry_after?: number } } } {
return (
typeof error === "object" &&
error !== null &&
"response" in error &&
typeof (error as any).response === "object"
)
}

View file

@ -1,5 +0,0 @@
import { Context } from "telegraf"
export const replyToMessageId = (ctx: Context) => {
return ctx.message?.message_id
}

View file

@ -1,2 +0,0 @@
botApiUrl = "http://kowalski:3030"
databaseUrl = "postgres://kowalski:kowalski@localhost:5432/kowalski"

42
webui/.gitignore vendored
View file

@ -1,42 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>

View file

@ -1,549 +0,0 @@
import { Button } from "@/components/ui/button"
import {
Sparkles,
Users,
Download,
Brain,
Shield,
Zap,
Tv,
Heart,
Code,
Globe,
MessageSquare,
Layers,
Network,
Lock,
UserCheck,
BarChart3,
Languages,
Trash2,
FileText,
Headphones,
CloudSun,
Smartphone,
Dices,
Cat,
Music,
Bot
} from "lucide-react";
import { SiTypescript, SiPostgresql, SiDocker, SiNextdotjs, SiBun, SiForgejo } from "react-icons/si";
import { RiTelegram2Line } from "react-icons/ri";
import { BsInfoLg } from "react-icons/bs";
import { TbRocket, TbSparkles } from "react-icons/tb";
import Link from "next/link";
import { TbPalette } from "react-icons/tb";
import Footer from "@/components/footer";
export default function About() {
return (
<div className="flex flex-col min-h-screen">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<BsInfoLg className="w-10 h-10" />
</div>
</div>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
About Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Kowalski is an open-source, feature-rich Telegram bot built with modern web technologies.
From AI-powered conversations to video downloads, user management, and community features
it&apos;s designed to enhance your Telegram experience while respecting your privacy.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
<Button size="lg" className="min-w-32" asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot" target="_blank">
<SiForgejo />
View Source Code
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://p0ntus.com/services/hosting" target="_blank">
<TbRocket />
Deploy free with p0ntus
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://t.me/KowalskiNodeBot" target="_blank">
<RiTelegram2Line />
Try on Telegram
</Link>
</Button>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">Architecture</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
We&apos;ve built Kowalski with modern technologies and best practices for reliability and maintainability.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<Code className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Tech Stack</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is built completely in TypeScript with Node.js and Telegraf.
The web interface uses Next.js with Tailwind CSS, while data persistence is handled by PostgreSQL with Drizzle ORM.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiTypescript className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">TypeScript + Node.js</div>
<div className="text-sm text-muted-foreground">Type-safe backend w/ Telegraf</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiNextdotjs className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Next.js WebUI</div>
<div className="text-sm text-muted-foreground">Modern, responsive admin and user panel</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiPostgresql className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">PostgreSQL + Drizzle ORM</div>
<div className="text-sm text-muted-foreground">Reliable data persistence</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-green-500/10 text-green-500">
<SiDocker className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Deployment</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is built to be deployed anywhere, and has been tested on multiple platforms.
We prioritize support for Docker and Bun for easy deployment.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<SiDocker className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Docker Support</div>
<div className="text-sm text-muted-foreground">Easy containerized deployment w/ Docker Compose</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<SiBun className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Bun</div>
<div className="text-sm text-muted-foreground">A fast JavaScript runtime for best performance</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Layers className="w-5 h-5 mx-3 text-green-500" />
<div> {/* some ppl probably don't know what af means :( */}
<div className="font-medium">Modular AF Backend</div>
<div className="text-sm text-muted-foreground">Command-based structure for easy feature addition</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">AI Integrations</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Powered by Ollama, Kowalski has support for 50+ AI models, with customizable
options for users and admins.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-500/10 text-purple-500">
<Brain className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Vast Model Support</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski has support for 50+ models, both thinking and non-thinking. We have
good Markdown parsing, with customizable options for both users and admins.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<TbSparkles className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/ask - Quick Responses</div>
<div className="text-sm text-muted-foreground">Fast answers using smaller non-thinking models</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Brain className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/think - Deep Reasoning</div>
<div className="text-sm text-muted-foreground">Advanced thinking models with togglable reasoning visibility</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Bot className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/ai - Your Custom Model!</div>
<div className="text-sm text-muted-foreground">Use your personally configured AI model</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-orange-500/10 text-orange-500">
<Zap className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Kowalski&apos;s <span className="italic">Powerful</span></h3>
</div>
<p className="text-muted-foreground leading-relaxed">
We have amazing Markdown V2 parsing, queue management, and usage statistics tracking.
It&apos;s hella private, too. AI is disabled by default for the best user experience.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Network className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Streaming</div>
<div className="text-sm text-muted-foreground">Real-time Markdown V2 message updates as the model generates</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<BarChart3 className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Usage Stats</div>
<div className="text-sm text-muted-foreground">Track your AI requests and usage with /aistats</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<UserCheck className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Queues</div>
<div className="text-sm text-muted-foreground">High usage limits with intelligent request queuing</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">We&apos;re User-First</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Kowalski has privacy-focused user management with customizable settings,
multilingual support, and transparent data handling.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-emerald-500/10 text-emerald-500">
<Lock className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Privacy</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
User data is minimized and linked only by Telegram ID. No personal information
is shared with third parties, and users maintain full control over their data
with easy account deletion options.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Shield className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Limited Data Collection</div>
<div className="text-sm text-muted-foreground">Only essential data is stored, linked by Telegram ID</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<FileText className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Transparent Policies</div>
<div className="text-sm text-muted-foreground">Clear privacy policy accessible via /privacy</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Trash2 className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Easy Account Deletion</div>
<div className="text-sm text-muted-foreground">You can delete your data at any time</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<TbPalette className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Customization</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Personalize your experience with custom AI preferences,
temperature settings, language selection, and detailed usage statistics.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Bot className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">AI Preferences</div>
<div className="text-sm text-muted-foreground">Choose default models and configure temperature</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Languages className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Multilingual Support</div>
<div className="text-sm text-muted-foreground">English and Portuguese language options</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<BarChart3 className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Usage Analytics</div>
<div className="text-sm text-muted-foreground">Personal statistics and usage tracking</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">There&apos;s <span className="text-5xl">WAYYYYY</span> more!</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Beyond AI, Kowalski has a ton of entertainment, utility, fun, configuration, and information
commands.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-red-500/10 text-red-500">
<Download className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Media Downloads</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Download videos from YouTube and 1000s of other platforms using yt-dlp.
Featuring automatic size checking for Telegram&apos;.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Tv className="w-4 h-4 text-red-500" />
<span>/yt [URL] - Video downloads</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Shield className="w-4 h-4 text-red-500" />
<span>Automatic size limit handling</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-blue-500/10 text-blue-500">
<Globe className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Information & Utilities</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Access real-world information like weather reports, device specifications,
HTTP status codes, and a Last.fm music integration.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<CloudSun className="w-4 h-4 text-blue-500" />
<span>/weather - Weather reports</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Smartphone className="w-4 h-4 text-blue-500" />
<span>/device - GSMArena specs</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Headphones className="w-4 h-4 text-blue-500" />
<span>/last - Last.fm integration</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-green-500/10 text-green-500">
<Heart className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Entertainment</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Interactive emojis, random animal pictures, My Little Pony,
and fun commands to engage you and your community.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Dices className="w-4 h-4 text-green-500" />
<span>/dice, /slot - Interactive games</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Cat className="w-4 h-4 text-green-500" />
<span>/cat, /dog - Random animals</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Music className="w-4 h-4 text-green-500" />
<span>/mlp - My Little Pony DB</span>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">Our Community</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Kowalski is built by developers, for developers. We use open licenses and
take input from our development communities.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-500/10 text-purple-500">
<SiForgejo className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Open Development</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is licensed under BSD-3-Clause with components under Unlicense. Our
codebase is available on our Forgejo and GitHub, with lots of documentation.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiForgejo className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Public Code</div>
<div className="text-sm text-muted-foreground">Feel free to contribute or review our code</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<FileText className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Documentation</div>
<div className="text-sm text-muted-foreground">We have documentation to help contributors, users, and admins</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Users className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Contributor Friendly</div>
<div className="text-sm text-muted-foreground">Our communities are welcoming to new contributors</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-orange-500/10 text-orange-500">
<Heart className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Community Centric</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski was created by Lucas Gabriel (lucmsilva). It is now also maintained by ihatenodejs,
givfnz2, and other contributors. Thank you to all of our contributors!
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<MessageSquare className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Active Maintenance</div>
<div className="text-sm text-muted-foreground">Regular updates and fixes w/ room for input and feedback</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Code className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Quality Code</div>
<div className="text-sm text-muted-foreground">We use TypeScript, linting, and modern standards</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Sparkles className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Focus on New Features</div>
<div className="text-sm text-muted-foreground">We are always looking for new features to add</div>
</div>
</div>
</div>
</div>
</div>
<div className="mt-16 text-center">
<div className="inline-flex items-center gap-4 p-6 rounded-lg bg-muted/50 border">
<div className="flex items-center gap-2">
<SiForgejo className="w-5 h-5" />
<span className="font-medium">Ready to contribute?</span>
</div>
<Button asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot" target="_blank">
<SiForgejo />
View on Forgejo
</Link>
</Button>
</div>
</div>
</div>
</section>
<Footer />
</div>
);
}

View file

@ -1,204 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Trash2, ArrowLeft, AlertTriangle } from "lucide-react";
import Link from "next/link";
import { useAuth } from "@/contexts/auth-context";
import { motion } from "framer-motion";
export default function DeleteAccountPage() {
const [isDeleting, setIsDeleting] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const { user, isAuthenticated, loading } = useAuth();
const router = useRouter();
const handleDeleteAccount = async () => {
if (!user) return;
setIsDeleting(true);
try {
const response = await fetch('/api/user/delete', {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
alert('Your account has been deleted. You will now be redirected to the home page. Thanks for using Kowalski!');
window.location.href = '/';
} else {
const error = await response.json();
alert(`Failed to delete account: ${error.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error deleting account:', error);
alert('An error occurred while deleting your account. Please try again.');
} finally {
setIsDeleting(false);
setDialogOpen(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!isAuthenticated) {
router.push('/login');
return null;
}
return (
<div className="w-full h-full bg-background">
<div className="container mx-auto px-6 py-8 max-w-2xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="flex items-center gap-4 mb-8">
<Button variant="outline" size="sm" asChild>
<Link href="/account">
<ArrowLeft className="w-4 h-4" />
Back to Account
</Link>
</Button>
</div>
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
<Trash2 className="w-8 h-8 text-red-600" />
</div>
<div>
<h1 className="text-3xl font-bold">Delete Account</h1>
<p className="text-muted-foreground">Permanently remove your account and data</p>
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
<div className="flex items-start gap-3">
<AlertTriangle className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="font-semibold text-yellow-800 dark:text-yellow-200">
This action cannot be undone
</h3>
<p className="text-sm text-yellow-700 dark:text-yellow-300">
Deleting your account will permanently remove all your data, including:
</p>
<ul className="text-sm text-yellow-700 dark:text-yellow-300 list-disc list-inside space-y-1 ml-2">
<li>Your user profile and settings</li>
<li>AI usage statistics and request history</li>
<li>Custom AI model preferences</li>
<li>Command configuration and disabled commands</li>
<li>All associated sessions and authentication data</li>
</ul>
</div>
</div>
</div>
<div className="bg-card border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">Account Information</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Username:</span>
<span className="font-medium">@{user?.username}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Name:</span>
<span className="font-medium">{user?.firstName} {user?.lastName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Telegram ID:</span>
<span className="font-medium">{user?.telegramId}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">AI Requests:</span>
<span className="font-medium">{user?.aiRequests.toLocaleString()}</span>
</div>
</div>
</div>
<div className="pt-6 border-t">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Ready to delete your account?</h3>
<p className="text-sm text-muted-foreground">
This will immediately and permanently delete your account.
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="destructive" className="gap-2">
<Trash2 className="w-4 h-4" />
Delete Account
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-600" />
Confirm Account Deletion
</DialogTitle>
<DialogDescription className="space-y-2">
<p>
Are you absolutely sure you want to delete your account? This action cannot be undone.
</p>
<p className="font-medium">
Your account <span className="font-bold">@{user?.username}</span> and all associated data will be permanently removed.
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteAccount}
disabled={isDeleting}
className="gap-2"
>
{isDeleting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="w-4 h-4" />
Yes, Delete Account
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</motion.div>
</div>
</div>
);
}

View file

@ -1,725 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
User,
Bot,
Brain,
Settings,
CloudSun,
Smartphone,
Heart,
Cat,
Dices,
Thermometer,
BarChart3,
LogOut,
Edit3,
Save,
X,
Network,
Cpu,
Languages,
Bug,
Lightbulb,
ExternalLink,
Quote,
Info,
Shuffle,
Rainbow,
Database,
Hash,
Download,
Archive
} from "lucide-react";
import { RiTelegram2Line } from "react-icons/ri";
import { motion } from "framer-motion";
import { ModelPicker } from "@/components/account/model-picker";
import { useAuth } from "@/contexts/auth-context";
import { FaLastfm } from "react-icons/fa";
import { TiInfinity } from "react-icons/ti";
interface CommandCard {
id: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
title: string;
description: string;
commands: string[];
category: "ai" | "entertainment" | "utility" | "media" | "admin" | "animals";
gradient: string;
enabled: boolean;
}
const allCommands: CommandCard[] = [
{
id: "ai-ask-think",
icon: Brain,
title: "AI Chats",
description: "Chat with AI models and use deep thinking",
commands: ["/ask", "/think"],
category: "ai",
gradient: "from-purple-500 to-pink-500",
enabled: true
},
{
id: "ai-custom",
icon: Bot,
title: "Custom AI Model",
description: "Use your personally configured AI model",
commands: ["/ai"],
category: "ai",
gradient: "from-indigo-500 to-purple-500",
enabled: true
},
{
id: "ai-stats",
icon: BarChart3,
title: "AI Statistics",
description: "View your AI usage statistics",
commands: ["/aistats"],
category: "ai",
gradient: "from-purple-600 to-indigo-600",
enabled: true
},
{
id: "games-dice",
icon: Dices,
title: "Interactive Emojis",
description: "Roll dice, play slots, and other interactive emojis",
commands: ["/dice", "/slot", "/ball", "/dart", "/bowling"],
category: "entertainment",
gradient: "from-green-500 to-teal-500",
enabled: true
},
{
id: "fun-random",
icon: Shuffle,
title: "Fun Commands",
description: "Random numbers and fun responses",
commands: ["/random", "/furry", "/gay"],
category: "entertainment",
gradient: "from-pink-500 to-rose-500",
enabled: true
},
{
id: "infinite-dice",
icon: TiInfinity,
title: "Infinite Dice",
description: "Sends an infinite dice sticker",
commands: ["/idice"],
category: "entertainment",
gradient: "from-yellow-500 to-orange-500",
enabled: true
},
{
id: "animals-basic",
icon: Cat,
title: "Animal Pictures",
description: "Get random cute animal pictures",
commands: ["/cat", "/dog", "/duck", "/fox"],
category: "animals",
gradient: "from-orange-500 to-red-500",
enabled: true
},
{
id: "soggy-cat",
icon: Heart,
title: "Soggy Cat",
description: "Wet cats!",
commands: ["/soggy", "/soggycat"],
category: "animals",
gradient: "from-blue-500 to-purple-500",
enabled: true
},
{
id: "weather",
icon: CloudSun,
title: "Weather",
description: "Get current weather for any location",
commands: ["/weather", "/clima"],
category: "utility",
gradient: "from-blue-500 to-cyan-500",
enabled: true
},
{
id: "device-specs",
icon: Smartphone,
title: "Device Specifications",
description: "Look up phone specifications via GSMArena",
commands: ["/device", "/d"],
category: "utility",
gradient: "from-slate-500 to-gray-500",
enabled: true
},
{
id: "http-status",
icon: Network,
title: "HTTP Status Codes",
description: "Look up HTTP status codes and meanings",
commands: ["/http", "/httpcat"],
category: "utility",
gradient: "from-emerald-500 to-green-500",
enabled: true
},
{
id: "codename-lookup",
icon: Hash,
title: "Codename Lookup",
description: "Look up codenames and meanings",
commands: ["/codename", "/whatis"],
category: "utility",
gradient: "from-teal-500 to-cyan-500",
enabled: true
},
{
id: "info-commands",
icon: Info,
title: "Information",
description: "Get chat and user information",
commands: ["/chatinfo", "/userinfo"],
category: "utility",
gradient: "from-indigo-500 to-blue-500",
enabled: true
},
{
id: "quotes",
icon: Quote,
title: "Random Quotes",
description: "Get random quotes",
commands: ["/quote"],
category: "utility",
gradient: "from-amber-500 to-yellow-500",
enabled: true
},
{
id: "youtube-download",
icon: Download,
title: "Video Downloads",
description: "Download videos from YouTube and 1000+ platforms",
commands: ["/yt", "/ytdl", "/video", "/dl"],
category: "media",
gradient: "from-red-500 to-pink-500",
enabled: true
},
{
id: "lastfm",
icon: FaLastfm,
title: "Last.fm Integration",
description: "Connect your music listening history",
commands: ["/last", "/lfm", "/setuser"],
category: "media",
gradient: "from-violet-500 to-purple-500",
enabled: true
},
{
id: "mlp-content",
icon: Database,
title: "MLP Database",
description: "My Little Pony content and information",
commands: ["/mlp", "/mlpchar", "/mlpep", "/mlpcomic"],
category: "media",
gradient: "from-fuchsia-500 to-pink-500",
enabled: true
},
{
id: "modarchive",
icon: Archive,
title: "Mod Archive",
description: "Access classic tracker music files",
commands: ["/modarchive", "/tma"],
category: "media",
gradient: "from-cyan-500 to-blue-500",
enabled: true
},
{
id: "random-pony",
icon: Rainbow,
title: "Random Pony Art",
description: "Get random My Little Pony artwork",
commands: ["/rpony", "/randompony", "/mlpart"],
category: "media",
gradient: "from-pink-500 to-purple-500",
enabled: true
},
];
const categoryColors = {
ai: "bg-purple-500/10 text-purple-600 border-purple-200 dark:border-purple-800",
entertainment: "bg-green-500/10 text-green-600 border-green-200 dark:border-green-800",
utility: "bg-blue-500/10 text-blue-600 border-blue-200 dark:border-blue-800",
media: "bg-red-500/10 text-red-600 border-red-200 dark:border-red-800",
admin: "bg-orange-500/10 text-orange-600 border-orange-200 dark:border-orange-800",
animals: "bg-emerald-500/10 text-emerald-600 border-emerald-200 dark:border-emerald-800"
};
const languageOptions = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'pt', name: 'Português', flag: '🇧🇷' },
];
export default function AccountPage() {
const [editingTemp, setEditingTemp] = useState(false);
const [tempValue, setTempValue] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [reportTab, setReportTab] = useState("bug");
const [commands, setCommands] = useState<CommandCard[]>(allCommands);
const { user, loading, logout, refreshUser } = useAuth();
useEffect(() => {
if (user) {
setTempValue(user.aiTemperature.toString());
setCommands(allCommands.map(cmd => ({
...cmd,
enabled: !user.disabledCommands.includes(cmd.id)
})));
}
}, [user]);
const updateSetting = async (setting: string, value: boolean | number | string) => {
try {
const response = await fetch('/api/user/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ [setting]: value }),
credentials: 'include'
});
if (response.ok) {
await refreshUser();
}
} catch (error) {
console.error('Error updating setting:', error);
}
};
const saveTemperature = () => {
const temp = parseFloat(tempValue);
if (temp >= 0.1 && temp <= 2.0) {
updateSetting('aiTemperature', temp);
setEditingTemp(false);
}
};
const toggleCommand = async (commandId: string) => {
if (!user) return;
const commandToToggle = commands.find(cmd => cmd.id === commandId);
if (!commandToToggle) return;
const newEnabledState = !commandToToggle.enabled;
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: newEnabledState } : cmd
));
try {
let newDisabledCommands: string[];
if (newEnabledState) {
newDisabledCommands = user.disabledCommands.filter(id => id !== commandId);
} else {
newDisabledCommands = [...user.disabledCommands, commandId];
}
const response = await fetch('/api/user/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ disabledCommands: newDisabledCommands }),
credentials: 'include'
});
if (response.ok) {
await refreshUser();
} else {
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: !newEnabledState } : cmd
));
console.error('Failed to update command state');
}
} catch (error) {
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: !newEnabledState } : cmd
));
console.error('Error updating command state:', error);
}
};
const filteredCommands = selectedCategory
? commands.filter(cmd => cmd.category === selectedCategory)
: commands;
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!user) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Authentication Required</h1>
<Button onClick={() => window.location.href = '/login'}>
Go to Login
</Button>
</div>
</div>
);
}
return (
<div className="w-full min-h-screen bg-background">
<div className="container mx-auto px-6 py-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-primary/10 items-center justify-center hidden md:flex">
<User className="w-8 h-8 text-primary" />
</div>
<div>
<h1 className="text-3xl font-bold">Welcome back, {user.firstName}!</h1>
<p className="text-muted-foreground">@{user.username}</p>
</div>
</div>
<Button variant="outline" onClick={logout} className="gap-2">
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-purple-500/10 to-pink-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<div className="flex items-center gap-3 mb-4">
<BarChart3 className="w-8 h-8 text-purple-600" />
<h3 className="text-xl font-semibold">AI Usage</h3>
</div>
<div className="space-y-2">
<p className="text-2xl font-bold">{user.aiRequests}</p>
<p className="text-sm text-muted-foreground">Total AI Requests</p>
<p className="text-lg">{user.aiCharacters.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">Characters Generated</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-blue-500/10 to-cyan-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<div className="flex items-center gap-3 mb-4">
<Settings className="w-8 h-8 text-blue-600" />
<h3 className="text-xl font-semibold">AI Settings</h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">AI Enabled</span>
<Button
size="sm"
variant={user.aiEnabled ? "default" : "outline"}
onClick={() => updateSetting('aiEnabled', !user.aiEnabled)}
className="h-8 px-3"
>
{user.aiEnabled ? "ON" : "OFF"}
</Button>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Show Thinking</span>
<Button
size="sm"
variant={user.showThinking ? "default" : "outline"}
onClick={() => updateSetting('showThinking', !user.showThinking)}
className="h-8 px-3"
>
{user.showThinking ? "ON" : "OFF"}
</Button>
</div>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-green-500/10 to-emerald-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
>
<div className="flex items-center gap-3 mb-4">
<Thermometer className="w-8 h-8 text-green-600" />
<h3 className="text-xl font-semibold">Temperature</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
{editingTemp ? (
<>
<Input
type="number"
min="0.1"
max="2.0"
step="0.1"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
className="h-8 w-20"
/>
<Button size="sm" onClick={saveTemperature} className="h-8 w-8 p-0">
<Save className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => setEditingTemp(false)} className="h-8 w-8 p-0">
<X className="w-4 h-4" />
</Button>
</>
) : (
<>
<span className="text-2xl font-bold">{user.aiTemperature}</span>
<Button size="sm" variant="outline" onClick={() => setEditingTemp(true)} className="h-8 w-8 p-0">
<Edit3 className="w-4 h-4" />
</Button>
</>
)}
</div>
<p className="text-xs text-muted-foreground">Controls randomness in AI responses. Lower values (0.1-0.5) = more focused, higher values (0.7-2.0) = more creative.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-teal-500/10 to-cyan-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
>
<div className="flex items-center gap-3 mb-4">
<Languages className="w-8 h-8 text-teal-600" />
<h3 className="text-xl font-semibold">Language Options</h3>
</div>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-2">
{languageOptions.map((lang) => (
<Button
key={lang.code}
variant={user.languageCode === lang.code ? "default" : "outline"}
onClick={() => updateSetting('languageCode', lang.code)}
className="justify-start gap-3 h-10"
>
<span className="text-lg">{lang.flag}</span>
<span>{lang.name}</span>
</Button>
))}
</div>
<p className="text-xs text-muted-foreground">Choose your preferred language for bot responses and interface text.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-indigo-500/10 to-violet-500/10 col-span-1 md:col-span-2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.5 }}
>
<div className="flex items-center gap-3 mb-4">
<Cpu className="w-8 h-8 text-indigo-600" />
<h3 className="text-xl font-semibold">My Model</h3>
</div>
<div className="space-y-3">
<ModelPicker
value={user.customAiModel}
onValueChange={(newModel) => updateSetting('customAiModel', newModel)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">Your selected AI model for custom /ai commands. Different models have varying capabilities, speeds, and response styles.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-orange-500/10 to-red-500/10 col-span-1 md:col-span-2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.6 }}
>
<div className="flex items-center gap-3 mb-4">
<Bug className="w-8 h-8 text-orange-600" />
<h3 className="text-xl font-semibold">Report An Issue</h3>
</div>
<div className="space-y-4">
<Tabs value={reportTab} onValueChange={setReportTab}>
<TabsList className="grid w-full grid-cols-2 gap-2">
<TabsTrigger value="bug" className="gap-2">
<Bug className="w-4 h-4" />
Bug Report
</TabsTrigger>
<TabsTrigger value="feature" className="gap-2">
<Lightbulb className="w-4 h-4" />
Feature Request
</TabsTrigger>
</TabsList>
<div className="mt-4">
<TabsContent value="bug" className="space-y-12">
<p className="text-sm text-muted-foreground">Found a bug or issue? Report it to help us improve Kowalski.</p>
<Button asChild className="w-full gap-2">
<a
href="https://libre-cloud.atlassian.net/jira/software/c/form/4a535b59-dc7e-4b55-b905-a79ff831928e?atlOrigin=eyJpIjoiNzQwYTcxZDdmMjJkNDljNzgzNTY2MjliYjliMjMzMDkiLCJwIjoiaiJ9"
target="_blank"
rel="noopener noreferrer"
>
<Bug className="w-4 h-4" />
Report Bug
<ExternalLink className="w-4 h-4" />
</a>
</Button>
</TabsContent>
<TabsContent value="feature" className="space-y-12">
<p className="text-sm text-muted-foreground">Have an idea for a new feature? Let us know what you&apos;d like to see!</p>
<Button asChild className="w-full gap-2">
<a
href="https://libre-cloud.atlassian.net/jira/software/c/form/5ce1e6e9-9618-4b46-94ee-122e7bde2ba1?atlOrigin=eyJpIjoiZjMwZTc3MDVlY2MwNDBjODliYWNhMTgzN2ZjYzI5MDAiLCJwIjoiaiJ9"
target="_blank"
rel="noopener noreferrer"
>
<Lightbulb className="w-4 h-4" />
Request Feature
<ExternalLink className="w-4 h-4" />
</a>
</Button>
</TabsContent>
</div>
</Tabs>
</div>
</motion.div>
</div>
<motion.div
className="mb-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.7 }}
>
<h2 className="text-2xl font-bold mb-4">Command Management</h2>
<div className="flex flex-wrap gap-2">
<Button
variant={selectedCategory === null ? "default" : "outline"}
onClick={() => setSelectedCategory(null)}
className="mb-2"
>
All Commands
</Button>
{Object.entries(categoryColors).map(([category, colorClass]) => (
<Button
key={category}
variant={selectedCategory === category ? "default" : "outline"}
onClick={() => setSelectedCategory(category)}
className={`mb-2 capitalize ${selectedCategory === category ? '' : colorClass}`}
>
{category === "ai" ? "AI" : category}
</Button>
))}
</div>
</motion.div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.8 }}
>
{filteredCommands.map((command) => (
<div
key={command.id}
className={`p-4 rounded-lg border transition-all duration-200 ${
command.enabled
? 'bg-card hover:shadow-md shadow-sm'
: 'bg-muted/30 border-muted-foreground/20'
}`}
>
<div className="flex items-start justify-between mb-3">
<div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${command.gradient} flex items-center justify-center ${
command.enabled ? '' : 'grayscale opacity-50'
}`}>
<command.icon className="w-5 h-5 text-white" />
</div>
<div className="flex items-center space-x-2">
<div
className={`w-11 h-6 rounded-full cursor-pointer transition-colors duration-200 ${
command.enabled
? 'bg-green-500 dark:bg-green-600'
: 'bg-gray-300 dark:bg-gray-600'
}`}
onClick={() => toggleCommand(command.id)}
>
<div className={`w-5 h-5 rounded-full shadow-sm transition-transform duration-200 ${
command.enabled
? 'translate-x-5 bg-white dark:bg-gray-100'
: 'translate-x-0.5 bg-white dark:bg-gray-200'
} mt-0.5`} />
</div>
</div>
</div>
<h3 className={`text-base font-semibold mb-2 ${command.enabled ? '' : 'text-muted-foreground'}`}>
{command.title}
</h3>
<p className={`text-sm mb-3 ${command.enabled ? 'text-muted-foreground' : 'text-muted-foreground/60'}`}>
{command.description}
</p>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{command.commands.slice(0, 2).map((cmd, idx) => (
<code key={idx} className={`px-1.5 py-0.5 rounded text-xs ${
command.enabled
? 'bg-muted text-foreground'
: 'bg-muted-foreground/10 text-muted-foreground/60'
}`}>
{cmd}
</code>
))}
{command.commands.length > 2 && (
<span className={`text-xs ${command.enabled ? 'text-muted-foreground' : 'text-muted-foreground/60'}`}>
+{command.commands.length - 2}
</span>
)}
</div>
<div className={`px-2 py-1 rounded-full text-xs border ${
command.enabled
? categoryColors[command.category]
: 'bg-muted-foreground/10 text-muted-foreground/60 border-muted-foreground/20'
}`}>
{command.category === "ai" ? "AI" : command.category}
</div>
</div>
</div>
))}
</motion.div>
<motion.div
className="mt-12 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 1.0 }}
>
<div className="inline-flex items-center gap-16 p-6 px-8 rounded-lg bg-muted/50 border">
<span className="font-medium">Ready to start using Kowalski?</span>
<Button asChild>
<a href="https://t.me/KowalskiNodeBot" target="_blank" rel="noopener noreferrer">
<RiTelegram2Line />
Open on Telegram
</a>
</Button>
</div>
</motion.div>
</div>
</div>
);
}

View file

@ -1,34 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { invalidateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function POST(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (sessionToken) {
await invalidateSession(sessionToken);
}
const response = NextResponse.json({ success: true });
response.cookies.set(SESSION_COOKIE_NAME, '', {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
expires: new Date(0),
path: "/",
});
return response;
} catch (error) {
console.error("Error in logout API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -1,91 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import * as schema from "@/lib/schema";
import { db } from "@/lib/db";
export async function POST(request: NextRequest) {
try {
const requestContentType = request.headers.get('content-type');
if (!requestContentType || !requestContentType.includes('application/json')) {
return NextResponse.json({ success: false, error: "Invalid content type" }, { status: 400 });
}
const body = await request.json();
const { username } = body;
if (!username) {
return NextResponse.json({ success: false, error: "Username is required" }, { status: 400 });
}
if (typeof username !== 'string' || username.length < 3 || username.length > 32) {
return NextResponse.json({ success: false, error: "Invalid username format" }, { status: 400 });
}
const cleanUsername = username.replace('@', '');
const user = await db.query.usersTable.findFirst({
where: eq(schema.usersTable.username, cleanUsername),
columns: {
telegramId: true,
username: true,
},
});
if (!user) {
const botUsername = process.env.botUsername || "KowalskiNodeBot";
return NextResponse.json({ success: false, error: `Please DM @${botUsername} before signing in.` }, { status: 404 });
}
const botApiUrl = process.env.botApiUrl || "http://kowalski:3030";
const fullUrl = `${botApiUrl}/2fa/get`;
const botApiResponse = await fetch(fullUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId: user.telegramId }),
});
if (!botApiResponse.ok) {
const errorText = await botApiResponse.text();
console.error("Bot API error response:", errorText);
return NextResponse.json({
success: false,
error: `Bot API error: ${botApiResponse.status} - ${errorText.slice(0, 200)}`
}, { status: 500 });
}
const contentType = botApiResponse.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
const errorText = await botApiResponse.text();
console.error("Bot API returned non-JSON:", errorText.slice(0, 200));
return NextResponse.json({
success: false,
error: "Bot API returned invalid response format"
}, { status: 500 });
}
const botApiResult = await botApiResponse.json();
if (!botApiResult.generated) {
return NextResponse.json({
success: false,
error: botApiResult.error || "Failed to send 2FA code"
}, { status: 500 });
}
return NextResponse.json({
success: true,
message: "2FA code sent successfully",
userId: user.telegramId
});
} catch (error) {
console.error("Error in username API:", error);
return NextResponse.json({
success: false,
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -1,107 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { eq, and, gt } from "drizzle-orm";
import * as schema from "@/lib/schema";
import { db } from "@/lib/db";
import { createSession, getSessionCookieOptions } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function POST(request: NextRequest) {
try {
const contentType = request.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return NextResponse.json({
success: false,
error: "Invalid content type"
}, { status: 400 });
}
const body = await request.json();
const { userId, code } = body;
if (!userId || !code) {
return NextResponse.json({
success: false,
error: "User ID and code are required"
}, { status: 400 });
}
if (typeof userId !== 'string' || typeof code !== 'string') {
return NextResponse.json({
success: false,
error: "Invalid input format"
}, { status: 400 });
}
if (!/^\d{6}$/.test(code)) {
return NextResponse.json({
success: false,
error: "Invalid code format"
}, { status: 400 });
}
const twoFactorRecord = await db.query.twoFactorTable.findFirst({
where: and(
eq(schema.twoFactorTable.userId, userId),
gt(schema.twoFactorTable.codeExpiresAt, new Date())
),
});
if (!twoFactorRecord) {
return NextResponse.json({
success: false,
error: "No valid 2FA code found or code has expired"
}, { status: 404 });
}
if (twoFactorRecord.codeAttempts >= 5) {
await db.delete(schema.twoFactorTable)
.where(eq(schema.twoFactorTable.userId, userId));
return NextResponse.json({
success: false,
error: "Too many failed attempts. Please request a new code."
}, { status: 429 });
}
if (twoFactorRecord.currentCode !== code) {
await db.update(schema.twoFactorTable)
.set({
codeAttempts: twoFactorRecord.codeAttempts + 1,
updatedAt: new Date()
})
.where(eq(schema.twoFactorTable.userId, userId));
console.log(`2FA verification failed for user: ${userId}, attempts: ${twoFactorRecord.codeAttempts + 1}`);
return NextResponse.json({
success: false,
error: "Invalid 2FA code"
}, { status: 401 });
}
const session = await createSession(userId);
await db.delete(schema.twoFactorTable)
.where(eq(schema.twoFactorTable.userId, userId));
console.log("2FA verification successful for user:", userId);
const response = NextResponse.json({
success: true,
message: "2FA verification successful",
redirectTo: "/account",
sessionToken: session.sessionToken
});
const cookieOptions = getSessionCookieOptions();
response.cookies.set(SESSION_COOKIE_NAME, session.sessionToken, cookieOptions);
return response;
} catch (error) {
console.error("Error in verify API:", error);
return NextResponse.json({
success: false,
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -1,59 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
import { db } from "@/lib/db";
import { usersTable, sessionsTable, twoFactorTable } from "@/lib/schema";
import { eq } from "drizzle-orm";
export async function DELETE(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const userId = sessionData.user.telegramId;
await db.transaction(async (tx) => {
await tx.delete(sessionsTable)
.where(eq(sessionsTable.userId, userId));
await tx.delete(twoFactorTable)
.where(eq(twoFactorTable.userId, userId));
await tx.delete(usersTable)
.where(eq(usersTable.telegramId, userId));
});
const response = NextResponse.json({
success: true,
message: "Account deleted successfully"
});
response.cookies.set(SESSION_COOKIE_NAME, '', {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
expires: new Date(0),
path: "/",
});
return response;
} catch (error) {
console.error("Error deleting account:", error);
return NextResponse.json({
error: "Failed to delete account"
}, { status: 500 });
}
}

View file

@ -1,46 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function GET(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const { user } = sessionData;
const sanitizedUser = {
telegramId: user.telegramId,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
aiEnabled: user.aiEnabled,
showThinking: user.showThinking,
customAiModel: user.customAiModel,
aiTemperature: user.aiTemperature,
aiRequests: user.aiRequests,
aiCharacters: user.aiCharacters,
disabledCommands: user.disabledCommands,
languageCode: user.languageCode,
};
return NextResponse.json(sanitizedUser);
} catch (error) {
console.error("Error in profile API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -1,103 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
import { db } from "@/lib/db";
import * as schema from "@/lib/schema";
interface UserUpdates {
aiEnabled?: boolean;
showThinking?: boolean;
customAiModel?: string;
aiTemperature?: number;
disabledCommands?: string[];
languageCode?: string;
updatedAt?: Date;
}
export async function PATCH(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const contentType = request.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return NextResponse.json({ error: "Invalid content type" }, { status: 400 });
}
const updates = await request.json();
const userId = sessionData.user.telegramId;
if (!updates || typeof updates !== 'object') {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}
const allowedFields = [
'aiEnabled',
'showThinking',
'customAiModel',
'aiTemperature',
'disabledCommands',
'languageCode'
];
const filteredUpdates: UserUpdates = {};
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
if (key === 'aiEnabled' || key === 'showThinking') {
filteredUpdates[key] = Boolean(value);
} else if (key === 'aiTemperature') {
const temp = Number(value);
if (temp >= 0.1 && temp <= 2.0) {
filteredUpdates[key] = temp;
} else {
return NextResponse.json({ error: "Temperature must be between 0.1 and 2.0" }, { status: 400 });
}
} else if (key === 'customAiModel' || key === 'languageCode') {
if (typeof value === 'string' && value.length > 0 && value.length < 100) {
filteredUpdates[key] = value;
} else {
return NextResponse.json({ error: `Invalid ${key}` }, { status: 400 });
}
} else if (key === 'disabledCommands') {
if (Array.isArray(value) && value.every(item => typeof item === 'string' && item.length < 50) && value.length < 100) {
filteredUpdates[key] = value;
} else {
return NextResponse.json({ error: "Invalid disabled commands" }, { status: 400 });
}
}
}
}
if (Object.keys(filteredUpdates).length === 0) {
return NextResponse.json({ error: "No valid updates provided" }, { status: 400 });
}
filteredUpdates.updatedAt = new Date();
await db.update(schema.usersTable)
.set(filteredUpdates)
.where(eq(schema.usersTable.telegramId, userId));
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error in settings API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -1,126 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
body {
font-family: var(--font-sora);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -1,54 +0,0 @@
import type { Metadata } from "next";
import { Sora } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/providers";
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar";
import { AuthProvider } from "@/contexts/auth-context";
import { HeaderAuth } from "@/components/header-auth";
const sora = Sora({
variable: "--font-sora",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Kowalski",
description: "A powerful, multi-function Telegram bot",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning className="scroll-smooth">
<body className={`${sora.variable} antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<AuthProvider>
<SidebarProvider>
<AppSidebar />
<SidebarInset className="h-[calc(100vh-16px)] overflow-hidden rounded-lg border bg-background flex flex-col">
<header className="flex h-16 shrink-0 items-center gap-2 px-4 border-b bg-background">
<SidebarTrigger className="-ml-1" />
<div className="ml-auto">
<HeaderAuth />
</div>
</header>
<main className="flex-1 overflow-auto scroll-smooth">
{children}
</main>
</SidebarInset>
</SidebarProvider>
</AuthProvider>
</ThemeProvider>
</body>
</html>
);
}

View file

@ -1,311 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { RiTelegram2Line } from "react-icons/ri";
import { TbLoader } from "react-icons/tb";
import { useState, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
export const dynamic = 'force-dynamic'
type FormStep = "username" | "twofa";
type VerifyResponse = {
success: boolean;
message?: string;
redirectTo?: string;
sessionToken?: string;
error?: string;
};
const buttonVariants = {
initial: { scale: 1 },
tap: { scale: 0.98 },
};
function LoginForm() {
const [step, setStep] = useState<FormStep>("username");
const [username, setUsername] = useState("");
const [twoFaCode, setTwoFaCode] = useState("");
const [userId, setUserId] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const searchParams = useSearchParams();
const returnTo = searchParams.get('returnTo') || '/account';
const handleUsernameSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
setIsLoading(true);
setError("");
try {
const response = await fetch("/api/auth/username", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: username.trim() }),
});
const result = await response.json();
if (result.success) {
setUserId(result.userId);
setStep("twofa");
} else {
setError(result.error || "Failed to find user");
}
} catch (err) {
console.error("Username submission error:", err);
setError("Network error. Please try again.");
} finally {
setIsLoading(false);
}
};
const handleTwoFaSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!twoFaCode.trim() || twoFaCode.length !== 6) return;
setIsLoading(true);
setError("");
try {
const response = await fetch("/api/auth/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, code: twoFaCode }),
});
const result: VerifyResponse = await response.json();
if (result.success) {
const redirectTo = result.redirectTo || returnTo;
if (result.sessionToken) {
try {
localStorage.setItem('kowalski-session', result.sessionToken);
} catch (storageError) {
console.error('localStorage error:', storageError);
}
}
window.location.href = redirectTo;
} else {
setError(result.error || "Invalid 2FA code");
}
} catch (err) {
console.error("2FA verification error:", err);
console.log("Error details:", {
message: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : 'No stack trace',
name: err instanceof Error ? err.name : 'Unknown error type'
});
const errorMessage = err instanceof Error ?
`Error: ${err.message}` :
"Network error. Please try again.";
setError(errorMessage);
} finally {
setIsLoading(false);
}
};
const resetForm = () => {
setStep("username");
setUsername("");
setTwoFaCode("");
setUserId("");
setError("");
};
const LoadingSpinner = ({ text }: { text: string }) => (
<div className="flex items-center gap-3">
<TbLoader className="w-4 h-4 animate-spin" />
<span className="text-muted-foreground">{text}</span>
</div>
);
return (
<div className="flex flex-col h-full">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted flex-1">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<RiTelegram2Line className="w-10 h-10" />
</div>
</div>
<AnimatePresence mode="wait">
{step === "username" && (
<motion.div
key="username-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
className="max-w-md mx-auto"
>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent mb-4">
Login to Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-8">
Please enter your Telegram username to continue.
</p>
<form onSubmit={handleUsernameSubmit} className="max-w-md mx-auto space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="Enter your Telegram username"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
className="text-center text-lg py-6"
autoFocus
/>
</div>
<AnimatePresence>
{error && (
<motion.p
className="text-red-500 text-sm"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{error}
</motion.p>
)}
</AnimatePresence>
<motion.div
variants={buttonVariants}
initial="initial"
whileTap={!isLoading && username.trim() ? "tap" : undefined}
>
<Button
type="submit"
disabled={!username.trim() || isLoading}
className="w-full py-6 text-lg"
>
{isLoading ? (
<LoadingSpinner text="Finding your account..." />
) : (
"Continue"
)}
</Button>
</motion.div>
</form>
</motion.div>
)}
{step === "twofa" && (
<motion.div
key="twofa-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
className="max-w-md mx-auto"
>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent mb-4">
Enter 2FA Code
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-8">
We&apos;ve sent a 6-digit code to your Telegram. Please enter it below.
</p>
<form onSubmit={handleTwoFaSubmit} className="max-w-md mx-auto space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="000000"
value={twoFaCode}
onChange={(e) => setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
disabled={isLoading}
className="text-center text-2xl font-mono tracking-widest py-6"
maxLength={6}
autoFocus
/>
</div>
<AnimatePresence>
{error && (
<motion.p
className="text-red-500 text-sm"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{error}
</motion.p>
)}
</AnimatePresence>
<div className="flex gap-2">
<motion.div
variants={buttonVariants}
initial="initial"
whileTap="tap"
className="flex-1"
>
<Button
type="button"
variant="outline"
onClick={resetForm}
disabled={isLoading}
className="w-full py-6"
>
Back
</Button>
</motion.div>
<motion.div
variants={buttonVariants}
initial="initial"
whileTap={!isLoading && twoFaCode.length === 6 ? "tap" : undefined}
className="flex-1"
>
<Button
type="submit"
disabled={twoFaCode.length !== 6 || isLoading}
className="w-full py-6 text-lg"
>
{isLoading ? (
<LoadingSpinner text="Verifying..." />
) : (
"Verify"
)}
</Button>
</motion.div>
</div>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</section>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
}>
<LoginForm />
</Suspense>
);
}

View file

@ -1,251 +0,0 @@
import { Button } from "@/components/ui/button"
import {
Bot,
Sparkles,
Users,
Settings,
Download,
Brain,
Shield,
Zap,
Tv,
Trash,
Lock,
} from "lucide-react";
import { SiYoutube, SiForgejo } from "react-icons/si";
import { RiTelegram2Line } from "react-icons/ri";
import { TbEyeSpark } from "react-icons/tb";
import Image from "next/image";
import Footer from "@/components/footer";
import Link from "next/link";
export default function Home() {
return (
<div className="flex flex-col">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<Image
src="/kowalski.svg"
alt="Kowalski Logo"
width={48}
height={48}
className="dark:invert -mt-3"
/>
</div>
</div>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
A powerful, multi-function Telegram bot with AI capabilities, media downloading,
user management, and much more. Built for communities and power users.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
<Button size="lg" className="min-w-32" asChild>
<Link href="https://t.me/KowalskiNodeBot">
<RiTelegram2Line />
Try on Telegram
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32">
<Settings />
Documentation
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot">
<SiForgejo />
View on Forgejo
</Link>
</Button>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16" id="ai-features">
<h2 className="text-4xl font-bold mb-4">Features You&apos;ll Love</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Powered by TypeScript, Telegraf, Next.js, and AI.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-primary/10 text-primary">
<Sparkles className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">AI Commands</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Interact with over 50 AI models through simple commands. Get intelligent responses,
assistance, or problem-solving help right in Telegram.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Bot className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/ai</div>
<div className="text-sm text-muted-foreground">Ask questions to a custom AI model of your choice</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Zap className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/ask</div>
<div className="text-sm text-muted-foreground">Quick AI responses for everyday questions</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Brain className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/think</div>
<div className="text-sm text-muted-foreground">Deep reasoning with optional visible thinking</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3" id="youtube-features">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-red-500/10 text-red-500">
<SiYoutube className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">YouTube/Video Downloads</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Download videos directly from YouTube and other platforms and watch them in Telegram.
Supports thousands of sites with integrated yt-dlp.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Download className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">/yt [URL]</div>
<div className="text-sm text-muted-foreground">Quickly download videos up to 50MB</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Shield className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">Automatic Ratelimit Detection</div>
<div className="text-sm text-muted-foreground">We&apos;ll notify you if something goes wrong</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Tv className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">High Quality Downloads</div>
<div className="text-sm text-muted-foreground">Kowalski automatically chooses the best quality for you</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="user-features" className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">
Control <span className="italic mr-1.5">and</span> Fun
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Your user data is always minimized and under your control. That certainly
doesn&apos;t mean the experience is lacking!
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<Users className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">User Accounts</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Your user data is linked only by your Telegram ID. No data is ever sent to third parties
or used for anything other than providing you with the best experience.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Settings className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Personal Settings</div>
<div className="text-sm text-muted-foreground">Custom AI models, temperature, and language preferences</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Brain className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Account Statistics</div>
<div className="text-sm text-muted-foreground">Track AI requests, characters processed, and more</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Trash className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Leave at Any Time</div>
<div className="text-sm text-muted-foreground">We make it easy to delete your account at any time</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-green-500/10 text-green-500">
<Bot className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Web Interface</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski includes a web interface, made with Next.js, to make it easier to manage your
bot, user account, and more. It&apos;s tailored to both users and admins.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<TbEyeSpark className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Everything&apos;s Clean</div>
<div className="text-sm text-muted-foreground">We don&apos;t clutter your view with ads or distractions.</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Sparkles className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Do Everything!</div>
<div className="text-sm text-muted-foreground">We aim to integrate every feature into the web interface.</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Lock className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Private</div>
<div className="text-sm text-muted-foreground">We don&apos;t use any analytics, tracking, or third-party scripts.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<Footer />
</div>
);
}

View file

@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -1,434 +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: 'hf.co/unsloth/Phi-4-mini-reasoning-GGUF',
label: 'Phi4 Mini Reasoning',
parameterSize: '4B',
thinking: true,
uncensored: false
},
{
name: 'phi4:14b',
label: 'Phi4 14B',
parameterSize: '14B',
thinking: false,
uncensored: false
},
{
name: 'hf.co/unsloth/Phi-4-reasoning-plus-GGUF',
label: 'Phi4 Reasoning Plus',
parameterSize: '14B',
thinking: true,
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
},
]
},
];

View file

@ -1,155 +0,0 @@
"use client"
/*
Adapted from https://ui.shadcn.com/docs/components/combobox
*/
import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon, Cpu, Brain, ShieldOff } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { models } from "./ai"
interface ModelPickerProps {
value?: string
onValueChange?: (value: string) => void
disabled?: boolean
className?: string
}
export function ModelPicker({ value, onValueChange, disabled = false, className }: ModelPickerProps) {
const [open, setOpen] = React.useState(false)
const currentModel = React.useMemo(() => {
for (const category of models) {
const model = category.models.find(m => m.name === value)
if (model) {
return {
model,
category: category.label,
categoryDescription: category.descriptionEn
}
}
}
return null
}, [value])
const handleSelect = (modelName: string) => {
onValueChange?.(modelName)
setOpen(false)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn("w-full justify-between h-auto p-4", className)}
>
<div className="flex items-start gap-3 text-left flex-1">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mt-0.5">
<Cpu className="w-4 h-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
{currentModel ? (
<>
<div className="font-medium text-sm">{currentModel.model.label}</div>
<div className="text-xs text-muted-foreground">{currentModel.category}</div>
<div className="text-xs text-muted-foreground/70 mt-1 break-words">
{currentModel.categoryDescription}
</div>
<div className="inline-flex items-center gap-1 mt-2 flex-wrap">
<span className="px-2 py-0.5 bg-muted rounded text-xs font-mono">
{currentModel.model.parameterSize}
</span>
{currentModel.model.thinking && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300 rounded text-xs font-medium flex items-center gap-1">
<Brain />
Thinking
</span>
)}
{currentModel.model.uncensored && (
<span className="px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300 rounded text-xs font-medium flex items-center gap-1">
<ShieldOff />
Uncensored
</span>
)}
</div>
</>
) : (
<div className="text-muted-foreground">Select a model...</div>
)}
</div>
</div>
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align="start">
<Command>
<CommandInput placeholder="Search models..." />
<CommandList className="max-h-[300px]">
<CommandEmpty>No model found.</CommandEmpty>
{models.map((category) => (
<CommandGroup key={category.name} heading={category.label}>
<div className="pb-2 ml-2 mb-1 text-xs text-muted-foreground/70">
{category.descriptionEn}
</div>
{category.models.map((model) => (
<CommandItem
key={model.name}
value={`${category.label} ${model.label} ${model.parameterSize}`}
onSelect={() => handleSelect(model.name)}
className="flex items-center gap-3 py-3"
>
<CheckIcon
className={cn(
"h-4 w-4",
value === model.name ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex-1">
<div className="font-medium text-sm">{model.label}</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="px-2 py-0.5 bg-muted rounded text-xs font-mono">
{model.parameterSize}
</span>
{model.thinking && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300 rounded text-xs font-medium flex items-center gap-1">
<Brain />
Thinking
</span>
)}
{model.uncensored && (
<span className="px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300 rounded text-xs font-medium flex items-center gap-1">
<ShieldOff />
Uncensored
</span>
)}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View file

@ -1,232 +0,0 @@
"use client";
import * as React from "react"
import {
Home,
MessageSquare,
Users,
Sparkles,
User,
Trash2,
LogOut
} from "lucide-react"
import Link from "next/link"
import Image from "next/image"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { ThemeToggle } from "@/components/theme-toggle"
import { SiYoutube } from "react-icons/si"
import { RiTelegram2Line } from "react-icons/ri"
import { useAuth } from "@/contexts/auth-context"
import { Badge } from "@/components/ui/badge"
interface AccountItem {
title: string;
url: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
danger?: boolean;
}
const navigation = [
{
title: "Home",
url: "/",
icon: Home,
},
{
title: "About",
url: "/about",
icon: MessageSquare,
},
]
const features = [
{
title: "AI Commands",
url: "/#ai-features",
icon: Sparkles,
},
{
title: "Video Download",
url: "/#youtube-features",
icon: SiYoutube,
},
{
title: "User Accounts & UI",
url: "/#user-features",
icon: Users,
},
]
export function AppSidebar() {
const { isAuthenticated, loading, logout } = useAuth();
const { setOpenMobile, isMobile } = useSidebar();
const handleMenuItemClick = () => {
if (isMobile) {
setOpenMobile(false);
}
};
const accountItems: AccountItem[] = React.useMemo(() => {
if (loading) {
return [];
}
if (isAuthenticated) {
return [
{
title: "My Account",
url: "/account",
icon: User,
},
{
title: "Logout",
url: "#",
icon: LogOut,
danger: true,
},
{
title: "Delete Account",
url: "/account/delete",
icon: Trash2,
danger: true,
},
];
} else {
return [
{
title: "Sign in with Telegram",
url: "/login",
icon: RiTelegram2Line,
},
];
}
}, [isAuthenticated, loading]);
return (
<Sidebar variant="inset">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<div className="flex flex-row justify-between gap-3">
<Link href="/" className="flex flex-row gap-2" onClick={handleMenuItemClick}>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary/10 p-1">
<Image
src="/kowalski.svg"
alt="Kowalski Logo"
width={20}
height={20}
className="dark:invert -mt-1"
/>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold text-lg">Kowalski</span>
</div>
</Link>
<Badge className="text-xs">Beta</Badge>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navigation.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.url} onClick={handleMenuItemClick}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{!loading && (
<SidebarGroup>
<SidebarGroupLabel>Account</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{accountItems.map((item) => (
<SidebarMenuItem key={item.title}>
{item.title === "Logout" ? (
<SidebarMenuButton
onClick={() => {
logout();
handleMenuItemClick();
}}
className={item.danger ? "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" : ""}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
) : (
<SidebarMenuButton asChild>
<Link
href={item.url}
onClick={handleMenuItemClick}
className={item.danger ? "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" : ""}
>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)}
<SidebarGroup>
<SidebarGroupLabel>Features</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{features.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.url} onClick={handleMenuItemClick}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<div className="flex items-center justify-end">
<ThemeToggle />
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
)
}

View file

@ -1,24 +0,0 @@
import Link from "next/link";
import Image from "next/image";
export default function Footer() {
return (
<footer className="py-12 px-6 border-t bg-background">
<div className="max-w-6xl mx-auto text-center">
<div className="flex items-center justify-center mb-4">
<Image
src="/kowalski.svg"
alt="Kowalski"
width={22}
height={22}
className="mr-2 dark:invert -mt-1"
/>
<span className="text-xl font-semibold">Kowalski</span>
</div>
<p className="text-muted-foreground">
Built with by <Link href="https://git.p0ntus.com/ABOCN" className="underline hover:text-primary transition-all duration-300">ABOCN</Link> and contributors under open source licenses.
</p>
</div>
</footer>
);
}

View file

@ -1,29 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { RiTelegram2Line } from "react-icons/ri";
import Link from "next/link";
import { useAuth } from "@/contexts/auth-context";
export function HeaderAuth() {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="w-8 h-8 animate-pulse bg-muted rounded-md" />
);
}
if (isAuthenticated) {
return null;
}
return (
<Button variant="ghost" size="sm" asChild>
<Link href="/login">
<RiTelegram2Line />
Sign in with Telegram
</Link>
</Button>
);
}

View file

@ -1,21 +0,0 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
{children}
</NextThemesProvider>
)
}

View file

@ -1,37 +0,0 @@
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<Button variant="ghost" size="icon">
<Sun className="h-4 w-4" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View file

@ -1,46 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

Some files were not shown because too many files have changed in this diff Show more