add postgres db, use settings and user data, lots of cleanup and logic fixes, bug fixes, better error handling, update docs and docker
Some checks are pending
njsscan sarif / njsscan code scanning (push) Waiting to run
Update AUTHORS File / update-authors (push) Waiting to run
Some checks are pending
njsscan sarif / njsscan code scanning (push) Waiting to run
Update AUTHORS File / update-authors (push) Waiting to run
This commit is contained in:
parent
765b1144fa
commit
4d540078f5
30 changed files with 1664 additions and 727 deletions
|
@ -6,3 +6,4 @@ npm-debug.log
|
||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
ollama/
|
ollama/
|
||||||
|
db/
|
|
@ -12,6 +12,9 @@ ollamaEnabled = false
|
||||||
# flashModel = "gemma3:4b"
|
# flashModel = "gemma3:4b"
|
||||||
# thinkingModel = "qwen3:4b"
|
# thinkingModel = "qwen3:4b"
|
||||||
|
|
||||||
|
# database
|
||||||
|
databaseUrl = "postgres://kowalski:kowalski@localhost:5432/kowalski"
|
||||||
|
|
||||||
# misc (botAdmins isnt a array here!)
|
# misc (botAdmins isnt a array here!)
|
||||||
maxRetries = 9999
|
maxRetries = 9999
|
||||||
botAdmins = 00000000, 00000000, 00000000
|
botAdmins = 00000000, 00000000, 00000000
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -151,3 +151,6 @@ ollama/
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
|
# postgres
|
||||||
|
db/
|
|
@ -19,6 +19,7 @@ Kowalski is a a simple Telegram bot made in Node.js.
|
||||||
- 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
|
### AI Requirements
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ If you prefer to use Docker directly, you can use these instructions instead.
|
||||||
- **handlerTimeout** (optional): How long handlers will wait before timing out. Set this high if using large AI models.
|
- **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
|
- **flashModel** (optional): Which model will be used for /ask
|
||||||
- **thinkingModel** (optional): Which model will be used for /think
|
- **thinkingModel** (optional): Which model will be used for /think
|
||||||
|
- **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.
|
||||||
|
@ -149,3 +151,5 @@ 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).
|
||||||
|
|
||||||
|
Featuring some components under Unlicense.
|
||||||
|
|
|
@ -13,3 +13,15 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./ollama:/root/.ollama
|
- ./ollama:/root/.ollama
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: kowalski-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 5433:5432
|
||||||
|
volumes:
|
||||||
|
- ./db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=kowalski
|
||||||
|
- POSTGRES_PASSWORD=kowalski
|
||||||
|
- POSTGRES_DB=kowalski
|
|
@ -7,3 +7,15 @@ services:
|
||||||
- ./.env:/usr/src/app/.env:ro
|
- ./.env:/usr/src/app/.env:ro
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: kowalski-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 5433:5432
|
||||||
|
volumes:
|
||||||
|
- ./db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=kowalski
|
||||||
|
- POSTGRES_PASSWORD=kowalski
|
||||||
|
- POSTGRES_DB=kowalski
|
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/db/schema.ts',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.databaseUrl!,
|
||||||
|
},
|
||||||
|
});
|
12
package.json
12
package.json
|
@ -1,14 +1,24 @@
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon src/bot.ts"
|
"start": "nodemon src/bot.ts",
|
||||||
|
"docs": "bunx typedoc",
|
||||||
|
"serve:docs": "bun run serve-docs.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dotenvx/dotenvx": "^1.45.1",
|
"@dotenvx/dotenvx": "^1.45.1",
|
||||||
"@types/bun": "^1.2.17",
|
"@types/bun": "^1.2.17",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"dotenv": "^17.0.0",
|
||||||
|
"drizzle-orm": "^0.44.2",
|
||||||
"node-html-parser": "^7.0.1",
|
"node-html-parser": "^7.0.1",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"telegraf": "^4.16.3",
|
"telegraf": "^4.16.3",
|
||||||
"youtube-url": "^0.5.0"
|
"youtube-url": "^0.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8.15.4",
|
||||||
|
"drizzle-kit": "^0.31.4",
|
||||||
|
"tsx": "^4.20.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
src/bot.ts
83
src/bot.ts
|
@ -1,63 +1,79 @@
|
||||||
import { Telegraf } from 'telegraf';
|
import { Telegraf } from 'telegraf';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { isOnSpamWatch } from './spamwatch/spamwatch';
|
import { isSpamwatchConnected } from './spamwatch/spamwatch';
|
||||||
import '@dotenvx/dotenvx';
|
import '@dotenvx/dotenvx';
|
||||||
|
import 'dotenv/config';
|
||||||
import './plugins/ytDlpWrapper';
|
import './plugins/ytDlpWrapper';
|
||||||
import { preChecks } from './commands/ai';
|
import { preChecks } from './commands/ai';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { Client } from 'pg';
|
||||||
|
import * as schema from './db/schema';
|
||||||
|
import { ensureUserInDb } from './utils/ensure-user';
|
||||||
|
import { getSpamwatchBlockedCount } from './spamwatch/spamwatch';
|
||||||
|
|
||||||
// Ensures bot token is set, and not default value
|
(async function main() {
|
||||||
if (!process.env.botToken || process.env.botToken === 'InsertYourBotTokenHere') {
|
const { botToken, handlerTimeout, maxRetries, databaseUrl, ollamaEnabled } = process.env;
|
||||||
console.error('Bot token is not set. Please set the bot token in the .env file.')
|
if (!botToken || botToken === 'InsertYourBotTokenHere') {
|
||||||
process.exit(1)
|
console.error('Bot token is not set. Please set the bot token in the .env file.');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect AI and run pre-checks
|
if (ollamaEnabled === "true") {
|
||||||
if (process.env.ollamaEnabled === "true") {
|
|
||||||
if (!(await preChecks())) {
|
if (!(await preChecks())) {
|
||||||
process.exit(1)
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = new Client({ connectionString: databaseUrl });
|
||||||
|
await client.connect();
|
||||||
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
const bot = new Telegraf(
|
const bot = new Telegraf(
|
||||||
process.env.botToken,
|
botToken,
|
||||||
{ handlerTimeout: Number(process.env.handlerTimeout) || 600_000 }
|
{ handlerTimeout: Number(handlerTimeout) || 600_000 }
|
||||||
);
|
);
|
||||||
const maxRetries = process.env.maxRetries || 5;
|
const maxRetriesNum = Number(maxRetries) || 5;
|
||||||
let restartCount = 0;
|
let restartCount = 0;
|
||||||
|
|
||||||
const loadCommands = () => {
|
bot.use(async (ctx, next) => {
|
||||||
const commandsPath = path.join(__dirname, 'commands');
|
await ensureUserInDb(ctx, db);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadCommands() {
|
||||||
|
const commandsPath = path.join(__dirname, 'commands');
|
||||||
|
let loadedCount = 0;
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(commandsPath)
|
const files = fs.readdirSync(commandsPath)
|
||||||
.filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
.filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
try {
|
try {
|
||||||
const commandPath = path.join(commandsPath, file);
|
const commandPath = path.join(commandsPath, file);
|
||||||
const command = require(commandPath).default || require(commandPath);
|
const command = require(commandPath).default || require(commandPath);
|
||||||
if (typeof command === 'function') {
|
if (typeof command === 'function') {
|
||||||
command(bot, isOnSpamWatch);
|
command(bot, db);
|
||||||
|
loadedCount++;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load command file ${file}: ${error.message}`);
|
console.error(`Failed to load command file ${file}: ${error.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log(`[🤖 BOT] Loaded ${loadedCount} commands.\n`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to read commands directory: ${error.message}`);
|
console.error(`Failed to read commands directory: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const startBot = async () => {
|
async function startBot() {
|
||||||
|
try {
|
||||||
const botInfo = await bot.telegram.getMe();
|
const botInfo = await bot.telegram.getMe();
|
||||||
console.log(`${botInfo.first_name} is running...`);
|
console.log(`${botInfo.first_name} is running...`);
|
||||||
try {
|
|
||||||
await bot.launch();
|
await bot.launch();
|
||||||
restartCount = 0;
|
restartCount = 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start bot:', error.message);
|
console.error('Failed to start bot:', error.message);
|
||||||
if (restartCount < Number(maxRetries)) {
|
if (restartCount < maxRetriesNum) {
|
||||||
restartCount++;
|
restartCount++;
|
||||||
console.log(`Retrying to start bot... Attempt ${restartCount}`);
|
console.log(`Retrying to start bot... Attempt ${restartCount}`);
|
||||||
setTimeout(startBot, 5000);
|
setTimeout(startBot, 5000);
|
||||||
|
@ -66,13 +82,13 @@ const startBot = async () => {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleShutdown = (signal) => {
|
function handleShutdown(signal: string) {
|
||||||
console.log(`Received ${signal}. Stopping bot...`);
|
console.log(`Received ${signal}. Stopping bot...`);
|
||||||
bot.stop(signal);
|
bot.stop(signal);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
}
|
||||||
|
|
||||||
process.once('SIGINT', () => handleShutdown('SIGINT'));
|
process.once('SIGINT', () => handleShutdown('SIGINT'));
|
||||||
process.once('SIGTERM', () => handleShutdown('SIGTERM'));
|
process.once('SIGTERM', () => handleShutdown('SIGTERM'));
|
||||||
|
@ -86,5 +102,28 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
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();
|
loadCommands();
|
||||||
startBot();
|
startBot();
|
||||||
|
})();
|
||||||
|
|
|
@ -38,6 +38,9 @@ import { languageCode } from "../utils/language-code"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { rateLimiter } from "../utils/rate-limiter"
|
import { rateLimiter } from "../utils/rate-limiter"
|
||||||
import { logger } from "../utils/log"
|
import { logger } from "../utils/log"
|
||||||
|
import { ensureUserInDb } from "../utils/ensure-user"
|
||||||
|
import * as schema from '../db/schema'
|
||||||
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres"
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch)
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch)
|
||||||
export const flash_model = process.env.flashModel || "gemma3:4b"
|
export const flash_model = process.env.flashModel || "gemma3:4b"
|
||||||
|
@ -45,6 +48,94 @@ export const thinking_model = process.env.thinkingModel || "qwen3:4b"
|
||||||
|
|
||||||
type TextContext = Context & { message: Message.TextMessage }
|
type TextContext = Context & { message: Message.TextMessage }
|
||||||
|
|
||||||
|
type User = typeof schema.usersTable.$inferSelect
|
||||||
|
|
||||||
|
interface ModelInfo {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
descriptionEn: string;
|
||||||
|
descriptionPt: string;
|
||||||
|
models: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
parameterSize: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OllamaResponse {
|
||||||
|
response: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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' },
|
||||||
|
{ name: 'gemma3n:e4b', label: 'Gemma3n e4b', parameterSize: '4B' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gemma3-abliterated',
|
||||||
|
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-abliterated 1B', parameterSize: '1b' },
|
||||||
|
{ name: 'huihui_ai/gemma3-abliterated:4b', label: 'Gemma3-abliterated 4B', parameterSize: '4b' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'qwen3',
|
||||||
|
label: 'Qwen3',
|
||||||
|
descriptionEn: 'Qwen3 is a multilingual reasoning model series.',
|
||||||
|
descriptionPt: 'Qwen3 é uma série de modelos multilingues.',
|
||||||
|
models: [
|
||||||
|
{ name: 'qwen3:4b', label: 'Qwen3 4B', parameterSize: '4B' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deepseek',
|
||||||
|
label: 'DeepSeek',
|
||||||
|
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' },
|
||||||
|
{ name: 'huihui_ai/deepseek-r1-abliterated:1.5b', label: 'DeepSeek Uncensored 1.5B', parameterSize: '1.5B' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const enSystemPrompt = `You are a plaintext-only, helpful assistant called {botName}.
|
||||||
|
Current Date/Time (UTC): {date}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Respond to the user's message:
|
||||||
|
{message}`
|
||||||
|
|
||||||
|
const ptSystemPrompt = `Você é um assistente de texto puro e útil chamado {botName}.
|
||||||
|
Data/Hora atual (UTC): {date}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Responda à mensagem do usuário:
|
||||||
|
{message}`
|
||||||
|
|
||||||
|
async function usingSystemPrompt(ctx: TextContext, db: NodePgDatabase<typeof schema>, botName: string): Promise<string> {
|
||||||
|
const user = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(ctx.from!.id)), limit: 1 });
|
||||||
|
if (user.length === 0) await ensureUserInDb(ctx, db);
|
||||||
|
const userData = user[0];
|
||||||
|
const lang = userData?.languageCode || "en";
|
||||||
|
const utcDate = new Date().toISOString();
|
||||||
|
const prompt = lang === "pt"
|
||||||
|
? ptSystemPrompt.replace("{botName}", botName).replace("{date}", utcDate).replace("{message}", ctx.message.text)
|
||||||
|
: enSystemPrompt.replace("{botName}", botName).replace("{date}", utcDate).replace("{message}", ctx.message.text);
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeForJson(text: string): string {
|
export function sanitizeForJson(text: string): string {
|
||||||
return text
|
return text
|
||||||
.replace(/\\/g, '\\\\')
|
.replace(/\\/g, '\\\\')
|
||||||
|
@ -69,23 +160,50 @@ export async function preChecks() {
|
||||||
}
|
}
|
||||||
checked++;
|
checked++;
|
||||||
}
|
}
|
||||||
console.log(`[✨ AI] Pre-checks passed [${checked}/${envs.length}]\n`)
|
|
||||||
|
const ollamaApi = process.env.ollamaApi
|
||||||
|
if (!ollamaApi) {
|
||||||
|
console.error("[✨ AI | !] ❌ ollamaApi not set!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let ollamaOk = false
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(ollamaApi, { timeout: 2000 })
|
||||||
|
if (res && res.data && typeof res.data === 'object' && 'ollama' in res.data) {
|
||||||
|
ollamaOk = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
ollamaOk = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ollamaOk) {
|
||||||
|
console.error("[✨ AI | !] ❌ Ollama API is not responding at ", ollamaApi)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
checked++;
|
||||||
|
console.log(`[✨ AI] Pre-checks passed [${checked}/${envs.length + 1}]`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAxiosError(error: unknown): error is { response?: { data?: { error?: string }, status?: number }, request?: unknown, message?: string } {
|
function isAxiosError(error: unknown): error is { response?: { data?: { error?: string }, status?: number, statusText?: string }, request?: unknown, message?: string } {
|
||||||
return typeof error === 'object' && error !== null && (
|
return typeof error === 'object' && error !== null && (
|
||||||
'response' in error || 'request' in error || 'message' in error
|
'response' in error || 'request' in error || 'message' in error
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAxiosErrorMessage(error: unknown): string {
|
function extractAxiosErrorMessage(error: unknown): string {
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
const err = error as Record<string, unknown>;
|
const err = error as { response?: { data?: { error?: string }, status?: number, statusText?: string }, request?: unknown, message?: string };
|
||||||
if (err.response && typeof err.response === 'object') {
|
if (err.response && typeof err.response === 'object') {
|
||||||
const resp = err.response as Record<string, unknown>;
|
const resp = err.response;
|
||||||
if (resp.data && typeof resp.data === 'object' && 'error' in resp.data) {
|
if (resp.data && typeof resp.data === 'object' && 'error' in resp.data) {
|
||||||
return String((resp.data as Record<string, unknown>).error);
|
return String(resp.data.error);
|
||||||
}
|
}
|
||||||
if ('status' in resp && 'statusText' in resp) {
|
if ('status' in resp && 'statusText' in resp) {
|
||||||
return `HTTP ${resp.status}: ${resp.statusText}`;
|
return `HTTP ${resp.status}: ${resp.statusText}`;
|
||||||
|
@ -102,71 +220,68 @@ function extractAxiosErrorMessage(error: unknown): string {
|
||||||
return 'An unexpected error occurred.';
|
return 'An unexpected error occurred.';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Message, model: string) {
|
async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Message, model: string, aiTemperature: number): Promise<{ success: boolean; response?: string; error?: string }> {
|
||||||
const Strings = getStrings(languageCode(ctx))
|
const Strings = getStrings(languageCode(ctx));
|
||||||
|
|
||||||
if (!ctx.chat) {
|
if (!ctx.chat) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: Strings.unexpectedErr.replace("{error}", "No chat found"),
|
error: Strings.unexpectedErr.replace("{error}", "No chat found"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aiResponse = await axios.post(
|
const aiResponse = await axios.post<unknown>(
|
||||||
`${process.env.ollamaApi}/api/generate`,
|
`${process.env.ollamaApi}/api/generate`,
|
||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
prompt,
|
prompt,
|
||||||
stream: true,
|
stream: true,
|
||||||
|
options: {
|
||||||
|
temperature: aiTemperature
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
let fullResponse = "";
|
||||||
let fullResponse = ""
|
let thoughts = "";
|
||||||
let thoughts = ""
|
let lastUpdate = Date.now();
|
||||||
let lastUpdate = Date.now()
|
const stream: NodeJS.ReadableStream = aiResponse.data as any;
|
||||||
|
|
||||||
const stream = aiResponse.data
|
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
const lines = chunk.toString().split('\n')
|
const lines = chunk.toString().split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim()) continue
|
if (!line.trim()) continue;
|
||||||
let ln
|
let ln: OllamaResponse;
|
||||||
try {
|
try {
|
||||||
ln = JSON.parse(line)
|
ln = JSON.parse(line);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[✨ AI | !] Error parsing chunk:", e)
|
console.error("[✨ AI | !] Error parsing chunk:", e);
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
if (model === thinking_model && ln.response) {
|
||||||
if (model === thinking_model) {
|
|
||||||
if (ln.response.includes('<think>')) {
|
if (ln.response.includes('<think>')) {
|
||||||
const thinkMatch = ln.response.match(/<think>([\s\S]*?)<\/think>/)
|
const thinkMatch = ln.response.match(/<think>([\s\S]*?)<\/think>/);
|
||||||
if (thinkMatch && thinkMatch[1].trim().length > 0) {
|
if (thinkMatch && thinkMatch[1].trim().length > 0) {
|
||||||
logger.logThinking(ctx.chat.id, replyGenerating.message_id, true)
|
logger.logThinking(ctx.chat.id, replyGenerating.message_id, true);
|
||||||
} else if (!thinkMatch) {
|
} else if (!thinkMatch) {
|
||||||
logger.logThinking(ctx.chat.id, replyGenerating.message_id, true)
|
logger.logThinking(ctx.chat.id, replyGenerating.message_id, true);
|
||||||
}
|
}
|
||||||
} else if (ln.response.includes('</think>')) {
|
} else if (ln.response.includes('</think>')) {
|
||||||
logger.logThinking(ctx.chat.id, replyGenerating.message_id, false)
|
logger.logThinking(ctx.chat.id, replyGenerating.message_id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const now = Date.now();
|
||||||
const now = Date.now()
|
|
||||||
if (ln.response) {
|
if (ln.response) {
|
||||||
if (model === thinking_model) {
|
if (model === thinking_model) {
|
||||||
let patchedThoughts = ln.response
|
let patchedThoughts = ln.response;
|
||||||
const thinkTagRx = /<think>([\s\S]*?)<\/think>/g
|
const thinkTagRx = /<think>([\s\S]*?)<\/think>/g;
|
||||||
patchedThoughts = patchedThoughts.replace(thinkTagRx, (match, p1) => p1.trim().length > 0 ? '`Thinking...`' + p1 + '`Finished thinking`' : '')
|
patchedThoughts = patchedThoughts.replace(thinkTagRx, (match, p1) => p1.trim().length > 0 ? '`Thinking...`' + p1 + '`Finished thinking`' : '');
|
||||||
patchedThoughts = patchedThoughts.replace(/<think>/g, '`Thinking...`')
|
patchedThoughts = patchedThoughts.replace(/<think>/g, '`Thinking...`');
|
||||||
patchedThoughts = patchedThoughts.replace(/<\/think>/g, '`Finished thinking`')
|
patchedThoughts = patchedThoughts.replace(/<\/think>/g, '`Finished thinking`');
|
||||||
thoughts += patchedThoughts
|
thoughts += patchedThoughts;
|
||||||
fullResponse += patchedThoughts
|
fullResponse += patchedThoughts;
|
||||||
} else {
|
} else {
|
||||||
fullResponse += ln.response
|
fullResponse += ln.response;
|
||||||
}
|
}
|
||||||
if (now - lastUpdate >= 1000) {
|
if (now - lastUpdate >= 1000) {
|
||||||
await rateLimiter.editMessageWithRetry(
|
await rateLimiter.editMessageWithRetry(
|
||||||
|
@ -175,67 +290,104 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me
|
||||||
replyGenerating.message_id,
|
replyGenerating.message_id,
|
||||||
thoughts,
|
thoughts,
|
||||||
{ parse_mode: 'Markdown' }
|
{ parse_mode: 'Markdown' }
|
||||||
)
|
);
|
||||||
lastUpdate = now
|
lastUpdate = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
response: fullResponse,
|
response: fullResponse,
|
||||||
}
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = extractAxiosErrorMessage(error)
|
const errorMsg = extractAxiosErrorMessage(error);
|
||||||
console.error("[✨ AI | !] Error:", errorMsg)
|
console.error("[✨ AI | !] Error:", errorMsg);
|
||||||
|
|
||||||
// model not found or 404
|
|
||||||
if (isAxiosError(error) && error.response && typeof error.response === 'object') {
|
if (isAxiosError(error) && error.response && typeof error.response === 'object') {
|
||||||
const resp = error.response as Record<string, unknown>;
|
const resp = error.response as { data?: { error?: string }, status?: number };
|
||||||
const errData = resp.data && typeof resp.data === 'object' && 'error' in resp.data ? (resp.data as Record<string, unknown>).error : undefined;
|
const errData = resp.data && typeof resp.data === 'object' && 'error' in resp.data ? (resp.data as { error?: string }).error : undefined;
|
||||||
const errStatus = 'status' in resp ? resp.status : undefined;
|
const errStatus = 'status' in resp ? resp.status : undefined;
|
||||||
if ((typeof errData === 'string' && errData.includes(`model '${model}' not found`)) || errStatus === 404) {
|
if ((typeof errData === 'string' && errData.includes(`model '${model}' not found`)) || errStatus === 404) {
|
||||||
ctx.telegram.editMessageText(
|
await ctx.telegram.editMessageText(
|
||||||
ctx.chat.id,
|
ctx.chat!.id,
|
||||||
replyGenerating.message_id,
|
replyGenerating.message_id,
|
||||||
undefined,
|
undefined,
|
||||||
`🔄 *Pulling ${model} from Ollama...*\n\nThis may take a few minutes...`,
|
Strings.ai.pulling.replace("{model}", model),
|
||||||
{ parse_mode: 'Markdown' }
|
{ parse_mode: 'Markdown' }
|
||||||
)
|
);
|
||||||
console.log(`[✨ AI | i] Pulling ${model} from ollama...`)
|
console.log(`[✨ AI | i] Pulling ${model} from ollama...`);
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${process.env.ollamaApi}/api/pull`,
|
`${process.env.ollamaApi}/api/pull`,
|
||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
stream: false,
|
stream: false,
|
||||||
timeout: process.env.ollamaApiTimeout || 10000,
|
timeout: Number(process.env.ollamaApiTimeout) || 10000,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const pullMsg = extractAxiosErrorMessage(e)
|
const pullMsg = extractAxiosErrorMessage(e);
|
||||||
console.error("[✨ AI | !] Pull error:", pullMsg)
|
console.error("[✨ AI | !] Pull error:", pullMsg);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `❌ Something went wrong while pulling ${model}: ${pullMsg}`,
|
error: `❌ Something went wrong while pulling ${model}: ${pullMsg}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
console.log(`[✨ AI | i] ${model} pulled successfully`);
|
||||||
console.log(`[✨ AI | i] ${model} pulled successfully`)
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
response: `✅ Pulled ${model} successfully, please retry the command.`,
|
response: `✅ Pulled ${model} successfully, please retry the command.`,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
async function handleAiReply(ctx: TextContext, db: NodePgDatabase<typeof schema>, model: string, prompt: string, replyGenerating: Message, aiTemperature: number) {
|
||||||
|
const Strings = getStrings(languageCode(ctx));
|
||||||
|
const aiResponse = await getResponse(prompt, ctx, replyGenerating, model, aiTemperature);
|
||||||
|
if (!aiResponse) return;
|
||||||
|
if (!ctx.chat) return;
|
||||||
|
if (aiResponse.success && aiResponse.response) {
|
||||||
|
const modelHeader = `🤖 *${model}* | 🌡️ *${aiTemperature}*\n\n`;
|
||||||
|
await rateLimiter.editMessageWithRetry(
|
||||||
|
ctx,
|
||||||
|
ctx.chat.id,
|
||||||
|
replyGenerating.message_id,
|
||||||
|
modelHeader + aiResponse.response,
|
||||||
|
{ parse_mode: 'Markdown' }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const error = Strings.unexpectedErr.replace("{error}", aiResponse.error);
|
||||||
|
await rateLimiter.editMessageWithRetry(
|
||||||
|
ctx,
|
||||||
|
ctx.chat.id,
|
||||||
|
replyGenerating.message_id,
|
||||||
|
error,
|
||||||
|
{ parse_mode: 'Markdown' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserWithStringsAndModel(ctx: Context, db: NodePgDatabase<typeof schema>): Promise<{ user: User; Strings: ReturnType<typeof getStrings>; languageCode: string; customAiModel: string; aiTemperature: number }> {
|
||||||
|
const userArr = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(ctx.from!.id)), limit: 1 });
|
||||||
|
let user = userArr[0];
|
||||||
|
if (!user) {
|
||||||
|
await ensureUserInDb(ctx, db);
|
||||||
|
const newUserArr = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(ctx.from!.id)), limit: 1 });
|
||||||
|
user = newUserArr[0];
|
||||||
|
const Strings = getStrings(user.languageCode);
|
||||||
|
return { user, Strings, languageCode: user.languageCode, customAiModel: user.customAiModel, aiTemperature: user.aiTemperature };
|
||||||
|
}
|
||||||
|
const Strings = getStrings(user.languageCode);
|
||||||
|
return { user, Strings, languageCode: user.languageCode, customAiModel: user.customAiModel, aiTemperature: user.aiTemperature };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (bot: Telegraf<Context>, db: NodePgDatabase<typeof schema>) => {
|
||||||
const botName = bot.botInfo?.first_name && bot.botInfo?.last_name ? `${bot.botInfo.first_name} ${bot.botInfo.last_name}` : "Kowalski"
|
const botName = bot.botInfo?.first_name && bot.botInfo?.last_name ? `${bot.botInfo.first_name} ${bot.botInfo.last_name}` : "Kowalski"
|
||||||
|
|
||||||
bot.command(["ask", "think"], spamwatchMiddleware, async (ctx) => {
|
bot.command(["ask", "think"], spamwatchMiddleware, async (ctx) => {
|
||||||
|
@ -244,65 +396,75 @@ export default (bot: Telegraf<Context>) => {
|
||||||
const model = isAsk ? flash_model : thinking_model
|
const model = isAsk ? flash_model : thinking_model
|
||||||
const textCtx = ctx as TextContext
|
const textCtx = ctx as TextContext
|
||||||
const reply_to_message_id = replyToMessageId(textCtx)
|
const reply_to_message_id = replyToMessageId(textCtx)
|
||||||
const Strings = getStrings(languageCode(textCtx))
|
const { Strings, aiTemperature } = await getUserWithStringsAndModel(textCtx, db)
|
||||||
const message = textCtx.message.text
|
const message = textCtx.message.text
|
||||||
const author = ("@" + ctx.from?.username) || ctx.from?.first_name
|
const author = ("@" + ctx.from?.username) || ctx.from?.first_name
|
||||||
|
|
||||||
logger.logCmdStart(author, model === flash_model ? "ask" : "think")
|
logger.logCmdStart(author, model === flash_model ? "ask" : "think")
|
||||||
|
|
||||||
if (!process.env.ollamaApi) {
|
if (!process.env.ollamaApi) {
|
||||||
await ctx.reply(Strings.aiDisabled, {
|
await ctx.reply(Strings.ai.disabled, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...({ reply_to_message_id })
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyGenerating = await ctx.reply(Strings.askGenerating.replace("{model}", model), {
|
const fixedMsg = message.replace(/^\/(ask|think)(@\w+)?\s*/, "").trim()
|
||||||
parse_mode: 'Markdown',
|
|
||||||
...({ reply_to_message_id })
|
|
||||||
})
|
|
||||||
|
|
||||||
const fixedMsg = message.replace(/\/(ask|think) /, "")
|
|
||||||
if (fixedMsg.length < 1) {
|
if (fixedMsg.length < 1) {
|
||||||
await ctx.reply(Strings.askNoMessage, {
|
await ctx.reply(Strings.ai.askNoMessage, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...({ reply_to_message_id })
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const replyGenerating = await ctx.reply(Strings.ai.askGenerating.replace("{model}", model), {
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
...({ reply_to_message_id })
|
||||||
|
})
|
||||||
|
|
||||||
logger.logPrompt(fixedMsg)
|
logger.logPrompt(fixedMsg)
|
||||||
|
|
||||||
const prompt = sanitizeForJson(
|
const prompt = sanitizeForJson(await usingSystemPrompt(textCtx, db, botName))
|
||||||
`You are a plaintext-only, helpful assistant called ${botName}.
|
await handleAiReply(textCtx, db, model, prompt, replyGenerating, aiTemperature)
|
||||||
Current Date/Time (UTC): ${new Date().toLocaleString()}
|
})
|
||||||
|
|
||||||
---
|
bot.command(["ai"], 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, customAiModel, aiTemperature } = await getUserWithStringsAndModel(textCtx, db)
|
||||||
|
const message = textCtx.message.text
|
||||||
|
const author = ("@" + ctx.from?.username) || ctx.from?.first_name
|
||||||
|
|
||||||
Respond to the user's message:
|
logger.logCmdStart(author, "ask")
|
||||||
${fixedMsg}`)
|
|
||||||
const aiResponse = await getResponse(prompt, textCtx, replyGenerating, model)
|
|
||||||
if (!aiResponse) return
|
|
||||||
|
|
||||||
if (!ctx.chat) return
|
if (!process.env.ollamaApi) {
|
||||||
if (aiResponse.success && aiResponse.response) {
|
await ctx.reply(Strings.ai.disabled, {
|
||||||
await rateLimiter.editMessageWithRetry(
|
parse_mode: 'Markdown',
|
||||||
ctx,
|
...({ reply_to_message_id })
|
||||||
ctx.chat.id,
|
})
|
||||||
replyGenerating.message_id,
|
|
||||||
aiResponse.response,
|
|
||||||
{ parse_mode: 'Markdown' }
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const error = Strings.unexpectedErr.replace("{error}", aiResponse.error)
|
|
||||||
await rateLimiter.editMessageWithRetry(
|
const fixedMsg = message.replace(/^\/ai(@\w+)?\s*/, "").trim()
|
||||||
ctx,
|
if (fixedMsg.length < 1) {
|
||||||
ctx.chat.id,
|
await ctx.reply(Strings.ai.askNoMessage, {
|
||||||
replyGenerating.message_id,
|
parse_mode: 'Markdown',
|
||||||
error,
|
...({ reply_to_message_id })
|
||||||
{ parse_mode: 'Markdown' }
|
})
|
||||||
)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyGenerating = await ctx.reply(Strings.ai.askGenerating.replace("{model}", customAiModel), {
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
...({ reply_to_message_id })
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.logPrompt(fixedMsg)
|
||||||
|
|
||||||
|
const prompt = sanitizeForJson(await usingSystemPrompt(textCtx, db, botName))
|
||||||
|
await handleAiReply(textCtx, db, customAiModel, prompt, replyGenerating, aiTemperature)
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -9,87 +9,87 @@ import { languageCode } from '../utils/language-code';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export const duckHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
bot.command("duck", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
try {
|
try {
|
||||||
const response = await axios(Resources.duckApi);
|
const response = await axios(Resources.duckApi);
|
||||||
ctx.replyWithPhoto(response.data.url, {
|
ctx.replyWithPhoto(response.data.url, {
|
||||||
caption: "🦆",
|
caption: "🦆",
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const message = Strings.duckApiErr.replace('{error}', error.message);
|
const message = Strings.duckApiErr.replace('{error}', error.message);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
bot.command("fox", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
export const foxHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
try {
|
try {
|
||||||
const response = await axios(Resources.foxApi);
|
const response = await axios(Resources.foxApi);
|
||||||
ctx.replyWithPhoto(response.data.image, {
|
ctx.replyWithPhoto(response.data.image, {
|
||||||
caption: "🦊",
|
caption: "🦊",
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.foxApiErr.replace('{error}', error.message);
|
const message = Strings.foxApiErr.replace('{error}', error.message);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
bot.command("dog", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
export const dogHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
try {
|
try {
|
||||||
const response = await axios(Resources.dogApi);
|
const response = await axios(Resources.dogApi);
|
||||||
ctx.replyWithPhoto(response.data.message, {
|
ctx.replyWithPhoto(response.data.message, {
|
||||||
caption: "🐶",
|
caption: "🐶",
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.foxApiErr.replace('{error}', error.message);
|
const message = Strings.dogApiErr.replace('{error}', error.message);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
bot.command("cat", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
export const catHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const apiUrl = `${Resources.catApi}?json=true`;
|
const apiUrl = `${Resources.catApi}?json=true`;
|
||||||
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
|
try {
|
||||||
const response = await axios.get(apiUrl);
|
const response = await axios.get(apiUrl);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
const imageUrl = `${data.url}`;
|
const imageUrl = `${data.url}`;
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ctx.replyWithPhoto(imageUrl, {
|
await ctx.replyWithPhoto(imageUrl, {
|
||||||
caption: `🐱`,
|
caption: `🐱`,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(Strings.catImgErr, {
|
const message = Strings.catImgErr.replace('{error}', error.message);
|
||||||
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
bot.command(['soggy', 'soggycat'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
export const soggyHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
const userInput = ctx.message.text.split(' ')[1];
|
const userInput = ctx.message.text.split(' ')[1];
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ export default (bot: Telegraf<Context>) => {
|
||||||
Resources.soggyCat2, {
|
Resources.soggyCat2, {
|
||||||
caption: Resources.soggyCat2,
|
caption: Resources.soggyCat2,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ export default (bot: Telegraf<Context>) => {
|
||||||
Resources.soggyCatAlt, {
|
Resources.soggyCatAlt, {
|
||||||
caption: Resources.soggyCatAlt,
|
caption: Resources.soggyCatAlt,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -124,9 +124,16 @@ export default (bot: Telegraf<Context>) => {
|
||||||
Resources.soggyCat, {
|
Resources.soggyCat, {
|
||||||
caption: Resources.soggyCat,
|
caption: Resources.soggyCat,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default (bot: Telegraf<Context>) => {
|
||||||
|
bot.command("duck", spamwatchMiddleware, duckHandler);
|
||||||
|
bot.command("fox", spamwatchMiddleware, foxHandler);
|
||||||
|
bot.command("dog", spamwatchMiddleware, dogHandler);
|
||||||
|
bot.command("cat", spamwatchMiddleware, catHandler);
|
||||||
|
bot.command(['soggy', 'soggycat'], spamwatchMiddleware, soggyHandler);
|
||||||
}
|
}
|
|
@ -5,8 +5,9 @@ import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import verifyInput from '../plugins/verifyInput';
|
import verifyInput from '../plugins/verifyInput';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
import { languageCode } from '../utils/language-code';
|
|
||||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||||
|
import * as schema from '../db/schema';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
|
@ -29,10 +30,31 @@ export async function getDeviceByCodename(codename: string): Promise<Device | nu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
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 } }) => {
|
bot.command(['codename', 'whatis'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const userInput = ctx.message.text.split(" ").slice(1).join(" ");
|
const userInput = ctx.message.text.split(" ").slice(1).join(" ");
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const { noCodename } = Strings.codenameCheck;
|
const { noCodename } = Strings.codenameCheck;
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,32 @@ import os from 'os';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { error } from 'console';
|
import { error } from 'console';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
import { languageCode } from '../utils/language-code';
|
import * as schema from '../db/schema';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
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() {
|
function getGitCommitHash() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec('git rev-parse --short HEAD', (error, stdout, stderr) => {
|
exec('git rev-parse --short HEAD', (error, stdout, stderr) => {
|
||||||
|
@ -55,7 +77,7 @@ function getSystemInfo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAdminCommand(ctx: Context & { message: { text: string } }, action: () => Promise<void>, successMessage: string, errorMessage: string) {
|
async function handleAdminCommand(ctx: Context & { message: { text: string } }, action: () => Promise<void>, successMessage: string, errorMessage: string) {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx);
|
||||||
const userId = ctx.from?.id;
|
const userId = ctx.from?.id;
|
||||||
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
|
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
|
||||||
if (userId && adminArray.includes(userId)) {
|
if (userId && adminArray.includes(userId)) {
|
||||||
|
@ -64,80 +86,72 @@ async function handleAdminCommand(ctx: Context & { message: { text: string } },
|
||||||
if (successMessage) {
|
if (successMessage) {
|
||||||
ctx.reply(successMessage, {
|
ctx.reply(successMessage, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(errorMessage.replace(/{error}/g, error.message), {
|
ctx.reply(errorMessage.replace(/{error}/g, error.message), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.reply(Strings.noPermission, {
|
ctx.reply(Strings.noPermission, {
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export default (bot: Telegraf<Context>, db) => {
|
||||||
bot.command('getbotstats', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('getbotstats', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
const stats = getSystemInfo();
|
const stats = getSystemInfo();
|
||||||
await ctx.reply(stats, {
|
await ctx.reply(stats, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}, '', Strings.errorRetrievingStats);
|
}, '', Strings.errorRetrievingStats);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('getbotcommit', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('getbotcommit', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
try {
|
try {
|
||||||
const commitHash = await getGitCommitHash();
|
const commitHash = await getGitCommitHash();
|
||||||
await ctx.reply(Strings.gitCurrentCommit.replace(/{commitHash}/g, commitHash), {
|
await ctx.reply(Strings.gitCurrentCommit.replace(/{commitHash}/g, commitHash), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(Strings.gitErrRetrievingCommit.replace(/{error}/g, error), {
|
ctx.reply(Strings.gitErrRetrievingCommit.replace(/{error}/g, error), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, '', Strings.gitErrRetrievingCommit);
|
}, '', Strings.gitErrRetrievingCommit);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('updatebot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('updatebot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
try {
|
try {
|
||||||
const result = await updateBot();
|
const result = await updateBot();
|
||||||
await ctx.reply(Strings.botUpdated.replace(/{result}/g, result), {
|
await ctx.reply(Strings.botUpdated.replace(/{result}/g, result), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(Strings.errorUpdatingBot.replace(/{error}/g, error), {
|
ctx.reply(Strings.errorUpdatingBot.replace(/{error}/g, error), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, '', Strings.errorUpdatingBot);
|
}, '', Strings.errorUpdatingBot);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('setbotname', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('setbotname', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const botName = ctx.message.text.split(' ').slice(1).join(' ');
|
const botName = ctx.message.text.split(' ').slice(1).join(' ');
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
await ctx.telegram.setMyName(botName);
|
await ctx.telegram.setMyName(botName);
|
||||||
|
@ -145,7 +159,7 @@ export default (bot: Telegraf<Context>) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('setbotdesc', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('setbotdesc', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const botDesc = ctx.message.text.split(' ').slice(1).join(' ');
|
const botDesc = ctx.message.text.split(' ').slice(1).join(' ');
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
await ctx.telegram.setMyDescription(botDesc);
|
await ctx.telegram.setMyDescription(botDesc);
|
||||||
|
@ -153,34 +167,31 @@ export default (bot: Telegraf<Context>) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('botkickme', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('botkickme', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
handleAdminCommand(ctx, async () => {
|
handleAdminCommand(ctx, async () => {
|
||||||
if (!ctx.chat) {
|
if (!ctx.chat) {
|
||||||
ctx.reply(Strings.chatNotFound, {
|
ctx.reply(Strings.chatNotFound, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.reply(Strings.kickingMyself, {
|
ctx.reply(Strings.kickingMyself, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
await ctx.telegram.leaveChat(ctx.chat.id);
|
await ctx.telegram.leaveChat(ctx.chat.id);
|
||||||
}, '', Strings.kickingMyselfErr);
|
}, '', Strings.kickingMyselfErr);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('getfile', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('getfile', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const botFile = ctx.message.text.split(' ').slice(1).join(' ');
|
const botFile = ctx.message.text.split(' ').slice(1).join(' ');
|
||||||
|
|
||||||
if (!botFile) {
|
if (!botFile) {
|
||||||
ctx.reply(Strings.noFileProvided, {
|
ctx.reply(Strings.noFileProvided, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -192,14 +203,12 @@ export default (bot: Telegraf<Context>) => {
|
||||||
source: botFile,
|
source: botFile,
|
||||||
caption: botFile
|
caption: botFile
|
||||||
}, {
|
}, {
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), {
|
ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, '', Strings.unexpectedErr);
|
}, '', Strings.unexpectedErr);
|
||||||
|
@ -217,21 +226,18 @@ export default (bot: Telegraf<Context>) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return ctx.reply(`\`${error.message}\``, {
|
return ctx.reply(`\`${error.message}\``, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
return ctx.reply(`\`${stderr}\``, {
|
return ctx.reply(`\`${stderr}\``, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ctx.reply(`\`${stdout}\``, {
|
ctx.reply(`\`${stdout}\``, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, '', "Nope!");
|
}, '', "Nope!");
|
||||||
|
@ -247,14 +253,12 @@ export default (bot: Telegraf<Context>) => {
|
||||||
const result = eval(code);
|
const result = eval(code);
|
||||||
ctx.reply(`Result: ${result}`, {
|
ctx.reply(`Result: ${result}`, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(`Error: ${error.message}`, {
|
ctx.reply(`Error: ${error.message}`, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,46 +3,63 @@ import { getStrings } from '../plugins/checklang';
|
||||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
import { languageCode } from '../utils/language-code';
|
import * as schema from '../db/schema';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
function sendRandomReply(ctx: Context & { message: { text: string } }, gifUrl: string, textKey: string) {
|
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
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 randomNumber = Math.floor(Math.random() * 100);
|
||||||
const shouldSendGif = randomNumber > 50;
|
const shouldSendGif = randomNumber > 50;
|
||||||
|
const caption = Strings[textKey].replace('{randomNum}', randomNumber);
|
||||||
const caption = Strings[textKey].replace('{randomNum}', randomNumber)
|
|
||||||
|
|
||||||
if (shouldSendGif) {
|
if (shouldSendGif) {
|
||||||
ctx.replyWithAnimation(gifUrl, {
|
ctx.replyWithAnimation(gifUrl, {
|
||||||
caption,
|
caption,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
const gifErr = Strings.gifErr.replace('{err}', err);
|
const gifErr = Strings.gifErr.replace('{err}', err);
|
||||||
ctx.reply(gifErr, {
|
ctx.reply(gifErr, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ctx.reply(caption, {
|
ctx.reply(caption, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDiceCommand(ctx: Context & { message: { text: string } }, emoji: string, delay: number, db: any) {
|
||||||
async function handleDiceCommand(ctx: Context & { message: { text: string } }, emoji: string, delay: number) {
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const Strings = getStrings(languageCode(ctx));
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const result = await ctx.sendDice({ emoji, reply_to_message_id: ctx.message.message_id });
|
const result = await ctx.sendDice({ emoji, ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) });
|
||||||
const botResponse = Strings.funEmojiResult
|
const botResponse = Strings.funEmojiResult
|
||||||
.replace('{emoji}', result.dice.emoji)
|
.replace('{emoji}', result.dice.emoji)
|
||||||
.replace('{value}', result.dice.value);
|
.replace('{value}', result.dice.value);
|
||||||
|
@ -50,8 +67,7 @@ async function handleDiceCommand(ctx: Context & { message: { text: string } }, e
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ctx.reply(botResponse, {
|
ctx.reply(botResponse, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
@ -60,54 +76,53 @@ function getRandomInt(max: number) {
|
||||||
return Math.floor(Math.random() * (max + 1));
|
return Math.floor(Math.random() * (max + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export default (bot: Telegraf<Context>, db) => {
|
||||||
bot.command('random', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('random', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const randomValue = getRandomInt(10);
|
const randomValue = getRandomInt(10);
|
||||||
const randomVStr = Strings.randomNum.replace('{number}', randomValue);
|
const randomVStr = Strings.randomNum.replace('{number}', randomValue);
|
||||||
|
|
||||||
ctx.reply(
|
ctx.reply(
|
||||||
randomVStr, {
|
randomVStr, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: maybe send custom stickers to match result of the roll? i think there are pre-existing ones
|
// 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 } }) => {
|
bot.command('dice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
await handleDiceCommand(ctx, '🎲', 4000);
|
await handleDiceCommand(ctx, '🎲', 4000, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('slot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('slot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
await handleDiceCommand(ctx, '🎰', 3000);
|
await handleDiceCommand(ctx, '<EFBFBD><EFBFBD>', 3000, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('ball', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('ball', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
await handleDiceCommand(ctx, '⚽', 3000);
|
await handleDiceCommand(ctx, '⚽', 3000, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('dart', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('dart', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
await handleDiceCommand(ctx, '🎯', 3000);
|
await handleDiceCommand(ctx, '🎯', 3000, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('bowling', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('bowling', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
await handleDiceCommand(ctx, '🎳', 3000);
|
await handleDiceCommand(ctx, '🎳', 3000, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('idice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('idice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
ctx.replyWithSticker(
|
ctx.replyWithSticker(
|
||||||
Resources.infiniteDice, {
|
Resources.infiniteDice, {
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('furry', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('furry', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
sendRandomReply(ctx, Resources.furryGif, 'furryAmount');
|
sendRandomReply(ctx, Resources.furryGif, 'furryAmount', db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('gay', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('gay', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
sendRandomReply(ctx, Resources.gayFlag, 'gayAmount');
|
sendRandomReply(ctx, Resources.gayFlag, 'gayAmount', db);
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -9,6 +9,8 @@ import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { parse } from 'node-html-parser';
|
import { parse } from 'node-html-parser';
|
||||||
import { getDeviceByCodename } from './codename';
|
import { getDeviceByCodename } from './codename';
|
||||||
|
import { getStrings } from '../plugins/checklang';
|
||||||
|
import { languageCode } from '../utils/language-code';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
|
@ -207,68 +209,130 @@ function getUsername(ctx){
|
||||||
return userName;
|
return userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceSelectionCache: Record<number, { results: PhoneSearchResult[], timeout: NodeJS.Timeout }> = {};
|
||||||
|
const lastSelectionMessageId: Record<number, number> = {};
|
||||||
|
|
||||||
export default (bot) => {
|
export default (bot) => {
|
||||||
bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => {
|
bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => {
|
||||||
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("Please provide the phone name.", { reply_to_message_id: ctx.message.message_id });
|
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 } } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[GSMArena] Searching for", phone);
|
console.log("[GSMArena] Searching for", phone);
|
||||||
const statusMsg = await ctx.reply(`Searching for \`${phone}\`...`, { reply_to_message_id: ctx.message.message_id, parse_mode: 'Markdown' });
|
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);
|
let results = await searchPhone(phone);
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
const codenameResults = await getDeviceByCodename(phone.split(" ")[0]);
|
const codenameResults = await getDeviceByCodename(phone.split(" ")[0]);
|
||||||
if (!codenameResults) {
|
if (!codenameResults) {
|
||||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, `No phones found for \`${phone}\`.`, { parse_mode: 'Markdown' });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, `Searching for ${codenameResults.name}...`, { parse_mode: 'Markdown' });
|
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);
|
const nameResults = await searchPhone(codenameResults.name);
|
||||||
if (nameResults.length === 0) {
|
if (nameResults.length === 0) {
|
||||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, `No phones found for \`${codenameResults.name}\` and \`${phone}\`.`, { parse_mode: 'Markdown' });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
results = nameResults;
|
results = nameResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testUser = `<a href=\"tg://user?id=${userId}\">${userName}</a>, please select your device:`;
|
if (deviceSelectionCache[userId]?.timeout) {
|
||||||
|
clearTimeout(deviceSelectionCache[userId].timeout);
|
||||||
|
}
|
||||||
|
deviceSelectionCache[userId] = {
|
||||||
|
results,
|
||||||
|
timeout: setTimeout(() => { delete deviceSelectionCache[userId]; }, 5 * 60 * 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
const options = {
|
||||||
parse_mode: 'HTML',
|
parse_mode: 'HTML',
|
||||||
reply_to_message_id: ctx.message.message_id,
|
reply_to_message_id: ctx.message.message_id,
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: results.map(result => [{ text: result.name, callback_data: `details:${result.url}:${ctx.from.id}` }])
|
inline_keyboard: results.map((result, idx) => {
|
||||||
|
const callbackData = `gsmadetails:${idx}:${ctx.from.id}`;
|
||||||
|
return [{ text: result.name, callback_data: callbackData }];
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, testUser, options);
|
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(/details:(.+):(.+)/, async (ctx) => {
|
bot.action(/gsmadetails:(\d+):(\d+)/, async (ctx) => {
|
||||||
const url = ctx.match[1];
|
const idx = parseInt(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}, you are not allowed to interact with this.`);
|
return ctx.answerCbQuery(`${userName}, ${Strings.gsmarenaNotAllowed || "[TODO: Add gsmarenaNotAllowed to locales] 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>, 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>, ${Strings.gsmarenaDeviceDetails || "[TODO: Add gsmarenaDeviceDetails to locales] these are the details of your device:"}</b>` + message, { parse_mode: 'HTML', disable_web_page_preview: false });
|
||||||
} else {
|
} else {
|
||||||
ctx.reply("Error fetching phone details.", { reply_to_message_id: ctx.message.message_id });
|
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 } } : {}) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
import { getStrings } from '../plugins/checklang';
|
import { getStrings } from '../plugins/checklang';
|
||||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import { languageCode } from '../utils/language-code';
|
import type { Context } from 'telegraf';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
interface MessageOptions {
|
interface MessageOptions {
|
||||||
parse_mode: string;
|
parse_mode: string;
|
||||||
disable_web_page_preview: boolean;
|
disable_web_page_preview: boolean;
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: { text: any; callback_data: string; }[][];
|
inline_keyboard: { text: string; callback_data: string; }[][];
|
||||||
};
|
};
|
||||||
reply_to_message_id?: number;
|
reply_to_message_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendHelpMessage(ctx, isEditing) {
|
async function sendHelpMessage(ctx, isEditing, db) {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const botInfo = await ctx.telegram.getMe();
|
const botInfo = await ctx.telegram.getMe();
|
||||||
const helpText = Strings.botHelp
|
const helpText = Strings.botHelp
|
||||||
.replace(/{botName}/g, botInfo.first_name)
|
.replace(/{botName}/g, botInfo.first_name)
|
||||||
|
@ -33,14 +50,14 @@ async function sendHelpMessage(ctx, isEditing) {
|
||||||
[{ 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' }]
|
[{ text: Strings.ai.helpEntry, callback_data: 'helpAi' }]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (includeReplyTo) {
|
if (includeReplyTo) {
|
||||||
const messageId = getMessageId(ctx);
|
const messageId = getMessageId(ctx);
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
options.reply_to_message_id = messageId;
|
(options as any).reply_parameters = { message_id: messageId };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return options;
|
return options;
|
||||||
|
@ -52,25 +69,22 @@ async function sendHelpMessage(ctx, isEditing) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot) => {
|
export default (bot, db) => {
|
||||||
bot.help(spamwatchMiddleware, async (ctx) => {
|
bot.help(spamwatchMiddleware, async (ctx) => {
|
||||||
await sendHelpMessage(ctx, false);
|
await sendHelpMessage(ctx, false, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("about", spamwatchMiddleware, async (ctx) => {
|
bot.command("about", spamwatchMiddleware, async (ctx) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`);
|
const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`);
|
||||||
ctx.reply(aboutMsg, {
|
ctx.reply(aboutMsg, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
|
|
||||||
bot.on('callback_query', async (ctx) => {
|
const options = (Strings) => ({
|
||||||
const callbackData = ctx.callbackQuery.data;
|
|
||||||
const Strings = getStrings(languageCode(ctx));
|
|
||||||
const options = {
|
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_markup: JSON.stringify({
|
reply_markup: JSON.stringify({
|
||||||
|
@ -78,52 +92,55 @@ export default (bot) => {
|
||||||
[{ text: Strings.varStrings.varBack, callback_data: 'helpBack' }],
|
[{ text: Strings.varStrings.varBack, callback_data: 'helpBack' }],
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
};
|
});
|
||||||
|
|
||||||
switch (callbackData) {
|
bot.action('helpMain', async (ctx) => {
|
||||||
case 'helpMain':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.mainCommandsDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.mainCommandsDesc, options);
|
});
|
||||||
break;
|
bot.action('helpUseful', async (ctx) => {
|
||||||
case 'helpUseful':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.usefulCommandsDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.usefulCommandsDesc, options);
|
});
|
||||||
break;
|
bot.action('helpInteractive', async (ctx) => {
|
||||||
case 'helpInteractive':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.interactiveEmojisDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.interactiveEmojisDesc, options);
|
});
|
||||||
break;
|
bot.action('helpFunny', async (ctx) => {
|
||||||
case 'helpFunny':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.funnyCommandsDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.funnyCommandsDesc, options);
|
});
|
||||||
break;
|
bot.action('helpLast', async (ctx) => {
|
||||||
case 'helpLast':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.lastFm.helpDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.lastFm.helpDesc, options);
|
});
|
||||||
break;
|
bot.action('helpYouTube', async (ctx) => {
|
||||||
case 'helpYouTube':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.ytDownload.helpDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.ytDownload.helpDesc, options);
|
});
|
||||||
break;
|
bot.action('helpAnimals', async (ctx) => {
|
||||||
case 'helpAnimals':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.animalCommandsDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.animalCommandsDesc, options);
|
});
|
||||||
break;
|
bot.action('helpMLP', async (ctx) => {
|
||||||
case 'helpMLP':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.ponyApi.helpDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.ponyApi.helpDesc, options);
|
});
|
||||||
break;
|
bot.action('helpAi', async (ctx) => {
|
||||||
case 'helpAi':
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
await ctx.editMessageText(Strings.ai.helpDesc, options(Strings));
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await ctx.editMessageText(Strings.aiCmdsDesc, options);
|
});
|
||||||
break;
|
bot.action('helpBack', async (ctx) => {
|
||||||
case 'helpBack':
|
await sendHelpMessage(ctx, true, db);
|
||||||
await ctx.answerCbQuery();
|
await ctx.answerCbQuery();
|
||||||
await sendHelpMessage(ctx, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
await ctx.answerCbQuery(Strings.errInvalidOption);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,37 @@ import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import verifyInput from '../plugins/verifyInput';
|
import verifyInput from '../plugins/verifyInput';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
|
import * as schema from '../db/schema';
|
||||||
import { languageCode } from '../utils/language-code';
|
import { languageCode } from '../utils/language-code';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
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 } }) => {
|
bot.command("http", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const reply_to_message_id = ctx.message.message_id;
|
const reply_to_message_id = ctx.message.message_id;
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
const userInput = ctx.message.text.split(' ')[1];
|
const userInput = ctx.message.text.split(' ')[1];
|
||||||
const apiUrl = Resources.httpApi;
|
const apiUrl = Resources.httpApi;
|
||||||
const { invalidCode } = Strings.httpCodes
|
const { invalidCode } = Strings.httpCodes
|
||||||
|
@ -34,19 +57,19 @@ export default (bot: Telegraf<Context>) => {
|
||||||
.replace("{description}", codeInfo.description);
|
.replace("{description}", codeInfo.description);
|
||||||
await ctx.reply(message, {
|
await ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await ctx.reply(Strings.httpCodes.notFound, {
|
await ctx.reply(Strings.httpCodes.notFound, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.httpCodes.fetchErr.replace("{error}", error);
|
const message = Strings.httpCodes.fetchErr.replace('{error}', error);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -63,7 +86,7 @@ export default (bot: Telegraf<Context>) => {
|
||||||
if (userInput.length !== 3) {
|
if (userInput.length !== 3) {
|
||||||
ctx.reply(Strings.httpCodes.invalidCode, {
|
ctx.reply(Strings.httpCodes.invalidCode, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -74,12 +97,12 @@ export default (bot: Telegraf<Context>) => {
|
||||||
await ctx.replyWithPhoto(apiUrl, {
|
await ctx.replyWithPhoto(apiUrl, {
|
||||||
caption: `🐱 ${apiUrl}`,
|
caption: `🐱 ${apiUrl}`,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.reply(Strings.catImgErr, {
|
ctx.reply(Strings.catImgErr, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,64 +2,81 @@ import { getStrings } from '../plugins/checklang';
|
||||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
|
import * as schema from '../db/schema';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
async function getUserInfo(ctx: Context & { message: { text: string } }) {
|
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||||
const Strings = getStrings(ctx.from?.language_code || 'en');
|
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;
|
let lastName = ctx.from?.last_name;
|
||||||
if (lastName === undefined) {
|
if (lastName === undefined) {
|
||||||
lastName = " ";
|
lastName = " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
const userInfo = Strings.userInfo
|
const userInfo = Strings.userInfo
|
||||||
.replace('{userName}', `${ctx.from?.first_name} ${lastName}` || Strings.varStrings.varUnknown)
|
.replace('{userName}', `${ctx.from?.first_name} ${lastName}` || Strings.varStrings.varUnknown)
|
||||||
.replace('{userId}', ctx.from?.id || Strings.varStrings.varUnknown)
|
.replace('{userId}', ctx.from?.id || Strings.varStrings.varUnknown)
|
||||||
.replace('{userHandle}', ctx.from?.username ? `@${ctx.from?.username}` : Strings.varStrings.varNone)
|
.replace('{userHandle}', ctx.from?.username ? `@${ctx.from?.username}` : Strings.varStrings.varNone)
|
||||||
.replace('{userPremium}', ctx.from?.is_premium ? Strings.varStrings.varYes : Strings.varStrings.varNo)
|
.replace('{userPremium}', ctx.from?.is_premium ? Strings.varStrings.varYes : Strings.varStrings.varNo)
|
||||||
.replace('{userLang}', ctx.from?.language_code || Strings.varStrings.varUnknown);
|
.replace('{userLang}', ctx.from?.language_code || Strings.varStrings.varUnknown);
|
||||||
|
|
||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChatInfo(ctx: Context & { message: { text: string } }) {
|
async function getChatInfo(ctx: Context & { message: { text: string } }, db: any) {
|
||||||
const Strings = getStrings(ctx.from?.language_code || 'en');
|
const { Strings } = await getUserAndStrings(ctx, db);
|
||||||
if (ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup') {
|
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
|
const chatInfo = Strings.chatInfo
|
||||||
.replace('{chatId}', ctx.chat?.id || Strings.varStrings.varUnknown)
|
.replace('{chatId}', chat?.id || Strings.varStrings.varUnknown)
|
||||||
.replace('{chatName}', ctx.chat?.title || Strings.varStrings.varUnknown)
|
.replace('{chatName}', chat?.title || Strings.varStrings.varUnknown)
|
||||||
// @ts-ignore
|
.replace('{chatHandle}', chat?.username ? `@${chat.username}` : Strings.varStrings.varNone)
|
||||||
.replace('{chatHandle}', ctx.chat?.username ? `@${ctx.chat?.username}` : Strings.varStrings.varNone)
|
|
||||||
.replace('{chatMembersCount}', await ctx.getChatMembersCount())
|
.replace('{chatMembersCount}', await ctx.getChatMembersCount())
|
||||||
.replace('{chatType}', ctx.chat?.type || Strings.varStrings.varUnknown)
|
.replace('{chatType}', chat?.type || Strings.varStrings.varUnknown)
|
||||||
// @ts-ignore
|
.replace('{isForum}', chat?.is_forum ? Strings.varStrings.varYes : Strings.varStrings.varNo);
|
||||||
.replace('{isForum}', ctx.chat?.is_forum ? Strings.varStrings.varYes : Strings.varStrings.varNo);
|
|
||||||
|
|
||||||
return chatInfo;
|
return chatInfo;
|
||||||
} else {
|
} else {
|
||||||
return Strings.groupOnly
|
return Strings.groupOnly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export default (bot: Telegraf<Context>, db) => {
|
||||||
bot.command('chatinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('chatinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const chatInfo = await getChatInfo(ctx);
|
const chatInfo = await getChatInfo(ctx, db);
|
||||||
ctx.reply(
|
ctx.reply(
|
||||||
chatInfo, {
|
chatInfo, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('userinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command('userinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const userInfo = await getUserInfo(ctx);
|
const userInfo = await getUserInfo(ctx, db);
|
||||||
ctx.reply(
|
ctx.reply(
|
||||||
userInfo, {
|
userInfo, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
// @ts-ignore
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
reply_to_message_id: ctx.message.message_id
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default (bot) => {
|
||||||
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,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ export default (bot) => {
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ export default (bot) => {
|
||||||
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,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ export default (bot) => {
|
||||||
return ctx.reply(noRecent, {
|
return ctx.reply(noRecent, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ export default (bot) => {
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,13 +200,13 @@ export default (bot) => {
|
||||||
caption: message,
|
caption: message,
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { 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,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -217,7 +217,7 @@ export default (bot) => {
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
reply_to_message_id: ctx.message.message_id
|
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,31 +3,390 @@ import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||||
import { Context, Telegraf } from 'telegraf';
|
import { Context, Telegraf } from 'telegraf';
|
||||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||||
import { languageCode } from '../utils/language-code';
|
import * as schema from '../db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { ensureUserInDb } from '../utils/ensure-user';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
import { models } from './ai';
|
||||||
|
import { langs } from '../locales/config';
|
||||||
|
|
||||||
|
type UserRow = typeof schema.usersTable.$inferSelect;
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
async function getUserAndStrings(ctx: Context, db: NodePgDatabase<typeof schema>): Promise<{ user: UserRow | null, Strings: any, languageCode: string }> {
|
||||||
bot.start(spamwatchMiddleware, async (ctx: Context) => {
|
let user: UserRow | null = null;
|
||||||
const Strings = getStrings(languageCode(ctx));
|
let languageCode = 'en';
|
||||||
const botInfo = await ctx.telegram.getMe();
|
if (!ctx.from) {
|
||||||
const reply_to_message_id = replyToMessageId(ctx)
|
const Strings = getStrings(languageCode);
|
||||||
const startMsg = Strings.botWelcome.replace(/{botName}/g, botInfo.first_name);
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
ctx.reply(startMsg, {
|
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;
|
||||||
|
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' },
|
||||||
|
{ text: `🧠 ${Strings.settings.ai.aiModel}: ${user.customAiModel}`, callback_data: 'settings_aiModel' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ text: `🌡️ ${Strings.settings.ai.aiTemperature}: ${user.aiTemperature}`, callback_data: 'settings_aiTemperature' },
|
||||||
|
{ text: `🌐 ${langLabel}`, callback_data: 'settings_language' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...({ reply_to_message_id })
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('privacy', spamwatchMiddleware, async (ctx: any) => {
|
bot.command(["settings"], spamwatchMiddleware, async (ctx: Context) => {
|
||||||
const Strings = getStrings(ctx.from.language_code);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
const message = Strings.botPrivacy.replace("{botPrivacy}", process.env.botPrivacy);
|
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', async (ctx) => {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settings_aiEnabled callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action('settings_aiModel', async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
await ctx.editMessageText(
|
||||||
|
`${Strings.settings.ai.selectSeries}`,
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: models.map(series => [
|
||||||
|
{ text: series.label, callback_data: `selectseries_${series.name}` }
|
||||||
|
]).concat([[
|
||||||
|
{ text: `⬅️ ${Strings.settings.ai.back}`, callback_data: 'settings_back' }
|
||||||
|
]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settings_aiModel callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action(/^selectseries_.+$/, async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
const data = (ctx.callbackQuery as any).data;
|
||||||
|
const seriesName = data.replace('selectseries_', '');
|
||||||
|
const series = models.find(s => s.name === seriesName);
|
||||||
|
if (!series) return;
|
||||||
|
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)}\n\n${Strings.settings.ai.parameterSizeExplanation}`,
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: series.models.map(m => [
|
||||||
|
{ text: `${m.label} (${m.parameterSize})`, callback_data: `setmodel_${series.name}_${m.name}` }
|
||||||
|
]).concat([[
|
||||||
|
{ text: `⬅️ ${Strings.settings.ai.back}`, callback_data: 'settings_aiModel' }
|
||||||
|
]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling selectseries callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action(/^setmodel_.+$/, async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
const data = (ctx.callbackQuery as any).data;
|
||||||
|
const parts = data.split('_');
|
||||||
|
const seriesName = parts[1];
|
||||||
|
const modelName = parts.slice(2).join('_');
|
||||||
|
const series = models.find(s => s.name === seriesName);
|
||||||
|
const model = series?.models.find(m => m.name === modelName);
|
||||||
|
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) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('[Settings] Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling setmodel callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action('settings_aiTemperature', async (ctx) => {
|
||||||
|
try {
|
||||||
|
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.editMessageReplyMarkup({
|
||||||
|
inline_keyboard: temps.map(t => [{ text: t.toString(), callback_data: `settemp_${t}` }]).concat([[{ text: `⬅️ ${Strings.settings.ai.back}`, callback_data: 'settings_back' }]])
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settings_aiTemperature callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action(/^settemp_.+$/, async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
const data = (ctx.callbackQuery as any).data;
|
||||||
|
const temp = parseFloat(data.replace('settemp_', ''));
|
||||||
|
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);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settemp callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action('settings_language', async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
await ctx.editMessageReplyMarkup({
|
||||||
|
inline_keyboard: langs.map(l => [{ text: l.label, callback_data: `setlang_${l.code}` }]).concat([[{ text: `⬅️ ${Strings.settings.ai.back}`, callback_data: 'settings_back' }]])
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settings_language callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.action('settings_back', async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) return;
|
||||||
|
await updateSettingsKeyboard(ctx, user, Strings);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling settings_back callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
disable_web_page_preview: true,
|
|
||||||
reply_to_message_id: ctx.message.message_id
|
reply_to_message_id: ctx.message.message_id
|
||||||
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bot.action(/^setlang_.+$/, async (ctx) => {
|
||||||
|
try {
|
||||||
|
await ctx.answerCbQuery();
|
||||||
|
const { user } = await getUserAndStrings(ctx, db);
|
||||||
|
if (!user) {
|
||||||
|
console.log('[Settings] No user found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = (ctx.callbackQuery as any).data;
|
||||||
|
const lang = data.replace('setlang_', '');
|
||||||
|
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) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err.response.description?.includes('query is too old') ||
|
||||||
|
err.response.description?.includes('query ID is invalid') ||
|
||||||
|
err.response.description?.includes('message is not modified') ||
|
||||||
|
err.response.description?.includes('message to edit not found')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.error('[Settings] Unexpected Telegram error:', err);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Settings] Error handling setlang callback:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -24,22 +24,17 @@ async function downloadModule(moduleId: string): Promise<ModuleResult | null> {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
});
|
});
|
||||||
|
|
||||||
const disposition = response.headers['content-disposition'];
|
const disposition = response.headers['content-disposition'];
|
||||||
let fileName = moduleId;
|
let fileName = moduleId;
|
||||||
|
|
||||||
if (disposition && disposition.includes('filename=')) {
|
if (disposition && disposition.includes('filename=')) {
|
||||||
fileName = disposition
|
fileName = disposition
|
||||||
.split('filename=')[1]
|
.split('filename=')[1]
|
||||||
.split(';')[0]
|
.split(';')[0]
|
||||||
.replace(/['"]/g, '');
|
.replace(/['"]/g, '');
|
||||||
}
|
}
|
||||||
|
const filePath = path.join(__dirname, fileName);
|
||||||
const filePath = path.resolve(__dirname, fileName);
|
|
||||||
|
|
||||||
const writer = fs.createWriteStream(filePath);
|
const writer = fs.createWriteStream(filePath);
|
||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
writer.on('finish', () => resolve({ filePath, fileName }));
|
writer.on('finish', () => resolve({ filePath, fileName }));
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
|
@ -49,39 +44,41 @@ async function downloadModule(moduleId: string): Promise<ModuleResult | null> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export const modarchiveHandler = async (ctx: Context) => {
|
||||||
bot.command(['modarchive', 'tma'], spamwatchMiddleware, async (ctx) => {
|
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
const moduleId = ctx.message?.text.split(' ')[1];
|
const moduleId = ctx.message && 'text' in ctx.message && typeof ctx.message.text === 'string'
|
||||||
|
? ctx.message.text.split(' ')[1]?.trim()
|
||||||
if (Number.isNaN(moduleId) || null) {
|
: undefined;
|
||||||
|
if (!moduleId || !/^\d+$/.test(moduleId)) {
|
||||||
return ctx.reply(Strings.maInvalidModule, {
|
return ctx.reply(Strings.maInvalidModule, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const numberRegex = /^\d+$/;
|
|
||||||
const isNumber = numberRegex.test(moduleId);
|
|
||||||
if (isNumber) {
|
|
||||||
const result = await downloadModule(moduleId);
|
const result = await downloadModule(moduleId);
|
||||||
if (result) {
|
if (result) {
|
||||||
const { filePath, fileName } = result;
|
const { filePath, fileName } = result;
|
||||||
const regexExtension = /\.\w+$/i;
|
const regexExtension = /\.\w+$/i;
|
||||||
const hasExtension = regexExtension.test(fileName);
|
const hasExtension = regexExtension.test(fileName);
|
||||||
if (hasExtension) {
|
if (hasExtension) {
|
||||||
|
try {
|
||||||
await ctx.replyWithDocument({ source: filePath }, {
|
await ctx.replyWithDocument({ source: filePath }, {
|
||||||
caption: fileName,
|
caption: fileName,
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
fs.unlinkSync(filePath);
|
} finally {
|
||||||
return;
|
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ctx.reply(Strings.maInvalidModule, {
|
return ctx.reply(Strings.maInvalidModule, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default (bot: Telegraf<Context>) => {
|
||||||
|
bot.command(['modarchive', 'tma'], spamwatchMiddleware, modarchiveHandler);
|
||||||
|
};
|
||||||
|
|
|
@ -53,34 +53,38 @@ function capitalizeFirstLetter(letter: string) {
|
||||||
return letter.charAt(0).toUpperCase() + letter.slice(1);
|
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>) => {
|
export default (bot: Telegraf<Context>) => {
|
||||||
bot.command("mlp", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command("mlp", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
|
sendReply(ctx, Strings.ponyApi.helpDesc, reply_to_message_id);
|
||||||
ctx.reply(Strings.ponyApi.helpDesc, {
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
...({ reply_to_message_id, disable_web_page_preview: true })
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("mlpchar", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command("mlpchar", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
|
const { message } = ctx;
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
const Strings = getStrings(languageCode(ctx) || 'en');
|
const Strings = getStrings(languageCode(ctx) || 'en');
|
||||||
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+");
|
const userInput = message.text.split(' ').slice(1).join(' ').trim().replace(/\s+/g, '+');
|
||||||
const { noCharName } = Strings.ponyApi
|
const { noCharName } = Strings.ponyApi;
|
||||||
|
|
||||||
if (verifyInput(ctx, userInput, noCharName)) {
|
if (verifyInput(ctx, userInput, noCharName)) return;
|
||||||
return;
|
if (!userInput || /[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
|
||||||
}
|
return sendReply(ctx, Strings.mlpInvalidCharacter, reply_to_message_id);
|
||||||
|
|
||||||
// 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 })
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const capitalizedInput = capitalizeFirstLetter(userInput);
|
const capitalizedInput = capitalizeFirstLetter(userInput);
|
||||||
|
@ -88,62 +92,29 @@ export default (bot: Telegraf<Context>) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios(apiUrl);
|
const response = await axios(apiUrl);
|
||||||
const charactersArray: Character[] = [];
|
const data = response.data.data;
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
if (Array.isArray(response.data.data)) {
|
const character = data[0];
|
||||||
response.data.data.forEach(character => {
|
const aliases = Array.isArray(character.alias)
|
||||||
let aliases: string[] = [];
|
? character.alias.join(', ')
|
||||||
if (character.alias) {
|
: character.alias || Strings.varStrings.varNone;
|
||||||
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
|
const result = Strings.ponyApi.charRes
|
||||||
.replace("{id}", charactersArray[0].id)
|
.replace("{id}", character.id)
|
||||||
.replace("{name}", charactersArray[0].name)
|
.replace("{name}", character.name)
|
||||||
.replace("{alias}", charactersArray[0].alias)
|
.replace("{alias}", aliases)
|
||||||
.replace("{url}", charactersArray[0].url)
|
.replace("{url}", character.url)
|
||||||
.replace("{sex}", charactersArray[0].sex)
|
.replace("{sex}", character.sex)
|
||||||
.replace("{residence}", charactersArray[0].residence)
|
.replace("{residence}", character.residence ? character.residence.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
|
||||||
.replace("{occupation}", charactersArray[0].occupation)
|
.replace("{occupation}", character.occupation ? character.occupation.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
|
||||||
.replace("{kind}", charactersArray[0].kind);
|
.replace("{kind}", Array.isArray(character.kind) ? character.kind.join(', ') : Strings.varStrings.varNone);
|
||||||
|
sendPhoto(ctx, character.image[0], result, reply_to_message_id);
|
||||||
ctx.replyWithPhoto(charactersArray[0].image[0], {
|
|
||||||
caption: `${result}`,
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
...({ reply_to_message_id, disable_web_page_preview: true })
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
ctx.reply(Strings.ponyApi.noCharFound, {
|
sendReply(ctx, Strings.ponyApi.noCharFound, reply_to_message_id);
|
||||||
parse_mode: 'Markdown',
|
}
|
||||||
...({ 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);
|
||||||
} catch (error) {
|
}
|
||||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
|
||||||
ctx.reply(message, {
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
...({ reply_to_message_id })
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("mlpep", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
bot.command("mlpep", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||||
|
@ -157,10 +128,10 @@ export default (bot: Telegraf<Context>) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number(userInput) > 100) {
|
if (Number(userInput) > 10000) {
|
||||||
ctx.reply(Strings.mlpInvalidEpisode, {
|
ctx.reply(Strings.mlpInvalidEpisode, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -205,21 +176,19 @@ export default (bot: Telegraf<Context>) => {
|
||||||
ctx.replyWithPhoto(episodeArray[0].image, {
|
ctx.replyWithPhoto(episodeArray[0].image, {
|
||||||
caption: `${result}`,
|
caption: `${result}`,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id, disable_web_page_preview: true })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ctx.reply(Strings.ponyApi.noEpisodeFound, {
|
ctx.reply(Strings.ponyApi.noEpisodeFound, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
...({ reply_to_message_id })
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
...({ reply_to_message_id })
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -239,7 +208,7 @@ export default (bot: Telegraf<Context>) => {
|
||||||
if (/[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
|
if (/[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
|
||||||
ctx.reply(Strings.mlpInvalidCharacter, {
|
ctx.reply(Strings.mlpInvalidCharacter, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -289,21 +258,19 @@ export default (bot: Telegraf<Context>) => {
|
||||||
ctx.replyWithPhoto(comicArray[0].image, {
|
ctx.replyWithPhoto(comicArray[0].image, {
|
||||||
caption: `${result}`,
|
caption: `${result}`,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id, disable_web_page_preview: true })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ctx.reply(Strings.ponyApi.noComicFound, {
|
ctx.reply(Strings.ponyApi.noComicFound, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
...({ reply_to_message_id })
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
||||||
ctx.reply(message, {
|
ctx.reply(message, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
...({ reply_to_message_id })
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,14 +9,12 @@ import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||||
|
|
||||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||||
|
|
||||||
export default (bot: Telegraf<Context>) => {
|
export const randomponyHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||||
// TODO: this would greatly benefit from a loading message
|
|
||||||
bot.command(["rpony", "randompony", "mlpart"], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
|
||||||
const Strings = getStrings(languageCode(ctx));
|
const Strings = getStrings(languageCode(ctx));
|
||||||
const reply_to_message_id = replyToMessageId(ctx);
|
const reply_to_message_id = replyToMessageId(ctx);
|
||||||
ctx.reply(Strings.ponyApi.searching, {
|
ctx.reply(Strings.ponyApi.searching, {
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await axios(Resources.randomPonyApi);
|
const response = await axios(Resources.randomPonyApi);
|
||||||
|
@ -33,15 +31,18 @@ export default (bot: Telegraf<Context>) => {
|
||||||
ctx.replyWithPhoto(response.data.pony.representations.full, {
|
ctx.replyWithPhoto(response.data.pony.representations.full, {
|
||||||
caption: `${response.data.pony.sourceURL}\n\n${tags.length > 0 ? tags.join(', ') : ''}`,
|
caption: `${response.data.pony.sourceURL}\n\n${tags.length > 0 ? tags.join(', ') : ''}`,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown',
|
||||||
...({ reply_to_message_id })
|
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
const message = Strings.ponyApi.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 ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default (bot: Telegraf<Context>) => {
|
||||||
|
bot.command(["rpony", "randompony", "mlpart"], spamwatchMiddleware, randomponyHandler);
|
||||||
}
|
}
|
23
src/db/schema.ts
Normal file
23
src/db/schema.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
varchar,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
real
|
||||||
|
} 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),
|
||||||
|
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),
|
||||||
|
languageCode: varchar({ length: 255 }).notNull(),
|
||||||
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp().notNull().defaultNow(),
|
||||||
|
});
|
4
src/locales/config.ts
Normal file
4
src/locales/config.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const langs = [
|
||||||
|
{ code: 'en', label: 'English' },
|
||||||
|
{ code: 'pt', label: 'Português' }
|
||||||
|
];
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"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!",
|
"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",
|
"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.",
|
"botPrivacy": "Check out [this link]({botPrivacy}) to read the bot's privacy policy.",
|
||||||
|
@ -53,7 +54,7 @@
|
||||||
"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\n- /settings: Show your user settings",
|
||||||
"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",
|
||||||
|
@ -62,8 +63,15 @@
|
||||||
"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",
|
"ai": {
|
||||||
"aiCmdsDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI",
|
"helpEntry": "✨ AI Commands",
|
||||||
|
"helpDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI\n- /think `<prompt>`: Ask a thinking model about a question",
|
||||||
|
"disabled": "✨ AI features are currently disabled",
|
||||||
|
"pulling": "🔄 *Pulling {model} from Ollama...*\n\nThis may take a few minutes...",
|
||||||
|
"askGenerating": "✨ _{model} is working..._",
|
||||||
|
"askNoMessage": "Please provide a message to ask the model.",
|
||||||
|
"languageCode": "Language"
|
||||||
|
},
|
||||||
"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": {
|
||||||
|
@ -81,6 +89,33 @@
|
||||||
"noLink": "Please provide a link to a video to download.",
|
"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."
|
"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}",
|
||||||
|
"back": "Back",
|
||||||
|
"selectSeries": "Please select a model series.",
|
||||||
|
"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})"
|
||||||
|
},
|
||||||
|
"languageCodeSetTo": "Language set to {languageCode}",
|
||||||
|
"unknownAction": "Unknown action."
|
||||||
|
},
|
||||||
"botUpdated": "Bot updated with success.\n\n```{result}```",
|
"botUpdated": "Bot updated with success.\n\n```{result}```",
|
||||||
"errorUpdatingBot": "Error updating bot\n\n{error}",
|
"errorUpdatingBot": "Error updating bot\n\n{error}",
|
||||||
"catImgErr": "Sorry, but I couldn't get the cat photo you wanted.",
|
"catImgErr": "Sorry, but I couldn't get the cat photo you wanted.",
|
||||||
|
@ -120,6 +155,13 @@
|
||||||
},
|
},
|
||||||
"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..._",
|
"gsmarenaProvidePhoneName": "Please provide the phone name.",
|
||||||
"aiDisabled": "AI features are currently disabled"
|
"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."
|
||||||
}
|
}
|
|
@ -33,8 +33,8 @@
|
||||||
"funEmojiResult": "*Você lançou {emoji} e obteve *`{value}`*!*\nVocê não sabe o que isso significa? Nem eu!",
|
"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}",
|
"gifErr": "*Algo deu errado ao enviar o GIF. Tente novamente mais tarde.*\n\n{err}",
|
||||||
"lastFm": {
|
"lastFm": {
|
||||||
"helpEntry": "Last.fm",
|
"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.",
|
"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>`",
|
"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>`",
|
"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}`*.*",
|
"noRecentTracks": "*Nenhuma faixa recente encontrada para o usuário do Last.fm* `{lastfmUser}`*.*",
|
||||||
|
@ -52,27 +52,34 @@
|
||||||
"apiErr": "*Ocorreu um erro ao obter o clima. Tente novamente mais tarde.*\n\n`{error}`",
|
"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.*"
|
"apiKeyErr": "*Uma chave de API não foi definida pelo proprietário do bot. Tente novamente mais tarde.*"
|
||||||
},
|
},
|
||||||
"mainCommands": "Comandos principais",
|
"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",
|
"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",
|
"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`",
|
"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",
|
"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",
|
"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",
|
"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!",
|
"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",
|
"ai": {
|
||||||
"aiCmdsDesc": "*Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA",
|
"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",
|
||||||
|
"disabled": "✨ Os recursos de IA estão desativados no momento",
|
||||||
|
"pulling": "🔄 *Puxando {model} do Ollama...*\n\nIsso pode levar alguns minutos...",
|
||||||
|
"askGenerating": "✨ _{model} está funcionando..._",
|
||||||
|
"askNoMessage": "Por favor, forneça uma mensagem para fazer a pergunta ao modelo.",
|
||||||
|
"languageCode": "Idioma"
|
||||||
|
},
|
||||||
"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": {
|
||||||
"helpEntry": "Download de vídeos",
|
"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.*",
|
"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...*",
|
"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.*",
|
"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...",
|
"checkingSize": "🔎 *Verificando se o vídeo excede o limite de 50 MB...*",
|
||||||
"uploadingVid": "*Enviando vídeo...*",
|
"uploadingVid": "⬆️ *Enviando vídeo...*",
|
||||||
"msgDesc": "{userMention}*, aqui está o seu vídeo baixado.*",
|
"msgDesc": "{userMention}*, aqui está o seu vídeo baixado.*",
|
||||||
"downloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`",
|
"downloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`",
|
||||||
"uploadErr": "Erro ao enviar o arquivo. Tente novamente mais tarde.",
|
"uploadErr": "Erro ao enviar o arquivo. Tente novamente mais tarde.",
|
||||||
|
@ -81,6 +88,33 @@
|
||||||
"noLink": "*Por favor, forneça um link de um vídeo para download.*",
|
"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."
|
"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.",
|
||||||
|
"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})",
|
||||||
|
"back": "Voltar"
|
||||||
|
},
|
||||||
|
"languageCodeSetTo": "Idioma definido para {languageCode}",
|
||||||
|
"unknownAction": "Ação desconhecida."
|
||||||
|
},
|
||||||
"botUpdated": "Bot atualizado com sucesso.\n\n```{result}```",
|
"botUpdated": "Bot atualizado com sucesso.\n\n```{result}```",
|
||||||
"errorUpdatingBot": "Erro ao atualizar o bot\n\n{error}",
|
"errorUpdatingBot": "Erro ao atualizar o bot\n\n{error}",
|
||||||
"catImgErr": "Desculpe, mas não consegui obter a foto do gato que você queria.",
|
"catImgErr": "Desculpe, mas não consegui obter a foto do gato que você queria.",
|
||||||
|
@ -97,8 +131,8 @@
|
||||||
"resultMsg": "*Código HTTP*: `{code}`\n*Nome*: `{message}`\n*Descrição*: `{description}`"
|
"resultMsg": "*Código HTTP*: `{code}`\n*Nome*: `{message}`\n*Descrição*: `{description}`"
|
||||||
},
|
},
|
||||||
"ponyApi": {
|
"ponyApi": {
|
||||||
"helpEntry": "My Little Pony",
|
"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.",
|
"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})",
|
"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})",
|
"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})",
|
"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})",
|
||||||
|
@ -119,6 +153,14 @@
|
||||||
"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.",
|
"noFileProvided": "Por favor, forneça um arquivo para envio.",
|
||||||
"askGenerating": "✨ _{modelo} está funcionando..._",
|
"gsmarenaProvidePhoneName": "Por favor, forneça o nome do celular.",
|
||||||
"aiDisabled": "Os recursos de IA estão desativados no momento"
|
"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.",
|
||||||
|
"userNotFound": "Usuário não encontrado."
|
||||||
}
|
}
|
||||||
|
|
64
src/utils/ensure-user.ts
Normal file
64
src/utils/ensure-user.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// 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 '../db/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,
|
||||||
|
customAiModel: "deepseek-r1:1.5b",
|
||||||
|
aiTemperature: 0.9,
|
||||||
|
aiRequests: 0,
|
||||||
|
aiCharacters: 0,
|
||||||
|
};
|
||||||
|
console.log('[💽 DB] Inserting user with values:', userToInsert);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,19 +63,24 @@ class Logger {
|
||||||
console.log(`[✨ AI | PROMPT] ${prompt.length} chars input`)
|
console.log(`[✨ AI | PROMPT] ${prompt.length} chars input`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error: any): void {
|
logError(error: unknown): void {
|
||||||
if (error.response?.error_code === 429) {
|
if (typeof error === 'object' && error !== null && 'response' in error) {
|
||||||
const retryAfter = error.response.parameters?.retry_after || 1
|
const err = error as { response?: { error_code?: number, parameters?: { retry_after?: number }, description?: string }, on?: { method?: string } };
|
||||||
console.error(`[✨ AI | RATE_LIMIT] Too Many Requests - retry after ${retryAfter}s`)
|
if (err.response?.error_code === 429) {
|
||||||
} else if (error.response?.error_code === 400 && error.response?.description?.includes("can't parse entities")) {
|
const retryAfter = err.response.parameters?.retry_after || 1;
|
||||||
console.error("[✨ AI | PARSE_ERROR] Markdown parsing failed, retrying with plain text")
|
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 {
|
} else {
|
||||||
const errorDetails = {
|
const errorDetails = {
|
||||||
code: error.response?.error_code,
|
code: err.response?.error_code,
|
||||||
description: error.response?.description,
|
description: err.response?.description,
|
||||||
method: error.on?.method
|
method: err.on?.method
|
||||||
|
};
|
||||||
|
console.error("[✨ AI | ERROR]", JSON.stringify(errorDetails, null, 2));
|
||||||
}
|
}
|
||||||
console.error("[✨ AI | ERROR]", JSON.stringify(errorDetails, null, 2))
|
} else {
|
||||||
|
console.error("[✨ AI | ERROR]", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,14 @@ class RateLimiter {
|
||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTelegramError(error: unknown, messageKey: string, options: any, ctx: Context, chatId: number, messageId: number): boolean {
|
private handleTelegramError(
|
||||||
|
error: unknown,
|
||||||
|
messageKey: string,
|
||||||
|
options: Record<string, unknown>,
|
||||||
|
ctx: Context,
|
||||||
|
chatId: number,
|
||||||
|
messageId: number
|
||||||
|
): boolean {
|
||||||
if (!isTelegramError(error)) return false
|
if (!isTelegramError(error)) return false
|
||||||
if (error.response.error_code === 429) {
|
if (error.response.error_code === 429) {
|
||||||
const retryAfter = error.response.parameters?.retry_after || 1
|
const retryAfter = error.response.parameters?.retry_after || 1
|
||||||
|
@ -130,7 +137,7 @@ class RateLimiter {
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
chatId: number,
|
chatId: number,
|
||||||
messageId: number,
|
messageId: number,
|
||||||
options: any
|
options: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const messageKey = this.getMessageKey(chatId, messageId)
|
const messageKey = this.getMessageKey(chatId, messageId)
|
||||||
const latestText = this.pendingUpdates.get(messageKey)
|
const latestText = this.pendingUpdates.get(messageKey)
|
||||||
|
@ -184,7 +191,7 @@ class RateLimiter {
|
||||||
const newMessage = await ctx.telegram.sendMessage(chatId, chunk, {
|
const newMessage = await ctx.telegram.sendMessage(chatId, chunk, {
|
||||||
...options,
|
...options,
|
||||||
reply_to_message_id: messageId
|
reply_to_message_id: messageId
|
||||||
})
|
} as any)
|
||||||
logger.logChunk(chatId, newMessage.message_id, chunk, true)
|
logger.logChunk(chatId, newMessage.message_id, chunk, true)
|
||||||
this.overflowMessages.set(messageKey, newMessage.message_id)
|
this.overflowMessages.set(messageKey, newMessage.message_id)
|
||||||
}
|
}
|
||||||
|
@ -226,7 +233,7 @@ class RateLimiter {
|
||||||
chatId: number,
|
chatId: number,
|
||||||
messageId: number,
|
messageId: number,
|
||||||
text: string,
|
text: string,
|
||||||
options: any
|
options: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const messageKey = this.getMessageKey(chatId, messageId)
|
const messageKey = this.getMessageKey(chatId, messageId)
|
||||||
this.pendingUpdates.set(messageKey, text)
|
this.pendingUpdates.set(messageKey, text)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue