add initial complete webui, more ai commands for moderation, add api

This commit is contained in:
Aidan 2025-07-05 14:36:17 -04:00
parent 19e794e34c
commit 173d4e7a52
112 changed files with 8176 additions and 780 deletions

1291
telegram/commands/ai.ts Executable file

File diff suppressed because it is too large Load diff

159
telegram/commands/animal.ts Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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, '![$1](https://en.wikipedia.org/wiki/File:$1)');
return input;
}
export default (bot: Telegraf<Context>) => {
bot.command("wiki", async (ctx) => {
const userInput = capitalizeFirstLetter(ctx.message.text.split(' ')[1]);
const apiUrl = `https://en.wikipedia.org/w/index.php?title=${userInput}&action=raw`;
const response = await axios(apiUrl, { headers: { 'Accept': "text/plain" } });
const convertedResponse = response.data.replace(/<\/?div>/g, "").replace(/{{Infobox.*?}}/s, "");
const result = mediaWikiToMarkdown(convertedResponse).slice(0, 2048);
const reply_to_message_id = replyToMessageId(ctx);
ctx.reply(result, { parse_mode: 'Markdown', ...({ reply_to_message_id, disable_web_page_preview: true }) });
});
};
*/

256
telegram/commands/youtube.ts Executable file
View 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,
});
}
});
};