Mohammed Foud commited on
Commit
5cceda9
·
1 Parent(s): 480e618

Fix some Bugs and add some Feathers

Browse files
src/bots/handlers/webookHandlers.ts CHANGED
@@ -36,6 +36,8 @@ export const setupWeBookHandlers = (bot: any) => {
36
  bot.action(/^webook_page_(\d+)$/, callbackReplyHandler(handleWeBookPageAction));
37
  bot.action('webook_back_to_menu', callbackReplyHandler(handleWeBookBackToMenuAction));
38
  bot.action('webook_book_by_url', callbackReplyHandler(handleWeBookBookByUrlAction));
 
 
39
  };
40
 
41
  // Inject dependencies for filter handlers
@@ -82,6 +84,57 @@ export const handleWeBookBookByUrlAction = async (ctx: BotContext) => {
82
  return null;
83
  };
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  // Export a handler for booking-by-URL text input
87
  export async function handleBookByUrlText(ctx: BotContext) {
@@ -287,8 +340,9 @@ export async function handleWeBookEventInfoByUrl(ctx: BotContext) {
287
  info += `<b>Title:</b> ${event.title || 'None'}\n`;
288
  // info += `<b>Subtitle:</b> ${event.subtitle || 'None'}\n`;
289
  info += `<b>ID:</b> ${event.id || 'None'}\n`;
290
- // info += `<b>Slug:</b> ${event.slug || 'None'}\n`;
291
- // info += `<b>Ticketing URL Slug:</b> ${event.ticketingUrlSlug || 'None'}\n`;
 
292
  info += `<b>Starting Price:</b> ${event.startingPrice || 'None'} ${event.currencyCode || ''}\n`;
293
 
294
  info += `<b>Is Streaming Event:</b> ${event.isStreamingEvent ? 'Yes' : 'No'}\n`;
@@ -404,6 +458,144 @@ export async function handleWeBookEventInfoByUrl(ctx: BotContext) {
404
  await ctx.reply(info, { parse_mode: 'HTML' });
405
  }
406
  await ctx.deleteMessage(loadingMsg.message_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  return event;
408
  }
409
 
 
36
  bot.action(/^webook_page_(\d+)$/, callbackReplyHandler(handleWeBookPageAction));
37
  bot.action('webook_back_to_menu', callbackReplyHandler(handleWeBookBackToMenuAction));
38
  bot.action('webook_book_by_url', callbackReplyHandler(handleWeBookBookByUrlAction));
39
+ bot.action(/^ticket_select_(.+)_(.+)$/, callbackReplyHandler(handleTicketSelectionAction));
40
+ bot.action('view_event_details', callbackReplyHandler(handleViewEventDetailsAction));
41
  };
42
 
43
  // Inject dependencies for filter handlers
 
84
  return null;
85
  };
86
 
87
+ export const handleTicketSelectionAction = async (ctx: BotContext) => {
88
+ const telegramId = ctx.from?.id;
89
+ if (!telegramId) return { message: messageManager.getMessage('error_user_not_found'), options: getBackToMainMenuButton() };
90
+
91
+ // Extract ticket ID and event ID from callback data
92
+ const callbackData = (ctx.callbackQuery as any)?.data;
93
+ if (!callbackData) return { message: 'Invalid ticket selection', options: getBackToMainMenuButton() };
94
+
95
+ const match = callbackData.match(/^ticket_select_(.+)_(.+)$/);
96
+ if (!match) return { message: 'Invalid ticket selection format', options: getBackToMainMenuButton() };
97
+
98
+ const ticketId = match[1];
99
+ const eventId = match[2];
100
+
101
+ // Delete the previous message with buttons
102
+ try {
103
+ await ctx.deleteMessage();
104
+ } catch (e) {
105
+ console.log('Could not delete previous message:', e);
106
+ }
107
+
108
+ // Initialize booking state for this ticket
109
+ bookByUrlStates.set(telegramId, {
110
+ step: 2,
111
+ eventUrl: `https://api.webook.com/api/v2/event-detail/${eventId}`,
112
+ tickets: 1,
113
+ lastBotMessageId: undefined
114
+ });
115
+
116
+ return {
117
+ message: `You selected a ticket. How many tickets would you like to purchase? (1-5)`,
118
+ options: getBackToMainMenuButton()
119
+ };
120
+ };
121
+
122
+ export const handleViewEventDetailsAction = async (ctx: BotContext) => {
123
+ const telegramId = ctx.from?.id;
124
+ if (!telegramId) return { message: messageManager.getMessage('error_user_not_found'), options: getBackToMainMenuButton() };
125
+
126
+ // Delete the previous message with buttons
127
+ try {
128
+ await ctx.deleteMessage();
129
+ } catch (e) {
130
+ console.log('Could not delete previous message:', e);
131
+ }
132
+
133
+ return {
134
+ message: `📋 <b>Event Details</b>\n\nThis feature will show comprehensive event information including:\n• Full event description\n• Detailed schedule\n• Venue information\n• Terms and conditions\n• Contact information\n\nPlease use the main menu to access this feature.`,
135
+ options: getBackToMainMenuButton()
136
+ };
137
+ };
138
 
