Spaces:
Paused
Paused
| 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<T>(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<BookingResult> { | |
| 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<number, BookingState> | |
| ) { | |
| 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<WebhookAccount>('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<number> { | |
| 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; | |
| } | |
| } |