Revert "[FEATURE] Add /ask command (#54)" (#55)

This reverts commit 4f88a85ccb1d53eb2692e6c0a12e7fee82430b34.
This commit is contained in:
Aidan 2025-05-07 23:04:16 -04:00 committed by GitHub
parent 4f88a85ccb
commit d373f44ca1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 27 additions and 654 deletions

View File

@ -4,5 +4,4 @@ npm-debug.log
.gitignore .gitignore
.env .env
*.md *.md
!README.md !README.md
ollama/

View File

@ -5,9 +5,6 @@ botSource = "https://github.com/ABOCN/TelegramBot"
# insert token here # insert token here
botToken = "" botToken = ""
# ai features
# ollamaApi = "http://ollama:11434"
# misc (botAdmins isnt a array here!) # misc (botAdmins isnt a array here!)
maxRetries = 9999 maxRetries = 9999
botAdmins = 00000000, 00000000, 00000000 botAdmins = 00000000, 00000000, 00000000

8
.gitignore vendored
View File

@ -144,10 +144,4 @@ yt-dlp
ffmpeg ffmpeg
# Bun # Bun
bun.lock* bun.lock*
# Ollama
ollama/
# Docker
docker-compose.yml

View File

@ -26,8 +26,6 @@ Kowalski is a a simple Telegram bot made in Node.js.
- 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)
_AI features require a higher-end system with a CPU/GPU_
## Running locally (non-Docker setup) ## Running locally (non-Docker setup)
First, clone the repo with Git: First, clone the repo with Git:
@ -57,23 +55,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 `.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
```
2. **Make sure to setup your `.env` file first!**
3. **Run the container**
```bash ```bash
docker compose up -d docker compose up -d
@ -97,9 +81,6 @@ If you prefer to use Docker directly, you can use these instructions instead.
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)/.env:/usr/src/app/.env:ro kowalski
``` ```
> [!NOTE]
> You must setup Ollama on your own if you would like to use AI features.
## .env Functions ## .env Functions
> [!IMPORTANT] > [!IMPORTANT]
@ -109,7 +90,6 @@ If you prefer to use Docker directly, you can use these instructions instead.
- **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).
- **ollamaApi** (optional): Ollama API endpoint for various AI features, will be disabled if not set
- **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.
@ -126,12 +106,6 @@ If you prefer to use Docker directly, you can use these instructions instead.
chmod +x src/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 `ollamaApi` in your `.env` file. Please remove or comment out this line to disable all AI functionality.
## Contributors ## Contributors
<a href="https://github.com/abocn/TelegramBot/graphs/contributors"> <a href="https://github.com/abocn/TelegramBot/graphs/contributors">

View File

@ -6,4 +6,4 @@ services:
volumes: volumes:
- ./.env:/usr/src/app/.env:ro - ./.env:/usr/src/app/.env:ro
environment: environment:
- NODE_ENV=production - NODE_ENV=production

View File

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

View File

