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:
341
telegram_bot/src/handlers/groups.ts
Normal file
341
telegram_bot/src/handlers/groups.ts
Normal 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user