add initial complete webui, more ai commands for moderation, add api
This commit is contained in:
parent
19e794e34c
commit
173d4e7a52
112 changed files with 8176 additions and 780 deletions
102
telegram/api/server.ts
Executable file
102
telegram/api/server.ts
Executable file
|
@ -0,0 +1,102 @@
|
|||
import express from "express";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { Client } from "pg";
|
||||
import * as schema from "../../database/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { twoFactorTable, usersTable } from "../../database/schema";
|
||||
import { Telegraf } from "telegraf";
|
||||
import { getStrings } from "../plugins/checklang";
|
||||
|
||||
const client = new Client({ connectionString: process.env.databaseUrl });
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
const bot = new Telegraf(process.env.botToken!);
|
||||
const botName = bot.botInfo?.first_name && bot.botInfo?.last_name ? `${bot.botInfo.first_name} ${bot.botInfo.last_name}` : "Kowalski"
|
||||
|
||||
function shouldLogLonger() {
|
||||
return process.env.longerLogs === 'true';
|
||||
}
|
||||
|
||||
export async function startServer() {
|
||||
await client.connect();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/health", (res) => {
|
||||
res.send("OK");
|
||||
});
|
||||
|
||||
app.post("/2fa/get", async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
console.log("[🌐 API] Missing userId in request");
|
||||
return res.status(400).json({ generated: false, error: "User ID is required" });
|
||||
}
|
||||
|
||||
if (shouldLogLonger()) {
|
||||
console.log("[🌐 API] Looking up user:", userId);
|
||||
}
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.telegramId, userId),
|
||||
columns: {
|
||||
languageCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.log("[🌐 API] User not found:", userId);
|
||||
return res.status(404).json({ generated: false, error: "User not found" });
|
||||
}
|
||||
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
console.log("[🌐 API] Inserting 2FA record");
|
||||
|
||||
await db.insert(twoFactorTable).values({
|
||||
userId,
|
||||
currentCode: code,
|
||||
codeAttempts: 0,
|
||||
codeExpiresAt: new Date(Date.now() + 1000 * 60 * 5),
|
||||
}).onConflictDoUpdate({
|
||||
target: twoFactorTable.userId,
|
||||
set: {
|
||||
currentCode: code,
|
||||
codeAttempts: 0,
|
||||
codeExpiresAt: new Date(Date.now() + 1000 * 60 * 5),
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldLogLonger()) {
|
||||
console.log("[🌐 API] Sending 2FA message");
|
||||
}
|
||||
|
||||
try {
|
||||
const Strings = getStrings(user.languageCode);
|
||||
const message = Strings.twoFactor.codeMessage
|
||||
.replace("{botName}", botName)
|
||||
.replace("{code}", code);
|
||||
await bot.telegram.sendMessage(userId, message, { parse_mode: "MarkdownV2" });
|
||||
if (shouldLogLonger()) {
|
||||
console.log("[🌐 API] Message sent successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[🌐 API] Error sending 2FA code to user", error);
|
||||
return res.status(500).json({ generated: false, error: "Error sending 2FA message" });
|
||||
}
|
||||
|
||||
res.json({ generated: true });
|
||||
|
||||
} catch (error) {
|
||||
console.error("[🌐 API] Unexpected error in 2FA endpoint:", error);
|
||||
return res.status(500).json({ generated: false, error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3030, () => {
|
||||
console.log("[🌐 API] Running on port 3030\n");
|
||||
});
|
||||
}
|
131
telegram/bot.ts
Executable file
131
telegram/bot.ts
Executable file
|
@ -0,0 +1,131 @@
|
|||
import { Telegraf } from 'telegraf';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSpamwatchConnected } from './spamwatch/spamwatch';
|
||||
import '@dotenvx/dotenvx';
|
||||
import 'dotenv/config';
|
||||
import './plugins/ytDlpWrapper';
|
||||
import { preChecks } from './commands/ai';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Client } from 'pg';
|
||||
import * as schema from '../database/schema';
|
||||
import { ensureUserInDb } from './utils/ensure-user';
|
||||
import { getSpamwatchBlockedCount } from './spamwatch/spamwatch';
|
||||
import { startServer } from './api/server';
|
||||
|
||||
(async function main() {
|
||||
const { botToken, handlerTimeout, maxRetries, databaseUrl, ollamaEnabled } = process.env;
|
||||
if (!botToken || botToken === 'InsertYourBotTokenHere') {
|
||||
console.error('Bot token is not set. Please set the bot token in the .env file.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (ollamaEnabled === "true") {
|
||||
if (!(await preChecks())) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const client = new Client({ connectionString: databaseUrl });
|
||||
await client.connect();
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
const bot = new Telegraf(
|
||||
botToken,
|
||||
{ handlerTimeout: Number(handlerTimeout) || 600_000 }
|
||||
);
|
||||
const maxRetriesNum = Number(maxRetries) || 5;
|
||||
let restartCount = 0;
|
||||
|
||||
bot.use(async (ctx, next) => {
|
||||
await ensureUserInDb(ctx, db);
|
||||
return next();
|
||||
});
|
||||
|
||||
function loadCommands() {
|
||||
const commandsPath = path.join(__dirname, 'commands');
|
||||
let loadedCount = 0;
|
||||
try {
|
||||
const files = fs.readdirSync(commandsPath)
|
||||
.filter(file => file.endsWith('.ts'));
|
||||
files.forEach((file) => {
|
||||
try {
|
||||
const commandPath = path.join(commandsPath, file);
|
||||
const command = require(commandPath).default || require(commandPath);
|
||||
if (typeof command === 'function') {
|
||||
command(bot, db);
|
||||
loadedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load command file ${file}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
console.log(`[🤖 BOT] Loaded ${loadedCount} commands.`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to read commands directory: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startBot() {
|
||||
try {
|
||||
const botInfo = await bot.telegram.getMe();
|
||||
console.log(`${botInfo.first_name} is running...`);
|
||||
await bot.launch();
|
||||
restartCount = 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to start bot:', error.message);
|
||||
if (restartCount < maxRetriesNum) {
|
||||
restartCount++;
|
||||
console.log(`Retrying to start bot... Attempt ${restartCount}`);
|
||||
setTimeout(startBot, 5000);
|
||||
} else {
|
||||
console.error('Maximum retry attempts reached. Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleShutdown(signal: string) {
|
||||
console.log(`Received ${signal}. Stopping bot...`);
|
||||
bot.stop(signal);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.once('SIGINT', () => handleShutdown('SIGINT'));
|
||||
process.once('SIGTERM', () => handleShutdown('SIGTERM'));
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught Exception:', error.message);
|
||||
console.error(error.stack);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
async function testDbConnection() {
|
||||
try {
|
||||
await db.query.usersTable.findMany({ limit: 1 });
|
||||
const users = await db.query.usersTable.findMany({});
|
||||
const userCount = users.length;
|
||||
console.log(`[💽 DB] Connected [${userCount} users]`);
|
||||
} catch (err) {
|
||||
console.error('[💽 DB] Failed to connect:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await testDbConnection();
|
||||
|
||||
if (isSpamwatchConnected()) {
|
||||
const blockedCount = getSpamwatchBlockedCount();
|
||||
// the 3 spaces are intentional
|
||||
console.log(`[🛡️ SW] Connected [${blockedCount} blocked]`);
|
||||
} else {
|
||||
console.log('[🛡️ SW] Not connected or blocklist empty');
|
||||
}
|
||||
|
||||
loadCommands();
|
||||
startServer();
|
||||
startBot();
|
||||
})();
|
1291
telegram/commands/ai.ts
Executable file
1291
telegram/commands/ai.ts
Executable file
File diff suppressed because it is too large
Load diff
159
telegram/commands/animal.ts
Executable file
159
telegram/commands/animal.ts
Executable file
|
@ -0,0 +1,159 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
export const duckHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
try {
|
||||
const response = await axios(Resources.duckApi);
|
||||
ctx.replyWithPhoto(response.data.url, {
|
||||
caption: "🦆",
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const message = Strings.duckApiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const foxHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
try {
|
||||
const response = await axios(Resources.foxApi);
|
||||
ctx.replyWithPhoto(response.data.image, {
|
||||
caption: "🦊",
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
const message = Strings.foxApiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const dogHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
try {
|
||||
const response = await axios(Resources.dogApi);
|
||||
ctx.replyWithPhoto(response.data.message, {
|
||||
caption: "🐶",
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
const message = Strings.dogApiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const catHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const apiUrl = `${Resources.catApi}?json=true`;
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
try {
|
||||
const response = await axios.get(apiUrl);
|
||||
const data = response.data;
|
||||
const imageUrl = `${data.url}`;
|
||||
await ctx.replyWithPhoto(imageUrl, {
|
||||
caption: `🐱`,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
const message = Strings.catImgErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const soggyHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const userInput = ctx.message.text.split(' ')[1];
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
switch (true) {
|
||||
case (userInput === "2" || userInput === "thumb"):
|
||||
ctx.replyWithPhoto(
|
||||
Resources.soggyCat2, {
|
||||
caption: Resources.soggyCat2,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
break;
|
||||
|
||||
case (userInput === "3" || userInput === "sticker"):
|
||||
ctx.replyWithSticker(
|
||||
Resources.soggyCatSticker,
|
||||
reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : undefined
|
||||
);
|
||||
break;
|
||||
|
||||
case (userInput === "4" || userInput === "alt"):
|
||||
ctx.replyWithPhoto(
|
||||
Resources.soggyCatAlt, {
|
||||
caption: Resources.soggyCatAlt,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
ctx.replyWithPhoto(
|
||||
Resources.soggyCat, {
|
||||
caption: Resources.soggyCat,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
export default (bot: Telegraf<Context>, db: any) => {
|
||||
bot.command("duck", spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
|
||||
await duckHandler(ctx);
|
||||
});
|
||||
|
||||
bot.command("fox", spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
|
||||
await foxHandler(ctx);
|
||||
});
|
||||
|
||||
bot.command("dog", spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
|
||||
await dogHandler(ctx);
|
||||
});
|
||||
|
||||
bot.command("cat", spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
|
||||
await catHandler(ctx);
|
||||
});
|
||||
|
||||
bot.command(['soggy', 'soggycat'], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'soggy-cat')) return;
|
||||
await soggyHandler(ctx);
|
||||
});
|
||||
}
|
88
telegram/commands/codename.ts
Executable file
88
telegram/commands/codename.ts
Executable file
|
@ -0,0 +1,88 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import verifyInput from '../plugins/verifyInput';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import * as schema from '../../database/schema';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
interface Device {
|
||||
brand: string;
|
||||
codename: string;
|
||||
model: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function getDeviceByCodename(codename: string): Promise<Device | null> {
|
||||
try {
|
||||
const response = await axios.get(Resources.codenameApi);
|
||||
const jsonRes = response.data;
|
||||
const deviceDetails = jsonRes[codename];
|
||||
if (!deviceDetails) return null;
|
||||
return deviceDetails.find((item: Device) => item.brand) || deviceDetails[0];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
if (from.language_code && languageCode === 'en') {
|
||||
languageCode = from.language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command(['codename', 'whatis'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'codename-lookup')) return;
|
||||
|
||||
const userInput = ctx.message.text.split(" ").slice(1).join(" ");
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const { noCodename } = Strings.codenameCheck;
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
if (verifyInput(ctx, userInput, noCodename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await getDeviceByCodename(userInput);
|
||||
|
||||
if (!device) {
|
||||
return ctx.reply(Strings.codenameCheck.notFound, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
}
|
||||
|
||||
const message = Strings.codenameCheck.resultMsg
|
||||
.replace('{brand}', device.brand)
|
||||
.replace('{codename}', userInput)
|
||||
.replace('{model}', device.model)
|
||||
.replace('{name}', device.name);
|
||||
|
||||
return ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
})
|
||||
}
|
271
telegram/commands/crew.ts
Executable file
271
telegram/commands/crew.ts
Executable file
|
@ -0,0 +1,271 @@
|
|||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import os from 'os';
|
||||
import { exec } from 'child_process';
|
||||
import { error } from 'console';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import * as schema from '../../database/schema';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
if (from.language_code && languageCode === 'en') {
|
||||
languageCode = from.language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
function getGitCommitHash() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('git rev-parse --short HEAD', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`Error: ${stderr}`);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateBot() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('git pull && echo "A" >> restart.txt', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`Error: ${stderr}`);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatUptime(uptime: number) {
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = Math.floor(uptime % 60);
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
function getSystemInfo() {
|
||||
const { platform, release, arch, cpus, totalmem, freemem, loadavg, uptime } = os;
|
||||
const [cpu] = cpus();
|
||||
return `*Server Stats*\n\n` +
|
||||
`*OS:* \`${platform()} ${release()}\`\n` +
|
||||
`*Arch:* \`${arch()}\`\n` +
|
||||
`*Node.js Version:* \`${process.version}\`\n` +
|
||||
`*CPU:* \`${cpu.model}\`\n` +
|
||||
`*CPU Cores:* \`${cpus().length} cores\`\n` +
|
||||
`*RAM:* \`${(freemem() / (1024 ** 3)).toFixed(2)} GB / ${(totalmem() / (1024 ** 3)).toFixed(2)} GB\`\n` +
|
||||
`*Load Average:* \`${loadavg().map(avg => avg.toFixed(2)).join(', ')}\`\n` +
|
||||
`*Uptime:* \`${formatUptime(uptime())}\`\n\n`;
|
||||
}
|
||||
|
||||
async function handleAdminCommand(ctx: Context & { message: { text: string } }, action: () => Promise<void>, successMessage: string, errorMessage: string) {
|
||||
const { Strings } = await getUserAndStrings(ctx);
|
||||
const userId = ctx.from?.id;
|
||||
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
|
||||
if (userId && adminArray.includes(userId)) {
|
||||
try {
|
||||
await action();
|
||||
if (successMessage) {
|
||||
ctx.reply(successMessage, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.reply(errorMessage.replace(/{error}/g, error.message), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ctx.reply(Strings.noPermission, {
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command('getbotstats', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
handleAdminCommand(ctx, async () => {
|
||||
const stats = getSystemInfo();
|
||||
await ctx.reply(stats, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}, '', Strings.errorRetrievingStats);
|
||||
});
|
||||
|
||||
bot.command('getbotcommit', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
handleAdminCommand(ctx, async () => {
|
||||
try {
|
||||
const commitHash = await getGitCommitHash();
|
||||
await ctx.reply(Strings.gitCurrentCommit.replace(/{commitHash}/g, commitHash), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.reply(Strings.gitErrRetrievingCommit.replace(/{error}/g, error), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
}, '', Strings.gitErrRetrievingCommit);
|
||||
});
|
||||
|
||||
bot.command('updatebot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
handleAdminCommand(ctx, async () => {
|
||||
try {
|
||||
const result = await updateBot();
|
||||
await ctx.reply(Strings.botUpdated.replace(/{result}/g, result), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.reply(Strings.errorUpdatingBot.replace(/{error}/g, error), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
}, '', Strings.errorUpdatingBot);
|
||||
});
|
||||
|
||||
bot.command('setbotname', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const botName = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
handleAdminCommand(ctx, async () => {
|
||||
await ctx.telegram.setMyName(botName);
|
||||
}, Strings.botNameChanged.replace(/{botName}/g, botName), Strings.botNameErr.replace(/{error}/g, error));
|
||||
});
|
||||
|
||||
bot.command('setbotdesc', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const botDesc = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
handleAdminCommand(ctx, async () => {
|
||||
await ctx.telegram.setMyDescription(botDesc);
|
||||
}, Strings.botDescChanged.replace(/{botDesc}/g, botDesc), Strings.botDescErr.replace(/{error}/g, error));
|
||||
});
|
||||
|
||||
bot.command('botkickme', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
handleAdminCommand(ctx, async () => {
|
||||
if (!ctx.chat) {
|
||||
ctx.reply(Strings.chatNotFound, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
ctx.reply(Strings.kickingMyself, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
await ctx.telegram.leaveChat(ctx.chat.id);
|
||||
}, '', Strings.kickingMyselfErr);
|
||||
});
|
||||
|
||||
bot.command('getfile', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const botFile = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
|
||||
if (!botFile) {
|
||||
ctx.reply(Strings.noFileProvided, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handleAdminCommand(ctx, async () => {
|
||||
try {
|
||||
await ctx.replyWithDocument({
|
||||
// @ts-ignore
|
||||
source: botFile,
|
||||
caption: botFile
|
||||
}, {
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.reply(Strings.unexpectedErr.replace(/{error}/g, error.message), {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
}, '', Strings.unexpectedErr);
|
||||
});
|
||||
|
||||
bot.command('run', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const command = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
handleAdminCommand(ctx, async () => {
|
||||
if (!command) {
|
||||
ctx.reply('Por favor, forneça um comando para executar.');
|
||||
return;
|
||||
}
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
return ctx.reply(`\`${error.message}\``, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
if (stderr) {
|
||||
return ctx.reply(`\`${stderr}\``, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
ctx.reply(`\`${stdout}\``, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
}, '', "Nope!");
|
||||
});
|
||||
|
||||
bot.command('eval', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
const code = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
if (!code) {
|
||||
return ctx.reply('Por favor, forneça um código para avaliar.');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = eval(code);
|
||||
ctx.reply(`Result: ${result}`, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.reply(`Error: ${error.message}`, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
bot.command('crash', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
handleAdminCommand(ctx, async () => {
|
||||
ctx.reply('Crashed!');
|
||||
}, '', "Nope!");
|
||||
});
|
||||
};
|
140
telegram/commands/fun.ts
Executable file
140
telegram/commands/fun.ts
Executable file
|
@ -0,0 +1,140 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import * as schema from '../../database/schema';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
if (from.language_code && languageCode === 'en') {
|
||||
languageCode = from.language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
function sendRandomReply(ctx: Context & { message: { text: string } }, gifUrl: string, textKey: string, db: any) {
|
||||
getUserAndStrings(ctx, db).then(({ Strings }) => {
|
||||
const randomNumber = Math.floor(Math.random() * 100);
|
||||
const shouldSendGif = randomNumber > 50;
|
||||
const caption = Strings[textKey].replace('{randomNum}', randomNumber);
|
||||
if (shouldSendGif) {
|
||||
ctx.replyWithAnimation(gifUrl, {
|
||||
caption,
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
}).catch(err => {
|
||||
const gifErr = Strings.gifErr.replace('{err}', err);
|
||||
ctx.reply(gifErr, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ctx.reply(caption, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDiceCommand(ctx: Context & { message: { text: string } }, emoji: string, delay: number, db: any) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
|
||||
// @ts-ignore
|
||||
const result = await ctx.sendDice({ emoji, ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) });
|
||||
const botResponse = Strings.funEmojiResult
|
||||
.replace('{emoji}', result.dice.emoji)
|
||||
.replace('{value}', result.dice.value);
|
||||
|
||||
setTimeout(() => {
|
||||
ctx.reply(botResponse, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function getRandomInt(max: number) {
|
||||
return Math.floor(Math.random() * (max + 1));
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command('random', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
|
||||
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const randomValue = getRandomInt(10);
|
||||
const randomVStr = Strings.randomNum.replace('{number}', randomValue);
|
||||
|
||||
ctx.reply(
|
||||
randomVStr, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: maybe send custom stickers to match result of the roll? i think there are pre-existing ones
|
||||
bot.command('dice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
|
||||
await handleDiceCommand(ctx, '🎲', 4000, db);
|
||||
});
|
||||
|
||||
bot.command('slot', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
|
||||
await handleDiceCommand(ctx, '🎰', 3000, db);
|
||||
});
|
||||
|
||||
bot.command('ball', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
|
||||
await handleDiceCommand(ctx, '⚽', 3000, db);
|
||||
});
|
||||
|
||||
bot.command('dart', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
|
||||
await handleDiceCommand(ctx, '🎯', 3000, db);
|
||||
});
|
||||
|
||||
bot.command('bowling', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'games-dice')) return;
|
||||
await handleDiceCommand(ctx, '🎳', 3000, db);
|
||||
});
|
||||
|
||||
bot.command('idice', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'infinite-dice')) return;
|
||||
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
ctx.replyWithSticker(
|
||||
Resources.infiniteDice, {
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
|
||||
bot.command('furry', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
|
||||
sendRandomReply(ctx, Resources.furryGif, 'furryAmount', db);
|
||||
});
|
||||
|
||||
bot.command('gay', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'fun-random')) return;
|
||||
sendRandomReply(ctx, Resources.gayFlag, 'gayAmount', db);
|
||||
});
|
||||
};
|
341
telegram/commands/gsmarena.ts
Executable file
341
telegram/commands/gsmarena.ts
Executable file
|
@ -0,0 +1,341 @@
|
|||
// Ported and improved from Hitalo's PyKorone bot
|
||||
// Copyright (c) 2024 Hitalo M. (https://github.com/HitaloM)
|
||||
// Original code license: BSD-3-Clause
|
||||
// With some help from GPT (I don't really like AI but whatever)
|
||||
// If this were a kang, I would not be giving credits to him!
|
||||
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import { parse } from 'node-html-parser';
|
||||
import { getDeviceByCodename } from './codename';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
interface PhoneSearchResult {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PhoneDetails {
|
||||
specs: Record<string, Record<string, string>>;
|
||||
name?: string;
|
||||
url?: string;
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
const HEADERS = {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
|
||||
};
|
||||
|
||||
function getDataFromSpecs(specsData, category, attributes) {
|
||||
const details = specsData?.specs?.[category] || {};
|
||||
|
||||
return attributes
|
||||
.map(attr => details[attr] || null)
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function parseSpecs(specsData: PhoneDetails): PhoneDetails {
|
||||
const categories = {
|
||||
"status": ["Launch", ["Status"]],
|
||||
"network": ["Network", ["Technology"]],
|
||||
"system": ["Platform", ["OS"]],
|
||||
"models": ["Misc", ["Models"]],
|
||||
"weight": ["Body", ["Weight"]],
|
||||
"jack": ["Sound", ["3.5mm jack"]],
|
||||
"usb": ["Comms", ["USB"]],
|
||||
"sensors": ["Features", ["Sensors"]],
|
||||
"battery": ["Battery", ["Type"]],
|
||||
"charging": ["Battery", ["Charging"]],
|
||||
"display_type": ["Display", ["Type"]],
|
||||
"display_size": ["Display", ["Size"]],
|
||||
"display_resolution": ["Display", ["Resolution"]],
|
||||
"platform_chipset": ["Platform", ["Chipset"]],
|
||||
"platform_cpu": ["Platform", ["CPU"]],
|
||||
"platform_gpu": ["Platform", ["GPU"]],
|
||||
"memory": ["Memory", ["Internal"]],
|
||||
"main_camera_single": ["Main Camera", ["Single"]],
|
||||
"main_camera_dual": ["Main Camera", ["Dual"]],
|
||||
"main_camera_triple": ["Main Camera", ["Triple"]],
|
||||
"main_camera_quad": ["Main Camera", ["Quad"]],
|
||||
"main_camera_features": ["Main Camera", ["Features"]],
|
||||
"main_camera_video": ["Main Camera", ["Video"]],
|
||||
"selfie_camera_single": ["Selfie Camera", ["Single"]],
|
||||
"selfie_camera_dual": ["Selfie Camera", ["Dual"]],
|
||||
"selfie_camera_triple": ["Selfie Camera", ["Triple"]],
|
||||
"selfie_camera_quad": ["Selfie Camera", ["Quad"]],
|
||||
"selfie_camera_features": ["Selfie Camera", ["Features"]],
|
||||
"selfie_camera_video": ["Selfie Camera", ["Video"]]
|
||||
};
|
||||
|
||||
const parsedData = Object.keys(categories).reduce((acc, key) => {
|
||||
const [cat, attrs] = categories[key];
|
||||
acc[key] = getDataFromSpecs(specsData, cat, attrs) || "";
|
||||
return acc;
|
||||
}, { specs: {} } as PhoneDetails);
|
||||
|
||||
parsedData["name"] = specsData.name || "";
|
||||
parsedData["url"] = specsData.url || "";
|
||||
|
||||
return parsedData;
|
||||
}
|
||||
|
||||
function formatPhone(phone: PhoneDetails) {
|
||||
const formattedPhone = parseSpecs(phone);
|
||||
const attributesDict = {
|
||||
"Status": "status",
|
||||
"Network": "network",
|
||||
"OS": "system",
|
||||
"Models": "models",
|
||||
"Weight": "weight",
|
||||
"3.5mm jack": "jack",
|
||||
"USB": "usb",
|
||||
"Sensors": "sensors",
|
||||
"Battery": "battery",
|
||||
"Charging": "charging",
|
||||
"Display Type": "display_type",
|
||||
"Display Size": "display_size",
|
||||
"Display Resolution": "display_resolution",
|
||||
"Chipset": "platform_chipset",
|
||||
"CPU": "platform_cpu",
|
||||
"GPU": "platform_gpu",
|
||||
"Memory": "memory",
|
||||
"Rear Camera (Single)": "main_camera_single",
|
||||
"Rear Camera (Dual)": "main_camera_dual",
|
||||
"Rear Camera (Triple)": "main_camera_triple",
|
||||
"Rear Camera (Quad)": "main_camera_quad",
|
||||
"Rear Camera (Features)": "main_camera_features",
|
||||
"Rear Camera (Video)": "main_camera_video",
|
||||
"Front Camera (Single)": "selfie_camera_single",
|
||||
"Front Camera (Dual)": "selfie_camera_dual",
|
||||
"Front Camera (Triple)": "selfie_camera_triple",
|
||||
"Front Camera (Quad)": "selfie_camera_quad",
|
||||
"Front Camera (Features)": "selfie_camera_features",
|
||||
"Front Camera (Video)": "selfie_camera_video"
|
||||
};
|
||||
|
||||
const attributes = Object.entries(attributesDict)
|
||||
.filter(([_, key]) => formattedPhone[key])
|
||||
.map(([label, key]) => `<b>${label}:</b> <code>${formattedPhone[key]}</code>`)
|
||||
.join("\n\n");
|
||||
|
||||
const deviceUrl = `<b>GSMArena page:</b> ${formattedPhone.url}`;
|
||||
const deviceImage = phone.picture ? `<b>Device Image</b>: ${phone.picture}` : '';
|
||||
|
||||
return `<b>\n\nName: </b><code>${formattedPhone.name}</code>\n\n${attributes}\n\n${deviceImage}\n\n${deviceUrl}`;
|
||||
}
|
||||
|
||||
async function fetchHtml(url: string) {
|
||||
try {
|
||||
const response = await axios.get(url, { headers: HEADERS });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching HTML:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function searchPhone(phone: string): Promise<PhoneSearchResult[]> {
|
||||
try {
|
||||
const searchUrl = `https://m.gsmarena.com/results.php3?sQuickSearch=yes&sName=${encodeURIComponent(phone)}`;
|
||||
const htmlContent = await fetchHtml(searchUrl);
|
||||
const root = parse(htmlContent);
|
||||
const foundPhones = root.querySelectorAll('.general-menu.material-card ul li');
|
||||
|
||||
return foundPhones.map((phoneTag) => {
|
||||
const name = phoneTag.querySelector('img')?.getAttribute('title') || "";
|
||||
const url = phoneTag.querySelector('a')?.getAttribute('href') || "";
|
||||
return { name, url };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error searching for phone:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPhoneDetails(url) {
|
||||
try {
|
||||
const htmlContent = await fetchHtml(`https://www.gsmarena.com/${url}`);
|
||||
const root = parse(htmlContent);
|
||||
const specsTables = root.querySelectorAll('table[cellspacing="0"]');
|
||||
const specsData = extractSpecs(specsTables);
|
||||
const metaScripts = root.querySelectorAll('script[language="javascript"]');
|
||||
const meta = metaScripts.length ? metaScripts[0].text.split("\n") : [];
|
||||
const name = extractMetaData(meta, "ITEM_NAME");
|
||||
const picture = extractMetaData(meta, "ITEM_IMAGE");
|
||||
|
||||
return { ...specsData, name, picture, url: `https://www.gsmarena.com/${url}` };
|
||||
} catch (error) {
|
||||
console.error("Error fetching phone details:", error);
|
||||
return { specs: {}, name: "", url: "", picture: "" };
|
||||
}
|
||||
}
|
||||
|
||||
function extractSpecs(specsTables) {
|
||||
return {
|
||||
specs: specsTables.reduce((acc, table) => {
|
||||
const feature = table.querySelector('th')?.text.trim() || "";
|
||||
table.querySelectorAll('tr').forEach((tr) => {
|
||||
const header = tr.querySelector('.ttl')?.text.trim() || "info";
|
||||
let detail = tr.querySelector('.nfo')?.text.trim() || "";
|
||||
detail = detail.replace(/\s*\n\s*/g, " / ").trim();
|
||||
if (!acc[feature]) {
|
||||
acc[feature] = {};
|
||||
}
|
||||
acc[feature][header] = acc[feature][header]
|
||||
? `${acc[feature][header]} / ${detail}`
|
||||
: detail;
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
|
||||
function extractMetaData(meta, key) {
|
||||
const line = meta.find((line) => line.includes(key));
|
||||
return line ? line.split('"')[1] : "";
|
||||
}
|
||||
|
||||
function getUsername(ctx){
|
||||
let userName = String(ctx.from.first_name);
|
||||
if(userName.includes("<") && userName.includes(">")) {
|
||||
userName = userName.replaceAll("<", "").replaceAll(">", "");
|
||||
}
|
||||
return userName;
|
||||
}
|
||||
|
||||
const deviceSelectionCache: Record<number, { results: PhoneSearchResult[], timeout: NodeJS.Timeout }> = {};
|
||||
const lastSelectionMessageId: Record<number, number> = {};
|
||||
|
||||
export default (bot, db) => {
|
||||
bot.command(['d', 'device'], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'device-specs')) return;
|
||||
|
||||
const userId = ctx.from.id;
|
||||
const userName = getUsername(ctx);
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
|
||||
const phone = ctx.message.text.split(" ").slice(1).join(" ");
|
||||
if (!phone) {
|
||||
return ctx.reply(Strings.gsmarenaProvidePhoneName || "[TODO: Add gsmarenaProvidePhoneName to locales] Please provide the phone name.", { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) });
|
||||
}
|
||||
|
||||
console.log("[GSMArena] Searching for", phone);
|
||||
const statusMsg = await ctx.reply((Strings.gsmarenaSearchingFor || "[TODO: Add gsmarenaSearchingFor to locales] Searching for {phone}...").replace('{phone}', phone), { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}), parse_mode: 'Markdown' });
|
||||
|
||||
let results = await searchPhone(phone);
|
||||
if (results.length === 0) {
|
||||
const codenameResults = await getDeviceByCodename(phone.split(" ")[0]);
|
||||
if (!codenameResults) {
|
||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaNoPhonesFound || "[TODO: Add gsmarenaNoPhonesFound to locales] No phones found for {phone}.").replace('{phone}', phone), { parse_mode: 'Markdown' });
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaSearchingFor || "[TODO: Add gsmarenaSearchingFor to locales] Searching for {phone}...").replace('{phone}', codenameResults.name), { parse_mode: 'Markdown' });
|
||||
const nameResults = await searchPhone(codenameResults.name);
|
||||
if (nameResults.length === 0) {
|
||||
await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, undefined, (Strings.gsmarenaNoPhonesFoundBoth || "[TODO: Add gsmarenaNoPhonesFoundBoth to locales] No phones found for {name} and {phone}.").replace('{name}', codenameResults.name).replace('{phone}', phone), { parse_mode: 'Markdown' });
|
||||
return;
|
||||
}
|
||||
results = nameResults;
|
||||
}
|
||||
|
||||
if (deviceSelectionCache[userId]?.timeout) {
|
||||
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 = {
|
||||
parse_mode: 'HTML',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: {
|
||||
inline_keyboard: results.map((result, idx) => {
|
||||
const callbackData = `gsmadetails:${idx}:${ctx.from.id}`;
|
||||
return [{ text: result.name, callback_data: callbackData }];
|
||||
})
|
||||
}
|
||||
};
|
||||
const selectionMsg = await ctx.reply(testUser, options);
|
||||
lastSelectionMessageId[userId] = selectionMsg.message_id;
|
||||
}
|
||||
} else {
|
||||
const testUser = `<a href=\"tg://user?id=${userId}\">${userName}</a>, ${Strings.gsmarenaSelectDevice || "[TODO: Add gsmarenaSelectDevice to locales] please select your device:"}`;
|
||||
const inlineKeyboard = results.map((result, idx) => {
|
||||
const callbackData = `gsmadetails:${idx}:${ctx.from.id}`;
|
||||
return [{ text: result.name, callback_data: callbackData }];
|
||||
});
|
||||
const options = {
|
||||
parse_mode: 'HTML',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: {
|
||||
inline_keyboard: inlineKeyboard
|
||||
}
|
||||
};
|
||||
const selectionMsg = await ctx.reply(testUser, options);
|
||||
lastSelectionMessageId[userId] = selectionMsg.message_id;
|
||||
}
|
||||
await ctx.telegram.deleteMessage(ctx.chat.id, statusMsg.message_id).catch(() => {});
|
||||
});
|
||||
|
||||
bot.action(/gsmadetails:(\d+):(\d+)/, async (ctx) => {
|
||||
const idx = parseInt(ctx.match[1]);
|
||||
const userId = parseInt(ctx.match[2]);
|
||||
const userName = getUsername(ctx);
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
|
||||
const callbackQueryUserId = ctx.update.callback_query.from.id;
|
||||
|
||||
if (userId !== callbackQueryUserId) {
|
||||
return ctx.answerCbQuery(`${userName}, ${Strings.gsmarenaNotAllowed || "[TODO: Add gsmarenaNotAllowed to locales] you are not allowed to interact with this."}`);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (phoneDetails.name) {
|
||||
const message = formatPhone(phoneDetails);
|
||||
ctx.editMessageText(`<b><a href=\"tg://user?id=${userId}\">${userName}</a>, ${Strings.gsmarenaDeviceDetails || "[TODO: Add gsmarenaDeviceDetails to locales] these are the details of your device:"}</b>` + message, { parse_mode: 'HTML', disable_web_page_preview: false });
|
||||
} else {
|
||||
ctx.reply(Strings.gsmarenaErrorFetchingDetails || "[TODO: Add gsmarenaErrorFetchingDetails to locales] Error fetching phone details.", { ...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {}) });
|
||||
}
|
||||
});
|
||||
};
|
154
telegram/commands/help.ts
Executable file
154
telegram/commands/help.ts
Executable file
|
@ -0,0 +1,154 @@
|
|||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import type { Context } from 'telegraf';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: any): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
function isAdmin(ctx: Context): boolean {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId) return false;
|
||||
const adminArray = process.env.botAdmins ? process.env.botAdmins.split(',').map(id => parseInt(id.trim())) : [];
|
||||
return adminArray.includes(userId);
|
||||
}
|
||||
|
||||
interface MessageOptions {
|
||||
parse_mode: string;
|
||||
disable_web_page_preview: boolean;
|
||||
reply_markup: {
|
||||
inline_keyboard: { text: string; callback_data: string; }[][];
|
||||
};
|
||||
reply_to_message_id?: number;
|
||||
}
|
||||
|
||||
async function sendHelpMessage(ctx, isEditing, db) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const botInfo = await ctx.telegram.getMe();
|
||||
const helpText = Strings.botHelp
|
||||
.replace(/{botName}/g, botInfo.first_name)
|
||||
.replace(/{sourceLink}/g, process.env.botSource);
|
||||
function getMessageId(ctx) {
|
||||
return ctx.message?.message_id || ctx.callbackQuery?.message?.message_id;
|
||||
};
|
||||
const createOptions = (ctx, includeReplyTo = false): MessageOptions => {
|
||||
const options: MessageOptions = {
|
||||
parse_mode: 'Markdown',
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: Strings.mainCommands, callback_data: 'helpMain' }, { text: Strings.usefulCommands, callback_data: 'helpUseful' }],
|
||||
[{ text: Strings.interactiveEmojis, callback_data: 'helpInteractive' }, { text: Strings.funnyCommands, callback_data: 'helpFunny' }],
|
||||
[{ text: Strings.lastFm.helpEntry, callback_data: 'helpLast' }, { text: Strings.animalCommands, callback_data: 'helpAnimals' }],
|
||||
[{ text: Strings.ytDownload.helpEntry, callback_data: 'helpYouTube' }, { text: Strings.ponyApi.helpEntry, callback_data: 'helpMLP' }],
|
||||
[{ text: Strings.ai.helpEntry, callback_data: 'helpAi' }]
|
||||
]
|
||||
}
|
||||
};
|
||||
if (includeReplyTo) {
|
||||
const messageId = getMessageId(ctx);
|
||||
if (messageId) {
|
||||
(options as any).reply_parameters = { message_id: messageId };
|
||||
};
|
||||
};
|
||||
return options;
|
||||
};
|
||||
if (isEditing) {
|
||||
await ctx.editMessageText(helpText, createOptions(ctx));
|
||||
} else {
|
||||
await ctx.reply(helpText, createOptions(ctx, true));
|
||||
};
|
||||
}
|
||||
|
||||
export default (bot, db) => {
|
||||
bot.help(spamwatchMiddleware, async (ctx) => {
|
||||
await sendHelpMessage(ctx, false, db);
|
||||
});
|
||||
|
||||
bot.command("about", spamwatchMiddleware, async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const aboutMsg = Strings.botAbout.replace(/{sourceLink}/g, `${process.env.botSource}`);
|
||||
ctx.reply(aboutMsg, {
|
||||
parse_mode: 'Markdown',
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
|
||||
const options = (Strings) => ({
|
||||
parse_mode: 'Markdown',
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: Strings.varStrings.varBack, callback_data: 'helpBack' }],
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
bot.action('helpMain', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.mainCommandsDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpUseful', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.usefulCommandsDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpInteractive', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.interactiveEmojisDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpFunny', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.funnyCommandsDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpLast', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.lastFm.helpDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpYouTube', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.ytDownload.helpDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpAnimals', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.animalCommandsDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpMLP', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
await ctx.editMessageText(Strings.ponyApi.helpDesc, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpAi', async (ctx) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const helpText = isAdmin(ctx) ? Strings.ai.helpDescAdmin : Strings.ai.helpDesc;
|
||||
await ctx.editMessageText(helpText, options(Strings));
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
bot.action('helpBack', async (ctx) => {
|
||||
await sendHelpMessage(ctx, true, db);
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
}
|
114
telegram/commands/http.ts
Executable file
114
telegram/commands/http.ts
Executable file
|
@ -0,0 +1,114 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import verifyInput from '../plugins/verifyInput';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import * as schema from '../../database/schema';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
if (from.language_code && languageCode === 'en') {
|
||||
languageCode = from.language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command("http", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'http-status')) return;
|
||||
|
||||
const reply_to_message_id = ctx.message.message_id;
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
const userInput = ctx.message.text.split(' ')[1];
|
||||
const apiUrl = Resources.httpApi;
|
||||
const { invalidCode } = Strings.httpCodes
|
||||
|
||||
if (verifyInput(ctx, userInput, invalidCode, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(apiUrl);
|
||||
const data = response.data;
|
||||
const codesArray = Array.isArray(data) ? data : Object.values(data);
|
||||
const codeInfo = codesArray.find(item => item.code === parseInt(userInput));
|
||||
|
||||
if (codeInfo) {
|
||||
const message = Strings.httpCodes.resultMsg
|
||||
.replace("{code}", codeInfo.code)
|
||||
.replace("{message}", codeInfo.message)
|
||||
.replace("{description}", codeInfo.description);
|
||||
await ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} else {
|
||||
await ctx.reply(Strings.httpCodes.notFound, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
const message = Strings.httpCodes.fetchErr.replace('{error}', error);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
bot.command("httpcat", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'animals-basic')) return;
|
||||
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = ctx.message.message_id;
|
||||
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(/\s+/g, '');
|
||||
const { invalidCode } = Strings.httpCodes
|
||||
|
||||
if (verifyInput(ctx, userInput, invalidCode, true)) {
|
||||
return;
|
||||
}
|
||||
if (userInput.length !== 3) {
|
||||
ctx.reply(Strings.httpCodes.invalidCode, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const apiUrl = `${Resources.httpCatApi}${userInput}`;
|
||||
|
||||
try {
|
||||
await ctx.replyWithPhoto(apiUrl, {
|
||||
caption: `🐱 ${apiUrl}`,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.reply(Strings.catImgErr, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
88
telegram/commands/info.ts
Executable file
88
telegram/commands/info.ts
Executable file
|
@ -0,0 +1,88 @@
|
|||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import * as schema from '../../database/schema';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db?: NodePgDatabase<typeof schema>): Promise<{ Strings: any, languageCode: string }> {
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
const from = ctx.from;
|
||||
if (db && from.id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(from.id)), limit: 1 });
|
||||
if (dbUser.length > 0) {
|
||||
languageCode = dbUser[0].languageCode;
|
||||
}
|
||||
}
|
||||
if (from.language_code && languageCode === 'en') {
|
||||
languageCode = from.language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', from.id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { Strings, languageCode };
|
||||
}
|
||||
|
||||
async function getUserInfo(ctx: Context & { message: { text: string } }, db: any) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
let lastName = ctx.from?.last_name;
|
||||
if (lastName === undefined) {
|
||||
lastName = " ";
|
||||
}
|
||||
const userInfo = Strings.userInfo
|
||||
.replace('{userName}', `${ctx.from?.first_name} ${lastName}` || Strings.varStrings.varUnknown)
|
||||
.replace('{userId}', ctx.from?.id || Strings.varStrings.varUnknown)
|
||||
.replace('{userHandle}', ctx.from?.username ? `@${ctx.from?.username}` : Strings.varStrings.varNone)
|
||||
.replace('{userPremium}', ctx.from?.is_premium ? Strings.varStrings.varYes : Strings.varStrings.varNo)
|
||||
.replace('{userLang}', ctx.from?.language_code || Strings.varStrings.varUnknown);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
async function getChatInfo(ctx: Context & { message: { text: string } }, db: any) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
if ((ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup')) {
|
||||
const chat = ctx.chat as (typeof ctx.chat & { username?: string; is_forum?: boolean });
|
||||
const chatInfo = Strings.chatInfo
|
||||
.replace('{chatId}', chat?.id || Strings.varStrings.varUnknown)
|
||||
.replace('{chatName}', chat?.title || Strings.varStrings.varUnknown)
|
||||
.replace('{chatHandle}', chat?.username ? `@${chat.username}` : Strings.varStrings.varNone)
|
||||
.replace('{chatMembersCount}', await ctx.getChatMembersCount())
|
||||
.replace('{chatType}', chat?.type || Strings.varStrings.varUnknown)
|
||||
.replace('{isForum}', chat?.is_forum ? Strings.varStrings.varYes : Strings.varStrings.varNo);
|
||||
return chatInfo;
|
||||
} else {
|
||||
return Strings.groupOnly;
|
||||
}
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command('chatinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'info-commands')) return;
|
||||
|
||||
const chatInfo = await getChatInfo(ctx, db);
|
||||
ctx.reply(
|
||||
chatInfo, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
bot.command('userinfo', spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'info-commands')) return;
|
||||
|
||||
const userInfo = await getUserInfo(ctx, db);
|
||||
ctx.reply(
|
||||
userInfo, {
|
||||
parse_mode: 'Markdown',
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
229
telegram/commands/lastfm.ts
Executable file
229
telegram/commands/lastfm.ts
Executable file
|
@ -0,0 +1,229 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
const scrobbler_url = Resources.lastFmApi;
|
||||
const api_key = process.env.lastKey;
|
||||
|
||||
const dbFile = 'telegram/props/lastfm.json';
|
||||
let users = {};
|
||||
|
||||
function loadUsers() {
|
||||
if (!fs.existsSync(dbFile)) {
|
||||
console.log(`WARN: Last.fm user database ${dbFile} not found. Creating a new one.`);
|
||||
saveUsers();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(dbFile, 'utf-8');
|
||||
users = JSON.parse(data);
|
||||
} catch (err) {
|
||||
console.log("WARN: Error loading the Last.fm user database:", err);
|
||||
users = {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveUsers() {
|
||||
try {
|
||||
fs.writeFileSync(dbFile, JSON.stringify(users, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
console.error("WARN: Error saving Last.fm users:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getFromMusicBrainz(mbid: string) {
|
||||
try {
|
||||
const response = await axios.get(`${Resources.musicBrainzApi}${mbid}`);
|
||||
const imgObjLarge = response.data.images[0]?.thumbnails?.['1200'];
|
||||
const imgObjMid = response.data.images[0]?.thumbnails?.large;
|
||||
const imageUrl = imgObjLarge || imgObjMid || '';
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getFromLast(track) {
|
||||
if (!track || !track.image) return '';
|
||||
|
||||
const imageExtralarge = track.image.find(img => img.size === 'extralarge');
|
||||
const imageMega = track.image.find(img => img.size === 'mega');
|
||||
const imageUrl = (imageExtralarge?.['#text']) || (imageMega?.['#text']) || '';
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
export default (bot, db) => {
|
||||
loadUsers();
|
||||
|
||||
bot.command('setuser', async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'lastfm')) return;
|
||||
|
||||
const userId = ctx.from.id;
|
||||
const Strings = getStrings(ctx.from.language_code);
|
||||
const lastUser = ctx.message.text.split(' ')[1];
|
||||
|
||||
if (!lastUser) {
|
||||
return ctx.reply(Strings.lastFm.noUser, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
|
||||
users[userId] = lastUser;
|
||||
saveUsers();
|
||||
|
||||
const message = Strings.lastFm.userHasBeenSet.replace('{lastUser}', lastUser);
|
||||
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
});
|
||||
|
||||
bot.command(['lt', 'lmu', 'last', 'lfm'], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'lastfm')) return;
|
||||
|
||||
const userId = ctx.from.id;
|
||||
const Strings = getStrings(ctx.from.language_code);
|
||||
const lastfmUser = users[userId];
|
||||
const genericImg = Resources.lastFmGenericImg;
|
||||
const botInfo = await ctx.telegram.getMe();
|
||||
|
||||
if (!lastfmUser) {
|
||||
return ctx.reply(Strings.lastFm.noUserSet, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.get(scrobbler_url, {
|
||||
params: {
|
||||
method: 'user.getRecentTracks',
|
||||
user: lastfmUser,
|
||||
api_key,
|
||||
format: 'json',
|
||||
limit: 1
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': `@${botInfo.username}-node-telegram-bot`
|
||||
}
|
||||
});
|
||||
|
||||
const track = response.data.recenttracks.track[0];
|
||||
|
||||
if (!track) {
|
||||
const noRecent = Strings.lastFm.noRecentTracks.replace('{lastfmUser}', lastfmUser);
|
||||
return ctx.reply(noRecent, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
|
||||
const trackName = track.name;
|
||||
const artistName = track.artist['#text'];
|
||||
const nowPlaying = track['@attr'] && track['@attr'].nowplaying ? Strings.varStrings.varIs : Strings.varStrings.varWas;
|
||||
const albumMbid = track.album.mbid;
|
||||
|
||||
let imageUrl = "";
|
||||
|
||||
if (albumMbid) {
|
||||
imageUrl = await getFromMusicBrainz(albumMbid);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
imageUrl = getFromLast(track);
|
||||
}
|
||||
|
||||
if (imageUrl == genericImg) {
|
||||
imageUrl = "";
|
||||
}
|
||||
|
||||
const trackUrl = `https://www.last.fm/music/${encodeURIComponent(artistName)}/_/${encodeURIComponent(trackName)}`;
|
||||
const artistUrl = `https://www.last.fm/music/${encodeURIComponent(artistName)}`;
|
||||
const userUrl = `https://www.last.fm/user/${encodeURIComponent(lastfmUser)}`;
|
||||
|
||||
let num_plays = 0;
|
||||
try {
|
||||
const response_plays = await axios.get(scrobbler_url, {
|
||||
params: {
|
||||
method: 'track.getInfo',
|
||||
api_key,
|
||||
track: trackName,
|
||||
artist: artistName,
|
||||
username: lastfmUser,
|
||||
format: 'json',
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': `@${botInfo.username}-node-telegram-bot`
|
||||
}
|
||||
});
|
||||
|
||||
num_plays = response_plays.data.track.userplaycount;
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
const message = Strings.lastFm.apiErr
|
||||
.replace("{lastfmUser}", `[${lastfmUser}](${userUrl})`)
|
||||
.replace("{err}", err);
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
|
||||
let message = Strings.lastFm.listeningTo
|
||||
.replace("{lastfmUser}", `[${lastfmUser}](${userUrl})`)
|
||||
.replace("{nowPlaying}", nowPlaying)
|
||||
.replace("{trackName}", `[${trackName}](${trackUrl})`)
|
||||
.replace("{artistName}", `[${artistName}](${artistUrl})`)
|
||||
|
||||
if (`${num_plays}` !== "0" && `${num_plays}` !== "1" && `${num_plays}` !== "2" && `${num_plays}` !== "3") {
|
||||
message = message
|
||||
.replace("{playCount}", Strings.lastFm.playCount)
|
||||
.replace("{plays}", `${num_plays}`);
|
||||
} else {
|
||||
message = message
|
||||
.replace("{playCount}", Strings.varStrings.varTo);
|
||||
};
|
||||
|
||||
if (imageUrl) {
|
||||
ctx.replyWithPhoto(imageUrl, {
|
||||
caption: message,
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
} else {
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
const userUrl = `https://www.last.fm/user/${encodeURIComponent(lastfmUser)}`;
|
||||
const message = Strings.lastFm.apiErr
|
||||
.replace("{lastfmUser}", `[${lastfmUser}](${userUrl})`)
|
||||
.replace("{err}", err);
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
...(ctx.message?.message_id ? { reply_parameters: { message_id: ctx.message.message_id } } : {})
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
556
telegram/commands/main.ts
Executable file
556
telegram/commands/main.ts
Executable file
|
@ -0,0 +1,556 @@
|
|||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import * as schema from '../../database/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ensureUserInDb } from '../utils/ensure-user';
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { getModelLabelByName } from './ai';
|
||||
import { models } from '../../config/ai';
|
||||
import { langs } from '../locales/config';
|
||||
import { modelPageSize, seriesPageSize } from '../../config/settings';
|
||||
|
||||
type UserRow = typeof schema.usersTable.$inferSelect;
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
async function getUserAndStrings(ctx: Context, db: NodePgDatabase<typeof schema>): Promise<{ user: UserRow | null, Strings: any, languageCode: string }> {
|
||||
let user: UserRow | null = null;
|
||||
let languageCode = 'en';
|
||||
if (!ctx.from) {
|
||||
const Strings = getStrings(languageCode);
|
||||
return { user, Strings, languageCode };
|
||||
}
|
||||
const { id, language_code } = ctx.from;
|
||||
if (id) {
|
||||
const dbUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(id)), limit: 1 });
|
||||
if (dbUser.length === 0) {
|
||||
await ensureUserInDb(ctx, db);
|
||||
const newUser = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(id)), limit: 1 });
|
||||
if (newUser.length > 0) {
|
||||
user = newUser[0];
|
||||
languageCode = user.languageCode;
|
||||
}
|
||||
} else {
|
||||
user = dbUser[0];
|
||||
languageCode = user.languageCode;
|
||||
}
|
||||
}
|
||||
if (!user && language_code) {
|
||||
languageCode = language_code;
|
||||
console.warn('[WARN !] Falling back to Telegram language_code for user', id);
|
||||
}
|
||||
const Strings = getStrings(languageCode);
|
||||
return { user, Strings, languageCode };
|
||||
}
|
||||
|
||||
type SettingsMenu = { text: string, reply_markup: any };
|
||||
function getSettingsMenu(user: UserRow, Strings: any): SettingsMenu {
|
||||
const langObj = langs.find(l => l.code === user.languageCode);
|
||||
const langLabel = langObj ? langObj.label : user.languageCode;
|
||||
const userId = user.telegramId;
|
||||
return {
|
||||
text: `*${Strings.settings.selectSetting}*`,
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: `✨ ${Strings.settings.ai.aiEnabled}: ${user.aiEnabled ? Strings.settings.enabled : Strings.settings.disabled}`, callback_data: `settings_aiEnabled_${userId}` },
|
||||
{ text: `🧠 ${Strings.settings.ai.aiModel}: ${getModelLabelByName(user.customAiModel)}`, callback_data: `settings_aiModel_0_${userId}` }
|
||||
],
|
||||
[
|
||||
{ text: `🌡️ ${Strings.settings.ai.aiTemperature}: ${user.aiTemperature}`, callback_data: `settings_aiTemperature_${userId}` },
|
||||
{ text: `🌐 ${langLabel}`, callback_data: `settings_language_${userId}` }
|
||||
],
|
||||
[
|
||||
{ text: `🧠 ${Strings.settings.ai.showThinking}: ${user.showThinking ? Strings.settings.enabled : Strings.settings.disabled}`, callback_data: `settings_showThinking_${userId}` }
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function extractUserIdFromCallback(data: string): string | null {
|
||||
const match = data.match(/_(\d+)$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function getNotAllowedMessage(Strings: any) {
|
||||
return Strings.gsmarenaNotAllowed;
|
||||
}
|
||||
|
||||
function logSettingsAccess(action: string, ctx: Context, allowed: boolean, expectedUserId: string | null) {
|
||||
if (process.env.longerLogs === 'true') {
|
||||
const actualUserId = ctx.from?.id;
|
||||
const username = ctx.from?.username || ctx.from?.first_name || 'unknown';
|
||||
console.log(`[Settings] Action: ${action}, Callback from: ${username} (${actualUserId}), Expected: ${expectedUserId}, Allowed: ${allowed}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTelegramError(err: any, context: string) {
|
||||
const description = err?.response?.description || '';
|
||||
const ignoredErrors = [
|
||||
'query is too old',
|
||||
'query ID is invalid',
|
||||
'message is not modified',
|
||||
'message to edit not found',
|
||||
];
|
||||
|
||||
const isIgnored = ignoredErrors.some(errorString => description.includes(errorString));
|
||||
|
||||
if (!isIgnored) {
|
||||
console.error(`[${context}] Unexpected Telegram error:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db: NodePgDatabase<typeof schema>) => {
|
||||
bot.start(spamwatchMiddleware, async (ctx: Context) => {
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
const botInfo = await ctx.telegram.getMe();
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
const startMsg = Strings.botWelcome.replace(/{botName}/g, botInfo.first_name);
|
||||
if (!user) return;
|
||||
ctx.reply(
|
||||
startMsg.replace(
|
||||
/{aiEnabled}/g,
|
||||
user.aiEnabled ? Strings.settings.enabled : Strings.settings.disabled
|
||||
).replace(
|
||||
/{aiModel}/g,
|
||||
getModelLabelByName(user.customAiModel)
|
||||
).replace(
|
||||
/{aiTemperature}/g,
|
||||
user.aiTemperature.toString()
|
||||
).replace(
|
||||
/{aiRequests}/g,
|
||||
user.aiRequests.toString()
|
||||
).replace(
|
||||
/{aiCharacters}/g,
|
||||
user.aiCharacters.toString()
|
||||
).replace(
|
||||
/{languageCode}/g,
|
||||
user.languageCode
|
||||
), {
|
||||
parse_mode: 'Markdown',
|
||||
...({ reply_to_message_id })
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
bot.command(["settings"], spamwatchMiddleware, async (ctx: Context) => {
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const menu = getSettingsMenu(user, Strings);
|
||||
await ctx.reply(
|
||||
menu.text,
|
||||
{
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown',
|
||||
...({ reply_to_message_id })
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const updateSettingsKeyboard = async (ctx: Context, user: UserRow, Strings: any) => {
|
||||
const menu = getSettingsMenu(user, Strings);
|
||||
await ctx.editMessageReplyMarkup(menu.reply_markup);
|
||||
};
|
||||
|
||||
bot.action(/^settings_aiEnabled_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_aiEnabled', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
await db.update(schema.usersTable)
|
||||
.set({ aiEnabled: !user.aiEnabled })
|
||||
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
|
||||
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
|
||||
await updateSettingsKeyboard(ctx, updatedUser, Strings);
|
||||
});
|
||||
|
||||
bot.action(/^settings_showThinking_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_showThinking', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
await db.update(schema.usersTable)
|
||||
.set({ showThinking: !user.showThinking })
|
||||
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
|
||||
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
|
||||
await updateSettingsKeyboard(ctx, updatedUser, Strings);
|
||||
});
|
||||
|
||||
bot.action(/^settings_aiModel_(\d+)_(\d+)$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_aiModel', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
|
||||
const match = data.match(/^settings_aiModel_(\d+)_/);
|
||||
if (!match) return;
|
||||
|
||||
const page = parseInt(match[1], 10);
|
||||
const pageSize = 4;
|
||||
const start = page * pageSize;
|
||||
const end = start + pageSize;
|
||||
|
||||
const paginatedModels = models.slice(start, end);
|
||||
|
||||
const buttons = paginatedModels.map((series, idx) => {
|
||||
const originalIndex = start + idx;
|
||||
const isSelected = series.models.some(m => m.name === user.customAiModel);
|
||||
const label = isSelected ? `✅ ${series.label}` : series.label;
|
||||
return { text: label, callback_data: `selectseries_${originalIndex}_0_${user.telegramId}` };
|
||||
});
|
||||
|
||||
const navigationButtons: any[] = [];
|
||||
if (page > 0) {
|
||||
navigationButtons.push({ text: Strings.varStrings.varLess, callback_data: `settings_aiModel_${page - 1}_${user.telegramId}` });
|
||||
}
|
||||
if (end < models.length) {
|
||||
navigationButtons.push({ text: Strings.varStrings.varMore, callback_data: `settings_aiModel_${page + 1}_${user.telegramId}` });
|
||||
}
|
||||
|
||||
const keyboard: any[][] = [];
|
||||
for (const button of buttons) {
|
||||
keyboard.push([button]);
|
||||
}
|
||||
|
||||
if (navigationButtons.length > 0) {
|
||||
keyboard.push(navigationButtons);
|
||||
}
|
||||
keyboard.push([{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` }]);
|
||||
|
||||
try {
|
||||
await ctx.editMessageText(
|
||||
`${Strings.settings.ai.selectSeries}`,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: keyboard
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'settings_aiModel');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^selectseries_\d+_\d+_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('selectseries', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const match = data.match(/^selectseries_(\d+)_(\d+)_(\d+)$/);
|
||||
if (!match) return;
|
||||
const seriesIdx = parseInt(match[1], 10);
|
||||
const modelPage = parseInt(match[2], 10);
|
||||
const series = models[seriesIdx];
|
||||
if (!series) return;
|
||||
|
||||
const seriesPage = Math.floor(seriesIdx / seriesPageSize);
|
||||
|
||||
const start = modelPage * modelPageSize;
|
||||
const end = start + modelPageSize;
|
||||
const paginatedSeriesModels = series.models.slice(start, end);
|
||||
|
||||
const modelButtons = paginatedSeriesModels.map((m, idx) => {
|
||||
const originalModelIndex = start + idx;
|
||||
const isSelected = m.name === user.customAiModel;
|
||||
const label = isSelected ? `✅ ${m.label}` : m.label;
|
||||
return [{ text: `${label} (${m.parameterSize})`, callback_data: `setmodel_${seriesIdx}_${originalModelIndex}_${user.telegramId}` }];
|
||||
});
|
||||
|
||||
const navigationButtons: any[] = [];
|
||||
if (modelPage > 0) {
|
||||
navigationButtons.push({ text: Strings.varStrings.varLess, callback_data: `selectseries_${seriesIdx}_${modelPage - 1}_${user.telegramId}` });
|
||||
}
|
||||
if (end < series.models.length) {
|
||||
navigationButtons.push({ text: Strings.varStrings.varMore, callback_data: `selectseries_${seriesIdx}_${modelPage + 1}_${user.telegramId}` });
|
||||
}
|
||||
|
||||
const keyboard: any[][] = [...modelButtons];
|
||||
if (navigationButtons.length > 0) {
|
||||
keyboard.push(navigationButtons);
|
||||
}
|
||||
keyboard.push([{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_aiModel_${seriesPage}_${user.telegramId}` }]);
|
||||
const desc = user.languageCode === 'pt' ? series.descriptionPt : series.descriptionEn;
|
||||
try {
|
||||
await ctx.editMessageText(
|
||||
`${Strings.settings.ai.seriesDescription.replace('{seriesDescription}', desc)}\n\n${Strings.settings.ai.selectParameterSize.replace('{seriesLabel}', series.label).replace(' [ & Uncensored ]', '')}\n\n${Strings.settings.ai.parameterSizeExplanation}`,
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: keyboard
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'selectseries');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^setmodel_\d+_\d+_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('setmodel', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const match = data.match(/^setmodel_(\d+)_(\d+)_\d+$/);
|
||||
if (!match) return;
|
||||
const seriesIdx = parseInt(match[1], 10);
|
||||
const modelIdx = parseInt(match[2], 10);
|
||||
const series = models[seriesIdx];
|
||||
const model = series?.models[modelIdx];
|
||||
if (!series || !model) return;
|
||||
await db.update(schema.usersTable)
|
||||
.set({ customAiModel: model.name })
|
||||
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
|
||||
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
|
||||
const menu = getSettingsMenu(updatedUser, Strings);
|
||||
try {
|
||||
if (ctx.callbackQuery.message) {
|
||||
await ctx.editMessageText(
|
||||
menu.text,
|
||||
{
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await ctx.reply(menu.text, {
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'setmodel');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^settings_aiTemperature_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_aiTemperature', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const temps = [0.2, 0.5, 0.7, 0.9, 1.2];
|
||||
try {
|
||||
await ctx.editMessageText(
|
||||
`${Strings.settings.ai.temperatureExplanation}\n\n${Strings.settings.ai.selectTemperature}`,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: temps.map(t => [{ text: t.toString(), callback_data: `settemp_${t}_${user.telegramId}` }])
|
||||
.concat([
|
||||
[{ text: Strings.varStrings.varMore, callback_data: `show_more_temps_${user.telegramId}` }],
|
||||
[
|
||||
{ text: Strings.varStrings.varBack, callback_data: `settings_back_${user.telegramId}` }
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'settings_aiTemperature');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^show_more_temps_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('show_more_temps', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const moreTemps = [1.4, 1.6, 1.8, 2.0];
|
||||
try {
|
||||
await ctx.editMessageReplyMarkup({
|
||||
inline_keyboard: moreTemps.map(t => [{ text: `🔥 ${t}`, callback_data: `settemp_${t}_${user.telegramId}` }])
|
||||
.concat([
|
||||
[{ text: Strings.varStrings.varLess, callback_data: `settings_aiTemperature_${user.telegramId}` }],
|
||||
[{ text: Strings.varStrings.varBack, callback_data: `settings_back_${user.telegramId}` }]
|
||||
])
|
||||
});
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'show_more_temps');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^settemp_.+_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settemp', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const temp = parseFloat(data.replace(/^settemp_/, '').replace(/_\d+$/, ''));
|
||||
await db.update(schema.usersTable)
|
||||
.set({ aiTemperature: temp })
|
||||
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
|
||||
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
|
||||
await updateSettingsKeyboard(ctx, updatedUser, Strings);
|
||||
});
|
||||
|
||||
bot.action(/^settings_language_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_language', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
try {
|
||||
await ctx.editMessageText(
|
||||
Strings.settings.selectLanguage,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: langs.map(l => [{ text: l.label, callback_data: `setlang_${l.code}_${user.telegramId}` }]).concat([[{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` }]])
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'settings_language');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^settings_back_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('settings_back', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user, Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!user) return;
|
||||
const menu = getSettingsMenu(user, Strings);
|
||||
try {
|
||||
if (ctx.callbackQuery.message) {
|
||||
await ctx.editMessageText(
|
||||
menu.text,
|
||||
{
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await ctx.reply(menu.text, {
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'settings_back');
|
||||
}
|
||||
});
|
||||
|
||||
bot.action(/^setlang_.+_\d+$/, async (ctx) => {
|
||||
const data = (ctx.callbackQuery as any).data;
|
||||
const userId = extractUserIdFromCallback(data);
|
||||
const allowed = !!userId && String(ctx.from.id) === userId;
|
||||
logSettingsAccess('setlang', ctx, allowed, userId);
|
||||
if (!allowed) {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
return ctx.answerCbQuery(getNotAllowedMessage(Strings), { show_alert: true });
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
const { user } = await getUserAndStrings(ctx, db);
|
||||
if (!user) {
|
||||
console.log('[Settings] No user found');
|
||||
return;
|
||||
}
|
||||
const lang = data.replace(/^setlang_/, '').replace(/_\d+$/, '');
|
||||
await db.update(schema.usersTable)
|
||||
.set({ languageCode: lang })
|
||||
.where(eq(schema.usersTable.telegramId, String(user.telegramId)));
|
||||
const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0];
|
||||
const updatedStrings = getStrings(updatedUser.languageCode);
|
||||
const menu = getSettingsMenu(updatedUser, updatedStrings);
|
||||
try {
|
||||
if (ctx.callbackQuery.message) {
|
||||
await ctx.editMessageText(
|
||||
menu.text,
|
||||
{
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await ctx.reply(menu.text, {
|
||||
reply_markup: menu.reply_markup,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleTelegramError(err, 'setlang');
|
||||
}
|
||||
});
|
||||
|
||||
bot.command('privacy', spamwatchMiddleware, async (ctx: Context) => {
|
||||
const { Strings } = await getUserAndStrings(ctx, db);
|
||||
if (!ctx.from || !ctx.message) return;
|
||||
const message = Strings.botPrivacy.replace("{botPrivacy}", process.env.botPrivacy ?? "");
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id
|
||||
} as any);
|
||||
});
|
||||
};
|
88
telegram/commands/modarchive.ts
Executable file
88
telegram/commands/modarchive.ts
Executable file
|
@ -0,0 +1,88 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
interface ModuleResult {
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
async function downloadModule(moduleId: string): Promise<ModuleResult | null> {
|
||||
try {
|
||||
const downloadUrl = `${Resources.modArchiveApi}${moduleId}`;
|
||||
const response = await axios({
|
||||
url: downloadUrl,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
});
|
||||
const disposition = response.headers['content-disposition'];
|
||||
let fileName = moduleId;
|
||||
if (disposition && disposition.includes('filename=')) {
|
||||
fileName = disposition
|
||||
.split('filename=')[1]
|
||||
.split(';')[0]
|
||||
.replace(/['"]/g, '');
|
||||
}
|
||||
const filePath = path.join(__dirname, fileName);
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve({ filePath, fileName }));
|
||||
writer.on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const modarchiveHandler = async (ctx: Context) => {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
const moduleId = ctx.message && 'text' in ctx.message && typeof ctx.message.text === 'string'
|
||||
? ctx.message.text.split(' ')[1]?.trim()
|
||||
: undefined;
|
||||
if (!moduleId || !/^\d+$/.test(moduleId)) {
|
||||
return ctx.reply(Strings.maInvalidModule, {
|
||||
parse_mode: "Markdown",
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
}
|
||||
const result = await downloadModule(moduleId);
|
||||
if (result) {
|
||||
const { filePath, fileName } = result;
|
||||
const regexExtension = /\.\w+$/i;
|
||||
const hasExtension = regexExtension.test(fileName);
|
||||
if (hasExtension) {
|
||||
try {
|
||||
await ctx.replyWithDocument({ source: filePath }, {
|
||||
caption: fileName,
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} finally {
|
||||
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
return ctx.reply(Strings.maInvalidModule, {
|
||||
parse_mode: "Markdown",
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command(['modarchive', 'tma'], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'modarchive')) return;
|
||||
await modarchiveHandler(ctx);
|
||||
});
|
||||
};
|
286
telegram/commands/ponyapi.ts
Executable file
286
telegram/commands/ponyapi.ts
Executable file
|
@ -0,0 +1,286 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import verifyInput from '../plugins/verifyInput';
|
||||
import { Telegraf, Context } from 'telegraf';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
alias: string;
|
||||
url: string;
|
||||
sex: string;
|
||||
residence: string;
|
||||
occupation: string;
|
||||
kind: string;
|
||||
image: string[];
|
||||
}
|
||||
|
||||
interface Episode {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
url: string;
|
||||
season: string;
|
||||
episode: string;
|
||||
overall: string;
|
||||
airdate: string;
|
||||
storyby: string;
|
||||
writtenby: string;
|
||||
storyboard: string;
|
||||
}
|
||||
|
||||
interface Comic {
|
||||
id: string;
|
||||
name: string;
|
||||
series: string;
|
||||
image: string;
|
||||
url: string;
|
||||
writer: string;
|
||||
artist: string;
|
||||
colorist: string;
|
||||
letterer: string;
|
||||
editor: string;
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(letter: string) {
|
||||
return letter.charAt(0).toUpperCase() + letter.slice(1);
|
||||
}
|
||||
|
||||
function sendReply(ctx: Context, text: string, reply_to_message_id?: number) {
|
||||
return ctx.reply(text, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
}
|
||||
|
||||
function sendPhoto(ctx: Context, photo: string, caption: string, reply_to_message_id?: number) {
|
||||
return ctx.replyWithPhoto(photo, {
|
||||
caption,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command("mlp", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
|
||||
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
sendReply(ctx, Strings.ponyApi.helpDesc, reply_to_message_id);
|
||||
});
|
||||
|
||||
bot.command("mlpchar", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
|
||||
|
||||
const { message } = ctx;
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
const Strings = getStrings(languageCode(ctx) || 'en');
|
||||
const userInput = message.text.split(' ').slice(1).join(' ').trim().replace(/\s+/g, '+');
|
||||
const { noCharName } = Strings.ponyApi;
|
||||
|
||||
if (verifyInput(ctx, userInput, noCharName)) return;
|
||||
if (!userInput || /[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
|
||||
return sendReply(ctx, Strings.mlpInvalidCharacter, reply_to_message_id);
|
||||
}
|
||||
|
||||
const capitalizedInput = capitalizeFirstLetter(userInput);
|
||||
const apiUrl = `${Resources.ponyApi}/character/${capitalizedInput}`;
|
||||
|
||||
try {
|
||||
const response = await axios(apiUrl);
|
||||
const data = response.data.data;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
const character = data[0];
|
||||
const aliases = Array.isArray(character.alias)
|
||||
? character.alias.join(', ')
|
||||
: character.alias || Strings.varStrings.varNone;
|
||||
const result = Strings.ponyApi.charRes
|
||||
.replace("{id}", character.id)
|
||||
.replace("{name}", character.name)
|
||||
.replace("{alias}", aliases)
|
||||
.replace("{url}", character.url)
|
||||
.replace("{sex}", character.sex)
|
||||
.replace("{residence}", character.residence ? character.residence.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
|
||||
.replace("{occupation}", character.occupation ? character.occupation.replace(/\n/g, ' / ') : Strings.varStrings.varNone)
|
||||
.replace("{kind}", Array.isArray(character.kind) ? character.kind.join(', ') : Strings.varStrings.varNone);
|
||||
sendPhoto(ctx, character.image[0], result, reply_to_message_id);
|
||||
} else {
|
||||
sendReply(ctx, Strings.ponyApi.noCharFound, reply_to_message_id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message || 'Unknown error');
|
||||
sendReply(ctx, message, reply_to_message_id);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("mlpep", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
|
||||
|
||||
const Strings = getStrings(languageCode(ctx) || 'en');
|
||||
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+");
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
const { noEpisodeNum } = Strings.ponyApi
|
||||
|
||||
if (verifyInput(ctx, userInput, noEpisodeNum, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(userInput) > 10000) {
|
||||
ctx.reply(Strings.mlpInvalidEpisode, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = `${Resources.ponyApi}/episode/by-overall/${userInput}`;
|
||||
|
||||
try {
|
||||
const response = await axios(apiUrl);
|
||||
const episodeArray: Episode[] = [];
|
||||
|
||||
if (Array.isArray(response.data.data)) {
|
||||
response.data.data.forEach((episode: Episode) => {
|
||||
episodeArray.push({
|
||||
id: episode.id,
|
||||
name: episode.name,
|
||||
image: episode.image,
|
||||
url: episode.url,
|
||||
season: episode.season,
|
||||
episode: episode.episode,
|
||||
overall: episode.overall,
|
||||
airdate: episode.airdate,
|
||||
storyby: episode.storyby ? episode.storyby.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
writtenby: episode.writtenby ? episode.writtenby.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
storyboard: episode.storyboard ? episode.storyboard.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (episodeArray.length > 0) {
|
||||
const result = Strings.ponyApi.epRes
|
||||
.replace("{id}", episodeArray[0].id)
|
||||
.replace("{name}", episodeArray[0].name)
|
||||
.replace("{url}", episodeArray[0].url)
|
||||
.replace("{season}", episodeArray[0].season)
|
||||
.replace("{episode}", episodeArray[0].episode)
|
||||
.replace("{overall}", episodeArray[0].overall)
|
||||
.replace("{airdate}", episodeArray[0].airdate)
|
||||
.replace("{storyby}", episodeArray[0].storyby)
|
||||
.replace("{writtenby}", episodeArray[0].writtenby)
|
||||
.replace("{storyboard}", episodeArray[0].storyboard);
|
||||
|
||||
ctx.replyWithPhoto(episodeArray[0].image, {
|
||||
caption: `${result}`,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} else {
|
||||
ctx.reply(Strings.ponyApi.noEpisodeFound, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
bot.command("mlpcomic", spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'mlp-content')) return;
|
||||
|
||||
const Strings = getStrings(languageCode(ctx) || 'en');
|
||||
const userInput = ctx.message.text.split(' ').slice(1).join(' ').replace(" ", "+");
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
const { noComicName } = Strings.ponyApi
|
||||
|
||||
if (verifyInput(ctx, userInput, noComicName)) {
|
||||
return;
|
||||
};
|
||||
|
||||
// if special characters or numbers (max 30 characters)
|
||||
if (/[^a-zA-Z\s]/.test(userInput) || userInput.length > 30) {
|
||||
ctx.reply(Strings.mlpInvalidCharacter, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = `${Resources.ponyApi}/comics-story/${userInput}`;
|
||||
|
||||
try {
|
||||
const response = await axios(apiUrl);
|
||||
const comicArray: Comic[] = [];
|
||||
if (Array.isArray(response.data.data)) {
|
||||
response.data.data.forEach(comic => {
|
||||
let letterers: string[] = [];
|
||||
if (comic.letterer) {
|
||||
if (typeof comic.letterer === 'string') {
|
||||
letterers.push(comic.letterer);
|
||||
} else if (Array.isArray(comic.letterer)) {
|
||||
letterers = letterers.concat(comic.letterer);
|
||||
}
|
||||
}
|
||||
comicArray.push({
|
||||
id: comic.id,
|
||||
name: comic.name,
|
||||
series: comic.series,
|
||||
image: comic.image,
|
||||
url: comic.url,
|
||||
writer: comic.writer ? comic.writer.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
artist: comic.artist ? comic.artist.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
colorist: comic.colorist ? comic.colorist.replace(/\n/g, ' / ') : Strings.varStrings.varNone,
|
||||
letterer: letterers.length > 0 ? letterers.join(', ') : Strings.varStrings.varNone,
|
||||
editor: comic.editor
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (comicArray.length > 0) {
|
||||
const result = Strings.ponyApi.comicRes
|
||||
.replace("{id}", comicArray[0].id)
|
||||
.replace("{name}", comicArray[0].name)
|
||||
.replace("{series}", comicArray[0].series)
|
||||
.replace("{url}", comicArray[0].url)
|
||||
.replace("{writer}", comicArray[0].writer)
|
||||
.replace("{artist}", comicArray[0].artist)
|
||||
.replace("{colorist}", comicArray[0].colorist)
|
||||
.replace("{letterer}", comicArray[0].letterer)
|
||||
.replace("{editor}", comicArray[0].editor);
|
||||
|
||||
ctx.replyWithPhoto(comicArray[0].image, {
|
||||
caption: `${result}`,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} else {
|
||||
ctx.reply(Strings.ponyApi.noComicFound, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
32
telegram/commands/quotes.ts
Executable file
32
telegram/commands/quotes.ts
Executable file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import escape from 'markdown-escape';
|
||||
import axios from 'axios';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
export default (bot) => {
|
||||
bot.command("quote", spamwatchMiddleware, async (ctx) => {
|
||||
const Strings = getStrings(ctx.from.language_code);
|
||||
|
||||
try {
|
||||
const response = await axios.get(Resources.quoteApi);
|
||||
const data = response.data;
|
||||
|
||||
ctx.reply(escape(`${escape(Strings.quoteResult)}\n> *${escape(data.quote)}*\n_${escape(data.author)}_`), {
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ctx.reply(Strings.quoteErr, {
|
||||
reply_to_message_id: ctx.message.id,
|
||||
parse_mode: 'MarkdownV2'
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
*/
|
52
telegram/commands/randompony.ts
Executable file
52
telegram/commands/randompony.ts
Executable file
|
@ -0,0 +1,52 @@
|
|||
import Resources from '../props/resources.json';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import axios from 'axios';
|
||||
import { Telegraf, Context } from 'telegraf';
|
||||
import { languageCode } from '../utils/language-code';
|
||||
import { replyToMessageId } from '../utils/reply-to-message-id';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
export const randomponyHandler = async (ctx: Context & { message: { text: string } }) => {
|
||||
const Strings = getStrings(languageCode(ctx));
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
ctx.reply(Strings.ponyApi.searching, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
try {
|
||||
const response = await axios(Resources.randomPonyApi);
|
||||
let tags: string[] = [];
|
||||
|
||||
if (response.data.pony.tags) {
|
||||
if (typeof response.data.pony.tags === 'string') {
|
||||
tags.push(response.data.pony.tags);
|
||||
} else if (Array.isArray(response.data.pony.tags)) {
|
||||
tags = tags.concat(response.data.pony.tags);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.replyWithPhoto(response.data.pony.representations.full, {
|
||||
caption: `${response.data.pony.sourceURL}\n\n${tags.length > 0 ? tags.join(', ') : ''}`,
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
const message = Strings.ponyApi.apiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id ? { reply_parameters: { message_id: reply_to_message_id } } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export default (bot: Telegraf<Context>, db) => {
|
||||
bot.command(["rpony", "randompony", "mlpart"], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'random-pony')) return;
|
||||
await randomponyHandler(ctx);
|
||||
});
|
||||
}
|
124
telegram/commands/weather.ts
Executable file
124
telegram/commands/weather.ts
Executable file
|
@ -0,0 +1,124 @@
|
|||
// Ported and improved from BubbalooTeam's PyCoala bot
|
||||
// Copyright (c) 2024 BubbalooTeam. (https://github.com/BubbalooTeam)
|
||||
// Minor code changes by lucmsilva (https://github.com/lucmsilva651)
|
||||
|
||||
import Resources from '../props/resources.json';
|
||||
import axios from 'axios';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import verifyInput from '../plugins/verifyInput';
|
||||
import { Context, Telegraf } from 'telegraf';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
const statusEmojis = {
|
||||
0: '⛈', 1: '⛈', 2: '⛈', 3: '⛈', 4: '⛈', 5: '🌨', 6: '🌨', 7: '🌨',
|
||||
8: '🌨', 9: '🌨', 10: '🌨', 11: '🌧', 12: '🌧', 13: '🌨', 14: '🌨',
|
||||
15: '🌨', 16: '🌨', 17: '⛈', 18: '🌧', 19: '🌫', 20: '🌫', 21: '🌫',
|
||||
22: '🌫', 23: '🌬', 24: '🌬', 25: '🌨', 26: '☁️', 27: '🌥', 28: '🌥',
|
||||
29: '⛅️', 30: '⛅️', 31: '🌙', 32: '☀️', 33: '🌤', 34: '🌤', 35: '⛈',
|
||||
36: '🔥', 37: '🌩', 38: '🌩', 39: '🌧', 40: '🌧', 41: '❄️', 42: '❄️',
|
||||
43: '❄️', 44: 'n/a', 45: '🌧', 46: '🌨', 47: '🌩'
|
||||
};
|
||||
|
||||
const getStatusEmoji = (statusCode: number) => statusEmojis[statusCode] || 'n/a';
|
||||
|
||||
function getLocaleUnit(countryCode: string) {
|
||||
const fahrenheitCountries: string[] = ['US', 'BS', 'BZ', 'KY', 'LR'];
|
||||
|
||||
if (fahrenheitCountries.includes(countryCode)) {
|
||||
return { temperatureUnit: 'F', speedUnit: 'mph', apiUnit: 'e' };
|
||||
} else {
|
||||
return { temperatureUnit: 'C', speedUnit: 'km/h', apiUnit: 'm' };
|
||||
}
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>, db: any) => {
|
||||
bot.command(['weather', 'clima'], spamwatchMiddleware, async (ctx: Context & { message: { text: string } }) => {
|
||||
if (await isCommandDisabled(ctx, db, 'weather')) return;
|
||||
|
||||
const reply_to_message_id = ctx.message.message_id;
|
||||
const userLang = ctx.from?.language_code || "en-US";
|
||||
const Strings = getStrings(userLang);
|
||||
const userInput = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
const { provideLocation } = Strings.weatherStatus
|
||||
|
||||
if (verifyInput(ctx, userInput, provideLocation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location: string = userInput;
|
||||
const apiKey: string = process.env.weatherKey || '';
|
||||
|
||||
if (!apiKey || apiKey === "InsertYourWeatherDotComApiKeyHere") {
|
||||
return ctx.reply(Strings.weatherStatus.apiKeyErr, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: this also needs to be sanitized and validated
|
||||
const locationResponse = await axios.get(`${Resources.weatherApi}/location/search`, {
|
||||
params: {
|
||||
apiKey: apiKey,
|
||||
format: 'json',
|
||||
language: userLang,
|
||||
query: location,
|
||||
},
|
||||
});
|
||||
|
||||
const locationData = locationResponse.data.location;
|
||||
if (!locationData || !locationData.address) {
|
||||
return ctx.reply(Strings.weatherStatus.invalidLocation, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
}
|
||||
|
||||
const addressFirst = locationData.address[0];
|
||||
const latFirst = locationData.latitude[0];
|
||||
const lonFirst = locationData.longitude[0];
|
||||
const countryCode = locationData.countryCode[0];
|
||||
const { temperatureUnit, speedUnit, apiUnit } = getLocaleUnit(countryCode);
|
||||
|
||||
const weatherResponse = await axios.get(`${Resources.weatherApi}/aggcommon/v3-wx-observations-current`, {
|
||||
params: {
|
||||
apiKey: apiKey,
|
||||
format: 'json',
|
||||
language: userLang,
|
||||
geocode: `${latFirst},${lonFirst}`,
|
||||
units: apiUnit,
|
||||
},
|
||||
});
|
||||
|
||||
const weatherData = weatherResponse.data['v3-wx-observations-current'];
|
||||
const { temperature, temperatureFeelsLike, relativeHumidity, windSpeed, iconCode, wxPhraseLong } = weatherData;
|
||||
|
||||
const weatherMessage = Strings.weatherStatus.resultMsg
|
||||
.replace('{addressFirst}', addressFirst)
|
||||
.replace('{getStatusEmoji(iconCode)}', getStatusEmoji(iconCode))
|
||||
.replace('{wxPhraseLong}', wxPhraseLong)
|
||||
.replace('{temperature}', temperature)
|
||||
.replace('{temperatureFeelsLike}', temperatureFeelsLike)
|
||||
.replace('{temperatureUnit}', temperatureUnit)
|
||||
.replace('{temperatureUnit2}', temperatureUnit)
|
||||
.replace('{relativeHumidity}', relativeHumidity)
|
||||
.replace('{windSpeed}', windSpeed)
|
||||
.replace('{speedUnit}', speedUnit);
|
||||
|
||||
ctx.reply(weatherMessage, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
} catch (error) {
|
||||
const message = Strings.weatherStatus.apiErr.replace('{error}', error.message);
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
41
telegram/commands/wiki.ts
Executable file
41
telegram/commands/wiki.ts
Executable file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
import axios from "axios";
|
||||
import { Context, Telegraf } from "telegraf";
|
||||
import { replyToMessageId } from "../utils/reply-to-message-id";
|
||||
|
||||
function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function mediaWikiToMarkdown(input: string) {
|
||||
input = input.replace(/===(.*?)===/g, '*$1*');
|
||||
input = input.replace(/==(.*?)==/g, '*$1*');
|
||||
input = input.replace(/=(.*?)=/g, '*$1*');
|
||||
input = input.replace(/'''(.*?)'''/g, '**$1**');
|
||||
input = input.replace(/''(.*?)''/g, '_$1_');
|
||||
input = input.replace(/^\*\s/gm, '- ');
|
||||
input = input.replace(/^\#\s/gm, '1. ');
|
||||
input = input.replace(/{{Quote(.*?)}}/g, "```\n$1```\n");
|
||||
input = input.replace(/\[\[(.*?)\|?(.*?)\]\]/g, (_, link, text) => {
|
||||
const sanitizedLink = link.replace(/ /g, '_');
|
||||
return text ? `[${text}](${sanitizedLink})` : `[${sanitizedLink}](${sanitizedLink})`;
|
||||
});
|
||||
input = input.replace(/\[\[File:(.*?)\|.*?\]\]/g, '');
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export default (bot: Telegraf<Context>) => {
|
||||
bot.command("wiki", async (ctx) => {
|
||||
const userInput = capitalizeFirstLetter(ctx.message.text.split(' ')[1]);
|
||||
const apiUrl = `https://en.wikipedia.org/w/index.php?title=${userInput}&action=raw`;
|
||||
const response = await axios(apiUrl, { headers: { 'Accept': "text/plain" } });
|
||||
const convertedResponse = response.data.replace(/<\/?div>/g, "").replace(/{{Infobox.*?}}/s, "");
|
||||
|
||||
const result = mediaWikiToMarkdown(convertedResponse).slice(0, 2048);
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
ctx.reply(result, { parse_mode: 'Markdown', ...({ reply_to_message_id, disable_web_page_preview: true }) });
|
||||
});
|
||||
};
|
||||
*/
|
256
telegram/commands/youtube.ts
Executable file
256
telegram/commands/youtube.ts
Executable file
|
@ -0,0 +1,256 @@
|
|||
import { getStrings } from '../plugins/checklang';
|
||||
import { isOnSpamWatch } from '../spamwatch/spamwatch';
|
||||
import spamwatchMiddlewareModule from '../spamwatch/Middleware';
|
||||
import { execFile } from 'child_process';
|
||||
import { isCommandDisabled } from '../utils/check-command-disabled';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as ytUrl from 'youtube-url';
|
||||
|
||||
const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch);
|
||||
|
||||
const ytDlpPaths = {
|
||||
linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'),
|
||||
win32: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp.exe'),
|
||||
darwin: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp_macos'),
|
||||
};
|
||||
|
||||
const getYtDlpPath = () => {
|
||||
const platform = os.platform();
|
||||
return ytDlpPaths[platform] || ytDlpPaths.linux;
|
||||
};
|
||||
|
||||
|
||||
const ffmpegPaths = {
|
||||
linux: '/usr/bin/ffmpeg',
|
||||
win32: path.resolve(__dirname, '../plugins/ffmpeg/bin/ffmpeg.exe'),
|
||||
};
|
||||
|
||||
const getFfmpegPath = () => {
|
||||
const platform = os.platform();
|
||||
return ffmpegPaths[platform] || ffmpegPaths.linux;
|
||||
};
|
||||
|
||||
const downloadFromYoutube = async (command: string, args: string[]): Promise<{ stdout: string; stderr: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject({ error, stdout, stderr });
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getApproxSize = async (command: string, videoUrl: string): Promise<number> => {
|
||||
let args: string[] = [];
|
||||
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
|
||||
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx', '--cookies', path.resolve(__dirname, "../props/cookies.txt")];
|
||||
} else {
|
||||
args = [videoUrl, '--compat-opt', 'manifest-filesize-approx', '-O', 'filesize_approx'];
|
||||
}
|
||||
try {
|
||||
const { stdout } = await downloadFromYoutube(command, args);
|
||||
const sizeInBytes = parseInt(stdout.trim(), 10);
|
||||
if (!isNaN(sizeInBytes)) {
|
||||
return sizeInBytes / (1024 * 1024);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const isValidUrl = (url: string): boolean => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default (bot, db) => {
|
||||
bot.command(['yt', 'ytdl', 'sdl', 'video', 'dl'], spamwatchMiddleware, async (ctx) => {
|
||||
if (await isCommandDisabled(ctx, db, 'youtube-download')) return;
|
||||
|
||||
const Strings = getStrings(ctx.from.language_code);
|
||||
const ytDlpPath = getYtDlpPath();
|
||||
const userId: number = ctx.from.id;
|
||||
const videoUrl: string = ctx.message.text.split(' ').slice(1).join(' ');
|
||||
const videoIsYoutube: boolean = ytUrl.valid(videoUrl);
|
||||
const randId: string = Math.random().toString(36).substring(2, 15);
|
||||
const mp4File: string = `tmp/${userId}-${randId}.mp4`;
|
||||
const tempMp4File: string = `tmp/${userId}-${randId}.f137.mp4`;
|
||||
const tempWebmFile: string = `tmp/${userId}-${randId}.f251.webm`;
|
||||
let cmdArgs: string = "";
|
||||
const dlpCommand: string = ytDlpPath;
|
||||
const ffmpegPath: string = getFfmpegPath();
|
||||
const ffmpegArgs: string[] = ['-i', tempMp4File, '-i', tempWebmFile, '-c:v copy -c:a copy -strict -2', mp4File];
|
||||
|
||||
/*
|
||||
for now, no checking is done for the video url
|
||||
yt-dlp should handle the validation, though it supports too many sites to hard-code
|
||||
*/
|
||||
if (!videoUrl) {
|
||||
return ctx.reply(Strings.ytDownload.noLink, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
reply_to_message_id: ctx.message.message_id
|
||||
});
|
||||
}
|
||||
|
||||
// make sure its a valid url
|
||||
if (!isValidUrl(videoUrl)) {
|
||||
console.log("[!] Invalid URL:", videoUrl)
|
||||
return ctx.reply(Strings.ytDownload.noLink, {
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
reply_to_message_id: ctx.message.message_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\nDownload Request:\nURL: ${videoUrl}\nYOUTUBE: ${videoIsYoutube}\n`)
|
||||
|
||||
if (fs.existsSync(path.resolve(__dirname, "../props/cookies.txt"))) {
|
||||
cmdArgs = "--max-filesize 2G --no-playlist --cookies telegram/props/cookies.txt --merge-output-format mp4 -o";
|
||||
} else {
|
||||
cmdArgs = `--max-filesize 2G --no-playlist --merge-output-format mp4 -o`;
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadingMessage = await ctx.reply(Strings.ytDownload.checkingSize, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
});
|
||||
|
||||
if (fs.existsSync(ytDlpPath)) {
|
||||
const approxSizeInMB = await Promise.race([
|
||||
getApproxSize(ytDlpPath, videoUrl),
|
||||
]);
|
||||
|
||||
if (approxSizeInMB > 50) {
|
||||
console.log("[!] Video size exceeds 50MB:", approxSizeInMB)
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
Strings.ytDownload.uploadLimit, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[i] Downloading video...")
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
Strings.ytDownload.downloadingVid, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
|
||||
const dlpArgs = [videoUrl, ...cmdArgs.split(' '), mp4File];
|
||||
await downloadFromYoutube(dlpCommand, dlpArgs);
|
||||
|
||||
console.log("[i] Uploading video...")
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
Strings.ytDownload.uploadingVid, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
|
||||
if (fs.existsSync(tempMp4File)) {
|
||||
await downloadFromYoutube(ffmpegPath, ffmpegArgs);
|
||||
}
|
||||
|
||||
if (fs.existsSync(mp4File)) {
|
||||
const message = Strings.ytDownload.msgDesc.replace("{userMention}", `[${ctx.from.first_name}](tg://user?id=${userId})`)
|
||||
|
||||
try {
|
||||
await ctx.replyWithVideo({
|
||||
source: mp4File
|
||||
}, {
|
||||
caption: message,
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
});
|
||||
|
||||
fs.unlinkSync(mp4File);
|
||||
} catch (error) {
|
||||
if (error.response.description.includes("Request Entity Too Large")) {
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
Strings.ytDownload.uploadLimit, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const errMsg = Strings.ytDownload.uploadErr.replace("{error}", error)
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
errMsg, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
fs.unlinkSync(mp4File);
|
||||
}
|
||||
} else {
|
||||
await ctx.reply(mp4File, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await ctx.telegram.editMessageText(
|
||||
ctx.chat.id,
|
||||
downloadingMessage.message_id,
|
||||
null,
|
||||
Strings.ytDownload.libNotFound, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
console.log("[i] Request completed\n")
|
||||
} catch (error) {
|
||||
let errMsg = Strings.ytDownload.uploadErr
|
||||
|
||||
if (error.stderr.includes("--cookies-from-browser")) {
|
||||
console.log("[!] Ratelimited by video provider:", error.stderr)
|
||||
errMsg = Strings.ytDownload.botDetection
|
||||
if (error.stderr.includes("youtube")) {
|
||||
errMsg = Strings.ytDownload.botDetection.replace("video provider", "YouTube")
|
||||
}
|
||||
} else {
|
||||
console.log("[!]", error.stderr)
|
||||
}
|
||||
|
||||
// will no longer edit the message as the message context is not outside the try block
|
||||
await ctx.reply(errMsg, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_to_message_id: ctx.message.message_id,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
4
telegram/locales/config.ts
Executable file
4
telegram/locales/config.ts
Executable file
|
@ -0,0 +1,4 @@
|
|||
export const langs = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'pt', label: 'Português' }
|
||||
];
|
237
telegram/locales/english.json
Executable file
237
telegram/locales/english.json
Executable file
|
@ -0,0 +1,237 @@
|
|||
{
|
||||
"userNotFound": "User not found.",
|
||||
"botWelcome": "*Hello! I'm {botName}!*\nI was made with love by some nerds who really love programming!\n\n*By using {botName}, you affirm that you have read to and agree with the privacy policy (/privacy). This helps you understand where your data goes when using this bot.*\n\nAlso, you can use /help to see the bot commands!",
|
||||
"botHelp": "*Hey, I'm {botName}, a simple bot made entirely from scratch in Telegraf and Node.js by some nerds who really love programming.*\n\nCheck out the source code: [Click here to go to GitHub]({sourceLink})\n\nClick on the buttons below to see which commands you can use!\n",
|
||||
"botPrivacy": "Check out [this link]({botPrivacy}) to read the bot's privacy policy.",
|
||||
"botAbout": "*About the bot*\n\nThe bot base was originally created by [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), now maintained by several people.\n\nThe bot's purpose is to bring fun to your groups here on Telegram in a relaxed and simple way. The bot also features some very useful commands, which you can see using the help command (/help).\n\nSpecial thanks to @givfnz2 for his many contributions to the bot!\n\nSee the source code: [Click here to go to GitHub]({sourceLink})",
|
||||
"aboutBot": "About the bot",
|
||||
"varStrings": {
|
||||
"varYes": "Yes",
|
||||
"varNo": "No",
|
||||
"varTo": "to",
|
||||
"varIs": "is",
|
||||
"varWas": "was",
|
||||
"varNone": "None",
|
||||
"varUnknown": "Unknown",
|
||||
"varBack": "⬅️ Back",
|
||||
"varMore": "➡️ More",
|
||||
"varLess": "➖ Less"
|
||||
},
|
||||
"unexpectedErr": "An unexpected error occurred: {error}",
|
||||
"errInvalidOption": "Whoops! Invalid option!",
|
||||
"commandDisabled": "🚫 This command is currently disabled for your account.\n\nYou can enable it in the web interface: {frontUrl}",
|
||||
"kickingMyself": "*Since you don't need me, I'll leave.*",
|
||||
"kickingMyselfErr": "Error leaving the chat.",
|
||||
"noPermission": "You don't have permission to run this command.",
|
||||
"privateOnly": "This command should only be used in private chats, not in groups.",
|
||||
"groupOnly": "This command should only be used in groups, not in private chats.",
|
||||
"botNameChanged": "*Bot name changed to* `{botName}`.",
|
||||
"botNameErr": "*Error changing bot name:*\n`{tgErr}`",
|
||||
"botDescChanged": "*Bot description changed to* `{botDesc}`.",
|
||||
"botDescErr": "*Error changing bot description:*\n`{tgErr}`",
|
||||
"gayAmount": "You are *{randomNum}%* gay!",
|
||||
"furryAmount": "You are *{randomNum}%* furry!",
|
||||
"randomNum": "*Generated number (0-10):* `{number}`.",
|
||||
"userInfo": "*User info*\n\n*Name:* `{userName}`\n*Username:* `{userHandle}`\n*User ID:* `{userId}`\n*Language:* `{userLang}`\n*Premium user:* `{userPremium}`",
|
||||
"chatInfo": "*Chat info*\n\n*Name:* `{chatName}`\n*Chat ID:* `{chatId}`\n*Handle:* `{chatHandle}`\n*Type:* `{chatType}`\n*Members:* `{chatMembersCount}`\n*Is a forum:* `{isForum}`",
|
||||
"funEmojiResult": "*You rolled {emoji} and got* `{value}`*!*\nYou don't know what that means? Me neither!",
|
||||
"gifErr": "*Something went wrong while sending the GIF. Please try again later.*\n\n{err}",
|
||||
"lastFm": {
|
||||
"helpEntry": "🎵 Last.fm",
|
||||
"helpDesc": "🎵 *Last.fm*\n\n- /lt | /lmu | /last | /lfm: Shows the last song from your Last.fm profile + the number of plays.\n- /setuser `<user>`: Sets the user for the command above.",
|
||||
"noUser": "*Please provide a Last.fm username.*\nExample: `/setuser <username>`",
|
||||
"noUserSet": "*You haven't set your Last.fm username yet.*\nUse the command /setuser to set.\n\nExample: `/setuser <username>`",
|
||||
"noRecentTracks": "*No recent tracks found for Last.fm user* `{lastfmUser}`*.*",
|
||||
"userHasBeenSet": "*Your Last.fm username has been set to:* `{lastUser}`.",
|
||||
"listeningTo": "{lastfmUser} *{nowPlaying} listening {playCount}*:\n\n{trackName} by {artistName}",
|
||||
"playCount": "to, for the {plays}th time",
|
||||
"apiErr": "*Error retrieving data for Last.fm user* {lastfmUser}.\n\n`{err}`"
|
||||
},
|
||||
"gitCurrentCommit": "*Current commit:* `{commitHash}`",
|
||||
"gitErrRetrievingCommit": "*Error retrieving commit:* {error}",
|
||||
"weatherStatus": {
|
||||
"provideLocation": "*Please provide a location.*",
|
||||
"invalidLocation": "*Invalid location. Try again.*",
|
||||
"resultMsg": "*Weather in {addressFirst}:*\n\n*Status:* `{getStatusEmoji(iconCode)} {wxPhraseLong}`\n*Temperature:* `{temperature} °{temperatureUnit}`\n*Feels like:* `{temperatureFeelsLike} °{temperatureUnit2}`\n*Humidity:* `{relativeHumidity}%`\n*Wind speed:* `{windSpeed} {speedUnit}`",
|
||||
"apiErr": "*An error occurred while retrieving the weather. Please try again later.*\n\n`{error}`",
|
||||
"apiKeyErr": "*An API key was not set by the bot owner. Please try again later.*"
|
||||
},
|
||||
"mainCommands": "ℹ️ Main Commands",
|
||||
"mainCommandsDesc": "ℹ️ *Main Commands*\n\n- /help: Show bot's help\n- /start: Start the bot\n- /privacy: Read the bot's Privacy Policy\n- /settings: Show your user settings",
|
||||
"usefulCommands": "🛠️ Useful Commands",
|
||||
"usefulCommandsDesc": "🛠️ *Useful commands*\n\n- /chatinfo: Send information about the group\n- /userinfo: Send information about yourself\n- /d | /device `<model>`: Search for a device on GSMArena and show its specs.\n/codename | /whatis `<device codename>`: Shows what device is based on the codename. Example: `/codename begonia`\n- /weather | /clima `<city>`: See weather status for a specific location.\n- /modarchive | /tma `<module id>`: Download a module from The Mod Archive.\n- /http `<HTTP code>`: Send details about a specific HTTP code. Example: `/http 404`",
|
||||
"funnyCommands": "😂 Funny Commands",
|
||||
"funnyCommandsDesc": "😂 *Funny Commands*\n\n- /gay: Check if you are gay\n- /furry: Check if you are a furry\n- /random: Pick a random number between 0-10",
|
||||
"interactiveEmojis": "🎲 Interactive Emojis",
|
||||
"interactiveEmojisDesc": "🎲 *Interactive emojis*\n\n- /dice: Roll a dice\n- /idice: Infinitely roll a colored dice\n- /slot: Try to combine the figures!\n- /ball: Try to kick the ball into the goal!\n- /bowling: Try to hit the pins!\n- /dart: Try to hit the target!",
|
||||
"animalCommands": "🐱 Animals",
|
||||
"animalCommandsDesc": "🐱 *Animals*\n\n- /soggy | /soggycat `<1 | 2 | 3 | 4 | orig | thumb | sticker | alt>`: Sends the [Soggy cat meme](https://knowyourmeme.com/memes/soggy-cat)\n- /cat: Sends a random picture of a cat.\n- /fox: Sends a random picture of a fox.\n- /duck: Sends a random picture of a duck.\n- /dog: Sends a random picture of a dog.\n- /httpcat `<http code>`: Send cat memes from http.cat with your specified HTTP code. Example: `/httpcat 404`",
|
||||
"ai": {
|
||||
"helpEntry": "✨ AI Commands",
|
||||
"helpDesc": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI model\n- /think `<prompt>`: Ask a thinking model about a question\n- /ai `<prompt>`: Ask your custom-set AI model a question\n- /aistop: Stop your current AI request\n- /aistats: Show your AI usage stats",
|
||||
"helpDescAdmin": "✨ *AI Commands*\n\n- /ask `<prompt>`: Ask a question to an AI model\n- /think `<prompt>`: Ask a thinking model about a question\n- /ai `<prompt>`: Ask your custom-set AI model a question\n- /aistop: Stop your current AI request\n- /aistats: Show your AI usage stats\n\n*Admin Commands:*\n- /queue: List current AI queue\n- /qdel `<user_id>`: Clear queue items for a user\n- /qlimit `<user_id>` `<duration>`: Timeout user from AI commands\n- /setexec `<user_id>` `<duration>`: Set max execution time for user\n- /rlimit `<user_id>`: Remove all AI limits for user\n- /limits: List all current AI limits",
|
||||
"disabled": "✨ AI features are currently disabled globally.",
|
||||
"disabledForUser": "✨ AI features are disabled for your account. You can enable them with the /settings command.",
|
||||
"pulling": "🔄 Model {model} not found locally, pulling...",
|
||||
"askGenerating": "✨ Generating response with {model}...",
|
||||
"askNoMessage": "✨ You need to ask me a question!",
|
||||
"languageCode": "Language",
|
||||
"thinking": "`🧠 Thinking...`",
|
||||
"finishedThinking": "`🧠 Done thinking.`",
|
||||
"urlWarning": "\n\n⚠️ Note: The model cannot access or visit links!",
|
||||
"inQueue": "ℹ️ You are {position} in the queue.",
|
||||
"queueFull": "🚫 You already have too many requests in the queue. Please wait for them to finish.",
|
||||
"startingProcessing": "✨ Starting to process your request...",
|
||||
"systemPrompt": "You are a friendly assistant called {botName}.\nCurrent Date/Time (UTC): {date}\n\n---\n\nUser message:\n{message}",
|
||||
"statusWaitingRender": "⏳ Streaming...",
|
||||
"statusRendering": "🖼️ Rendering...",
|
||||
"statusComplete": "✅ Complete!",
|
||||
"modelHeader": "🤖 *{model}* 🌡️ *{temperature}* {status}",
|
||||
"noChatFound": "No chat found",
|
||||
"pulled": "✅ Pulled {model} successfully, please retry the command.",
|
||||
"selectTemperature": "*Please select a temperature:*",
|
||||
"temperatureExplanation": "Temperature controls the randomness of the AI's responses. Lower values (e.g., 0.2) make the model more focused and deterministic, while higher values (e.g., 1.2 or above) make it more creative and random.",
|
||||
"queueEmpty": "✅ The AI queue is currently empty.",
|
||||
"queueList": "📋 *AI Queue Status*\n\n{queueItems}\n\n*Total items:* {totalItems}",
|
||||
"queueItem": "• User: {username} ({userId})\n Model: {model}\n Status: {status}\n",
|
||||
"queueCleared": "✅ Cleared {count} queue items for user {userId}.",
|
||||
"queueClearError": "❌ Error clearing queue for user {userId}: {error}",
|
||||
"noQueueItems": "ℹ️ No queue items found for user {userId}.",
|
||||
"userTimedOut": "⏱️ User {userId} has been timed out from AI commands until {timeoutEnd}.",
|
||||
"userTimeoutRemoved": "✅ AI timeout removed for user {userId}.",
|
||||
"userTimeoutError": "❌ Error setting timeout for user {userId}: {error}",
|
||||
"invalidDuration": "❌ Invalid duration format. Use: 1m, 1h, 1d, 1w, etc.",
|
||||
"userExecTimeSet": "⏱️ Max execution time set to {duration} for user {userId}.",
|
||||
"userExecTimeRemoved": "✅ Max execution time limit removed for user {userId}.",
|
||||
"userExecTimeError": "❌ Error setting execution time for user {userId}: {error}",
|
||||
"invalidUserId": "❌ Invalid user ID. Please provide a valid Telegram user ID.",
|
||||
"userNotFound": "❌ User {userId} not found in database.",
|
||||
"userTimedOutFromAI": "⏱️ You are currently timed out from AI commands until {timeoutEnd}.",
|
||||
"requestTooLong": "⏱️ Your request is taking too long. It has been cancelled to prevent system overload.",
|
||||
"userLimitsRemoved": "✅ All AI limits removed for user {userId}.",
|
||||
"userLimitRemoveError": "❌ Error removing limits for user {userId}: {error}",
|
||||
"limitsHeader": "📋 *Current AI Limits*",
|
||||
"noLimitsSet": "✅ No AI limits are currently set.",
|
||||
"timeoutLimitsHeader": "*🔒 Users with AI Timeouts:*",
|
||||
"timeoutLimitItem": "• {displayName} ({userId}) - Until: {timeoutEnd}",
|
||||
"execLimitsHeader": "*⏱️ Users with Execution Time Limits:*",
|
||||
"execLimitItem": "• {displayName} ({userId}) - Max: {execTime}",
|
||||
"limitsListError": "❌ Error retrieving limits: {error}",
|
||||
"requestStopped": "🛑 Your AI request has been stopped.",
|
||||
"requestRemovedFromQueue": "🛑 Your AI request has been removed from the queue.",
|
||||
"noActiveRequest": "ℹ️ You don't have any active AI requests to stop.",
|
||||
"executionTimeoutReached": "\n\n⏱️ Max execution time limit reached!"
|
||||
},
|
||||
"maInvalidModule": "Please provide a valid module ID from The Mod Archive.\nExample: `/modarchive 81574`",
|
||||
"maDownloadError": "Error downloading the file. Check the module ID and try again.",
|
||||
"ytDownload": {
|
||||
"helpEntry": "📺 Video Download",
|
||||
"helpDesc": "📺 *Video Download*\n\n- /yt | /ytdl | /sdl | /dl | /video `<video link>`: Download a video from some platforms (e.g. YouTube, Instagram, Facebook, etc.).\n\n See [this link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) for more information and which services are supported.\n\n*Note: Telegram is currently limiting bot uploads to 50MB, which means that if the video you want to download is larger than 50MB, the quality will be reduced to try to upload it anyway. We're trying our best to work around or fix this problem.*",
|
||||
"downloadingVid": "⬇️ *Downloading video...*",
|
||||
"libNotFound": "*It seems that the yt-dlp executable does not exist on our server...\n\nIn that case, the problem is on our end! Please wait until we have noticed and solved the problem.*",
|
||||
"checkingSize": "🔎 *Checking if the video exceeds the 50MB limit...*",
|
||||
"uploadingVid": "⬆️ *Uploading video...*",
|
||||
"msgDesc": "{userMention}*, there is your downloaded video.*",
|
||||
"downloadErr": "*Error during YT video download:*\n\n`{err}`",
|
||||
"uploadErr": "Error uploading file. Please try again later.",
|
||||
"uploadLimit": "*This video exceeds the 50 MB upload limit imposed by Telegram on our bot. Please try another video. We're doing our best to increase this limit.*",
|
||||
"sizeLimitWarn": "*This video had its quality reduced because it exceeded the 50MB limit for uploads imposed by Telegram.*",
|
||||
"noLink": "Please provide a link to a video to download.",
|
||||
"botDetection": "My server is being rate limited by the video provider! Please try again later, or ask the bot owner to add their cookies/account."
|
||||
},
|
||||
"settings": {
|
||||
"helpEntry": "🔧 Settings",
|
||||
"helpDesc": "🔧 *Settings*\n\n- /settings: Show your settings",
|
||||
"mainSettings": "🔧 *Settings*\n\n- AI Enabled: {aiEnabled}\n- /ai Custom Model: {aiModel}\n- AI Temperature: {aiTemperature}\n- Total AI Requests: {aiRequests}\n- Total AI Characters Sent/Recieved: {aiCharacters}\n- Language: {languageCode}",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"selectSetting": "Please select a setting to modify or view.",
|
||||
"ai": {
|
||||
"aiEnabled": "AI Enabled",
|
||||
"aiModel": "AI Model",
|
||||
"aiTemperature": "AI Temperature",
|
||||
"aiRequests": "Total AI Requests",
|
||||
"aiCharacters": "Total AI Characters Sent/Recieved",
|
||||
"languageCode": "Language",
|
||||
"aiEnabledSetTo": "AI Enabled set to {aiEnabled}",
|
||||
"aiModelSetTo": "AI Model set to {aiModel}",
|
||||
"aiTemperatureSetTo": "AI Temperature set to {aiTemperature}",
|
||||
"selectSeries": "*Please select a model series.*\n\nThis will be set as the default model for the /ai command.",
|
||||
"seriesDescription": "{seriesDescription}",
|
||||
"selectParameterSize": "*Please select a parameter size for {seriesLabel}*.",
|
||||
"parameterSizeExplanation": "Parameter size (e.g. 2B, 4B) refers to the number of parameters in the model. Larger models may be more capable but require more resources.",
|
||||
"modelSetTo": "Model set to {aiModel} ({parameterSize})",
|
||||
"selectTemperature": "*Please select a temperature:*",
|
||||
"temperatureExplanation": "Temperature controls the randomness of the AI's responses. Lower values (e.g., 0.2) make the model more focused and deterministic, while higher values (e.g., 1.2 or above) make it more creative and random.",
|
||||
"showThinking": "Show Model Thinking"
|
||||
},
|
||||
"selectLanguage": "*Please select a language:*",
|
||||
"languageCodeSetTo": "Language set to {languageCode}",
|
||||
"unknownAction": "Unknown action."
|
||||
},
|
||||
"botUpdated": "Bot updated with success.\n\n```{result}```",
|
||||
"errorUpdatingBot": "Error updating bot\n\n{error}",
|
||||
"catImgErr": "Sorry, but I couldn't get the cat photo you wanted.",
|
||||
"catGifErr": "Sorry, but I couldn't get the cat GIF you wanted.",
|
||||
"dogImgErr": "Sorry, but I couldn't get the dog photo you wanted.",
|
||||
"mlpInvalidCharacter": "Please provide a valid character name.",
|
||||
"mlpInvalidEpisode": "Please provide a valid episode number.",
|
||||
"foxApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
|
||||
"duckApiErr": "An error occurred while fetching data from the API.\n\n`{error}`",
|
||||
"httpCodes": {
|
||||
"invalidCode": "Please enter a valid HTTP code.",
|
||||
"fetchErr": "An error occurred while fetching the HTTP code.",
|
||||
"notFound": "HTTP code not found.",
|
||||
"resultMsg": "*HTTP Code*: {code}\n*Name*: `{message}`\n*Description*: {description}"
|
||||
},
|
||||
"ponyApi": {
|
||||
"helpEntry": "🐴 My Little Pony",
|
||||
"helpDesc": "🐴 *My Little Pony*\n\n- /mlp: Displays this help message.\n- /mlpchar `<character name>`: Shows specific information about a My Little Pony character. Example: `/mlpchar Twilight Sparkle`\n- /mlpep: Shows specific information about a My Little Pony episode. Example: `/mlpep 136`\n- /mlpcomic `<comic name>`: Shows specific information about a My Little Pony comic. Example: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Sends a random artwork made by the My Little Pony community.",
|
||||
"charRes": "*{name} (ID: {id})*\n\n*Alias:* `{alias}`\n*Sex:* `{sex}`\n*Residence:* `{residence}`\n*Occupation:* `{occupation}`\n*Kind:* `{kind}`\n\n*Fandom URL:*\n[{url}]({url})",
|
||||
"epRes": "*{name} (ID: {id})*\n\n*Season:* `{season}`\n*Episode:* `{episode}`\n*Overall Ep.:* `{overall}`\n*Release date:* `{airdate}`\n*Story by:* `{storyby}`\n*Written by:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*Fandom URL:*\n[{url}]({url})",
|
||||
"comicRes": "*{name} (ID: {id})*\n\n*Series:* `{series}`\n*Writer:* `{writer}`\n*Artist:* `{artist}`\n*Colorist:* `{colorist}`\n*Letterer:* `{letterer}`\n*Editor:* `{editor}`\n\n*Fandom URL:*\n[{url}]({url})",
|
||||
"noCharName": "Please provide the character's name.",
|
||||
"noCharFound": "No character found.",
|
||||
"noEpisodeNum": "Please provide the episode's number.",
|
||||
"noEpisodeFound": "No episode found.",
|
||||
"noComicName": "Please provide the comic's name.",
|
||||
"noComicFound": "No comic found.",
|
||||
"searching": "Searching for a character…",
|
||||
"apiErr": "An error occurred while fetching data from the API.\n\n`{error}`"
|
||||
},
|
||||
"codenameCheck": {
|
||||
"noCodename": "Please provide a codename to search.",
|
||||
"invalidCodename": "Invalid codename.",
|
||||
"notFound": "Phone not found.",
|
||||
"resultMsg": "*Name:* `{name}`\n*Brand:* `{brand}`\n*Model:* `{model}`\n*Codename:* `{codename}`",
|
||||
"apiErr": "An error occurred while fetching data from the API.\n\n`{err}`"
|
||||
},
|
||||
"chatNotFound": "Chat not found.",
|
||||
"noFileProvided": "Please provide a file to send.",
|
||||
"gsmarenaProvidePhoneName": "Please provide the phone name.",
|
||||
"gsmarenaSearchingFor": "Searching for `{phone}`...",
|
||||
"gsmarenaNoPhonesFound": "No phones found for `{phone}`.",
|
||||
"gsmarenaNoPhonesFoundBoth": "No phones found for `{name}` and `{phone}`.",
|
||||
"gsmarenaSelectDevice": "Please select your device:",
|
||||
"gsmarenaNotAllowed": "you are not allowed to interact with this.",
|
||||
"gsmarenaInvalidOrExpired": "Whoops, invalid or expired option. Please try again.",
|
||||
"gsmarenaDeviceDetails": "these are the details of your device:",
|
||||
"gsmarenaErrorFetchingDetails": "Error fetching phone details.",
|
||||
"info": {
|
||||
"ping": "Pong!",
|
||||
"pinging": "Pinging...",
|
||||
"pong": "Pong in {ms}ms.",
|
||||
"botInfo": "Kowalski is a multipurpose bot with a variety of features, including AI, moderation, and more.",
|
||||
"credits": "Kowalski was created by ihatenodejs/Aidan, with contributions from the open-source community. It is licensed under the Unlicense license."
|
||||
},
|
||||
"aiStats": {
|
||||
"header": "✨ *Your AI Usage Stats*",
|
||||
"requests": "*Total AI Requests:* {aiRequests}",
|
||||
"characters": "*Total AI Characters:* {aiCharacters}\n_That's around {bookCount} books of text!_"
|
||||
},
|
||||
"twoFactor": {
|
||||
"helpEntry": "🔒 2FA",
|
||||
"helpDesc": "🔒 *2FA*\n\n- /2fa: Show your 2FA settings",
|
||||
"codeMessage": "🔒 *{botName} 2FA*\n\nYour 2FA code is: `{code}`"
|
||||
}
|
||||
}
|
232
telegram/locales/portuguese.json
Executable file
232
telegram/locales/portuguese.json
Executable file
|
@ -0,0 +1,232 @@
|
|||
{
|
||||
"botWelcome": "*Olá! Eu sou o {botName}!*\nEu fui feito com amor por uns nerds que amam programação!\n\n*Ao usar o {botName}, você afirma que leu e concorda com a política de privacidade (/privacy). Isso ajuda você a entender onde seus dados vão ao usar este bot.*\n\nAlém disso, você pode usar /help para ver os meus comandos!",
|
||||
"botHelp": "*Oi, eu sou o {botName}, um bot simples feito do zero em Telegraf e Node.js por uns nerds que gostam de programação.*\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})\n\nClique nos botões abaixo para ver quais comandos você pode usar!\n",
|
||||
"botPrivacy": "Acesse [este link]({botPrivacy}) para ler a política de privacidade do bot.",
|
||||
"botAbout": "*Sobre o bot*\n\nA base deste bot foi feita originalmente por [Lucas Gabriel (lucmsilva)](https://github.com/lucmsilva651), agora sendo mantido por várias pessoas.\n\nA intenção do bot é trazer diversão para os seus grupos aqui no Telegram de uma maneira bem descontraida e simples. O bot também conta com alguns comandos bem úteis, que você consegue ver com o comando de ajuda (/help).\n\nAgradecimento especial ao @givfnz2 pelas suas várias contribuições ao bot!\n\nVeja o código fonte: [Clique aqui para ir ao GitHub]({sourceLink})",
|
||||
"aboutBot": "Sobre o bot",
|
||||
"varStrings": {
|
||||
"varYes": "Sim",
|
||||
"varNo": "Não",
|
||||
"varTo": "",
|
||||
"varIs": "está",
|
||||
"varWas": "estava",
|
||||
"varNone": "Nenhum",
|
||||
"varUnknown": "Desconhecido",
|
||||
"varBack": "⬅️ Voltar",
|
||||
"varMore": "➡️ Mais",
|
||||
"varLess": "➖ Menos"
|
||||
},
|
||||
"unexpectedErr": "Ocorreu um erro inesperado: {error}",
|
||||
"errInvalidOption": "Ops! Opção inválida!",
|
||||
"commandDisabled": "🚫 Este comando está atualmente desativado para sua conta.\n\nVocê pode habilitá-lo na interface web: {frontUrl}",
|
||||
"kickingMyself": "*Já que você não precisa de mim, vou sair daqui.*",
|
||||
"kickingMyselfErr": "Erro ao sair do chat.",
|
||||
"noPermission": "Você não tem permissão para executar este comando.",
|
||||
"privateOnly": "Este comando deve ser usado apenas em chats privados, não em grupos.",
|
||||
"groupOnly": "Este comando deve ser usado apenas em grupos, não em chats privados.",
|
||||
"botNameChanged": "*Nome do bot alterado para* `{botName}`.",
|
||||
"botNameErr": "*Erro ao alterar o nome do bot:*\n`{tgErr}`",
|
||||
"botDescChanged": "*Descrição do bot alterada para* `{botDesc}`.",
|
||||
"botDescErr": "*Erro ao alterar a descrição do bot:*\n`{tgErr}`",
|
||||
"gayAmount": "Você é *{randomNum}%* gay!",
|
||||
"furryAmount": "Você é *{randomNum}%* furry!",
|
||||
"randomNum": "*Número gerado (0-10):* `{number}`.",
|
||||
"userInfo": "*Informações do usuário*\n\n*Nome:* `{userName}`\n*Usuário:* `{userHandle}`\n*ID:* `{userId}`\n*Idioma:* `{userLang}`\n*Usuário Premium:* `{userPremium}`",
|
||||
"chatInfo": "*Informações do chat*\n\n*Nome:* `{chatName}`\n*ID do chat:* `{chatId}`\n*Identificador:* `{chatHandle}`\n*Tipo:* `{chatType}`\n*Membros:* `{chatMembersCount}`\n*É um fórum:* `{isForum}`",
|
||||
"funEmojiResult": "*Você lançou {emoji} e obteve *`{value}`*!*\nVocê não sabe o que isso significa? Nem eu!",
|
||||
"gifErr": "*Algo deu errado ao enviar o GIF. Tente novamente mais tarde.*\n\n{err}",
|
||||
"lastFm": {
|
||||
"helpEntry": "🎵 Last.fm",
|
||||
"helpDesc": "🎵 *Last.fm*\n\n- /lt | /lmu | /last | /lfm: Mostra a última música do seu perfil no Last.fm + o número de reproduções.\n- /setuser `<usuário>`: Define o usuário para o comando acima.",
|
||||
"noUser": "*Por favor, forneça um nome de usuário do Last.fm.*\nExemplo: `/setuser <username>`",
|
||||
"noUserSet": "*Você ainda não definiu seu nome de usuário do Last.fm.*\nUse o comando /setuser para definir.\n\nExemplo: `/setuser <username>`",
|
||||
"noRecentTracks": "*Nenhuma faixa recente encontrada para o usuário do Last.fm* `{lastfmUser}`*.*",
|
||||
"userHasBeenSet": "*Seu nome de usuário do Last.fm foi definido como:* `{lastUser}`.",
|
||||
"listeningTo": "{lastfmUser} *{nowPlaying} ouvindo{playCount}*:\n\n{trackName} por {artistName}",
|
||||
"playCount": " pela {plays}ª vez",
|
||||
"apiErr": "*Erro ao recuperar dados para o usuário do Last.fm* {lastfmUser}.\n\n`{err}`"
|
||||
},
|
||||
"gitCurrentCommit": "*Commit atual:* `{commitHash}`",
|
||||
"gitErrRetrievingCommit": "*Erro ao obter o commit:* {error}",
|
||||
"weatherStatus": {
|
||||
"provideLocation": "*Por favor, forneça uma localização.*",
|
||||
"invalidLocation": "*Localização inválida. Tente novamente.*",
|
||||
"resultMsg": "*Clima em {addressFirst}:*\n\n*Estado:* `{getStatusEmoji(iconCode)} {wxPhraseLong}`\n*Temperatura:* `{temperature} °{temperatureUnit}`\n*Sensação térmica:* `{temperatureFeelsLike} °{temperatureUnit2}`\n*Umidade:* `{relativeHumidity}%`\n*Velocidade do vento:* `{windSpeed} {speedUnit}`",
|
||||
"apiErr": "*Ocorreu um erro ao obter o clima. Tente novamente mais tarde.*\n\n`{error}`",
|
||||
"apiKeyErr": "*Uma chave de API não foi definida pelo proprietário do bot. Tente novamente mais tarde.*"
|
||||
},
|
||||
"mainCommands": "ℹ️ Comandos principais",
|
||||
"mainCommandsDesc": "ℹ️ *Comandos principais*\n\n- /help: Exibe a ajuda do bot\n- /start: Inicia o bot\n- /privacy: Leia a política de privacidade do bot\n- /settings: Exibe suas configurações",
|
||||
"usefulCommands": "🛠️ Comandos úteis",
|
||||
"usefulCommandsDesc": "🛠️ *Comandos úteis*\n\n- /chatinfo: Envia informações sobre o grupo\n- /userinfo: Envia informações sobre você\n- /d | /device `<modelo>`: Pesquisa um dispositivo no GSMArena e mostra suas especificações.\n- /weather | /clima `<cidade>`: Veja o status do clima para uma localização específica\n- /modarchive | /tma `<id do módulo>`: Baixa um módulo do The Mod Archive.\n- /http `<código HTTP>`: Envia detalhes sobre um código HTTP específico. Exemplo: `/http 404`",
|
||||
"funnyCommands": "😂 Comandos engraçados",
|
||||
"funnyCommandsDesc": "*Comandos engraçados*\n\n- /gay: Verifique se você é gay\n- /furry: Verifique se você é furry\n- /random: Escolhe um número aleatório entre 0-10",
|
||||
"interactiveEmojis": "🎲 Emojis interativos",
|
||||
"interactiveEmojisDesc": "🎲 *Emojis interativos*\n\n- /dice: Jogue um dado\n- /idice: Role infinitamente um dado colorido\n- /slot: Tente combinar as figuras!\n- /ball: Tente chutar a bola no gol!\n- /bowling: Tente derrubar os pinos!\n- /dart: Tente acertar o alvo!",
|
||||
"animalCommands": "🐱 Animais",
|
||||
"animalCommandsDesc": "🐱 *Animais*\n\n- /soggy | /soggycat `<1 | 2 | 3 | 4 | orig | thumb | sticker | alt>`: Envia o [meme do gato encharcado](https://knowyourmeme.com/memes/soggy-cat)\n- /cat - Envia uma foto aleatória de um gato.\n- /fox - Envia uma foto aleatória de uma raposa.\n- /duck - Envia uma foto aleatória de um pato.\n- /dog - Envia uma imagem aleatória de um cachorro.\n- /httpcat `<código http>`: Envia memes de gato do http.cat com o código HTTP especificado. Exemplo: `/httpcat 404`",
|
||||
"ai": {
|
||||
"helpEntry": "✨ Comandos de IA",
|
||||
"helpDesc": "✨ *Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA\n- /think `<prompt>`: Fazer uma pergunta a um modelo de pensamento\n- /ai `<prompt>`: Fazer uma pergunta a um modelo de IA personalizado\n- /aistop: Parar sua solicitação de IA atual\n- /aistats: Mostra suas estatísticas de uso de IA",
|
||||
"helpDescAdmin": "✨ *Comandos de IA*\n\n- /ask `<prompt>`: Fazer uma pergunta a uma IA\n- /think `<prompt>`: Fazer uma pergunta a um modelo de pensamento\n- /ai `<prompt>`: Fazer uma pergunta a um modelo de IA personalizado\n- /aistop: Parar sua solicitação de IA atual\n- /aistats: Mostra suas estatísticas de uso de IA\n\n*Comandos de Admin:*\n- /queue: Listar fila atual de IA\n- /qdel `<user_id>`: Limpar itens da fila para um usuário\n- /qlimit `<user_id>` `<duration>`: Timeout de usuário dos comandos de IA\n- /setexec `<user_id>` `<duration>`: Definir tempo máximo de execução para usuário\n- /rlimit `<user_id>`: Remover todos os limites de IA para usuário\n- /limits: Listar todos os limites atuais de IA",
|
||||
"disabled": "A AIApi foi desativada\\.",
|
||||
"disabledForUser": "As funções de IA estão desativadas para a sua conta. Você pode ativá-las com o comando /settings.",
|
||||
"pulling": "🔄 Modelo {model} não encontrado localmente, baixando...",
|
||||
"askGenerating": "✨ Gerando resposta com {model}...",
|
||||
"askNoMessage": "⚠️ Você precisa fazer uma pergunta.",
|
||||
"thinking": "`🧠 Pensando...`",
|
||||
"finishedThinking": "`🧠 Pensamento concluido.`",
|
||||
"urlWarning": "\n\n⚠️ Nota: O modelo de IA não pode acessar ou visitar links!",
|
||||
"inQueue": "ℹ️ Você é o {position} na fila.",
|
||||
"queueFull": "🚫 Você já tem muitas solicitações na fila. Por favor, espere que elas terminem.",
|
||||
"startingProcessing": "✨ Começando a processar o seu pedido...",
|
||||
"aiEnabled": "IA",
|
||||
"aiModel": "Modelo de IA",
|
||||
"aiTemperature": "Temperatura",
|
||||
"selectSeries": "*Por favor, selecione uma série de modelos de IA.*",
|
||||
"seriesDescription": "{seriesDescription}",
|
||||
"selectParameterSize": "Por favor, selecione um tamanho de parâmetro para {seriesLabel}.",
|
||||
"parameterSizeExplanation": "O tamanho do parâmetro (ex: 2B, 4B) refere-se ao número de parâmetros do modelo. Modelos maiores podem ser mais capazes, mas exigem mais recursos.",
|
||||
"systemPrompt": "Você é um assistente de Telegram chamado {botName}.\nData/Hora atual (UTC): {date}\n\n---\n\nMensagem do usuário:\n{message}",
|
||||
"statusWaitingRender": "⏳ Transmitindo...",
|
||||
"statusRendering": "🖼️ Renderizando...",
|
||||
"statusComplete": "✅ Completo!",
|
||||
"modelHeader": "🤖 *{model}* 🌡️ *{temperature}* {status}",
|
||||
"noChatFound": "Nenhum chat encontrado",
|
||||
"pulled": "✅ {model} baixado com sucesso, por favor tente o comando novamente.",
|
||||
"queueEmpty": "✅ A fila de IA está atualmente vazia.",
|
||||
"queueList": "📋 *Status da Fila de IA*\n\n{queueItems}\n\n*Total de itens:* {totalItems}",
|
||||
"queueItem": "• Usuário: {username} ({userId})\n Modelo: {model}\n Status: {status}\n",
|
||||
"queueCleared": "✅ Limpos {count} itens da fila para o usuário {userId}.",
|
||||
"queueClearError": "❌ Erro ao limpar fila para o usuário {userId}: {error}",
|
||||
"noQueueItems": "ℹ️ Nenhum item da fila encontrado para o usuário {userId}.",
|
||||
"userTimedOut": "⏱️ Usuário {userId} foi suspenso dos comandos de IA até {timeoutEnd}.",
|
||||
"userTimeoutRemoved": "✅ Timeout de IA removido para o usuário {userId}.",
|
||||
"userTimeoutError": "❌ Erro ao definir timeout para o usuário {userId}: {error}",
|
||||
"invalidDuration": "❌ Formato de duração inválido. Use: 1m, 1h, 1d, 1w, etc.",
|
||||
"userExecTimeSet": "⏱️ Tempo máximo de execução definido para {duration} para o usuário {userId}.",
|
||||
"userExecTimeRemoved": "✅ Limite de tempo máximo de execução removido para o usuário {userId}.",
|
||||
"userExecTimeError": "❌ Erro ao definir tempo de execução para o usuário {userId}: {error}",
|
||||
"invalidUserId": "❌ ID de usuário inválido. Por favor, forneça um ID de usuário válido do Telegram.",
|
||||
"userNotFound": "❌ Usuário {userId} não encontrado na base de dados.",
|
||||
"userTimedOutFromAI": "⏱️ Você está atualmente suspenso dos comandos de IA até {timeoutEnd}.",
|
||||
"requestTooLong": "⏱️ Sua solicitação está demorando muito. Foi cancelada para evitar sobrecarga do sistema.",
|
||||
"userLimitsRemoved": "✅ Todos os limites de IA removidos para o usuário {userId}.",
|
||||
"userLimitRemoveError": "❌ Erro ao remover limites para o usuário {userId}: {error}",
|
||||
"limitsHeader": "📋 *Limites Atuais de IA*",
|
||||
"noLimitsSet": "✅ Nenhum limite de IA está atualmente definido.",
|
||||
"timeoutLimitsHeader": "*🔒 Usuários com Timeouts de IA:*",
|
||||
"timeoutLimitItem": "• {displayName} ({userId}) - Até: {timeoutEnd}",
|
||||
"execLimitsHeader": "*⏱️ Usuários com Limites de Tempo de Execução:*",
|
||||
"execLimitItem": "• {displayName} ({userId}) - Máx: {execTime}",
|
||||
"limitsListError": "❌ Erro ao recuperar limites: {error}",
|
||||
"requestStopped": "🛑 Sua solicitação de IA foi interrompida.",
|
||||
"requestRemovedFromQueue": "🛑 Sua solicitação de IA foi removida da fila.",
|
||||
"noActiveRequest": "ℹ️ Você não tem nenhuma solicitação ativa de IA para parar.",
|
||||
"executionTimeoutReached": "\n\n⏱️ Limite máximo de tempo de execução atingido!"
|
||||
},
|
||||
"maInvalidModule": "Por favor, forneça um ID de módulo válido do The Mod Archive.\nExemplo: `/modarchive 81574`",
|
||||
"maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.",
|
||||
"ytDownload": {
|
||||
"helpEntry": "📺 Download de vídeos",
|
||||
"helpDesc": "📺 *Download de vídeos*\n\n- /yt | /ytdl | /sdl | /dl | /video `<link do vídeo>`: Baixa um vídeo de algumas plataformas (ex: YouTube, Instagram, Facebook, etc.).\n\nConsulte [este link](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) para obter mais informações e saber quais serviços são compatíveis.\n\n*Nota: O Telegram está atualmente limitando os uploads de bots a 50MB, o que significa que se o vídeo que você deseja baixar for maior que 50MB, a qualidade será reduzida para tentar carregá-lo de qualquer maneira. Estamos fazendo o possível para contornar ou corrigir esse problema.*",
|
||||
"downloadingVid": "⬇️ *Baixando vídeo...*",
|
||||
"libNotFound": "*Parece que o executável do yt-dlp não existe no nosso servidor...\n\nNesse caso, o problema está no nosso lado! Aguarde até que tenhamos notado e resolvido o problema.*",
|
||||
"checkingSize": "🔎 *Verificando se o vídeo excede o limite de 50 MB...*",
|
||||
"uploadingVid": "⬆️ *Enviando vídeo...*",
|
||||
"msgDesc": "{userMention}*, aqui está o seu vídeo baixado.*",
|
||||
"downloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`",
|
||||
"uploadErr": "Erro ao enviar o arquivo. Tente novamente mais tarde.",
|
||||
"uploadLimit": "*Este vídeo excede o limite de carregamento de 50 MB imposto pelo Telegram ao nosso bot. Por favor, tente outro vídeo. Estamos fazendo o possível para aumentar esse limite.*",
|
||||
"sizeLimitWarn": "*Esse vídeo teve a qualidade reduzida por estar excedendo o limite de 50MB para uploads imposto pelo Telegram.*",
|
||||
"noLink": "*Por favor, forneça um link de um vídeo para download.*",
|
||||
"botDetection": "Meu servidor está com a taxa limitada pelo provedor de vídeo! Tente novamente mais tarde ou peça ao proprietário do bot para adicionar seus cookies/conta."
|
||||
},
|
||||
"settings": {
|
||||
"helpEntry": "🔧 Configurações",
|
||||
"helpDesc": "🔧 *Configurações*\n\n- /settings: Mostrar suas configurações",
|
||||
"mainSettings": "🔧 *Configurações*\n\n- Inteligência Artificial Ativado: {aiEnabled}\n- /ai Modelo personalizado: {aiModel}\n- Inteligência Artificial Temperatura: {aiTemperature}\n- Total de Requests: {aiRequests}\n- Total de Caracteres Enviados/Recebidos: {aiCharacters}\n- Idioma: {languageCode}",
|
||||
"enabled": "Ativado",
|
||||
"disabled": "Desativado",
|
||||
"selectSetting": "Por favor, selecione uma configuração para modificar ou visualizar.",
|
||||
"ai": {
|
||||
"aiEnabled": "IA",
|
||||
"aiModel": "Modelo",
|
||||
"aiTemperature": "Temperatura",
|
||||
"aiRequests": "Total de Requests",
|
||||
"aiCharacters": "Total de Caracteres Enviados/Recebidos",
|
||||
"languageCode": "Idioma",
|
||||
"aiEnabledSetTo": "Inteligência Artificial definido para {aiEnabled}",
|
||||
"aiModelSetTo": "Modelo personalizado definido para {aiModel}",
|
||||
"aiTemperatureSetTo": "Temperatura definida para {aiTemperature}",
|
||||
"selectSeries": "*Por favor, selecione uma série de modelos.*\n\nIsso será definido como o modelo padrão para o comando /ai.",
|
||||
"seriesDescription": "{seriesDescription}",
|
||||
"selectParameterSize": "Por favor, selecione um tamanho de parâmetro para {seriesLabel}.",
|
||||
"parameterSizeExplanation": "O tamanho do parâmetro (ex: 2B, 4B) refere-se ao número de parâmetros do modelo. Modelos maiores podem ser mais capazes, mas exigem mais recursos.",
|
||||
"modelSetTo": "Modelo definido para {aiModel} ({parameterSize})",
|
||||
"selectTemperature": "*Por favor, selecione uma temperatura:*",
|
||||
"temperatureExplanation": "A temperatura controla a aleatoriedade das respostas da IA. Valores mais baixos (ex: 0.2) tornam o modelo mais focado e determinístico, enquanto valores mais altos (ex: 1.2 ou mais) tornam as respostas mais criativas e aleatórias.",
|
||||
"showThinking": "Mostrar Pensamento do Modelo"
|
||||
},
|
||||
"selectLanguage": "*Por favor, selecione um idioma:*",
|
||||
"languageCodeSetTo": "Idioma definido para {languageCode}",
|
||||
"unknownAction": "Ação desconhecida."
|
||||
},
|
||||
"botUpdated": "Bot atualizado com sucesso.\n\n```{result}```",
|
||||
"errorUpdatingBot": "Erro ao atualizar o bot\n\n{error}",
|
||||
"catImgErr": "Desculpe, mas não consegui obter a foto do gato que você queria.",
|
||||
"catGifErr": "Desculpe, mas não consegui obter o GIF do gato que você queria.",
|
||||
"dogImgErr": "Desculpe, mas não consegui obter a foto do cacbhorro que você queria.",
|
||||
"mlpInvalidCharacter": "Por favor, forneça um nome de personagem válido.",
|
||||
"mlpInvalidEpisode": "Por favor, forneça um número de episódio válido.",
|
||||
"foxApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
|
||||
"duckApiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`",
|
||||
"httpCodes": {
|
||||
"invalidCode": "Por favor, insira um código HTTP válido.",
|
||||
"fetchErr": "Ocorreu um erro ao buscar o código HTTP.",
|
||||
"notFound": "Código HTTP não encontrado.",
|
||||
"resultMsg": "*Código HTTP*: `{code}`\n*Nome*: `{message}`\n*Descrição*: `{description}`"
|
||||
},
|
||||
"ponyApi": {
|
||||
"helpEntry": "🐴 My Little Pony",
|
||||
"helpDesc": "🐴 *My Little Pony*\n\n- /mlp: Exibe esta mensagem de ajuda.\n- /mlpchar `<nome do personagem>`: Mostra informações específicas sobre um personagem de My Little Pony em inglês. Exemplo: `/mlpchar twilight`\n- /mlpep: Mostra informações específicas sobre um episódio de My Little Pony em inglês. Exemplo: `/mlpep 136`\n- /mlpcomic `<nome da comic>`: Mostra informações específicas sobre uma comic de My Little Pony em inglês. Exemplo: `/mlpcomic Nightmare Rarity`\n- /rpony | /randompony | /mlpart: Envia uma arte aleatória feita pela comunidade de My Little Pony.",
|
||||
"charRes": "*{name} (ID: {id})*\n\n*Apelido:* `{alias}`\n*Sexo:* `{sex}`\n*Residência:* `{residence}`\n*Ocupação:* `{occupation}`\n*Tipo:* `{kind}`\n\n*URL no Fandom:*\n[{url}]({url})",
|
||||
"epRes": "*{name} (ID: {id})*\n\n*Temporada:* `{season}`\n*Episódio:* `{episode}`\n*Número do Episódio:* `{overall}`\n*Data de lançamento:* `{airdate}`\n*História por:* `{storyby}`\n*Escrito por:* `{writtenby}`\n*Storyboard:* `{storyboard}`\n\n*URL no Fandom:*\n[{url}]({url})",
|
||||
"comicRes": "*{name} (ID: {id})*\n\n*Série:* `{series}`\n*Roteirista:* `{writer}`\n*Artista:* `{artist}`\n*Colorista:* `{colorist}`\n*Letrista:* `{letterer}`\n*Editor:* `{editor}`\n\n*URL no Fandom:*\n[{url}]({url})",
|
||||
"noCharName": "Por favor, forneça o nome do personagem.",
|
||||
"noCharFound": "Nenhum personagem encontrado.",
|
||||
"noEpisodeNum": "Por favor, forneça o número do episódio.",
|
||||
"noEpisodeFound": "Nenhum episódio encontrado.",
|
||||
"noComicName": "Por favor, forneça o nome da comic.",
|
||||
"noComicFound": "Nenhuma comic foi encontrada.",
|
||||
"searching": "Procurando por um personagem…",
|
||||
"apiErr": "Ocorreu um erro ao buscar dados da API.\n\n`{error}`"
|
||||
},
|
||||
"codenameCheck": {
|
||||
"noCodename": "Por favor, forneça um codinome para pesquisar.",
|
||||
"invalidCodename": "Codinome inválido.",
|
||||
"notFound": "Celular não encontrado.",
|
||||
"resultMsg": "*Nome:* `{name}`\n*Marca:* `{brand}`\n*Modelo:* `{model}`\n*Codinome:* `{codename}`",
|
||||
"apiErr": "Ocorreu um erro ao buscar os dados da API.\n\n`{err}`"
|
||||
},
|
||||
"noFileProvided": "Por favor, forneça um arquivo para envio.",
|
||||
"gsmarenaProvidePhoneName": "Por favor, forneça o nome do celular.",
|
||||
"gsmarenaSearchingFor": "Procurando por `{phone}`...",
|
||||
"gsmarenaNoPhonesFound": "Nenhum celular encontrado para `{phone}`.",
|
||||
"gsmarenaNoPhonesFoundBoth": "Nenhum celular encontrado para `{name}` e `{phone}`.",
|
||||
"gsmarenaSelectDevice": "Por favor, selecione seu dispositivo:",
|
||||
"gsmarenaNotAllowed": "você não tem permissão para interagir com isso.",
|
||||
"gsmarenaInvalidOrExpired": "Ops! Opção inválida ou expirada. Por favor, tente novamente.",
|
||||
"gsmarenaDeviceDetails": "estes são os detalhes do seu dispositivo:",
|
||||
"gsmarenaErrorFetchingDetails": "Erro ao buscar detalhes do celular.",
|
||||
"aiStats": {
|
||||
"header": "✨ *Suas estatísticas de uso de IA*",
|
||||
"requests": "*Total de requisições de IA:* {aiRequests}",
|
||||
"characters": "*Total de caracteres de IA:* {aiCharacters}\n_Isso é cerca de {bookCount} livros de texto!_"
|
||||
},
|
||||
"twoFactor": {
|
||||
"helpEntry": "🔒 2FA",
|
||||
"helpDesc": "🔒 *2FA*\n\n- /2fa: Mostra suas configurações de 2FA",
|
||||
"codeMessage": "🔒 *{botName} 2FA*\n\nSeu código de 2FA é: `{code}`"
|
||||
}
|
||||
}
|
23
telegram/plugins/checklang.ts
Executable file
23
telegram/plugins/checklang.ts
Executable file
|
@ -0,0 +1,23 @@
|
|||
const languageFiles = {
|
||||
'pt': '../locales/portuguese.json',
|
||||
'pt-br': '../locales/portuguese.json',
|
||||
'pt-pt': '../locales/portuguese.json',
|
||||
'en': '../locales/english.json',
|
||||
'en-us': '../locales/english.json',
|
||||
'en-gb': '../locales/english.json'
|
||||
};
|
||||
|
||||
function getStrings(languageCode?: string) {
|
||||
if (!languageCode) {
|
||||
return require(languageFiles['en']);
|
||||
}
|
||||
const filePath: string = languageFiles[languageCode] || languageFiles['en'];
|
||||
try {
|
||||
return require(filePath);
|
||||
} catch (error) {
|
||||
console.error(`Error loading language file for code ${languageCode}:`, error);
|
||||
return require(languageFiles['en']);
|
||||
}
|
||||
}
|
||||
|
||||
export { getStrings };
|
14
telegram/plugins/verifyInput.ts
Executable file
14
telegram/plugins/verifyInput.ts
Executable file
|
@ -0,0 +1,14 @@
|
|||
import { Context } from "telegraf";
|
||||
import { replyToMessageId } from "../utils/reply-to-message-id";
|
||||
|
||||
export default function verifyInput(ctx: Context, userInput: string, message: string, verifyNaN = false) {
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
if (!userInput || (verifyNaN && isNaN(Number(userInput)))) {
|
||||
ctx.reply(message, {
|
||||
parse_mode: "Markdown",
|
||||
...({ reply_to_message_id })
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
55
telegram/plugins/ytDlpWrapper.ts
Executable file
55
telegram/plugins/ytDlpWrapper.ts
Executable file
|
@ -0,0 +1,55 @@
|
|||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const downloadDir = path.resolve(__dirname, 'yt-dlp');
|
||||
|
||||
const urls = {
|
||||
linux: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp',
|
||||
win32: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe',
|
||||
darwin: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos',
|
||||
};
|
||||
|
||||
function getDownloadUrl() {
|
||||
const platform = os.platform();
|
||||
return urls[platform] || urls.linux;
|
||||
};
|
||||
|
||||
async function downloadYtDlp() {
|
||||
const url = getDownloadUrl();
|
||||
const fileName = url.split('/').pop();
|
||||
const filePath = path.join(downloadDir, fileName);
|
||||
|
||||
if (!fs.existsSync(downloadDir)) {
|
||||
fs.mkdirSync(downloadDir, { recursive: true });
|
||||
};
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
try {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
writer.on('finish', () => {
|
||||
if (os.platform() !== 'win32') {
|
||||
fs.chmodSync(filePath, '-x');
|
||||
}
|
||||
});
|
||||
|
||||
writer.on('error', (err) => {
|
||||
console.error('WARN: yt-dlp download failed:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('WARN: yt-dlp download failed:', err.message);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
downloadYtDlp();
|
24
telegram/props/resources.json
Executable file
24
telegram/props/resources.json
Executable file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"soggyCat": "https://soggy.cat/img/soggycat.webp",
|
||||
"soggyCatAlt": "https://i.kym-cdn.com/photos/images/original/002/705/636/38d.jpg",
|
||||
"soggyCat2": "https://i.kym-cdn.com/photos/images/original/002/705/653/079.jpg",
|
||||
"soggyCatSticker": "CAACAgEAAxkBAAJ9SWb0vY0Xgg4RtNQeU5iLOx3iTVRAAAKgAwACN-NRRFf8v9p0Nz1INgQ",
|
||||
"infiniteDice": "CAACAgQAAxkBAAJxjWbSSP-8ZNEhEpAJjQsHsGf-UuEPAAJCAAPI-uwTAAEBVWWh4ucINQQ",
|
||||
"gayFlag": "https://c.tenor.com/VTBe5BFc73sAAAAC/tenor.gif",
|
||||
"furryGif": "https://c.tenor.com/_V_k0BVj48IAAAAd/tenor.gif",
|
||||
"codenameApi": "https://raw.githubusercontent.com/androidtrackers/certified-android-devices/master/by_device.json",
|
||||
"catApi": "https://cataas.com/cat",
|
||||
"dogApi": "https://dog.ceo/api/breeds/image/random",
|
||||
"httpCatApi": "https://http.cat/",
|
||||
"httpApi": "https://status.js.org/codes.json",
|
||||
"lastFmApi": "http://ws.audioscrobbler.com/2.0/",
|
||||
"musicBrainzApi": "https://coverartarchive.org/release/",
|
||||
"lastFmGenericImg": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png",
|
||||
"modArchiveApi": "https://api.modarchive.org/downloads.php?moduleid=",
|
||||
"ponyApi": "http://ponyapi.net/v1",
|
||||
"quoteApi": "https://quotes-api-self.vercel.app/quote",
|
||||
"weatherApi": "https://api.weather.com/v3",
|
||||
"randomPonyApi": "https://theponyapi.com/api/v1/pony/random",
|
||||
"foxApi": "https://randomfox.ca/floof/",
|
||||
"duckApi": "https://random-d.uk/api/v2/random"
|
||||
}
|
72
telegram/utils/check-command-disabled.ts
Executable file
72
telegram/utils/check-command-disabled.ts
Executable file
|
@ -0,0 +1,72 @@
|
|||
// CHECK-COMMAND-DISABLED.TS
|
||||
// by ihatenodejs/Aidan
|
||||
//
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
// Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
// distribute this software, either in source code form or as a compiled
|
||||
// binary, for any purpose, commercial or non-commercial, and by any
|
||||
// means.
|
||||
//
|
||||
// In jurisdictions that recognize copyright laws, the author or authors
|
||||
// of this software dedicate any and all copyright interest in the
|
||||
// software to the public domain. We make this dedication for the benefit
|
||||
// of the public at large and to the detriment of our heirs and
|
||||
// successors. We intend this dedication to be an overt act of
|
||||
// relinquishment in perpetuity of all present and future rights to this
|
||||
// software under copyright law.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// For more information, please refer to <https://unlicense.org/>
|
||||
|
||||
import { Context } from 'telegraf';
|
||||
import { getStrings } from '../plugins/checklang';
|
||||
import { replyToMessageId } from './reply-to-message-id';
|
||||
|
||||
export async function isCommandDisabled(ctx: Context, db: any, commandId: string): Promise<boolean> {
|
||||
if (!ctx.from) return false;
|
||||
|
||||
const telegramId = String(ctx.from.id);
|
||||
|
||||
try {
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.telegramId, telegramId),
|
||||
columns: {
|
||||
disabledCommands: true,
|
||||
languageCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
const isDisabled = user.disabledCommands?.includes(commandId) || false;
|
||||
|
||||
if (isDisabled) {
|
||||
const Strings = getStrings(user.languageCode);
|
||||
const frontUrl = process.env.frontUrl || 'https://kowalski.social';
|
||||
const reply_to_message_id = replyToMessageId(ctx);
|
||||
|
||||
await ctx.reply(
|
||||
Strings.commandDisabled.replace('{frontUrl}', frontUrl),
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
...(reply_to_message_id && { reply_parameters: { message_id: reply_to_message_id } })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return isDisabled;
|
||||
} catch (error) {
|
||||
console.error('[💽 DB] Error checking disabled commands:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
67
telegram/utils/ensure-user.ts
Executable file
67
telegram/utils/ensure-user.ts
Executable file
|
@ -0,0 +1,67 @@
|
|||
// ENSURE-USER.TS
|
||||
// by ihatenodejs/Aidan
|
||||
//
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
// Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
// distribute this software, either in source code form or as a compiled
|
||||
// binary, for any purpose, commercial or non-commercial, and by any
|
||||
// means.
|
||||
//
|
||||
// In jurisdictions that recognize copyright laws, the author or authors
|
||||
// of this software dedicate any and all copyright interest in the
|
||||
// software to the public domain. We make this dedication for the benefit
|
||||
// of the public at large and to the detriment of our heirs and
|
||||
// successors. We intend this dedication to be an overt act of
|
||||
// relinquishment in perpetuity of all present and future rights to this
|
||||
// software under copyright law.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// For more information, please refer to <https://unlicense.org/>
|
||||
|
||||
import { usersTable } from '../../database/schema';
|
||||
|
||||
export async function ensureUserInDb(ctx, db) {
|
||||
if (!ctx.from) return;
|
||||
const telegramId = String(ctx.from.id);
|
||||
const username = ctx.from.username || '';
|
||||
const firstName = ctx.from.first_name || ' ';
|
||||
const lastName = ctx.from.last_name || ' ';
|
||||
const languageCode = ctx.from.language_code || 'en';
|
||||
|
||||
const existing = await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, telegramId), limit: 1 });
|
||||
if (existing.length === 0) {
|
||||
const userToInsert = {
|
||||
telegramId,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
languageCode,
|
||||
aiEnabled: false,
|
||||
showThinking: false,
|
||||
customAiModel: "deepseek-r1:1.5b",
|
||||
aiTemperature: 0.9,
|
||||
aiRequests: 0,
|
||||
aiCharacters: 0,
|
||||
disabledCommands: [],
|
||||
aiTimeoutUntil: null,
|
||||
aiMaxExecutionTime: 0,
|
||||
};
|
||||
try {
|
||||
await db.insert(usersTable).values(userToInsert);
|
||||
console.log(`[💽 DB] Added new user: ${username || firstName} (${telegramId})`);
|
||||
} catch (err) {
|
||||
console.error('[💽 DB] Error inserting user:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
9
telegram/utils/language-code.ts
Executable file
9
telegram/utils/language-code.ts
Executable file
|
@ -0,0 +1,9 @@
|
|||
import { Context } from "telegraf";
|
||||
|
||||
export const languageCode = (ctx: Context) => {
|
||||
if(ctx.from) {
|
||||
return ctx.from.language_code
|
||||
} else {
|
||||
return 'en'
|
||||
}
|
||||
}
|
92
telegram/utils/log.ts
Executable file
92
telegram/utils/log.ts
Executable file
|
@ -0,0 +1,92 @@
|
|||
// LOG.TS
|
||||
// by ihatenodejs/Aidan
|
||||
//
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
// Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
// distribute this software, either in source code form or as a compiled
|
||||
// binary, for any purpose, commercial or non-commercial, and by any
|
||||
// means.
|
||||
//
|
||||
// In jurisdictions that recognize copyright laws, the author or authors
|
||||
// of this software dedicate any and all copyright interest in the
|
||||
// software to the public domain. We make this dedication for the benefit
|
||||
// of the public at large and to the detriment of our heirs and
|
||||
// successors. We intend this dedication to be an overt act of
|
||||
// relinquishment in perpetuity of all present and future rights to this
|
||||
// software under copyright law.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// For more information, please refer to <https://unlicense.org/>
|
||||
|
||||
import { flash_model, thinking_model } from "../commands/ai"
|
||||
|
||||
class Logger {
|
||||
private static instance: Logger
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger()
|
||||
}
|
||||
return Logger.instance
|
||||
}
|
||||
|
||||
logCmdStart(user: string, command: string, model: string): void {
|
||||
console.log(`\n[✨ AI | START] Received /${command} for model ${model} (from ${user})`)
|
||||
}
|
||||
|
||||
logThinking(chatId: number, messageId: number, thinking: boolean): void {
|
||||
if (thinking) {
|
||||
console.log(`[✨ AI | THINKING | ${chatId}:${messageId}] Model started thinking`)
|
||||
} else {
|
||||
console.log(`[✨ AI | THINKING | ${chatId}:${messageId}] Model stopped thinking`)
|
||||
}
|
||||
}
|
||||
|
||||
logChunk(chatId: number, messageId: number, text: string, isOverflow: boolean = false): void {
|
||||
if (process.env.longerLogs === 'true') {
|
||||
const prefix = isOverflow ? "[✨ AI | OVERFLOW]" : "[✨ AI | CHUNK]"
|
||||
console.log(`${prefix} [${chatId}:${messageId}] ${text.length} chars pushed to Telegram`)
|
||||
}
|
||||
}
|
||||
|
||||
logPrompt(prompt: string): void {
|
||||
if (process.env.longerLogs === 'true') {
|
||||
console.log(`[✨ AI | PROMPT] ${prompt}`)
|
||||
}
|
||||
}
|
||||
|
||||
logError(error: unknown): void {
|
||||
if (typeof error === 'object' && error !== null && 'response' in error) {
|
||||
const err = error as { response?: { error_code?: number, parameters?: { retry_after?: number }, description?: string }, on?: { method?: string } };
|
||||
if (err.response?.error_code === 429) {
|
||||
const retryAfter = err.response.parameters?.retry_after || 1;
|
||||
console.error(`[✨ AI | RATE_LIMIT] Too Many Requests - retry after ${retryAfter}s`);
|
||||
} else if (err.response?.error_code === 400 && err.response?.description?.includes("can't parse entities")) {
|
||||
console.error("[✨ AI | PARSE_ERROR] Markdown parsing failed, retrying with plain text");
|
||||
} else {
|
||||
const errorDetails = {
|
||||
code: err.response?.error_code,
|
||||
description: err.response?.description,
|
||||
method: err.on?.method
|
||||
};
|
||||
console.error("[✨ AI | ERROR]", JSON.stringify(errorDetails, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.error("[✨ AI | ERROR]", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = Logger.getInstance()
|
254
telegram/utils/rate-limiter.ts
Executable file
254
telegram/utils/rate-limiter.ts
Executable file
|
@ -0,0 +1,254 @@
|
|||
// RATE-LIMITER.TS
|
||||
// by ihatenodejs/Aidan
|
||||
//
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
// Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
// distribute this software, either in source code form or as a compiled
|
||||
// binary, for any purpose, commercial or non-commercial, and by any
|
||||
// means.
|
||||
//
|
||||
// In jurisdictions that recognize copyright laws, the author or authors
|
||||
// of this software dedicate any and all copyright interest in the
|
||||
// software to the public domain. We make this dedication for the benefit
|
||||
// of the public at large and to the detriment of our heirs and
|
||||
// successors. We intend this dedication to be an overt act of
|
||||
// relinquishment in perpetuity of all present and future rights to this
|
||||
// software under copyright law.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// For more information, please refer to <https://unlicense.org/>
|
||||
|
||||
import { Context } from 'telegraf'
|
||||
import { logger } from './log'
|
||||
|
||||
class RateLimiter {
|
||||
private lastEditTimes: Map<string, number> = new Map()
|
||||
private readonly minInterval: number = 5000
|
||||
private pendingUpdates: Map<string, string> = new Map()
|
||||
private updateQueue: Map<string, NodeJS.Timeout> = new Map()
|
||||
private readonly max_msg_length: number = 3500
|
||||
private overflowMessages: Map<string, number> = new Map()
|
||||
private isRateLimited: boolean = false
|
||||
private rateLimitEndTime: number = 0
|
||||
|
||||
private getMessageKey(chatId: number, messageId: number): string {
|
||||
return `${chatId}:${messageId}`
|
||||
}
|
||||
|
||||
private async waitForRateLimit(chatId: number, messageId: number): Promise<void> {
|
||||
if (!this.isRateLimited) return
|
||||
console.log(`[✨ AI | RATELIMIT] [${chatId}:${messageId}] Ratelimited, waiting for end of ${this.rateLimitEndTime - Date.now()}ms`)
|
||||
const now = Date.now()
|
||||
if (now < this.rateLimitEndTime) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.rateLimitEndTime - now))
|
||||
}
|
||||
this.isRateLimited = false
|
||||
}
|
||||
|
||||
private chunkText(text: string): string[] {
|
||||
const chunks: string[] = []
|
||||
let currentChunk = ''
|
||||
let currentLength = 0
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
if (currentLength + line.length + 1 > this.max_msg_length) {
|
||||
if (currentChunk) {
|
||||
chunks.push(currentChunk)
|
||||
currentChunk = ''
|
||||
currentLength = 0
|
||||
}
|
||||
if (line.length > this.max_msg_length) {
|
||||
for (let i = 0; i < line.length; i += this.max_msg_length) {
|
||||
chunks.push(line.substring(i, i + this.max_msg_length))
|
||||
}
|
||||
} else {
|
||||
currentChunk = line
|
||||
currentLength = line.length
|
||||
}
|
||||
} else {
|
||||
if (currentChunk) {
|
||||
currentChunk += '\n'
|
||||
currentLength++
|
||||
}
|
||||
currentChunk += line
|
||||
currentLength += line.length
|
||||
}
|
||||
}
|
||||
if (currentChunk) {
|
||||
chunks.push(currentChunk)
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
private handleTelegramError(
|
||||
error: unknown,
|
||||
messageKey: string,
|
||||
options: Record<string, unknown>,
|
||||
ctx: Context,
|
||||
chatId: number,
|
||||
messageId: number
|
||||
): boolean {
|
||||
if (!isTelegramError(error)) return false
|
||||
if (error.response.error_code === 429) {
|
||||
const retryAfter = error.response.parameters?.retry_after || 1
|
||||
this.isRateLimited = true
|
||||
this.rateLimitEndTime = Date.now() + (retryAfter * 1000)
|
||||
const existingTimeout = this.updateQueue.get(messageKey)
|
||||
if (existingTimeout) clearTimeout(existingTimeout)
|
||||
const timeout = setTimeout(() => {
|
||||
this.processUpdate(ctx, chatId, messageId, options)
|
||||
}, retryAfter * 1000)
|
||||
this.updateQueue.set(messageKey, timeout)
|
||||
return true
|
||||
}
|
||||
if (error.response.error_code === 400) {
|
||||
if (error.response.description?.includes("can't parse entities") || error.response.description?.includes("MESSAGE_TOO_LONG")) {
|
||||
const plainOptions = { ...options, parse_mode: undefined }
|
||||
this.processUpdate(ctx, chatId, messageId, plainOptions)
|
||||
return true
|
||||
}
|
||||
if (error.response.description?.includes("message is not modified")) {
|
||||
this.pendingUpdates.delete(messageKey)
|
||||
this.updateQueue.delete(messageKey)
|
||||
return true
|
||||
}
|
||||
logger.logError(error)
|
||||
this.pendingUpdates.delete(messageKey)
|
||||
this.updateQueue.delete(messageKey)
|
||||
return true
|
||||
}
|
||||
logger.logError(error)
|
||||
this.pendingUpdates.delete(messageKey)
|
||||
this.updateQueue.delete(messageKey)
|
||||
return true
|
||||
}
|
||||
|
||||
private async processUpdate(
|
||||
ctx: Context,
|
||||
chatId: number,
|
||||
messageId: number,
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const messageKey = this.getMessageKey(chatId, messageId)
|
||||
const latestText = this.pendingUpdates.get(messageKey)
|
||||
if (!latestText) return
|
||||
|
||||
const now = Date.now()
|
||||
const lastEditTime = this.lastEditTimes.get(messageKey) || 0
|
||||
const timeSinceLastEdit = now - lastEditTime
|
||||
await this.waitForRateLimit(chatId, messageId)
|
||||
|
||||
if (timeSinceLastEdit < this.minInterval) {
|
||||
const existingTimeout = this.updateQueue.get(messageKey)
|
||||
if (existingTimeout) clearTimeout(existingTimeout)
|
||||
const timeout = setTimeout(() => {
|
||||
this.processUpdate(ctx, chatId, messageId, options)
|
||||
}, this.minInterval - timeSinceLastEdit)
|
||||
this.updateQueue.set(messageKey, timeout)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (latestText.length > this.max_msg_length) {
|
||||
const chunks = this.chunkText(latestText)
|
||||
const firstChunk = chunks[0]
|
||||
logger.logChunk(chatId, messageId, firstChunk)
|
||||
try {
|
||||
await ctx.telegram.editMessageText(chatId, messageId, undefined, firstChunk, options)
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
isTelegramError(error) &&
|
||||
!error.response.description?.includes("message is not modified")
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < chunks.length; i++) {
|
||||
const chunk = chunks[i]
|
||||
const overflowMessageId = this.overflowMessages.get(messageKey)
|
||||
if (overflowMessageId) {
|
||||
try {
|
||||
await ctx.telegram.editMessageText(chatId, overflowMessageId, undefined, chunk, options)
|
||||
logger.logChunk(chatId, overflowMessageId, chunk, true)
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
isTelegramError(error) &&
|
||||
!error.response.description?.includes("message is not modified")
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const newMessage = await ctx.telegram.sendMessage(chatId, chunk, {
|
||||
...options,
|
||||
reply_to_message_id: messageId
|
||||
} as any)
|
||||
logger.logChunk(chatId, newMessage.message_id, chunk, true)
|
||||
this.overflowMessages.set(messageKey, newMessage.message_id)
|
||||
}
|
||||
}
|
||||
this.pendingUpdates.set(messageKey, firstChunk)
|
||||
if (chunks.length > 1) {
|
||||
this.pendingUpdates.set(
|
||||
this.getMessageKey(chatId, this.overflowMessages.get(messageKey)!),
|
||||
chunks[chunks.length - 1]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.logChunk(chatId, messageId, latestText)
|
||||
try {
|
||||
await ctx.telegram.editMessageText(chatId, messageId, undefined, latestText, options)
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
isTelegramError(error) &&
|
||||
!error.response.description?.includes("message is not modified")
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
this.pendingUpdates.delete(messageKey)
|
||||
}
|
||||
this.lastEditTimes.set(messageKey, Date.now())
|
||||
this.updateQueue.delete(messageKey)
|
||||
} catch (error: unknown) {
|
||||
if (!this.handleTelegramError(error, messageKey, options, ctx, chatId, messageId)) {
|
||||
logger.logError(error)
|
||||
this.pendingUpdates.delete(messageKey)
|
||||
this.updateQueue.delete(messageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async editMessageWithRetry(
|
||||
ctx: Context,
|
||||
chatId: number,
|
||||
messageId: number,
|
||||
text: string,
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const messageKey = this.getMessageKey(chatId, messageId)
|
||||
this.pendingUpdates.set(messageKey, text)
|
||||
await this.processUpdate(ctx, chatId, messageId, options)
|
||||
}
|
||||
}
|
||||
|
||||
export const rateLimiter = new RateLimiter()
|
||||
|
||||
function isTelegramError(error: unknown): error is { response: { description?: string, error_code?: number, parameters?: { retry_after?: number } } } {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"response" in error &&
|
||||
typeof (error as any).response === "object"
|
||||
)
|
||||
}
|
5
telegram/utils/reply-to-message-id.ts
Executable file
5
telegram/utils/reply-to-message-id.ts
Executable file
|
@ -0,0 +1,5 @@
|
|||
import { Context } from "telegraf"
|
||||
|
||||
export const replyToMessageId = (ctx: Context) => {
|
||||
return ctx.message?.message_id
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue