feat: Add Telegram bot with group support

- Full Telegram bot implementation for Lightning Jackpot
- Commands: /start, /buy, /tickets, /wins, /address, /jackpot, /help
- Lightning invoice generation with QR codes
- Payment polling and confirmation notifications
- User state management (Redis/in-memory fallback)
- Group support with admin settings panel
- Configurable draw announcements and reminders
- Centralized messages for easy i18n
- Docker configuration included
This commit is contained in:
Michilis
2025-11-27 23:10:25 +00:00
parent d3bf8080b6
commit f743a6749c
29 changed files with 7616 additions and 1 deletions

View File

@@ -0,0 +1,341 @@
import TelegramBot from 'node-telegram-bot-api';
import { groupStateManager } from '../services/groupState';
import { logger, logUserAction } from '../services/logger';
import { messages } from '../messages';
/**
* Check if a user is an admin in a group
*/
async function isGroupAdmin(
bot: TelegramBot,
chatId: number,
userId: number
): Promise<boolean> {
try {
const member = await bot.getChatMember(chatId, userId);
return ['creator', 'administrator'].includes(member.status);
} catch (error) {
logger.error('Failed to check admin status', { error, chatId, userId });
return false;
}
}
/**
* Handle bot being added to a group
*/
export async function handleBotAddedToGroup(
bot: TelegramBot,
msg: TelegramBot.Message
): Promise<void> {
const chatId = msg.chat.id;
const chatTitle = msg.chat.title || 'Unknown Group';
const addedBy = msg.from?.id || 0;
logger.info('Bot added to group', { chatId, chatTitle, addedBy });
try {
const settings = await groupStateManager.registerGroup(chatId, chatTitle, addedBy);
await bot.sendMessage(chatId, messages.groups.welcome(chatTitle), {
parse_mode: 'Markdown',
});
} catch (error) {
logger.error('Failed to register group', { error, chatId });
}
}
/**
* Handle bot being removed from a group
*/
export async function handleBotRemovedFromGroup(
bot: TelegramBot,
msg: TelegramBot.Message
): Promise<void> {
const chatId = msg.chat.id;
logger.info('Bot removed from group', { chatId });
try {
await groupStateManager.removeGroup(chatId);
} catch (error) {
logger.error('Failed to remove group', { error, chatId });
}
}
/**
* Handle /settings command (group admin only)
*/
export async function handleGroupSettings(
bot: TelegramBot,
msg: TelegramBot.Message
): Promise<void> {
const chatId = msg.chat.id;
const userId = msg.from?.id;
// Only works in groups
if (msg.chat.type === 'private') {
await bot.sendMessage(chatId, messages.groups.privateChat);
return;
}
if (!userId) return;
// Check if user is admin
const isAdmin = await isGroupAdmin(bot, chatId, userId);
if (!isAdmin) {
await bot.sendMessage(chatId, messages.groups.adminOnly);
return;
}
logUserAction(userId, 'Viewed group settings', { groupId: chatId });
try {
const settings = await groupStateManager.getGroup(chatId);
if (!settings) {
// Register group if not found
await groupStateManager.registerGroup(
chatId,
msg.chat.title || 'Group',
userId
);
}
const currentSettings = settings || await groupStateManager.getGroup(chatId);
if (!currentSettings) {
await bot.sendMessage(chatId, messages.errors.generic);
return;
}
await bot.sendMessage(
chatId,
messages.groups.settingsOverview(currentSettings),
{
parse_mode: 'Markdown',
reply_markup: getGroupSettingsKeyboard(currentSettings),
}
);
} catch (error) {
logger.error('Error in handleGroupSettings', { error, chatId });
await bot.sendMessage(chatId, messages.errors.generic);
}
}
/**
* Handle group settings toggle callback
*/
export async function handleGroupSettingsCallback(
bot: TelegramBot,
query: TelegramBot.CallbackQuery,
action: string
): Promise<void> {
const chatId = query.message?.chat.id;
const userId = query.from.id;
const messageId = query.message?.message_id;
if (!chatId || !messageId) return;
// Check if user is admin
const isAdmin = await isGroupAdmin(bot, chatId, userId);
if (!isAdmin) {
await bot.answerCallbackQuery(query.id, {
text: messages.groups.adminOnly,
show_alert: true,
});
return;
}
try {
let setting: 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed';
switch (action) {
case 'toggle_enabled':
setting = 'enabled';
break;
case 'toggle_announcements':
setting = 'drawAnnouncements';
break;
case 'toggle_reminders':
setting = 'reminders';
break;
case 'toggle_purchases':
setting = 'ticketPurchaseAllowed';
break;
default:
await bot.answerCallbackQuery(query.id);
return;
}
const currentSettings = await groupStateManager.getGroup(chatId);
if (!currentSettings) {
await bot.answerCallbackQuery(query.id, { text: 'Group not found' });
return;
}
const newValue = !currentSettings[setting];
const updatedSettings = await groupStateManager.updateSetting(chatId, setting, newValue);
if (!updatedSettings) {
await bot.answerCallbackQuery(query.id, { text: 'Failed to update' });
return;
}
logUserAction(userId, 'Updated group setting', {
groupId: chatId,
setting,
newValue,
});
await bot.answerCallbackQuery(query.id, {
text: `${setting} ${newValue ? 'enabled' : 'disabled'}`,
});
// Update the message with new settings
await bot.editMessageText(
messages.groups.settingsOverview(updatedSettings),
{
chat_id: chatId,
message_id: messageId,
parse_mode: 'Markdown',
reply_markup: getGroupSettingsKeyboard(updatedSettings),
}
);
} catch (error) {
logger.error('Error in handleGroupSettingsCallback', { error, chatId, action });
await bot.answerCallbackQuery(query.id, { text: 'Error updating settings' });
}
}
/**
* Generate inline keyboard for group settings
*/
function getGroupSettingsKeyboard(settings: {
enabled: boolean;
drawAnnouncements: boolean;
reminders: boolean;
ticketPurchaseAllowed: boolean;
}): TelegramBot.InlineKeyboardMarkup {
const onOff = (val: boolean) => val ? '✅' : '❌';
return {
inline_keyboard: [
[{
text: `${onOff(settings.enabled)} Bot Enabled`,
callback_data: 'group_toggle_enabled',
}],
[{
text: `${onOff(settings.drawAnnouncements)} Draw Announcements`,
callback_data: 'group_toggle_announcements',
}],
[{
text: `${onOff(settings.reminders)} Draw Reminders`,
callback_data: 'group_toggle_reminders',
}],
[{
text: `${onOff(settings.ticketPurchaseAllowed)} Allow Ticket Purchases`,
callback_data: 'group_toggle_purchases',
}],
[{
text: '🔄 Refresh',
callback_data: 'group_refresh',
}],
],
};
}
/**
* Handle refresh callback
*/
export async function handleGroupRefresh(
bot: TelegramBot,
query: TelegramBot.CallbackQuery
): Promise<void> {
const chatId = query.message?.chat.id;
const messageId = query.message?.message_id;
if (!chatId || !messageId) return;
await bot.answerCallbackQuery(query.id, { text: 'Refreshed!' });
const settings = await groupStateManager.getGroup(chatId);
if (!settings) return;
await bot.editMessageText(
messages.groups.settingsOverview(settings),
{
chat_id: chatId,
message_id: messageId,
parse_mode: 'Markdown',
reply_markup: getGroupSettingsKeyboard(settings),
}
);
}
/**
* Send draw announcement to all enabled groups
*/
export async function broadcastDrawAnnouncement(
bot: TelegramBot,
announcement: string
): Promise<number> {
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
let sent = 0;
for (const group of groups) {
try {
await bot.sendMessage(group.groupId, announcement, { parse_mode: 'Markdown' });
sent++;
} catch (error) {
logger.error('Failed to send announcement to group', {
groupId: group.groupId,
error,
});
// If bot was removed from group, clean up
if ((error as any)?.response?.statusCode === 403) {
await groupStateManager.removeGroup(group.groupId);
}
}
}
logger.info('Broadcast draw announcement', { sent, total: groups.length });
return sent;
}
/**
* Send draw reminder to all enabled groups
*/
export async function broadcastDrawReminder(
bot: TelegramBot,
reminder: string
): Promise<number> {
const groups = await groupStateManager.getGroupsWithFeature('reminders');
let sent = 0;
for (const group of groups) {
try {
await bot.sendMessage(group.groupId, reminder, { parse_mode: 'Markdown' });
sent++;
} catch (error) {
logger.error('Failed to send reminder to group', {
groupId: group.groupId,
error,
});
if ((error as any)?.response?.statusCode === 403) {
await groupStateManager.removeGroup(group.groupId);
}
}
}
logger.info('Broadcast draw reminder', { sent, total: groups.length });
return sent;
}
export default {
handleBotAddedToGroup,
handleBotRemovedFromGroup,
handleGroupSettings,
handleGroupSettingsCallback,
handleGroupRefresh,
broadcastDrawAnnouncement,
broadcastDrawReminder,
};