diff --git a/.gitignore b/.gitignore index 5f5b37b..9cd4cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env node_modules -postgres_data \ No newline at end of file +postgres_data +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index d7c4285..afdddf6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ -# Ability Telegram +# Ability Telegram Bot 🎯 -adoro le abilitΓ  \ No newline at end of file +A Telegram bot that helps you track abilities and points for users in your group. + +## Features + +- Track multiple abilities per group +- Award and remove points to/from users +- View leaderboards for each ability +- Admin-only ability management +- Inline button support for quick point adjustments + +## Commands + +- `/start` - Get a welcome message +- `/help` - Show all available commands +- `/info` - Get your user ID and chat ID +- `/create [ability]` - Create a new ability (admins only) +- `/remove [ability]` - Remove an ability (admins only) +- `/list` - List all abilities with pagination +- `/add [ability]` - Add a point to the replied user (reply to their message) +- `/leaderboard [ability]` - Show leaderboard for an ability + +## Setup + +### Environment Variables + +Create a `.env` file with the following variables: + +``` +DATABASE_URL=postgresql://user:password@host:port/database +TOKEN=your_telegram_bot_token +``` + +### Using Docker + +```bash +docker-compose up -d +``` + +### Running Migrations + +```bash +npm run migrate up +``` + +### Starting the Bot + +```bash +npm start +``` + +## Usage + +1. Add the bot to your Telegram group +2. Admins can create abilities using `/create [ability name]` +3. Reply to a user's message and use `/add [ability name]` to award points +4. Use `/leaderboard [ability name]` to view rankings +5. Click the βž•/βž– buttons on point messages to adjust points + +## License + +This project is provided as-is without any specific license. \ No newline at end of file diff --git a/package.json b/package.json index 1aecbcd..ba89b1d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "postgres": "^3.4.7" }, "devDependencies": { - "node-pg-migrate": "^8.0.3" + "@types/node": "^24.9.2", + "node-pg-migrate": "^8.0.3", + "typescript": "^5.9.3" } } diff --git a/src/main.ts b/src/main.ts index 4a67140..545910c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,210 +9,301 @@ if (!process.env.DATABASE_URL || !process.env.TOKEN) { const sql = postgres(process.env.DATABASE_URL); const bot = new TelegramBot(process.env.TOKEN, { polling: true }); -bot.onText(/\/info/, async (msg) => { - await bot.sendMessage(msg.chat.id, `User ID: ${msg.from!.id}\nChat ID: ${msg.chat.id}`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); -}); +console.log('πŸ€– Ability Telegram Bot starting...'); -bot.onText(/^\/add(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { - if (!msg.reply_to_message?.from) { - await bot.sendMessage(msg.chat.id, 'Please reply to a message', { reply_to_message_id: msg.message_id }); - return; - } +bot.onText(/\/start/, async (msg) => { + const welcomeMessage = `Welcome to Ability Telegram Bot! 🎯 - if (!match || !match[1]) { - await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); - return; - } +This bot helps you track abilities and points for users in your group. - if (msg.reply_to_message?.from.id == msg.from!.id) { - await bot.sendMessage(msg.chat.id, 'You cannot add points to yourself', { reply_to_message_id: msg.message_id }); - return; - } +Use /help to see all available commands.`; + + await bot.sendMessage(msg.chat.id, welcomeMessage, { reply_to_message_id: msg.message_id }); +}); - const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${match![1]}`; - if (abilities.length == 0) { - await bot.sendMessage(msg.chat.id, `Ability ${match![1]} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); - return; - } +bot.onText(/\/help/, async (msg) => { + const helpMessage = `Available Commands: - const points = await sql`INSERT INTO points (user_id, ability_id, group_id, points) VALUES (${msg.reply_to_message.from!.id}, ${abilities[0].id}, ${msg.chat.id}, 1) ON CONFLICT (user_id, ability_id, group_id) DO UPDATE SET points = points.points + 1 RETURNING points.points`; +/info - Get your user ID and chat ID +/create [ability] - Create a new ability (admins only) +/remove [ability] - Remove an ability (admins only) +/list - List all abilities +/add [ability] - Add a point to the replied user +/leaderboard [ability] - Show leaderboard for an ability - const sent = await bot.sendMessage(msg.chat.id, `Added 1 ${match![1]} point to @${msg.reply_to_message.from.username}\nThey now have ${points[0].points} points\n\nAdded by: @${msg.from!.username}`, { +Note: To add points, reply to a user's message and use /add [ability]`; + + await bot.sendMessage(msg.chat.id, helpMessage, { reply_to_message_id: msg.message_id, - parse_mode: 'HTML', - reply_markup: { - inline_keyboard: [ - [ - { - text: 'βž•', - callback_data: `add_point-${abilities[0].id}-${msg.reply_to_message.from!.id}` - }, - { - text: 'βž–', - callback_data: `remove_point-${abilities[0].id}-${msg.reply_to_message.from!.id}` - } - ] - ] - } + parse_mode: 'HTML' }); - const users = [ - { - id: msg.from!.id, - username: msg.from!.username - } - ] +}); - await sql`INSERT INTO messages (message_id, chat_id, users) VALUES (${sent.message_id}, ${sent.chat.id}, ${JSON.stringify(users)})`; +bot.onText(/\/info/, async (msg) => { + await bot.sendMessage(msg.chat.id, `User ID: ${msg.from!.id}\nChat ID: ${msg.chat.id}`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); }); +bot.onText(/^\/add(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { + try { + if (!msg.reply_to_message?.from) { + await bot.sendMessage(msg.chat.id, 'Please reply to a message', { reply_to_message_id: msg.message_id }); + return; + } -type Ability = { id: number, name: string }; + if (!match || !match[1]) { + await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); + return; + } -bot.onText(/^\/leaderboard(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { - if (!match || !match[1]) { - const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id}` as Ability[]; + if (msg.reply_to_message?.from.id == msg.from!.id) { + await bot.sendMessage(msg.chat.id, 'You cannot add points to yourself', { reply_to_message_id: msg.message_id }); + return; + } + + const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${match![1]}`; + if (abilities.length == 0) { + await bot.sendMessage(msg.chat.id, `Ability ${match![1]} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); + return; + } + + const points = await sql`INSERT INTO points (user_id, ability_id, group_id, points) VALUES (${msg.reply_to_message.from!.id}, ${abilities[0].id}, ${msg.chat.id}, 1) ON CONFLICT (user_id, ability_id, group_id) DO UPDATE SET points = points.points + 1 RETURNING points.points`; - const chunks: Ability[][] = []; - for (let i = 0; i < abilities.length; i += 3) - chunks.push(abilities.slice(i, i + 3)); + const recipientName = msg.reply_to_message.from.username + ? `@${msg.reply_to_message.from.username}` + : (msg.reply_to_message.from.first_name || `User ${msg.reply_to_message.from.id}`); + + const adderName = msg.from!.username + ? `@${msg.from!.username}` + : (msg.from!.first_name || `User ${msg.from!.id}`); - await bot.sendMessage(msg.chat.id, 'Please specify an ability', { + const sent = await bot.sendMessage(msg.chat.id, `Added 1 ${match![1]} point to ${recipientName}\nThey now have ${points[0].points} points\n\nAdded by: ${adderName}`, { reply_to_message_id: msg.message_id, + parse_mode: 'HTML', reply_markup: { - inline_keyboard: chunks.map(chunk => chunk.map(ability => ({ - text: ability.name, - callback_data: `leaderboard-${ability.id}` - }))) + inline_keyboard: [ + [ + { + text: 'βž•', + callback_data: `add_point-${abilities[0].id}-${msg.reply_to_message.from!.id}` + }, + { + text: 'βž–', + callback_data: `remove_point-${abilities[0].id}-${msg.reply_to_message.from!.id}` + } + ] + ] } }); - return; - } + const users = [ + { + id: msg.from!.id, + username: msg.from!.username, + first_name: msg.from!.first_name + } + ] - const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${match![1]}`; - if (abilities.length == 0) { - await bot.sendMessage(msg.chat.id, `Ability ${match![1]} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); - return; + await sql`INSERT INTO messages (message_id, chat_id, users) VALUES (${sent.message_id}, ${sent.chat.id}, ${JSON.stringify(users)})`; + } catch (error) { + console.error('Error in /add command:', error); + await bot.sendMessage(msg.chat.id, 'An error occurred while adding the point. Please try again.', { reply_to_message_id: msg.message_id }); } +}); - const points = await sql`SELECT user_id, points FROM points WHERE ability_id = ${abilities[0].id} AND group_id = ${msg.chat.id} ORDER BY points DESC`; - let leaderboard = ''; +type Ability = { id: number, name: string }; - for (const point of points) { - const info = await bot.getChatMember(msg.chat.id, point.user_id); - if (info.user) - leaderboard += `@${info.user.username}: ${point.points}\n`; - else leaderboard += `${point.user_id}: ${point.points}\n`; - } +bot.onText(/^\/leaderboard(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { + try { + if (!match || !match[1]) { + const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id}` as Ability[]; + + const chunks: Ability[][] = []; + for (let i = 0; i < abilities.length; i += 3) + chunks.push(abilities.slice(i, i + 3)); + + await bot.sendMessage(msg.chat.id, 'Please specify an ability', { + reply_to_message_id: msg.message_id, + reply_markup: { + inline_keyboard: chunks.map(chunk => chunk.map(ability => ({ + text: ability.name, + callback_data: `leaderboard-${ability.id}` + }))) + } + }); + return; + } - await bot.sendMessage(msg.chat.id, `${match![1]} leaderboard:\n\n${leaderboard}`, { - reply_to_message_id: msg.message_id, - parse_mode: 'HTML', - disable_notification: true - }); -}); + const abilities = await sql`SELECT id, name FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${match![1]}`; + if (abilities.length == 0) { + await bot.sendMessage(msg.chat.id, `Ability ${match![1]} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); + return; + } -bot.onText(/^\/create(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { - if (["group", "supergroup"].indexOf(msg.chat.type) === -1) { - bot.sendMessage(msg.chat.id, 'You are not in a group!'); - return - } + const points = await sql`SELECT user_id, points FROM points WHERE ability_id = ${abilities[0].id} AND group_id = ${msg.chat.id} ORDER BY points DESC`; - const admins = await bot.getChatAdministrators(msg.chat.id); - const isAdmin = admins.some(admin => admin.user.id == msg.from!.id); + let leaderboard = ''; - if (!isAdmin) { - bot.sendMessage(msg.chat.id, 'Only admins can use this command!'); - return - } + if (points.length === 0) { + leaderboard = 'No points yet for this ability'; + } else { + for (const point of points) { + try { + const info = await bot.getChatMember(msg.chat.id, point.user_id); + if (info.user) { + const userName = info.user.username + ? `@${info.user.username}` + : (info.user.first_name || `User ${info.user.id}`); + leaderboard += `${userName}: ${point.points}\n`; + } else { + leaderboard += `${point.user_id}: ${point.points}\n`; + } + } catch { + leaderboard += `${point.user_id}: ${point.points}\n`; + } + } + } - if (!match || !match[1]) { - await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); - return; + await bot.sendMessage(msg.chat.id, `${match![1]} leaderboard:\n\n${leaderboard}`, { + reply_to_message_id: msg.message_id, + parse_mode: 'HTML', + disable_notification: true + }); + } catch (error) { + console.error('Error in /leaderboard command:', error); + await bot.sendMessage(msg.chat.id, 'An error occurred while fetching the leaderboard. Please try again.', { reply_to_message_id: msg.message_id }); } +}); - const ability = match[1]; +bot.onText(/^\/create(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { try { - await sql`INSERT INTO abilities (group_id, name) VALUES (${msg.chat.id}, ${ability})`; - } catch (e) { - if (e.code === '23505') - await bot.sendMessage(msg.chat.id, `Ability ${ability} already exists`, { reply_to_message_id: msg.message_id }); - else - await bot.sendMessage(msg.chat.id, 'Something went wrong', { reply_to_message_id: msg.message_id }); - return; + if (["group", "supergroup"].indexOf(msg.chat.type) === -1) { + bot.sendMessage(msg.chat.id, 'You are not in a group!'); + return + } + + const admins = await bot.getChatAdministrators(msg.chat.id); + const isAdmin = admins.some(admin => admin.user.id == msg.from!.id); + + if (!isAdmin) { + bot.sendMessage(msg.chat.id, 'Only admins can use this command!'); + return + } + + if (!match || !match[1]) { + await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); + return; + } + + const ability = match[1].trim(); + + // Validate ability name + if (ability.length === 0) { + await bot.sendMessage(msg.chat.id, 'Ability name cannot be empty', { reply_to_message_id: msg.message_id }); + return; + } + + if (ability.length > 100) { + await bot.sendMessage(msg.chat.id, 'Ability name is too long (max 100 characters)', { reply_to_message_id: msg.message_id }); + return; + } + + try { + await sql`INSERT INTO abilities (group_id, name) VALUES (${msg.chat.id}, ${ability})`; + } catch (e) { + if (e.code === '23505') + await bot.sendMessage(msg.chat.id, `Ability ${ability} already exists`, { reply_to_message_id: msg.message_id }); + else + await bot.sendMessage(msg.chat.id, 'Something went wrong', { reply_to_message_id: msg.message_id }); + return; + } + await bot.sendMessage(msg.chat.id, `Added ability ${ability}`, { + reply_to_message_id: msg.message_id, + parse_mode: 'HTML' + }); + } catch (error) { + console.error('Error in /create command:', error); + await bot.sendMessage(msg.chat.id, 'An error occurred while creating the ability. Please try again.', { reply_to_message_id: msg.message_id }); } - await bot.sendMessage(msg.chat.id, `Added ability ${ability}`, { - reply_to_message_id: msg.message_id, - parse_mode: 'HTML' - }); }); bot.onText(/^\/remove(?:@\w+)?(?:\s+(.+))?$/, async (msg, match) => { - if (["group", "supergroup"].indexOf(msg.chat.type) === -1) { - bot.sendMessage(msg.chat.id, 'You are not in a group!'); - return - } + try { + if (["group", "supergroup"].indexOf(msg.chat.type) === -1) { + bot.sendMessage(msg.chat.id, 'You are not in a group!'); + return + } - const admins = await bot.getChatAdministrators(msg.chat.id); - const isAdmin = admins.some(admin => admin.user.id == msg.from!.id); + const admins = await bot.getChatAdministrators(msg.chat.id); + const isAdmin = admins.some(admin => admin.user.id == msg.from!.id); - if (!isAdmin) { - bot.sendMessage(msg.chat.id, 'Only admins can use this command!'); - return - } + if (!isAdmin) { + bot.sendMessage(msg.chat.id, 'Only admins can use this command!'); + return + } - if (!match || !match[1]) { - await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); - return; - } + if (!match || !match[1]) { + await bot.sendMessage(msg.chat.id, 'Please specify an ability', { reply_to_message_id: msg.message_id }); + return; + } - const ability = match[1]; - const check = await sql`SELECT id FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${ability}`; - if (check.length == 0) { - await bot.sendMessage(msg.chat.id, `Ability ${ability} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); - return; - } + const ability = match[1]; + const check = await sql`SELECT id FROM abilities WHERE group_id = ${msg.chat.id} AND name = ${ability}`; + if (check.length == 0) { + await bot.sendMessage(msg.chat.id, `Ability ${ability} does not exist`, { reply_to_message_id: msg.message_id, parse_mode: 'HTML' }); + return; + } - await sql`DELETE FROM abilities WHERE group_id = ${msg.chat.id} AND id=${check[0].id}`; + await sql`DELETE FROM abilities WHERE group_id = ${msg.chat.id} AND id=${check[0].id}`; - await bot.sendMessage(msg.chat.id, `Deleted ability ${ability}`, { - reply_to_message_id: msg.message_id, - parse_mode: 'HTML' - }); + await bot.sendMessage(msg.chat.id, `Deleted ability ${ability}`, { + reply_to_message_id: msg.message_id, + parse_mode: 'HTML' + }); + } catch (error) { + console.error('Error in /remove command:', error); + await bot.sendMessage(msg.chat.id, 'An error occurred while removing the ability. Please try again.', { reply_to_message_id: msg.message_id }); + } }); bot.onText(/\/list/, async (msg) => { - const offset = 5; - const count = await sql`SELECT COUNT(*) FROM abilities WHERE group_id = ${msg.chat.id}`; - const abilities = await sql`SELECT name FROM abilities WHERE group_id = ${msg.chat.id} ORDER BY name ASC LIMIT ${offset}`; - if (abilities.length == 0) - await bot.sendMessage(msg.chat.id, 'No abilities found', { reply_to_message_id: msg.message_id }); - + try { + const offset = 5; + const count = await sql`SELECT COUNT(*) FROM abilities WHERE group_id = ${msg.chat.id}`; + const abilities = await sql`SELECT name FROM abilities WHERE group_id = ${msg.chat.id} ORDER BY name ASC LIMIT ${offset}`; + if (abilities.length == 0) { + await bot.sendMessage(msg.chat.id, 'No abilities found', { reply_to_message_id: msg.message_id }); + return; + } - const list = abilities.map((ability, i) => `${i + 1}. ${ability.name}`).join('\n') + `\n\nPage: 1 / ${Math.ceil(count[0].count / offset)}`; + const list = abilities.map((ability, i) => `${i + 1}. ${ability.name}`).join('\n') + `\n\nPage: 1 / ${Math.ceil(count[0].count / offset)}`; - await bot.sendMessage(msg.chat.id, list, { - reply_to_message_id: msg.message_id, - parse_mode: 'HTML', - reply_markup: count[0].count > offset ? { - inline_keyboard: [ - [ - { - text: '◀️', - callback_data: `list-${Math.ceil(count[0].count / offset)}-${msg.from!.id}` - }, - { - text: '▢️', - callback_data: `list-2-${msg.from!.id}` - } + await bot.sendMessage(msg.chat.id, list, { + reply_to_message_id: msg.message_id, + parse_mode: 'HTML', + reply_markup: count[0].count > offset ? { + inline_keyboard: [ + [ + { + text: '◀️', + callback_data: `list-${Math.ceil(count[0].count / offset)}-${msg.from!.id}` + }, + { + text: '▢️', + callback_data: `list-2-${msg.from!.id}` + } + ] ] - ] - } : undefined - }); + } : undefined + }); + } catch (error) { + console.error('Error in /list command:', error); + await bot.sendMessage(msg.chat.id, 'An error occurred while listing abilities. Please try again.', { reply_to_message_id: msg.message_id }); + } }); bot.on('callback_query', async (callbackQuery) => { - if (callbackQuery.data?.startsWith('remove_point')) { + try { + if (callbackQuery.data?.startsWith('remove_point')) { const abilityId = callbackQuery.data.split('-')[1]; const userId = callbackQuery.data.split('-')[2]; @@ -237,13 +328,23 @@ bot.on('callback_query', async (callbackQuery) => { let users = JSON.parse(messages[0].users); if (!users.map(x => x.id).includes(callbackQuery.from.id)) { bot.answerCallbackQuery(callbackQuery.id, { - text: "🚨 You never addeded this point", + text: "🚨 You never added this point", show_alert: true }); return; } - const points = await sql`UPDATE points SET points = points.points - 1 WHERE user_id=${userId} AND ability_id = ${abilityId} AND group_id = ${callbackQuery.message!.chat.id} RETURNING points`; + // Check current points to prevent going negative + const currentPoints = await sql`SELECT points FROM points WHERE user_id=${userId} AND ability_id = ${abilityId} AND group_id = ${callbackQuery.message!.chat.id}`; + if (currentPoints.length === 0 || currentPoints[0].points < 1) { + bot.answerCallbackQuery(callbackQuery.id, { + text: "🚨 Cannot remove points - user has 0 points", + show_alert: true + }); + return; + } + + const points = await sql`UPDATE points SET points = points - 1 WHERE user_id=${userId} AND ability_id = ${abilityId} AND group_id = ${callbackQuery.message!.chat.id} RETURNING points`; users = users.filter(x => x.id != callbackQuery.from.id); await sql`UPDATE messages SET users = ${JSON.stringify(users)} WHERE message_id = ${callbackQuery.message!.message_id} AND chat_id = ${callbackQuery.message!.chat.id}`; @@ -251,8 +352,10 @@ bot.on('callback_query', async (callbackQuery) => { let messageContent = callbackQuery.message!.text!.split('\n')[0]; messageContent += `\nThey now have ${points[0].points} points`; - if (users.length > 0) - messageContent += `\nAdded by: ${users.map(x => `@${x.username}`).join(', ')}`; + if (users.length > 0) { + const userNames = users.map(u => u.username ? `@${u.username}` : (u.first_name || `User ${u.id}`)).join(', '); + messageContent += `\nAdded by: ${userNames}`; + } await bot.editMessageText(messageContent, { message_id: callbackQuery.message!.message_id, @@ -308,17 +411,19 @@ bot.on('callback_query', async (callbackQuery) => { return; } - const points = await sql`UPDATE points SET points = points.points + 1 WHERE user_id=${userId} AND ability_id = ${abilityId} AND group_id = ${callbackQuery.message!.chat.id} RETURNING points`; + const points = await sql`UPDATE points SET points = points + 1 WHERE user_id=${userId} AND ability_id = ${abilityId} AND group_id = ${callbackQuery.message!.chat.id} RETURNING points`; users.push({ id: callbackQuery.from.id, - username: callbackQuery.from.username + username: callbackQuery.from.username, + first_name: callbackQuery.from.first_name }); await sql`UPDATE messages SET users = ${JSON.stringify(users)} WHERE message_id = ${callbackQuery.message!.message_id} AND chat_id = ${callbackQuery.message!.chat.id}`; let messageContent = callbackQuery.message!.text!.split('\n')[0]; - messageContent += `\nThey now have ${points[0].points} points\n\nAdded by: ${users.map(x => `@${x.username}`).join(', ')}`; + const userNames = users.map(u => u.username ? `@${u.username}` : (u.first_name || `User ${u.id}`)).join(', '); + messageContent += `\nThey now have ${points[0].points} points\n\nAdded by: ${userNames}`; await bot.editMessageText(messageContent, { message_id: callbackQuery.message!.message_id, @@ -358,12 +463,19 @@ bot.on('callback_query', async (callbackQuery) => { let leaderboard = ''; - for (const point of points) { - try { - const info = await bot.getChatMember(callbackQuery.message!.chat.id, point.user_id); - leaderboard += `@${info.user.username}: ${point.points}\n`; - } catch { - leaderboard += `${point.user_id}: ${point.points}\n`; + if (points.length === 0) { + leaderboard = 'No points yet for this ability'; + } else { + for (const point of points) { + try { + const info = await bot.getChatMember(callbackQuery.message!.chat.id, point.user_id); + const userName = info.user.username + ? `@${info.user.username}` + : (info.user.first_name || `User ${info.user.id}`); + leaderboard += `${userName}: ${point.points}\n`; + } catch { + leaderboard += `${point.user_id}: ${point.points}\n`; + } } } @@ -419,4 +531,36 @@ bot.on('callback_query', async (callbackQuery) => { }); await bot.answerCallbackQuery(callbackQuery.id); } + } catch (error) { + console.error('Error in callback query handler:', error); + bot.answerCallbackQuery(callbackQuery.id, { + text: "🚨 An error occurred. Please try again.", + show_alert: true + }); + } +}); + +// Error handling +bot.on('polling_error', (error) => { + console.error('Polling error:', error); +}); + +// Bot ready notification +bot.on('polling', () => { + console.log('βœ… Bot is running and polling for updates'); +}); + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log('Shutting down gracefully...'); + await bot.stopPolling(); + await sql.end(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('Shutting down gracefully...'); + await bot.stopPolling(); + await sql.end(); + process.exit(0); }); \ No newline at end of file