@ -1,245 +0,0 @@
// AI.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 { isOnSpamWatch } from "../spamwatch/spamwatch"
import spamwatchMiddlewareModule from "../spamwatch/Middleware"
import { Telegraf, Context } from "telegraf"
import type { Message } from "telegraf/types"
import { replyToMessageId } from "../utils/reply-to-message-id"
import { getStrings } from "../plugins/checklang"
import { languageCode } from "../utils/language-code"
import axios from "axios"
import { rateLimiter } from "../utils/rate-limiter"
import { logger } from "../utils/log"
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch)
//const model = "qwen3:0.6b"
const model = "deepseek-r1:1.5b"
type TextContext = Context & { message: Message.TextMessage }
export function sanitizeForJson(text: string): string {
return text
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
}
async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Message) {
const Strings = getStrings(languageCode(ctx))
if (!ctx.chat) return {
"success": false,
"error": Strings.unexpectedErr.replace("{error}", "No chat found"),
}
try {
const aiResponse = await axios.post(`${process.env.ollamaApi}/api/generate`, {
model: model,
prompt: prompt,
stream: true,
}, {
responseType: "stream",
})
let fullResponse = ""
let thoughts = ""
let lastUpdate = Date.now()
for await (const chunk of aiResponse.data) {
const lines = chunk.toString().split('\n')
for (const line of lines) {
if (!line.trim()) continue
let ln = JSON.parse(line)
if (ln.response.includes("<think>")) { logger.logThinking(true) } else if (ln.response.includes("</think>")) { logger.logThinking(false) }
try {
const now = Date.now()
if (ln.response) {
const patchedThoughts = ln.response.replace("<think>", "`Thinking...`").replace("</think>", "`Finished thinking`")
thoughts += patchedThoughts
fullResponse += patchedThoughts
if (now - lastUpdate >= 1000) {
await rateLimiter.editMessageWithRetry(
ctx,
ctx.chat.id,
replyGenerating.message_id,
thoughts,
{ parse_mode: 'Markdown' }
)
lastUpdate = now
}
}
} catch (e) {
console.error("Error parsing chunk:", e)
}
}
}
return {
"success": true,
"response": fullResponse,
}
} catch (error: any) {
let shouldPullModel = false
if (error.response?.data?.error) {
if (error.response.data.error.includes(`model '${model}' not found`) || error.status === 404) {
shouldPullModel = true
} else {
console.error("[!] 1", error.response.data.error)
return {
"success": false,
"error": error.response.data.error,
}
}
} else if (error.status === 404) {
shouldPullModel = true
}
if (shouldPullModel) {
ctx.telegram.editMessageText(ctx.chat.id, replyGenerating.message_id, undefined, `🔄 Pulling ${model} from ollama...`)
console.log(`[i] Pulling ${model} from ollama...`)
const pullModelStream = await axios.post(`${process.env.ollamaApi}/api/pull`, {
model: model,
stream: false,
})
if (pullModelStream.data.status !== ("success")) {
console.error("[!] Something went wrong:", pullModelStream.data)
return {
"success": false,
"error": `❌ Something went wrong while pulling ${model}, please try your command again!`,
}
}
console.log("[i] Model pulled successfully")
return {
"success": true,
"response": `✅ Pulled ${model} successfully, please retry the command.`,
}
}
if (error.response) {
console.error("[!] 2", error.response)
return {
"success": false,
"error": error.response,
}
}
if (error.statusText) {
console.error("[!] 3", error.statusText)
return {
"success": false,
"error": error.statusText,
}
}
return {
"success": false,
"error": "An unexpected error occurred",
}
}
}
export default (bot: Telegraf<Context>) => {
bot.command("ask", spamwatchMiddleware, async (ctx) => {
if (!ctx.message || !('text' in ctx.message)) return;
const textCtx = ctx as TextContext;
const reply_to_message_id = replyToMessageId(textCtx)
const Strings = getStrings(languageCode(textCtx))
const message = textCtx.message.text
const author = ("@" + ctx.from?.username) || ctx.from?.first_name
logger.logCmdStart(author)
if (!process.env.ollamaApi) {
await ctx.reply(Strings.aiDisabled, {
parse_mode: 'Markdown',
...({ reply_to_message_id })
})
return
}
const replyGenerating = await ctx.reply(Strings.askGenerating.replace("{model}", model), {
parse_mode: 'Markdown',
...({ reply_to_message_id })
})
const fixedMsg = message.replace(/\/ask /, "")
if (fixedMsg.length < 1) {
await ctx.reply(Strings.askNoMessage, {
parse_mode: 'Markdown',
...({ reply_to_message_id })
})
return
}
logger.logPrompt(fixedMsg)
const prompt = sanitizeForJson(
`You are a helpful assistant named Kowalski, who has been given a message from a user.
The message is:
${fixedMsg}`)
const aiResponse = await getResponse(prompt, textCtx, replyGenerating)
if (!aiResponse) return
if (aiResponse.success && aiResponse.response) {
if (!ctx.chat) return
await rateLimiter.editMessageWithRetry(
ctx,
ctx.chat.id,
replyGenerating.message_id,
aiResponse.response,
{ parse_mode: 'Markdown' }
)
} else {
if (!ctx.chat) return
const error = Strings.unexpectedErr.replace("{error}", aiResponse.error)
await rateLimiter.editMessageWithRetry(
ctx,
ctx.chat.id,
replyGenerating.message_id,
error,
{ parse_mode: 'Markdown' }
)
console.error("[!] Error sending response:", aiResponse.error)
}
})
}

View File

