mirror of
https://github.com/abocn/TelegramBot.git
synced 2025-05-09 20:14:59 +00:00
This reverts commit 4f88a85ccb1d53eb2692e6c0a12e7fee82430b34.
This commit is contained in:
parent
4f88a85ccb
commit
d373f44ca1
@ -4,5 +4,4 @@ npm-debug.log
|
||||
.gitignore
|
||||
.env
|
||||
*.md
|
||||
!README.md
|
||||
ollama/
|
||||
!README.md
|
@ -5,9 +5,6 @@ botSource = "https://github.com/ABOCN/TelegramBot"
|
||||
# insert token here
|
||||
botToken = ""
|
||||
|
||||
# ai features
|
||||
# ollamaApi = "http://ollama:11434"
|
||||
|
||||
# misc (botAdmins isnt a array here!)
|
||||
maxRetries = 9999
|
||||
botAdmins = 00000000, 00000000, 00000000
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -144,10 +144,4 @@ yt-dlp
|
||||
ffmpeg
|
||||
|
||||
# Bun
|
||||
bun.lock*
|
||||
|
||||
# Ollama
|
||||
ollama/
|
||||
|
||||
# Docker
|
||||
docker-compose.yml
|
||||
bun.lock*
|
30
README.md
30
README.md
@ -26,8 +26,6 @@ Kowalski is a a simple Telegram bot made in Node.js.
|
||||
- FFmpeg (only for the `/yt` command)
|
||||
- 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)
|
||||
|
||||
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
|
||||
|
||||
1. **Copy compose file**
|
||||
1. **Make sure to setup your `.env` file first!**
|
||||
|
||||
_Without AI (Ollama)_
|
||||
|
||||
```bash
|
||||
mv docker-compose.yml.example docker-compose.yml
|
||||
```
|
||||
|
||||
_With AI (Ollama)_
|
||||
|
||||
```bash
|
||||
mv docker-compose.yml.ai.example docker-compose.yml
|
||||
```
|
||||
|
||||
2. **Make sure to setup your `.env` file first!**
|
||||
|
||||
3. **Run the container**
|
||||
2. **Run the container**
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You must setup Ollama on your own if you would like to use AI features.
|
||||
|
||||
## .env Functions
|
||||
|
||||
> [!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.
|
||||
- **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).
|
||||
- **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.
|
||||
- **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.
|
||||
@ -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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<a href="https://github.com/abocn/TelegramBot/graphs/contributors">
|
||||
|
@ -6,4 +6,4 @@ services:
|
||||
volumes:
|
||||
- ./.env:/usr/src/app/.env:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NODE_ENV=production
|
@ -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
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
@ -32,8 +32,7 @@ async function sendHelpMessage(ctx, isEditing) {
|
||||
[{ 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.aiCmds, callback_data: 'helpAi' }]
|
||||
[{ text: Strings.ytDownload.helpEntry, callback_data: 'helpYouTube' }, { text: Strings.ponyApi.helpEntry, callback_data: 'helpMLP' }]
|
||||
]
|
||||
}
|
||||
};
|
||||
@ -113,10 +112,6 @@ export default (bot) => {
|
||||
await ctx.answerCbQuery();
|
||||
await ctx.editMessageText(Strings.ponyApi.helpDesc, options);
|
||||
break;
|
||||
case 'helpAi':
|
||||
await ctx.answerCbQuery();
|
||||
await ctx.editMessageText(Strings.aiCmdsDesc, options);
|
||||
break;
|
||||
case 'helpBack':
|
||||
await ctx.answerCbQuery();
|
||||
await sendHelpMessage(ctx, true);
|
||||
|
@ -33,8 +33,8 @@
|
||||
"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.",
|
||||
"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}`*.*",
|
||||
@ -52,27 +52,25 @@
|
||||
"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",
|
||||
"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`",
|
||||
"aiCmds": "✨ AI Commands",
|
||||
"aiCmdsDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI",
|
||||
"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": "📺 YouTube 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.*",
|
||||
"downloadingVid": "⬇️ *Downloading video...*",
|
||||
"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...*",
|
||||
"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}",
|
||||
@ -95,8 +93,8 @@
|
||||
"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.",
|
||||
"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})",
|
||||
@ -116,7 +114,5 @@
|
||||
"apiErr": "An error occurred while fetching data from the API.\n\n`{err}`"
|
||||
},
|
||||
"chatNotFound": "Chat not found.",
|
||||
"noFileProvided": "Please provide a file to send.",
|
||||
"askGenerating": "✨ _{model} is working..._",
|
||||
"aiDisabled": "AI features are currently disabled"
|
||||
"noFileProvided": "Please provide a file to send."
|
||||
}
|
@ -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!",
|
||||
"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`",
|
||||
"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`",
|
||||
"maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.",
|
||||
"ytDownload": {
|
||||
@ -114,8 +112,5 @@
|
||||
"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.",
|
||||
"askGenerating": "✨ _{modelo} está funcionando..._",
|
||||
"aiDisabled": "Os recursos de IA estão desativados no momento"
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
@ -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()
|
Loading…
x
Reference in New Issue
Block a user