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