@ -32,8 +32,7 @@ async function sendHelpMessage(ctx, isEditing) {
[{ text: Strings.mainCommands, callback_data: 'helpMain' }, { text: Strings.usefulCommands, callback_data: 'helpUseful' }], [{ 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.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.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.ytDownload.helpEntry, callback_data: 'helpYouTube' }, { text: Strings.ponyApi.helpEntry, callback_data: 'helpMLP' }]
[{ text: Strings.aiCmds, callback_data: 'helpAi' }]
] ]
} }
}; };
@ -113,10 +112,6 @@ export default (bot) => {
await ctx.answerCbQuery(); await ctx.answerCbQuery();
await ctx.editMessageText(Strings.ponyApi.helpDesc, options); await ctx.editMessageText(Strings.ponyApi.helpDesc, options);
break; break;
case 'helpAi':
await ctx.answerCbQuery();
await ctx.editMessageText(Strings.aiCmdsDesc, options);
break;
case 'helpBack': case 'helpBack':
await ctx.answerCbQuery(); await ctx.answerCbQuery();
await sendHelpMessage(ctx, true); await sendHelpMessage(ctx, true);

View File

@ -33,8 +33,8 @@
"funEmojiResult": "*You rolled {emoji} and got* `{value}`*!*\nYou don't know what that means? Me neither!", "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}", "gifErr": "*Something went wrong while sending the GIF. Please try again later.*\n\n{err}",
"lastFm": { "lastFm": {
"helpEntry": "🎵 Last.fm", "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.", "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>`", "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>`", "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}`*.*", "noRecentTracks": "*No recent tracks found for Last.fm user* `{lastfmUser}`*.*",
@ -52,27 +52,25 @@
"apiErr": "*An error occurred while retrieving the weather. Please try again later.*\n\n`{error}`", "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.*" "apiKeyErr": "*An API key was not set by the bot owner. Please try again later.*"
}, },
"mainCommands": " Main Commands", "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", "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", "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`", "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", "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", "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", "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!", "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", "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`", "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`",
"aiCmds": "✨ AI Commands",
"aiCmdsDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI",
"maInvalidModule": "Please provide a valid module ID from The Mod Archive.\nExample: `/modarchive 81574`", "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.", "maDownloadError": "Error downloading the file. Check the module ID and try again.",
"ytDownload": { "ytDownload": {
"helpEntry": "📺 YouTube Download", "helpEntry": "Video download",
"helpDesc": "📺 *YouTube 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.*", "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...*", "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.*", "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...*", "checkingSize": "*Checking if the video exceeds the 50MB limit...*",
"uploadingVid": "⬆️ *Uploading video...*", "uploadingVid": "*Uploading video...*",
"msgDesc": "{userMention}*, there is your downloaded video.*", "msgDesc": "{userMention}*, there is your downloaded video.*",
"downloadErr": "*Error during YT video download:*\n\n`{err}`", "downloadErr": "*Error during YT video download:*\n\n`{err}`",
"uploadErr": "Error uploading file. Please try again later.\n\n{error}", "uploadErr": "Error uploading file. Please try again later.\n\n{error}",
@ -95,8 +93,8 @@
"resultMsg": "*HTTP Code*: {code}\n*Name*: `{message}`\n*Description*: {description}" "resultMsg": "*HTTP Code*: {code}\n*Name*: `{message}`\n*Description*: {description}"
}, },
"ponyApi": { "ponyApi": {
"helpEntry": "🐴 My Little Pony", "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.", "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})", "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})", "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})", "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})",
@ -116,7 +114,5 @@
"apiErr": "An error occurred while fetching data from the API.\n\n`{err}`" "apiErr": "An error occurred while fetching data from the API.\n\n`{err}`"
}, },
"chatNotFound": "Chat not found.", "chatNotFound": "Chat not found.",
"noFileProvided": "Please provide a file to send.", "noFileProvided": "Please provide a file to send."
"askGenerating": "✨ _{model} is working..._",
"aiDisabled": "AI features are currently disabled"
} }

View File

@ -62,8 +62,6 @@
"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!", "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", "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`", "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`",
"aiCmds": "Comandos de IA",
"aiCmdsDesc": "*Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA",
"maInvalidModule": "Por favor, forneça um ID de módulo válido do The Mod Archive.\nExemplo: `/modarchive 81574`", "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.", "maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.",
"ytDownload": { "ytDownload": {
@ -114,8 +112,5 @@
"notFound": "Celular não encontrado.", "notFound": "Celular não encontrado.",
"resultMsg": "*Nome:* `{name}`\n*Marca:* `{brand}`\n*Modelo:* `{model}`\n*Codinome:* `{codename}`", "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}`" "apiErr": "Ocorreu um erro ao buscar os dados da API.\n\n`{err}`"
}, }
"noFileProvided": "Por favor, forneça um arquivo para envio.",
"askGenerating": "✨ _{modelo} está funcionando..._",
"aiDisabled": "Os recursos de IA estão desativados no momento"
} }

View File

@ -1,82 +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/>
class Logger {
private static instance: Logger
private thinking: boolean = false
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
logCmdStart(user: string): void {
console.log(`[START] Received /ask from ${user}`)
}
logThinking(thinking: boolean): void {
if (thinking) {
console.log("[THINKING] Started")
} else {
console.log("[THINKING] Ended")
}
}
logChunk(chatId: number, messageId: number, text: string, isOverflow: boolean = false): void {
const prefix = isOverflow ? "[OVERFLOW]" : "[CHUNK]"
console.log(`${prefix} [${chatId}:${messageId}] ${text.length} chars`)
}
logPrompt(prompt: string): void {
console.log(`[PROMPT] ${prompt.length} chars: ${prompt.substring(0, 50)}${prompt.length > 50 ? "..." : ""}`)
}
logError(error: any): void {
if (error.response?.error_code === 429) {
const retryAfter = error.response.parameters?.retry_after || 1
console.error(`[RATE_LIMIT] Too Many Requests - retry after ${retryAfter}s`)
} else if (error.response?.error_code === 400 && error.response?.description?.includes("can't parse entities")) {
console.error("[PARSE_ERROR] Markdown parsing failed, retrying with plain text")
} else {
const errorDetails = {
code: error.response?.error_code,
description: error.response?.description,
method: error.on?.method
}
console.error("[ERROR]", JSON.stringify(errorDetails, null, 2))
}
}
}
export const logger = Logger.getInstance()

View File

@ -1,235 +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 lastEditTime: number = 0
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(): Promise<void> {
if (this.isRateLimited) {
const now = Date.now()
if (now < this.rateLimitEndTime) {
const waitTime = this.rateLimitEndTime - now
await new Promise(resolve => setTimeout(resolve, waitTime))
}
this.isRateLimited = false
}
}
private async processUpdate(
ctx: Context,
chatId: number,
messageId: number,
options: any
): Promise<void> {
const messageKey = this.getMessageKey(chatId, messageId)
const latestText = this.pendingUpdates.get(messageKey)
if (!latestText) return
const now = Date.now()
const timeSinceLastEdit = now - this.lastEditTime
await this.waitForRateLimit()
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: string[] = []
let currentChunk = ''
let currentLength = 0
// Split text into chunks while preserving markdown formatting
const lines = latestText.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 a single line is too long, split
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)
}
const firstChunk = chunks[0]
logger.logChunk(chatId, messageId, firstChunk)
try {
await ctx.telegram.editMessageText(chatId, messageId, undefined, firstChunk, options)
} catch (error: any) {
if (!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) {
logger.logChunk(chatId, overflowMessageId, chunk, true)
try {
await ctx.telegram.editMessageText(chatId, overflowMessageId, undefined, chunk, options)
} catch (error: any) {
if (!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
})
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 {
const sanitizedText = latestText
logger.logChunk(chatId, messageId, sanitizedText)
try {
await ctx.telegram.editMessageText(chatId, messageId, undefined, sanitizedText, options)
} catch (error: any) {
if (!error.response?.description?.includes("message is not modified")) {
throw error
}
}
this.pendingUpdates.delete(messageKey)
}
this.lastEditTime = Date.now()
this.updateQueue.delete(messageKey)
} catch (error: any) {
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)
} else if (error.response?.error_code === 400) {
if (error.response?.description?.includes("can't parse entities")) {
// try again with plain text
const plainOptions = { ...options, parse_mode: undefined }
await this.processUpdate(ctx, chatId, messageId, plainOptions)
} else if (error.response?.description?.includes("MESSAGE_TOO_LONG")) {
const plainOptions = { ...options, parse_mode: undefined }
await this.processUpdate(ctx, chatId, messageId, plainOptions)
} else if (error.response?.description?.includes("message is not modified")) {
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
} else {
logger.logError(error)
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
}
} else {
logger.logError(error)
this.pendingUpdates.delete(messageKey)
this.updateQueue.delete(messageKey)
}
}
}
async editMessageWithRetry(
ctx: Context,
chatId: number,
messageId: number,
text: string,
options: any
): 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()