import { BotContext } from "../types/botTypes"; import { messageManager } from "../utils/messageManager"; import { WeBookBooking } from '../../webook/book'; import { WeBookLogin } from '../../webook/login'; import { fetchDataFromTable } from '../../db/supabaseHelper'; import { Markup } from "telegraf"; export interface WebhookAccount { id: string; email: string; password: string; } export interface BookingState { eventUrl: string; fixedUrl?: string; tickets: number; step: number; lastBotMessageId?: number; } export interface BookingResult { success: boolean; tickets?: number; account: string; paymentUrl?: string; error?: string; } export function chunkAccounts(accounts: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < accounts.length; i += chunkSize) { chunks.push(accounts.slice(i, i + chunkSize)); } return chunks; } export async function bookTicketsForAccount( ctx: BotContext, account: WebhookAccount, eventUrl: string, withPayment: boolean, ticketsObtained: number, ticketsNeeded: number, isLogin: boolean = false ): Promise { let login: WeBookLogin | undefined; try { if (isLogin) { await ctx.reply(messageManager.getMessage('webook_logging_in').replace('{email}', account.email), { parse_mode: 'HTML' }); login = new WeBookLogin(account.email, account.password, `${account.id}`); const page = await login.login(); if (!page || typeof page !== 'object') { await ctx.reply(messageManager.getMessage('webook_login_failed').replace('{email}', account.email), { parse_mode: 'HTML' }); return { success: false, account: account.email }; } await ctx.reply(messageManager.getMessage('webook_attempting_booking').replace('{email}', account.email), { parse_mode: 'HTML' }); const booking = new WeBookBooking(page); const bookingResult = await booking.bookEvent(eventUrl, withPayment); if (withPayment) { if (typeof bookingResult === 'number') { const bookedCount = bookingResult; if (bookedCount > 0) { await ctx.reply(messageManager.getMessage('webook_booking_success_auto') .replace('{tickets}', bookedCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + bookedCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' }); return { success: true, tickets: bookedCount, account: account.email }; } else { await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' }); return { success: false, account: account.email }; } } } else { if (typeof bookingResult === 'object' && bookingResult !== null && 'ticketsCount' in bookingResult) { const result = bookingResult as { ticketsCount: number, paymentUrl?: string }; if (result.ticketsCount > 0) { if (result.paymentUrl) { await ctx.reply( messageManager.getMessage('webook_booking_success_manual_with_payment') .replace('{tickets}', result.ticketsCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML', ...Markup.inlineKeyboard([ [Markup.button.url('Complete Payment', result.paymentUrl)], [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')] ]) } ); } await ctx.reply(messageManager.getMessage('webook_booking_success_manual') .replace('{tickets}', result.ticketsCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' }); return { success: true, tickets: result.ticketsCount, account: account.email, paymentUrl: result.paymentUrl }; } else { await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' }); return { success: false, account: account.email }; } } } return { success: false, account: account.email }; } else { // No login, use direct booking await ctx.reply(messageManager.getMessage('webook_attempting_booking').replace('{email}', account.email), { parse_mode: 'HTML' }); const bookingResult = await WeBookBooking.bookDirect(eventUrl, `${account.id}`, withPayment); if (withPayment) { if (typeof bookingResult === 'number') { const bookedCount = bookingResult; if (bookedCount > 0) { await ctx.reply(messageManager.getMessage('webook_booking_success_auto') .replace('{tickets}', bookedCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + bookedCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' }); return { success: true, tickets: bookedCount, account: account.email }; } else { await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' }); return { success: false, account: account.email }; } } } else { if (typeof bookingResult === 'object' && bookingResult !== null && 'ticketsCount' in bookingResult) { const result = bookingResult as { ticketsCount: number, paymentUrl?: string }; if (result.ticketsCount > 0) { if (result.paymentUrl) { await ctx.reply( messageManager.getMessage('webook_booking_success_manual_with_payment') .replace('{tickets}', result.ticketsCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML', ...Markup.inlineKeyboard([ [Markup.button.url('Complete Payment', result.paymentUrl)], [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')] ]) } ); } await ctx.reply(messageManager.getMessage('webook_booking_success_manual') .replace('{tickets}', result.ticketsCount.toString()) .replace('{email}', account.email) .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString()) .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' }); return { success: true, tickets: result.ticketsCount, account: account.email, paymentUrl: result.paymentUrl }; } else { await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' }); return { success: false, account: account.email }; } } } return { success: false, account: account.email }; } } catch (e: any) { if (e.message && e.message.includes('Tickets sold out')) { await ctx.reply(messageManager.getMessage('webook_tickets_sold_out').replace('{email}', account.email), { parse_mode: 'HTML' }); } else { await ctx.reply(messageManager.getMessage('webook_booking_error').replace('{email}', account.email).replace('{error}', e.message), { parse_mode: 'HTML' }); } return { success: false, account: account.email, error: e.message }; } finally { if (login) { await login.close(); } } } export async function sendBookingSummary( ctx: BotContext, state: BookingState, paymentType: string, availableAccounts: number, maxConcurrent: number ) { await ctx.reply( messageManager.getMessage('webook_starting_process') .replace('{event_url}', state.eventUrl) .replace('{fixed_url}', state.fixedUrl || state.eventUrl) .replace('{tickets_requested}', state.tickets.toString()) .replace('{payment_type}', paymentType) .replace('{available_accounts}', availableAccounts.toString()) .replace('{max_concurrent}', maxConcurrent.toString()), { parse_mode: 'HTML' } ); } export async function orchestrateBookingProcess( ctx: BotContext, state: BookingState, withPayment: boolean, bookByUrlStates: Map ) { const telegramId = ctx.from?.id; if (!telegramId) return; let ticketsObtained = 0; const paymentUrls: string[] = []; const { eventUrl, tickets: ticketsNeeded } = state; const { data: accounts, error } = await fetchDataFromTable('account_webhook', 100, 0, { is_active: true }); if (error || !accounts) { await ctx.reply(messageManager.getMessage('webook_error_fetching_accounts'), { parse_mode: 'HTML' }); bookByUrlStates.delete(telegramId); return; } const availableAccounts = accounts.filter((a) => a.id && a.email && a.password); if (availableAccounts.length === 0) { await ctx.reply(messageManager.getMessage('webook_no_available_accounts'), { parse_mode: 'HTML' }); bookByUrlStates.delete(telegramId); return; } const paymentType = withPayment ? messageManager.getMessage('webook_automatic_payment') : messageManager.getMessage('webook_manual_payment'); const MAX_CONCURRENT_ACCOUNTS = 3; await sendBookingSummary(ctx, state, paymentType, availableAccounts.length, MAX_CONCURRENT_ACCOUNTS); const accountChunks = chunkAccounts(availableAccounts, MAX_CONCURRENT_ACCOUNTS); for (const accountChunk of accountChunks) { if (ticketsObtained >= ticketsNeeded) { await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' }); break; } const bookingResults = await Promise.all( accountChunk.map(account => ticketsObtained >= ticketsNeeded ? Promise.resolve(null) : bookTicketsForAccount(ctx, account, eventUrl, withPayment, ticketsObtained, ticketsNeeded) ) ); for (const result of bookingResults) { if (result && result.success && result.tickets) { ticketsObtained += result.tickets; if (!withPayment && result.paymentUrl) { paymentUrls.push(result.paymentUrl); } } } if (ticketsObtained >= ticketsNeeded) { await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' }); break; } } let finalMessage = messageManager.getMessage('webook_booking_completed') .replace('{event_url}', eventUrl) .replace('{fixed_url}', state.fixedUrl || eventUrl) .replace('{tickets_requested}', ticketsNeeded.toString()) .replace('{tickets_obtained}', ticketsObtained.toString()) .replace('{payment_type}', paymentType) .replace('{accounts_used}', availableAccounts.length.toString()) .replace('{max_concurrent}', MAX_CONCURRENT_ACCOUNTS.toString()); if (!withPayment && ticketsObtained > 0 && paymentUrls.length > 0) { finalMessage += messageManager.getMessage('webook_payment_buttons_sent'); await ctx.reply( finalMessage, { parse_mode: 'HTML', ...Markup.inlineKeyboard([ [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')] ]) } ); } else if (!withPayment && ticketsObtained > 0) { finalMessage += messageManager.getMessage('webook_manual_payment_required'); await ctx.reply(finalMessage, { parse_mode: 'HTML' }); } else { await ctx.reply(finalMessage, { parse_mode: 'HTML' }); } bookByUrlStates.delete(telegramId); } export function isValidWeBookEventUrl(url: string): boolean { try { const u = new URL(url); return u.hostname === 'webook.com' && /\/events\//.test(u.pathname); } catch { return false; } } export function fixWeBookEventUrl(url: string): string { try { const u = new URL(url); // Replace any language code in the path before /events/ with /en/ u.pathname = u.pathname.replace(/^\/(?:[a-zA-Z-]+)\/(events\/)/, '/en/$1'); // If /en/ is not present before /events/, insert it if (!/\/en\/events\//.test(u.pathname)) { u.pathname = u.pathname.replace(/\/events\//, '/en/events/'); } // Ensure the path ends with /book if (!/\/book$/.test(u.pathname)) { u.pathname = u.pathname.replace(/\/?$/, '/book'); } return u.toString(); } catch { // fallback to previous logic if URL parsing fails let fixed = url.replace(/\/[a-zA-Z-]+\/(events\/)/, '/en/$1'); if (!/\/en\//.test(fixed)) fixed = fixed.replace('/events/', '/en/events/'); if (!/\/book$/.test(fixed)) fixed = fixed.replace(/\/?$/, '/book'); return fixed; } } export async function getMaxTicketsAllowed(eventUrl: string): Promise { const email = process.env.WEBOOK_EMAIL || 'mfoud444@gmail.com'; const password = process.env.WEBOOK_PASSWORD || '009988Ppooii@@@@'; try { return 5; // Real implementation commented out for now // const login = new WeBookLogin(email, password); // const page = await login.login(); // if (!page) throw new Error('Login failed'); // const booking = new WeBookBooking(page); // await page.goto(eventUrl); // if (!(await booking.areTicketsAvailable())) { // if (page.context()) await page.context().close(); // return 0; // } // const clickCount = await booking.selectMaxTickets(); // if (page.context()) await page.context().close(); // return clickCount; } catch (e) { console.error('Error in getMaxTicketsAllowed:', e); return 5; } }