139
  // Export a handler for booking-by-URL text input
140
  export async function handleBookByUrlText(ctx: BotContext) {
 
340
  info += `<b>Title:</b> ${event.title || 'None'}\n`;
341
  // info += `<b>Subtitle:</b> ${event.subtitle || 'None'}\n`;
342
  info += `<b>ID:</b> ${event.id || 'None'}\n`;
343
+ info += `<b>Slug:</b> ${event.slug || 'None'}\n`;
344
+
345
+ info += `<b>Ticketing URL Slug:</b> ${event.ticketingUrlSlug || 'None'}\n`;
346
  info += `<b>Starting Price:</b> ${event.startingPrice || 'None'} ${event.currencyCode || ''}\n`;
347
 
348
  info += `<b>Is Streaming Event:</b> ${event.isStreamingEvent ? 'Yes' : 'No'}\n`;
 
458
  await ctx.reply(info, { parse_mode: 'HTML' });
459
  }
460
  await ctx.deleteMessage(loadingMsg.message_id);
461
+
462
+ // Call REST API using ticketingUrlSlug if available
463
+ if (event.ticketingUrlSlug) {
464
+ try {
465
+ console.log("Attempting to fetch REST API data for ticketingUrlSlug:", event.ticketingUrlSlug);
466
+ // Call REST API directly using ticketingUrlSlug
467
+ const restEvent = event.ticketingUrlSlug
468
+ ? await (await import('axios')).default.get(
469
+ `https://api.webook.com/api/v2/event-detail/${event.ticketingUrlSlug}-tickets?lang=en&visible_in=rs`,
470
+ {
471
+ headers: {
472
+ "Accept": "*/*",
473
+ "Accept-Encoding": "gzip, deflate, br, zstd",
474
+ "Accept-Language": "en-US,en;q=0.9,ar;q=0.8",
475
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
476
+ "Origin": "https://webook.com",
477
+ "Referer": "https://webook.com/",
478
+ "Sec-Fetch-Dest": "empty",
479
+ "Sec-Fetch-Mode": "cors",
480
+ "Sec-Fetch-Site": "same-site",
481
+ "Cache-Control": "no-cache",
482
+ "Pragma": "no-cache"
483
+ },
484
+ timeout: 10000
485
+ }
486
+ ).then(res => res.data)
487
+ : null;
488
+ console.log("restEvent", restEvent);
489
+
490
+ if (restEvent) {
491
+ // Format event info from REST API response
492
+ let restInfo = `<b>🎫 Event Information (REST API)</b>\n\n`;
493
+
494
+
495
+ // Additional event details
496
+ restInfo += `<b>❌ Is Sold Out:</b> ${restEvent.is_soldout ? 'Yes' : 'No'}\n`;
497
+
498
+ restInfo += `<b>🎫 Event Order Limit:</b> ${restEvent.event_order_limit || 'None'}\n\n`;
499
+
500
+ // Create ticket buttons
501
+ const ticketButtons: any[] = [];
502
+
503
+ if (restEvent.event_tickets && Array.isArray(restEvent.event_tickets)) {
504
+ restInfo += `<b>🎟️ Available Tickets:</b>\n`;
505
+
506
+ // Group tickets by availability and create buttons
507
+ const availableTickets = restEvent.event_tickets.filter((ticket: any) => !ticket.sold_out && ticket.remaining > 0);
508
+ const soldOutTickets = restEvent.event_tickets.filter((ticket: any) => ticket.sold_out || ticket.remaining <= 0);
509
+
510
+
511
+
512
+ // Add available tickets as buttons (limit text length for Telegram)
513
+ availableTickets.forEach((ticket: any, index: number) => {
514
+ const buttonText = `${ticket.title} - ${ticket.price} ${ticket.currency}`;
515
+ const callbackData = `ticket_select_${ticket._id}_${restEvent._id}`;
516
+ ticketButtons.push([Markup.button.callback(buttonText, callbackData)]);
517
+
518
+ // Add ticket details to the message text
519
+ restInfo += `• ${ticket.title}: ${ticket.price} ${ticket.currency} (${ticket.remaining} available)\n`;
520
+ });
521
+
522
+ // Add sold out tickets as text
523
+ if (soldOutTickets.length > 0) {
524
+ restInfo += `\n<b>❌ Sold Out Tickets:</b>\n`;
525
+ soldOutTickets.forEach((ticket: any) => {
526
+ restInfo += `• ${ticket.title} - ${ticket.price} ${ticket.currency} (Sold Out)\n`;
527
+ });
528
+ }
529
+ }
530
+
531
+ // Add navigation buttons
532
+ ticketButtons.push([
533
+ Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu'),
534
+ Markup.button.callback('📋 View All Details', 'view_event_details')
535
+ ]);
536
+
537
+ // Send image if available, else send info as text
538
+ if (restEvent.poster) {
539
+ await ctx.replyWithPhoto(
540
+ { url: restEvent.poster },
541
+ {
542
+ caption: restInfo,
543
+ parse_mode: 'HTML',
544
+ ...Markup.inlineKeyboard(ticketButtons)
545
+ }
546
+ );
547
+ } else if (restEvent.mobile_poster) {
548
+ await ctx.replyWithPhoto(
549
+ { url: restEvent.mobile_poster },
550
+ {
551
+ caption: restInfo,
552
+ parse_mode: 'HTML',
553
+ ...Markup.inlineKeyboard(ticketButtons)
554
+ }
555
+ );
556
+ } else {
557
+ await ctx.reply(
558
+ restInfo,
559
+ {
560
+ parse_mode: 'HTML',
561
+ ...Markup.inlineKeyboard(ticketButtons)
562
+ }
563
+ );
564
+ }
565
+ } else {
566
+ // REST API returned null or failed, send a fallback message
567
+ console.log("REST API returned null, sending fallback message");
568
+ await ctx.reply(
569
+ `🎫 <b>Event Information</b>\n\n` +
570
+ `📋 <b>Title:</b> ${event.title || 'None'}\n` +
571
+ `🔗 <b>Slug:</b> ${event.slug || 'None'}\n` +
572
+ `🎫 <b>Ticketing URL Slug:</b> ${event.ticketingUrlSlug || 'None'}\n` +
573
+ `💰 <b>Starting Price:</b> ${event.startingPrice || 'None'} ${event.currencyCode || ''}\n\n` +
574
+ `ℹ️ <i>Detailed ticket information is currently unavailable. Please visit the event page for more details.</i>`,
575
+ {
576
+ parse_mode: 'HTML',
577
+ ...getBackToMainMenuButton()
578
+ }
579
+ );
580
+ }
581
+ } catch (error) {
582
+ console.error('Error calling REST API with ticketingUrlSlug:', error);
583
+ // Send a fallback message when REST API fails
584
+ await ctx.reply(
585
+ `🎫 <b>Event Information</b>\n\n` +
586
+ `📋 <b>Title:</b> ${event.title || 'None'}\n` +
587
+ `🔗 <b>Slug:</b> ${event.slug || 'None'}\n` +
588
+ `🎫 <b>Ticketing URL Slug:</b> ${event.ticketingUrlSlug || 'None'}\n` +
589
+ `💰 <b>Starting Price:</b> ${event.startingPrice || 'None'} ${event.currencyCode || ''}\n\n` +
590
+ `ℹ️ <i>Detailed ticket information is currently unavailable. Please visit the event page for more details.</i>`,
591
+ {
592
+ parse_mode: 'HTML',
593
+ ...getBackToMainMenuButton()
594
+ }
595
+ );
596
+ }
597
+ }
598
+
599
  return event;
