From a952ddfc67b637bff68138a50ed5945e6e8b0ea5 Mon Sep 17 00:00:00 2001 From: Aidan Date: Tue, 1 Jul 2025 12:26:51 -0400 Subject: [PATCH] better logging, stats tracking command, clean up url warning, user permissions on settings --- .env.example | 1 + README.md | 1 + src/commands/ai.ts | 49 +++- src/commands/main.ts | 510 ++++++++++++++++++++---------------- src/locales/english.json | 7 +- src/locales/portuguese.json | 9 +- src/utils/log.ts | 14 +- 7 files changed, 340 insertions(+), 251 deletions(-) diff --git a/.env.example b/.env.example index 2aa13c0..db7a321 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,4 @@ maxRetries = 9999 botAdmins = 00000000, 00000000, 00000000 lastKey = "InsertYourLastFmApiKeyHere" weatherKey = "InsertYourWeatherDotComApiKeyHere" +longerLogs = true \ No newline at end of file diff --git a/README.md b/README.md index ba6ecef..612db3a 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ If you prefer to use Docker directly, you can use these instructions instead. - **botAdmins**: Put the ID of the people responsible for managing the bot. They can use some administrative + exclusive commands on any group. - **lastKey**: Last.fm API key, for use on `lastfm.js` functions, like see who is listening to what song and etc. - **weatherKey**: Weather.com API key, used for the `/weather` command. +- **longerLogs**: Set to `true` to enable verbose logging whenever possible. ## Troubleshooting diff --git a/src/commands/ai.ts b/src/commands/ai.ts index 3781e9f..6b8706d 100644 --- a/src/commands/ai.ts +++ b/src/commands/ai.ts @@ -41,6 +41,7 @@ import { logger } from "../utils/log" import { ensureUserInDb } from "../utils/ensure-user" import * as schema from '../db/schema' import type { NodePgDatabase } from "drizzle-orm/node-postgres" +import { eq, sql } from 'drizzle-orm' const spamwatchMiddleware = spamwatchMiddlewareModule(isOnSpamWatch) export const flash_model = process.env.flashModel || "gemma3:4b" @@ -270,7 +271,7 @@ function containsUrls(text: string): boolean { return text.includes('http://') || text.includes('https://'); } -async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Message, model: string, aiTemperature: number, originalMessage: string): Promise<{ success: boolean; response?: string; error?: string }> { +async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Message, model: string, aiTemperature: number, originalMessage: string, db: NodePgDatabase, userId: string): Promise<{ success: boolean; response?: string; error?: string }> { const Strings = getStrings(languageCode(ctx)); if (!ctx.chat) { return { @@ -283,7 +284,11 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me .replace("{model}", model) .replace("{temperature}", aiTemperature) .replace("{status}", status) + "\n\n"; - const urlWarning = containsUrls(originalMessage) ? Strings.ai.urlWarning : ''; + + const promptCharCount = originalMessage.length; + await db.update(schema.usersTable) + .set({ aiCharacters: sql`${schema.usersTable.aiCharacters} + ${promptCharCount}` }) + .where(eq(schema.usersTable.telegramId, userId)); try { const aiResponse = await axios.post( @@ -314,7 +319,7 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me try { ln = JSON.parse(line); } catch (e) { - console.error("[✨ AI | !] Error parsing chunk:", e); + console.error("[✨ AI | !] Error parsing chunk"); continue; } if (model === thinking_model && ln.response) { @@ -351,7 +356,7 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me ctx, ctx.chat.id, replyGenerating.message_id, - modelHeader + urlWarning + escapeMarkdown(fullResponse), + modelHeader + escapeMarkdown(fullResponse), { parse_mode: 'Markdown' } ); lastUpdateCharCount = fullResponse.length; @@ -365,7 +370,7 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me ctx, ctx.chat.id, replyGenerating.message_id, - modelHeader + urlWarning + escapeMarkdown(fullResponse), + modelHeader + escapeMarkdown(fullResponse), { parse_mode: 'Markdown' } ); lastUpdateCharCount = fullResponse.length; @@ -383,9 +388,16 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me ctx, ctx.chat.id, replyGenerating.message_id, - modelHeader + urlWarning + escapeMarkdown(fullResponse), + modelHeader + escapeMarkdown(fullResponse), { parse_mode: 'Markdown' } ); + const responseCharCount = fullResponse.length; + await db.update(schema.usersTable) + .set({ + aiCharacters: sql`${schema.usersTable.aiCharacters} + ${responseCharCount}`, + aiRequests: sql`${schema.usersTable.aiRequests} + 1` + }) + .where(eq(schema.usersTable.telegramId, userId)); return { success: true, response: fullResponse, @@ -437,9 +449,9 @@ async function getResponse(prompt: string, ctx: TextContext, replyGenerating: Me } } -async function handleAiReply(ctx: TextContext, model: string, prompt: string, replyGenerating: Message, aiTemperature: number, originalMessage: string) { +async function handleAiReply(ctx: TextContext, model: string, prompt: string, replyGenerating: Message, aiTemperature: number, originalMessage: string, db: NodePgDatabase, userId: string) { const Strings = getStrings(languageCode(ctx)); - const aiResponse = await getResponse(prompt, ctx, replyGenerating, model, aiTemperature, originalMessage); + const aiResponse = await getResponse(prompt, ctx, replyGenerating, model, aiTemperature, originalMessage, db, userId); if (!aiResponse) return; if (!ctx.chat) return; if (aiResponse.success && aiResponse.response) { @@ -453,7 +465,7 @@ async function handleAiReply(ctx: TextContext, model: string, prompt: string, re ctx, ctx.chat.id, replyGenerating.message_id, - modelHeader + urlWarning + sanitizeMarkdownForTelegram(aiResponse.response), + modelHeader + sanitizeMarkdownForTelegram(aiResponse.response) + urlWarning, { parse_mode: 'Markdown' } ); return; @@ -545,11 +557,11 @@ export default (bot: Telegraf, db: NodePgDatabase) => { if (command === 'ai') { model = customAiModel || flash_model; fixedMsg = message.replace(/^\/ai(@\w+)?\s*/, "").trim(); - logger.logCmdStart(author, "ask"); + logger.logCmdStart(author, command, model); } else { model = command === 'ask' ? flash_model : thinking_model; fixedMsg = message.replace(/^\/(ask|think)(@\w+)?\s*/, "").trim(); - logger.logCmdStart(author, command); + logger.logCmdStart(author, command, model); } if (!process.env.ollamaApi) { @@ -573,9 +585,8 @@ export default (bot: Telegraf, db: NodePgDatabase) => { parse_mode: 'Markdown', ...(reply_to_message_id && { reply_parameters: { message_id: reply_to_message_id } }) }); - logger.logPrompt(fixedMsg); const prompt = sanitizeForJson(await usingSystemPrompt(ctx, db, botName, fixedMsg)); - await handleAiReply(ctx, model, prompt, replyGenerating, aiTemperature, fixedMsg); + await handleAiReply(ctx, model, prompt, replyGenerating, aiTemperature, fixedMsg, db, user.telegramId); }; if (isProcessing) { @@ -601,4 +612,16 @@ export default (bot: Telegraf, db: NodePgDatabase) => { if (!ctx.message || !('text' in ctx.message)) return; await aiCommandHandler(ctx as TextContext, 'ai'); }); + + bot.command(["aistats"], spamwatchMiddleware, async (ctx) => { + const { user, Strings } = await getUserWithStringsAndModel(ctx, db); + if (!user) { + await ctx.reply(Strings.userNotFound || "User not found."); + return; + } + const bookCount = Math.max(1, Math.round(user.aiCharacters / 500000)); + const bookWord = bookCount === 1 ? 'book' : 'books'; + const msg = `${Strings.aiStats.header}\n\n${Strings.aiStats.requests.replace('{aiRequests}', user.aiRequests)}\n${Strings.aiStats.characters.replace('{aiCharacters}', user.aiCharacters).replace('{bookCount}', bookCount).replace('books', bookWord)}`; + await ctx.reply(msg, { parse_mode: 'Markdown' }); + }); } diff --git a/src/commands/main.ts b/src/commands/main.ts index 55ccc00..9082275 100644 --- a/src/commands/main.ts +++ b/src/commands/main.ts @@ -48,23 +48,41 @@ 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' }, - { text: `🧠 ${Strings.settings.ai.aiModel}: ${getModelLabelByName(user.customAiModel)}`, callback_data: 'settings_aiModel' } + { 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_${userId}` } ], [ - { text: `🌡️ ${Strings.settings.ai.aiTemperature}: ${user.aiTemperature}`, callback_data: 'settings_aiTemperature' }, - { text: `🌐 ${langLabel}`, callback_data: 'settings_language' } + { text: `🌡️ ${Strings.settings.ai.aiTemperature}: ${user.aiTemperature}`, callback_data: `settings_aiTemperature_${userId}` }, + { text: `🌐 ${langLabel}`, callback_data: `settings_language_${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}`); + } +} + export default (bot: Telegraf, db: NodePgDatabase) => { bot.start(spamwatchMiddleware, async (ctx: Context) => { const { user, Strings } = await getUserAndStrings(ctx, db); @@ -118,218 +136,297 @@ export default (bot: Telegraf, db: NodePgDatabase) => { await ctx.editMessageReplyMarkup(menu.reply_markup); }; - bot.action('settings_aiEnabled', async (ctx) => { - try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - await db.update(schema.usersTable) - .set({ aiEnabled: !user.aiEnabled }) - .where(eq(schema.usersTable.telegramId, String(user.telegramId))); - const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0]; - await updateSettingsKeyboard(ctx, updatedUser, Strings); - } catch (err) { - console.error('Error handling settings_aiEnabled callback:', err); + bot.action(/^settings_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_aiModel', async (ctx) => { + bot.action(/^settings_aiModel_\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; try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - try { - await ctx.editMessageText( - `${Strings.settings.ai.selectSeries}`, - { - reply_markup: { - inline_keyboard: models.map(series => [ - { text: series.label, callback_data: `selectseries_${series.name}` } - ]).concat([[ - { text: `${Strings.varStrings.varBack}`, callback_data: 'settings_back' } - ]]) - } + await ctx.editMessageText( + `${Strings.settings.ai.selectSeries}`, + { + reply_markup: { + inline_keyboard: models.map(series => [ + { text: series.label, callback_data: `selectseries_${series.name}_${user.telegramId}` } + ]).concat([[ + { text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` } + ]]) } - ); - } catch (err) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('Unexpected Telegram error:', err); - } + } + ); } catch (err) { - console.error('Error handling settings_aiModel callback:', err); + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('Unexpected Telegram error:', err); } }); - bot.action(/^selectseries_.+$/, async (ctx) => { + bot.action(/^selectseries_.+_\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 seriesName = data.replace(/^selectseries_/, '').replace(/_\d+$/, ''); + const series = models.find(s => s.name === seriesName); + if (!series) return; + const desc = user.languageCode === 'pt' ? series.descriptionPt : series.descriptionEn; try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - const data = (ctx.callbackQuery as any).data; - const seriesName = data.replace('selectseries_', ''); - const series = models.find(s => s.name === seriesName); - if (!series) return; - const desc = user.languageCode === 'pt' ? series.descriptionPt : series.descriptionEn; - try { - await ctx.editMessageText( - `${Strings.settings.ai.seriesDescription.replace('{seriesDescription}', desc)}\n\n${Strings.settings.ai.selectParameterSize.replace('{seriesLabel}', series.label)}\n\n${Strings.settings.ai.parameterSizeExplanation}`, - { - reply_markup: { - inline_keyboard: series.models.map(m => [ - { text: `${m.label} (${m.parameterSize})`, callback_data: `setmodel_${series.name}_${m.name}` } - ]).concat([[ - { text: `${Strings.varStrings.varBack}`, callback_data: 'settings_aiModel' } - ]]) - } + await ctx.editMessageText( + `${Strings.settings.ai.seriesDescription.replace('{seriesDescription}', desc)}\n\n${Strings.settings.ai.selectParameterSize.replace('{seriesLabel}', series.label)}\n\n${Strings.settings.ai.parameterSizeExplanation}`, + { + reply_markup: { + inline_keyboard: series.models.map(m => [ + { text: `${m.label} (${m.parameterSize})`, callback_data: `setmodel_${series.name}_${m.name}_${user.telegramId}` } + ]).concat([[ + { text: `${Strings.varStrings.varBack}`, callback_data: `settings_aiModel_${user.telegramId}` } + ]]) } - ); - } catch (err) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('Unexpected Telegram error:', err); - } + } + ); } catch (err) { - console.error('Error handling selectseries callback:', err); + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('Unexpected Telegram error:', err); } }); - bot.action(/^setmodel_.+$/, async (ctx) => { + bot.action(/^setmodel_.+_\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 parts = data.split('_'); + const seriesName = parts[1]; + const modelName = parts.slice(2, -1).join('_'); + const series = models.find(s => s.name === seriesName); + const model = series?.models.find(m => m.name === modelName); + if (!series || !model) return; + await db.update(schema.usersTable) + .set({ customAiModel: model.name }) + .where(eq(schema.usersTable.telegramId, String(user.telegramId))); + const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0]; + const menu = getSettingsMenu(updatedUser, Strings); try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - const data = (ctx.callbackQuery as any).data; - const parts = data.split('_'); - const seriesName = parts[1]; - const modelName = parts.slice(2).join('_'); - const series = models.find(s => s.name === seriesName); - const model = series?.models.find(m => m.name === modelName); - if (!series || !model) return; - await db.update(schema.usersTable) - .set({ customAiModel: model.name }) - .where(eq(schema.usersTable.telegramId, String(user.telegramId))); - const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0]; - const menu = getSettingsMenu(updatedUser, Strings); - try { - if (ctx.callbackQuery.message) { - await ctx.editMessageText( - menu.text, - { - reply_markup: menu.reply_markup, - parse_mode: 'Markdown' - } - ); - } else { - await ctx.reply(menu.text, { + if (ctx.callbackQuery.message) { + await ctx.editMessageText( + menu.text, + { reply_markup: menu.reply_markup, parse_mode: 'Markdown' - }); - } - } catch (err) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('[Settings] Unexpected Telegram error:', err); - } - } catch (err) { - console.error('Error handling setmodel callback:', err); - } - }); - - bot.action('settings_aiTemperature', async (ctx) => { - try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - const temps = [0.2, 0.5, 0.7, 0.9, 1.2]; - try { - await ctx.editMessageReplyMarkup({ - inline_keyboard: temps.map(t => [{ text: t.toString(), callback_data: `settemp_${t}` }]).concat([[{ text: `${Strings.varStrings.varBack}`, callback_data: 'settings_back' }]]) + } + ); + } else { + await ctx.reply(menu.text, { + reply_markup: menu.reply_markup, + parse_mode: 'Markdown' }); - } catch (err) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('Unexpected Telegram error:', err); } } catch (err) { - console.error('Error handling settings_aiTemperature callback:', err); + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('[Settings] Unexpected Telegram error:', err); } }); - bot.action(/^settemp_.+$/, async (ctx) => { + 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.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - const data = (ctx.callbackQuery as any).data; - const temp = parseFloat(data.replace('settemp_', '')); - await db.update(schema.usersTable) - .set({ aiTemperature: temp }) - .where(eq(schema.usersTable.telegramId, String(user.telegramId))); - const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0]; - await updateSettingsKeyboard(ctx, updatedUser, Strings); + await ctx.editMessageReplyMarkup({ + inline_keyboard: temps.map(t => [{ text: t.toString(), callback_data: `settemp_${t}_${user.telegramId}` }]).concat([[{ text: `${Strings.varStrings.varBack}`, callback_data: `settings_back_${user.telegramId}` }]]) + }); } catch (err) { - console.error('Error handling settemp callback:', err); + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('Unexpected Telegram error:', err); } }); - bot.action('settings_language', async (ctx) => { + 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.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - try { - await ctx.editMessageReplyMarkup({ - inline_keyboard: langs.map(l => [{ text: l.label, callback_data: `setlang_${l.code}` }]).concat([[{ text: `${Strings.varStrings.varBack}`, callback_data: 'settings_back' }]]) + await ctx.editMessageReplyMarkup({ + 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) { + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('Unexpected Telegram error:', err); + } + }); + + 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; + await updateSettingsKeyboard(ctx, user, Strings); + }); + + 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) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('Unexpected Telegram error:', err); } } catch (err) { - console.error('Error handling settings_language callback:', err); - } - }); - - bot.action('settings_back', async (ctx) => { - try { - await ctx.answerCbQuery(); - const { user, Strings } = await getUserAndStrings(ctx, db); - if (!user) return; - await updateSettingsKeyboard(ctx, user, Strings); - } catch (err) { - console.error('Error handling settings_back callback:', err); + if ( + !( + err.response.description?.includes('query is too old') || + err.response.description?.includes('query ID is invalid') || + err.response.description?.includes('message is not modified') || + err.response.description?.includes('message to edit not found') + ) + ) + console.error('[Settings] Unexpected Telegram error:', err); } }); @@ -342,51 +439,4 @@ export default (bot: Telegraf, db: NodePgDatabase) => { reply_to_message_id: ctx.message.message_id } as any); }); - - bot.action(/^setlang_.+$/, async (ctx) => { - try { - await ctx.answerCbQuery(); - const { user } = await getUserAndStrings(ctx, db); - if (!user) { - console.log('[Settings] No user found'); - return; - } - const data = (ctx.callbackQuery as any).data; - const lang = data.replace('setlang_', ''); - await db.update(schema.usersTable) - .set({ languageCode: lang }) - .where(eq(schema.usersTable.telegramId, String(user.telegramId))); - const updatedUser = (await db.query.usersTable.findMany({ where: (fields, { eq }) => eq(fields.telegramId, String(user.telegramId)), limit: 1 }))[0]; - const updatedStrings = getStrings(updatedUser.languageCode); - const menu = getSettingsMenu(updatedUser, updatedStrings); - try { - if (ctx.callbackQuery.message) { - await ctx.editMessageText( - menu.text, - { - reply_markup: menu.reply_markup, - parse_mode: 'Markdown' - } - ); - } else { - await ctx.reply(menu.text, { - reply_markup: menu.reply_markup, - parse_mode: 'Markdown' - }); - } - } catch (err) { - if ( - !( - err.response.description?.includes('query is too old') || - err.response.description?.includes('query ID is invalid') || - err.response.description?.includes('message is not modified') || - err.response.description?.includes('message to edit not found') - ) - ) - console.error('[Settings] Unexpected Telegram error:', err); - } - } catch (err) { - console.error('[Settings] Error handling setlang callback:', err); - } - }); }; \ No newline at end of file diff --git a/src/locales/english.json b/src/locales/english.json index 3acb49b..298e11e 100644 --- a/src/locales/english.json +++ b/src/locales/english.json @@ -74,7 +74,7 @@ "languageCode": "Language", "thinking": "Thinking...", "finishedThinking": "Done.", - "urlWarning": "\n\n⚠️ The user provided one or more URLs in their message. Please do not visit any suspicious URLs.", + "urlWarning": "\n\n⚠️ Note: The model cannot access or visit links!", "inQueue": "ℹ️ You are {position} in the queue.", "startingProcessing": "✨ Starting to process your request...", "systemPrompt": "You are a friendly assistant called {botName}, capable of Telegram MarkdownV2.\nYou are currently in a chat with a user, who has sent a message to you.\nCurrent Date/Time (UTC): {date}\n\n---\n\nRespond to the user's message:\n{message}", @@ -182,5 +182,10 @@ "pong": "Pong in {ms}ms.", "botInfo": "Kowalski is a multipurpose bot with a variety of features, including AI, moderation, and more.", "credits": "Kowalski was created by ihatenodejs/Aidan, with contributions from the open-source community. It is licensed under the Unlicense license." + }, + "aiStats": { + "header": "✨ *Your AI Usage Stats*", + "requests": "*Total AI Requests:* {aiRequests}", + "characters": "*Total AI Characters:* {aiCharacters}\n_That's around {bookCount} books of text!_" } } diff --git a/src/locales/portuguese.json b/src/locales/portuguese.json index 1d2579d..2dd9832 100644 --- a/src/locales/portuguese.json +++ b/src/locales/portuguese.json @@ -72,7 +72,7 @@ "askNoMessage": "Você precisa fazer uma pergunta\\.", "thinking": "Pensando\\.\\.\\.", "finishedThinking": "Pronto\\.", - "urlWarning": "\n\n⚠️ O usuário forneceu um ou mais URLs na sua mensagem\\. Por favor, não visite URLs suspeitos\\.", + "urlWarning": "\n\n⚠️ Nota: O modelo de IA não pode acessar ou visitar links!", "inQueue": "ℹ️ Você é o {position} na fila.", "startingProcessing": "✨ Começando a processar o seu pedido\\.\\.\\.", "aiEnabled": "IA", @@ -179,5 +179,10 @@ "gsmarenaNotAllowed": "você não tem permissão para interagir com isso.", "gsmarenaInvalidOrExpired": "Ops! Opção inválida ou expirada. Por favor, tente novamente.", "gsmarenaDeviceDetails": "estes são os detalhes do seu dispositivo:", - "gsmarenaErrorFetchingDetails": "Erro ao buscar detalhes do celular." + "gsmarenaErrorFetchingDetails": "Erro ao buscar detalhes do celular.", + "aiStats": { + "header": "✨ *Suas estatísticas de uso de IA*", + "requests": "*Total de requisições de IA:* {aiRequests}", + "characters": "*Total de caracteres de IA:* {aiCharacters}\n_Isso é cerca de {bookCount} livros de texto!_" + } } diff --git a/src/utils/log.ts b/src/utils/log.ts index 2f7e9ac..801e650 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -42,8 +42,8 @@ class Logger { return Logger.instance } - logCmdStart(user: string, type: "ask" | "think"): void { - console.log(`\n[✨ AI | START] Received /${type} for model ${type === "ask" ? flash_model : thinking_model}`) + logCmdStart(user: string, command: string, model: string): void { + console.log(`\n[✨ AI | START] Received /${command} for model ${model} (from ${user})`) } logThinking(chatId: number, messageId: number, thinking: boolean): void { @@ -55,12 +55,16 @@ class Logger { } logChunk(chatId: number, messageId: number, text: string, isOverflow: boolean = false): void { - const prefix = isOverflow ? "[✨ AI | OVERFLOW]" : "[✨ AI | CHUNK]" - console.log(`${prefix} [${chatId}:${messageId}] ${text.length} chars pushed to Telegram`) + if (process.env.longerLogs === 'true') { + const prefix = isOverflow ? "[✨ AI | OVERFLOW]" : "[✨ AI | CHUNK]" + console.log(`${prefix} [${chatId}:${messageId}] ${text.length} chars pushed to Telegram`) + } } logPrompt(prompt: string): void { - console.log(`[✨ AI | PROMPT] ${prompt.length} chars input`) + if (process.env.longerLogs === 'true') { + console.log(`[✨ AI | PROMPT] ${prompt}`) + } } logError(error: unknown): void {