600
  }
601
 
src/bots/services/WeBookBookingService.ts CHANGED
@@ -42,8 +42,9 @@ export async function bookTicketsForAccount(
42
  withPayment: boolean,
43
  ticketsObtained: number,
44
  ticketsNeeded: number,
45
- isLogin: boolean = true
46
  ): Promise<BookingResult> {
 
47
  let login: WeBookLogin | undefined;
48
  try {
49
  if (isLogin) {
 
42
  withPayment: boolean,
43
  ticketsObtained: number,
44
  ticketsNeeded: number,
45
+ isLogin: boolean = false
46
  ): Promise<BookingResult> {
47
+
48
  let login: WeBookLogin | undefined;
49
  try {
50
  if (isLogin) {
src/index.ts CHANGED
@@ -42,4 +42,4 @@ startServer();
42
 
43
 
44
 
45
- // handleAddTelegrafBot("1e186256-fdd6-453f-8344-fd824660b8bd")
 
42
 
43
 
44
 
45
+ handleAddTelegrafBot("1e186256-fdd6-453f-8344-fd824660b8bd")
src/webook/book.ts CHANGED
@@ -41,8 +41,8 @@ export class WeBookBooking extends WeBookBase {
41
  if (!(await this.areTicketsAvailable())) {
42
  return withPayment ? 0 : { ticketsCount: 0, paymentUrl: undefined };
43
  }
44
-
45
- const ticketsCount = await this.selectMaxTickets();
46
  await this.proceedToSummary();
47
  await this.skipToPaymentIfAvailable();
48
  await this.acceptTermsIfAvailable();
@@ -111,6 +111,50 @@ export class WeBookBooking extends WeBookBase {
111
  return clickCount ;
112
  }
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  private async proceedToSummary() {
115
  const summaryButton = this.page.locator('[data-testid="ticketing_tickets_go_to_summary_button"]');
116
  await summaryButton.waitFor({ state: 'visible', timeout: 5000 });
 
41
  if (!(await this.areTicketsAvailable())) {
42
  return withPayment ? 0 : { ticketsCount: 0, paymentUrl: undefined };
43
  }
44
+ // const ticketsCount = await this.selectMaxTickets();
45
+ const ticketsCount = await this.selectMaxTicketsSmart();
46
  await this.proceedToSummary();
47
  await this.skipToPaymentIfAvailable();
48
  await this.acceptTermsIfAvailable();
 
111
  return clickCount ;
112
  }
113
 
114
+ /**
115
+ * Smart ticket selection: chooses seats from seat chart if present, otherwise uses plus button logic
116
+ * @param maxSeats number (default 10)
117
+ */
118
+ public async selectMaxTicketsSmart(maxSeats: number = 10): Promise<number> {
119
+ if (!this.page) throw new Error('Page not initialized');
120
+
121
+ // Check for seat chart
122
+ const seatChart = this.page.locator('[data-testid="seat-chart"]');
123
+ if (await seatChart.count() > 0) {
124
+ // Try to select seats from the chart
125
+ try {
126
+ // Wait for iframe inside seat chart
127
+ const iframeElement = seatChart.locator('iframe');
128
+ await iframeElement.waitFor({ state: 'attached', timeout: 10000 });
129
+ const frame = await iframeElement.first().contentFrame();
130
+ if (!frame) throw new Error('Could not get seat chart iframe content');
131
+
132
+ // Find available seats (update selector as needed)
133
+ const availableSeats = await frame.locator('.seatsio-seat.available, .seat.available').elementHandles();
134
+ let selected = 0;
135
+ for (const seat of availableSeats) {
136
+ if (selected >= maxSeats) break;
137
+ try {
138
+ await seat.click();
139
+ selected++;
140
+ await this.page.waitForTimeout(200);
141
+ } catch (e) {
142
+ continue;
143
+ }
144
+ }
145
+ console.info(`Selected ${selected} seats from seat chart`);
146
+ return selected;
147
+ } catch (e) {
148
+ console.warn('Failed to select from seat chart, falling back to plus button:', e);
149
+ // Fallback to plus button logic
150
+ return await this.selectMaxTickets();
151
+ }
152
+ } else {
153
+ // No seat chart, use plus button logic
154
+ return await this.selectMaxTickets();
155
+ }
156
+ }
157
+
158
  private async proceedToSummary() {
159
  const summaryButton = this.page.locator('[data-testid="ticketing_tickets_go_to_summary_button"]');
160
  await summaryButton.waitFor({ state: 'visible', timeout: 5000 });
src/webook/webookApi.ts CHANGED
@@ -410,24 +410,68 @@ export async function getEventDetailsBySlug(
410
  visibleIn: string = 'rs'
411
  ): Promise<any | null> {
412
  try {
413
- const url = `https://api.webook.com/api/v2/event-detail/${slug}?lang=${lang}&visible_in=${visibleIn}`;
 
414
 
415
- const response = await axios.get(url, {
 
 
416
  headers: {
417
  "Accept": "*/*",
418
  "Accept-Encoding": "gzip, deflate, br, zstd",
419
  "Accept-Language": "en-US,en;q=0.9,ar;q=0.8",
420
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
421
- }
 
 
 
 
 
 
 
 
422
  });
423
 
424
- if (response.data && response.data.status === 'success' && response.data.data) {
425
- return response.data.data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  }
427
 
428
  return null;
429
  } catch (error) {
430
  console.error('Error fetching event details by slug:', error);
 
 
 
 
 
 
431
  return null;
432
  }
433
  }
 
410
  visibleIn: string = 'rs'
411
  ): Promise<any | null> {
412
  try {
413
+ // Try the public CDN endpoint first
414
+ const cdnUrl = `https://cdn.webook.com/api/v2/event-detail/${slug}?lang=${lang}&visible_in=${visibleIn}`;
415
 
416
+ console.log('Trying CDN endpoint:', cdnUrl);
417
+
418
+ const cdnResponse = await axios.get(cdnUrl, {
419
  headers: {
420
  "Accept": "*/*",
421
  "Accept-Encoding": "gzip, deflate, br, zstd",
422
  "Accept-Language": "en-US,en;q=0.9,ar;q=0.8",
423
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
424
+ "Origin": "https://webook.com",
425
+ "Referer": "https://webook.com/",
426
+ "Sec-Fetch-Dest": "empty",
427
+ "Sec-Fetch-Mode": "cors",
428
+ "Sec-Fetch-Site": "same-site",
429
+ "Cache-Control": "no-cache",
430
+ "Pragma": "no-cache"
431
+ },
432
+ timeout: 10000
433
  });
434
 
435
+ if (cdnResponse.data && cdnResponse.data.status === 'success' && cdnResponse.data.data) {
436
+ console.log('CDN endpoint successful');
437
+ return cdnResponse.data.data;
438
+ }
439
+
440
+ // If CDN fails, try the API endpoint
441
+ const apiUrl = `https://api.webook.com/api/v2/event-detail/${slug}?lang=${lang}&visible_in=${visibleIn}`;
442
+ console.log('Trying API endpoint:', apiUrl);
443
+
444
+ const apiResponse = await axios.get(apiUrl, {
445
+ headers: {
446
+ "Accept": "*/*",
447
+ "Accept-Encoding": "gzip, deflate, br, zstd",
448
+ "Accept-Language": "en-US,en;q=0.9,ar;q=0.8",
449
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
450
+ "Origin": "https://webook.com",
451
+ "Referer": "https://webook.com/",
452
+ "Sec-Fetch-Dest": "empty",
453
+ "Sec-Fetch-Mode": "cors",
454
+ "Sec-Fetch-Site": "same-site",
455
+ "Cache-Control": "no-cache",
456
+ "Pragma": "no-cache"
457
+ },
458
+ timeout: 10000
459
+ });
460
+
461
+ if (apiResponse.data && apiResponse.data.status === 'success' && apiResponse.data.data) {
462
+ console.log('API endpoint successful');
463
+ return apiResponse.data.data;
464
  }
465
 
466
  return null;
467
  } catch (error) {
468
  console.error('Error fetching event details by slug:', error);
469
+ // Log more details about the error
470
+ if (error.response) {
471
+ console.error('Response status:', error.response.status);
472
+ console.error('Response headers:', error.response.headers);
473
+ console.error('Response data:', error.response.data);
474
+ }
475
  return null;
476
  }
477
  }