Mohammed Foud commited on
Commit
472b2ed
·
1 Parent(s): e39b867

Fix some Bugs and add some Feathers

Browse files
d.sh CHANGED
@@ -1,5 +1,5 @@
1
  git add .
2
- git commit -m "all"
3
  git push
4
 
5
 
 
1
  git add .
2
+ git commit -m "Fix some Bugs and add some Feathers"
3
  git push
4
 
5
 
src/bots/botManager.ts CHANGED
@@ -72,8 +72,9 @@ export const initializeBot = async (botToken: string, botData?: BotData) => {
72
  // Set bot ID and load messages
73
  await messageManager.loadMessages();
74
 
 
75
  // Set language based on bot settings or default to Arabic
76
- messageManager.setLanguage(botData?.settings?.language || 'en');
77
 
78
  // Add bot data to context for use in handlers
79
  bot.context.botData = botData || null;
 
72
  // Set bot ID and load messages
73
  await messageManager.loadMessages();
74
 
75
+
76
  // Set language based on bot settings or default to Arabic
77
+ messageManager.setLanguage(botData?.settings?.language || 'ar');
78
 
79
  // Add bot data to context for use in handlers
80
  bot.context.botData = botData || null;
src/bots/handlers/index.ts CHANGED
@@ -34,4 +34,6 @@ export const setupCallbackHandlers = (bot: any) => {
34
  });
35
 
36
 
37
- };
 
 
 
34
  });
35
 
36
 
37
+ };
38
+
39
+ export { setupWeBookFiltersHandlers } from './webookFiltersHandlers';
src/bots/handlers/webookFiltersHandlers.ts ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { messageManager } from "../utils/messageManager";
3
+ import { getBackToMainMenuButton } from "../utils/keyboardUtils";
4
+ import { EventFilters } from '../../webook/webookApi';
5
+ import { createLogger } from '../../utils/logger';
6
+ import { callbackReplyHandler } from "../utils/handlerUtils";
7
+ import { Markup } from "telegraf";
8
+ import { getLoggedInMenuKeyboard } from "../utils/keyboardUtils";
9
+ import { webookAPI } from '../../webook/webookApi';
10
+
11
+ const logger = createLogger('WeBookFiltersHandlers');
12
+
13
+ // These will be injected or imported from the main handler file
14
+ export let userStates: Map<number, { page: number; filters: EventFilters; totalEvents: number }>;
15
+ export let EVENTS_PER_PAGE: number;
16
+
17
+ export function injectWeBookFilterDeps(deps: {
18
+ userStates: typeof userStates,
19
+ EVENTS_PER_PAGE: number
20
+ }) {
21
+ userStates = deps.userStates;
22
+ EVENTS_PER_PAGE = deps.EVENTS_PER_PAGE;
23
+ }
24
+
25
+ export const handleWeBookFilterAction = async (ctx: BotContext) => {
26
+ try {
27
+ const telegramId = ctx.from?.id;
28
+ if (!telegramId) {
29
+ return {
30
+ message: messageManager.getMessage('error_user_not_found'),
31
+ options: getBackToMainMenuButton()
32
+ };
33
+ }
34
+
35
+ const match = ctx.match;
36
+ if (!match) {
37
+ return {
38
+ message: messageManager.getMessage('error_invalid_filter'),
39
+ options: getBackToMainMenuButton()
40
+ };
41
+ }
42
+
43
+ const filterType = match[1];
44
+ const userState = userStates.get(telegramId);
45
+ if (!userState) {
46
+ return {
47
+ message: messageManager.getMessage('error_user_state_not_found'),
48
+ options: getBackToMainMenuButton()
49
+ };
50
+ }
51
+
52
+ // Apply filter based on type
53
+ switch (filterType) {
54
+ case 'today':
55
+ userState.filters.date_filter = 'today';
56
+ break;
57
+ case 'tomorrow':
58
+ userState.filters.date_filter = 'tomorrow';
59
+ break;
60
+ case 'this_week':
61
+ userState.filters.date_filter = 'this_week';
62
+ break;
63
+ case 'free':
64
+ userState.filters.price_max = 0;
65
+ break;
66
+ case 'paid':
67
+ userState.filters.price_min = 1;
68
+ break;
69
+ default:
70
+ // Handle category filters
71
+ if (filterType.startsWith('cat_')) {
72
+ const category = filterType.replace('cat_', '');
73
+ userState.filters.category = category;
74
+ }
75
+ }
76
+
77
+ userState.page = 0; // Reset to first page when applying filter
78
+ userStates.set(telegramId, userState);
79
+
80
+ return await fetchAndDisplayEvents(telegramId, 0);
81
+ } catch (error: any) {
82
+ return {
83
+ message: messageManager.getMessage('error_general'),
84
+ options: getBackToMainMenuButton()
85
+ };
86
+ }
87
+ };
88
+
89
+ export const handleWeBookClearFiltersAction = async (ctx: BotContext) => {
90
+ try {
91
+ const telegramId = ctx.from?.id;
92
+ if (!telegramId) {
93
+ return {
94
+ message: messageManager.getMessage('error_user_not_found'),
95
+ options: getBackToMainMenuButton()
96
+ };
97
+ }
98
+
99
+ // Reset user state to default
100
+ userStates.set(telegramId, {
101
+ page: 0,
102
+ filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
103
+ totalEvents: 0
104
+ });
105
+
106
+ return await fetchAndDisplayEvents(telegramId, 0);
107
+ } catch (error: any) {
108
+ return {
109
+ message: messageManager.getMessage('error_general'),
110
+ options: getBackToMainMenuButton()
111
+ };
112
+ }
113
+ };
114
+
115
+ export function getActiveFiltersText(filters: EventFilters): string {
116
+ const activeFilters = [];
117
+
118
+ if (filters.date_filter) {
119
+ const dateMap: Record<string, string> = {
120
+ 'today': 'Today',
121
+ 'tomorrow': 'Tomorrow',
122
+ 'this_week': 'This Week'
123
+ };
124
+ activeFilters.push(dateMap[filters.date_filter]);
125
+ }
126
+
127
+ if (filters.price_min !== undefined && filters.price_min > 0) {
128
+ activeFilters.push('Paid Events');
129
+ }
130
+
131
+ if (filters.price_max !== undefined && filters.price_max === 0) {
132
+ activeFilters.push('Free Events');
133
+ }
134
+
135
+ if (filters.category) {
136
+ const category = Array.isArray(filters.category) ? filters.category[0] : filters.category;
137
+ activeFilters.push(`Category: ${category}`);
138
+ }
139
+
140
+ return activeFilters.length > 0 ? activeFilters.join(', ') : '';
141
+ }
142
+
143
+ export function setupWeBookFiltersHandlers(bot: any) {
144
+ bot.action(/^webook_filter_(.+)$/, handleWeBookFilterAction);
145
+ bot.action('webook_clear_filters', handleWeBookClearFiltersAction);
146
+ }
147
+
148
+ export const handleWeBookEventsAction = async (ctx: BotContext) => {
149
+ try {
150
+ const telegramId = ctx.from?.id;
151
+ if (!telegramId) {
152
+ return {
153
+ message: messageManager.getMessage('error_user_not_found'),
154
+ options: getBackToMainMenuButton()
155
+ };
156
+ }
157
+
158
+ // Initialize user state
159
+ userStates.set(telegramId, {
160
+ page: 0,
161
+ filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
162
+ totalEvents: 0
163
+ });
164
+
165
+ return await fetchAndDisplayEvents(telegramId, 0);
166
+ } catch (error: any) {
167
+ logger.error(`Error in WeBook events action: ${error.message}`);
168
+ return {
169
+ message: messageManager.getMessage('error_general'),
170
+ options: getBackToMainMenuButton()
171
+ };
172
+ }
173
+ };
174
+
175
+ export const handleWeBookPageAction = async (ctx: BotContext) => {
176
+ try {
177
+ const telegramId = ctx.from?.id;
178
+ if (!telegramId) {
179
+ return {
180
+ message: messageManager.getMessage('error_user_not_found'),
181
+ options: getBackToMainMenuButton()
182
+ };
183
+ }
184
+
185
+ const match = ctx.match;
186
+ if (!match) {
187
+ return {
188
+ message: messageManager.getMessage('error_invalid_page'),
189
+ options: getBackToMainMenuButton()
190
+ };
191
+ }
192
+
193
+ const page = parseInt(match[1]);
194
+ return await fetchAndDisplayEvents(telegramId, page);
195
+ } catch (error: any) {
196
+ logger.error(`Error in WeBook page action: ${error.message}`);
197
+ return {
198
+ message: messageManager.getMessage('error_general'),
199
+ options: getBackToMainMenuButton()
200
+ };
201
+ }
202
+ };
203
+
204
+ export async function fetchAndDisplayEvents(telegramId: number, page: number) {
205
+ try {
206
+ const userState = userStates.get(telegramId);
207
+ if (!userState) {
208
+ return {
209
+ message: messageManager.getMessage('error_user_state_not_found'),
210
+ options: getBackToMainMenuButton()
211
+ };
212
+ }
213
+
214
+ // Update page and calculate skip
215
+ userState.page = page;
216
+ const skip = page * EVENTS_PER_PAGE;
217
+ const filters = { ...userState.filters, skip, limit: EVENTS_PER_PAGE };
218
+
219
+ // Fetch events from WeBook API
220
+ const response = await webookAPI.getEvents(filters);
221
+ const events = response.data.eventCollection.items;
222
+ const total = response.data.eventCollection.total;
223
+
224
+ userState.totalEvents = total;
225
+ userStates.set(telegramId, userState);
226
+
227
+ if (events.length === 0) {
228
+ return {
229
+ message: '🎭 No events found with the current filters.\n\nTry adjusting your search criteria or clearing filters.',
230
+ options: getWeBookEventsKeyboard(page, total, userState.filters)
231
+ };
232
+ }
233
+
234
+ // Build events message
235
+ let message = `🎭 <b>WeBook Events</b>\n\n`;
236
+ message += `📊 Showing ${skip + 1}-${Math.min(skip + events.length, total)} of ${total} events\n\n`;
237
+
238
+ // Add active filters info
239
+ const activeFilters = getActiveFiltersText(userState.filters);
240
+ if (activeFilters) {
241
+ message += `🔍 <b>Active Filters:</b> ${activeFilters}\n\n`;
242
+ }
243
+
244
+ // Display events
245
+ events.forEach((event: any, index: number) => {
246
+ const eventNumber = skip + index + 1;
247
+ message += `${eventNumber}. <b>${event.title || 'Untitled Event'}</b>\n`;
248
+
249
+ if (event.subtitle) {
250
+ message += ` ${event.subtitle}\n`;
251
+ }
252
+
253
+ if (event.schedule?.openDateTime) {
254
+ const eventDate = new Date(event.schedule.openDateTime).toLocaleDateString('en-US', {
255
+ weekday: 'short',
256
+ year: 'numeric',
257
+ month: 'short',
258
+ day: 'numeric'
259
+ });
260
+ message += ` 📅 ${eventDate}\n`;
261
+ }
262
+
263
+ if (event.location?.city) {
264
+ message += ` 📍 ${event.location.city}\n`;
265
+ }
266
+
267
+ if (event.startingPrice !== undefined) {
268
+ const priceText = event.startingPrice === 0 ? 'Free' : `${event.startingPrice} ${event.currencyCode || 'SAR'}`;
269
+ message += ` 💰 ${priceText}\n`;
270
+ }
271
+
272
+ if (event.zone?.title) {
273
+ message += ` 🎪 ${event.zone.title}\n`;
274
+ }
275
+
276
+ message += '\n';
277
+ });
278
+
279
+ return {
280
+ message,
281
+ options: getWeBookEventsKeyboard(page, total, userState.filters)
282
+ };
283
+ } catch (error: any) {
284
+ logger.error(`Error fetching WeBook events: ${error.message}`);
285
+ return {
286
+ message: '❌ Error fetching events. Please try again later.',
287
+ options: getBackToMainMenuButton()
288
+ };
289
+ }
290
+ }
291
+
292
+ export function getWeBookEventsKeyboard(page: number, total: number, filters: EventFilters) {
293
+ const buttons = [];
294
+ const totalPages = Math.ceil(total / EVENTS_PER_PAGE);
295
+
296
+ // Filter buttons
297
+ const filterRow = [];
298
+ filterRow.push(Markup.button.callback('📅 Today', 'webook_filter_today'));
299
+ filterRow.push(Markup.button.callback('📅 Tomorrow', 'webook_filter_tomorrow'));
300
+ filterRow.push(Markup.button.callback('📅 This Week', 'webook_filter_this_week'));
301
+ buttons.push(filterRow);
302
+
303
+ const priceRow = [];
304
+ priceRow.push(Markup.button.callback('🆓 Free', 'webook_filter_free'));
305
+ priceRow.push(Markup.button.callback('💰 Paid', 'webook_filter_paid'));
306
+ priceRow.push(Markup.button.callback('🧹 Clear Filters', 'webook_clear_filters'));
307
+ buttons.push(priceRow);
308
+
309
+ // Add new button for booking by URL
310
+ buttons.push([Markup.button.callback('🎟️ Book Tickets by Event URL', 'webook_book_by_url')]);
311
+
312
+ // Pagination buttons
313
+ const paginationRow = [];
314
+ if (page > 0) {
315
+ paginationRow.push(Markup.button.callback('⬅️ Previous', `webook_page_${page - 1}`));
316
+ }
317
+
318
+ paginationRow.push(Markup.button.callback(`📄 ${page + 1}/${totalPages}`, 'noop'));
319
+
320
+ if (page < totalPages - 1) {
321
+ paginationRow.push(Markup.button.callback('Next ➡️', `webook_page_${page + 1}`));
322
+ }
323
+
324
+ if (paginationRow.length > 0) {
325
+ buttons.push(paginationRow);
326
+ }
327
+
328
+ // Back button
329
+ buttons.push([Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]);
330
+
331
+ return Markup.inlineKeyboard(buttons);
332
+ }
src/bots/handlers/webookHandlers.ts CHANGED
@@ -5,14 +5,16 @@ import { messageManager } from "../utils/messageManager";
5
  import { webookAPI, EventFilters } from '../../webook/webookApi';
6
  import { Markup } from "telegraf";
7
  import { getBackToMainMenuButton, getLoggedInMenuKeyboard } from "../utils/keyboardUtils";
8
- import { WeBookBooking } from '../../webook/book';
9
- import { WeBookLogin } from '../../webook/login';
10
- import { fetchDataFromTable } from '../../db/supabaseHelper';
 
 
 
11
 
12
  const logger = createLogger('WeBookHandlers');
13
 
14
- // Store user states for pagination and filters
15
- const userStates = new Map<number, {
16
  page: number;
17
  filters: EventFilters;
18
  totalEvents: number;
@@ -32,159 +34,15 @@ const EVENTS_PER_PAGE = 5;
32
  export const setupWeBookHandlers = (bot: any) => {
33
  bot.action('webook_events', callbackReplyHandler(handleWeBookEventsAction));
34
  bot.action(/^webook_page_(\d+)$/, callbackReplyHandler(handleWeBookPageAction));
35
- bot.action(/^webook_filter_(.+)$/, callbackReplyHandler(handleWeBookFilterAction));
36
  bot.action('webook_back_to_menu', callbackReplyHandler(handleWeBookBackToMenuAction));
37
- bot.action('webook_clear_filters', callbackReplyHandler(handleWeBookClearFiltersAction));
38
  bot.action('webook_book_by_url', callbackReplyHandler(handleWeBookBookByUrlAction));
39
  };
40
 
41
- export const handleWeBookEventsAction = async (ctx: BotContext) => {
42
- try {
43
- const telegramId = ctx.from?.id;
44
- if (!telegramId) {
45
- return {
46
- message: messageManager.getMessage('error_user_not_found'),
47
- options: getBackToMainMenuButton()
48
- };
49
- }
50
-
51
- // Initialize user state
52
- userStates.set(telegramId, {
53
- page: 0,
54
- filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
55
- totalEvents: 0
56
- });
57
-
58
- return await fetchAndDisplayEvents(telegramId, 0);
59
- } catch (error: any) {
60
- logger.error(`Error in WeBook events action: ${error.message}`);
61
- return {
62
- message: messageManager.getMessage('error_general'),
63
- options: getBackToMainMenuButton()
64
- };
65
- }
66
- };
67
-
68
- export const handleWeBookPageAction = async (ctx: BotContext) => {
69
- try {
70
- const telegramId = ctx.from?.id;
71
- if (!telegramId) {
72
- return {
73
- message: messageManager.getMessage('error_user_not_found'),
74
- options: getBackToMainMenuButton()
75
- };
76
- }
77
-
78
- const match = ctx.match;
79
- if (!match) {
80
- return {
81
- message: messageManager.getMessage('error_invalid_page'),
82
- options: getBackToMainMenuButton()
83
- };
84
- }
85
-
86
- const page = parseInt(match[1]);
87
- return await fetchAndDisplayEvents(telegramId, page);
88
- } catch (error: any) {
89
- logger.error(`Error in WeBook page action: ${error.message}`);
90
- return {
91
- message: messageManager.getMessage('error_general'),
92
- options: getBackToMainMenuButton()
93
- };
94
- }
95
- };
96
-
97
- export const handleWeBookFilterAction = async (ctx: BotContext) => {
98
- try {
99
- const telegramId = ctx.from?.id;
100
- if (!telegramId) {
101
- return {
102
- message: messageManager.getMessage('error_user_not_found'),
103
- options: getBackToMainMenuButton()
104
- };
105
- }
106
-
107
- const match = ctx.match;
108
- if (!match) {
109
- return {
110
- message: messageManager.getMessage('error_invalid_filter'),
111
- options: getBackToMainMenuButton()
112
- };
113
- }
114
-
115
- const filterType = match[1];
116
- const userState = userStates.get(telegramId);
117
- if (!userState) {
118
- return {
119
- message: messageManager.getMessage('error_user_state_not_found'),
120
- options: getBackToMainMenuButton()
121
- };
122
- }
123
-
124
- // Apply filter based on type
125
- switch (filterType) {
126
- case 'today':
127
- userState.filters.date_filter = 'today';
128
- break;
129
- case 'tomorrow':
130
- userState.filters.date_filter = 'tomorrow';
131
- break;
132
- case 'this_week':
133
- userState.filters.date_filter = 'this_week';
134
- break;
135
- case 'free':
136
- userState.filters.price_max = 0;
137
- break;
138
- case 'paid':
139
- userState.filters.price_min = 1;
140
- break;
141
- default:
142
- // Handle category filters
143
- if (filterType.startsWith('cat_')) {
144
- const category = filterType.replace('cat_', '');
145
- userState.filters.category = category;
146
- }
147
- }
148
-
149
- userState.page = 0; // Reset to first page when applying filter
150
- userStates.set(telegramId, userState);
151
-
152
- return await fetchAndDisplayEvents(telegramId, 0);
153
- } catch (error: any) {
154
- logger.error(`Error in WeBook filter action: ${error.message}`);
155
- return {
156
- message: messageManager.getMessage('error_general'),
157
- options: getBackToMainMenuButton()
158
- };
159
- }
160
- };
161
-
162
- export const handleWeBookClearFiltersAction = async (ctx: BotContext) => {
163
- try {
164
- const telegramId = ctx.from?.id;
165
- if (!telegramId) {
166
- return {
167
- message: messageManager.getMessage('error_user_not_found'),
168
- options: getBackToMainMenuButton()
169
- };
170
- }
171
-
172
- // Reset user state to default
173
- userStates.set(telegramId, {
174
- page: 0,
175
- filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
176
- totalEvents: 0
177
- });
178
-
179
- return await fetchAndDisplayEvents(telegramId, 0);
180
- } catch (error: any) {
181
- logger.error(`Error in WeBook clear filters action: ${error.message}`);
182
- return {
183
- message: messageManager.getMessage('error_general'),
184
- options: getBackToMainMenuButton()
185
- };
186
- }
187
- };
188
 
189
  export const handleWeBookBackToMenuAction = async (ctx: BotContext) => {
190
  // Delete the previous message with buttons
@@ -225,164 +83,6 @@ export const handleWeBookBookByUrlAction = async (ctx: BotContext) => {
225
  };
226
 
227
 
228
- async function fetchAndDisplayEvents(telegramId: number, page: number) {
229
- try {
230
- const userState = userStates.get(telegramId);
231
- if (!userState) {
232
- return {
233
- message: messageManager.getMessage('error_user_state_not_found'),
234
- options: getBackToMainMenuButton()
235
- };
236
- }
237
-
238
- // Update page and calculate skip
239
- userState.page = page;
240
- const skip = page * EVENTS_PER_PAGE;
241
- const filters = { ...userState.filters, skip, limit: EVENTS_PER_PAGE };
242
-
243
- // Fetch events from WeBook API
244
- const response = await webookAPI.getEvents(filters);
245
- const events = response.data.eventCollection.items;
246
- const total = response.data.eventCollection.total;
247
-
248
- userState.totalEvents = total;
249
- userStates.set(telegramId, userState);
250
-
251
- if (events.length === 0) {
252
- return {
253
- message: '🎭 No events found with the current filters.\n\nTry adjusting your search criteria or clearing filters.',
254
- options: getWeBookEventsKeyboard(page, total, userState.filters)
255
- };
256
- }
257
-
258
- // Build events message
259
- let message = `🎭 <b>WeBook Events</b>\n\n`;
260
- message += `📊 Showing ${skip + 1}-${Math.min(skip + events.length, total)} of ${total} events\n\n`;
261
-
262
- // Add active filters info
263
- const activeFilters = getActiveFiltersText(userState.filters);
264
- if (activeFilters) {
265
- message += `🔍 <b>Active Filters:</b> ${activeFilters}\n\n`;
266
- }
267
-
268
- // Display events
269
- events.forEach((event, index) => {
270
- const eventNumber = skip + index + 1;
271
- message += `${eventNumber}. <b>${event.title || 'Untitled Event'}</b>\n`;
272
-
273
- if (event.subtitle) {
274
- message += ` ${event.subtitle}\n`;
275
- }
276
-
277
- if (event.schedule?.openDateTime) {
278
- const eventDate = new Date(event.schedule.openDateTime).toLocaleDateString('en-US', {
279
- weekday: 'short',
280
- year: 'numeric',
281
- month: 'short',
282
- day: 'numeric'
283
- });
284
- message += ` 📅 ${eventDate}\n`;
285
- }
286
-
287
- if (event.location?.city) {
288
- message += ` 📍 ${event.location.city}\n`;
289
- }
290
-
291
- if (event.startingPrice !== undefined) {
292
- const priceText = event.startingPrice === 0 ? 'Free' : `${event.startingPrice} ${event.currencyCode || 'SAR'}`;
293
- message += ` 💰 ${priceText}\n`;
294
- }
295
-
296
- if (event.zone?.title) {
297
- message += ` 🎪 ${event.zone.title}\n`;
298
- }
299
-
300
- message += '\n';
301
- });
302
-
303
- return {
304
- message,
305
- options: getWeBookEventsKeyboard(page, total, userState.filters)
306
- };
307
- } catch (error: any) {
308
- logger.error(`Error fetching WeBook events: ${error.message}`);
309
- return {
310
- message: '❌ Error fetching events. Please try again later.',
311
- options: getBackToMainMenuButton()
312
- };
313
- }
314
- }
315
-
316
- function getWeBookEventsKeyboard(page: number, total: number, filters: EventFilters) {
317
- const buttons = [];
318
- const totalPages = Math.ceil(total / EVENTS_PER_PAGE);
319
-
320
- // Filter buttons
321
- const filterRow = [];
322
- filterRow.push(Markup.button.callback('📅 Today', 'webook_filter_today'));
323
- filterRow.push(Markup.button.callback('📅 Tomorrow', 'webook_filter_tomorrow'));
324
- filterRow.push(Markup.button.callback('📅 This Week', 'webook_filter_this_week'));
325
- buttons.push(filterRow);
326
-
327
- const priceRow = [];
328
- priceRow.push(Markup.button.callback('🆓 Free', 'webook_filter_free'));
329
- priceRow.push(Markup.button.callback('💰 Paid', 'webook_filter_paid'));
330
- priceRow.push(Markup.button.callback('🧹 Clear Filters', 'webook_clear_filters'));
331
- buttons.push(priceRow);
332
-
333
- // Add new button for booking by URL
334
- buttons.push([Markup.button.callback('🎟️ Book Tickets by Event URL', 'webook_book_by_url')]);
335
-
336
- // Pagination buttons
337
- const paginationRow = [];
338
- if (page > 0) {
339
- paginationRow.push(Markup.button.callback('⬅️ Previous', `webook_page_${page - 1}`));
340
- }
341
-
342
- paginationRow.push(Markup.button.callback(`📄 ${page + 1}/${totalPages}`, 'noop'));
343
-
344
- if (page < totalPages - 1) {
345
- paginationRow.push(Markup.button.callback('Next ➡️', `webook_page_${page + 1}`));
346
- }
347
-
348
- if (paginationRow.length > 0) {
349
- buttons.push(paginationRow);
350
- }
351
-
352
- // Back button
353
- buttons.push([Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]);
354
-
355
- return Markup.inlineKeyboard(buttons);
356
- }
357
-
358
- function getActiveFiltersText(filters: EventFilters): string {
359
- const activeFilters = [];
360
-
361
- if (filters.date_filter) {
362
- const dateMap = {
363
- 'today': 'Today',
364
- 'tomorrow': 'Tomorrow',
365
- 'this_week': 'This Week'
366
- };
367
- activeFilters.push(dateMap[filters.date_filter]);
368
- }
369
-
370
- if (filters.price_min !== undefined && filters.price_min > 0) {
371
- activeFilters.push('Paid Events');
372
- }
373
-
374
- if (filters.price_max !== undefined && filters.price_max === 0) {
375
- activeFilters.push('Free Events');
376
- }
377
-
378
- if (filters.category) {
379
- const category = Array.isArray(filters.category) ? filters.category[0] : filters.category;
380
- activeFilters.push(`Category: ${category}`);
381
- }
382
-
383
- return activeFilters.length > 0 ? activeFilters.join(', ') : '';
384
- }
385
-
386
  // Export a handler for booking-by-URL text input
387
  export async function handleBookByUrlText(ctx: BotContext) {
388
  const telegramId = ctx.from?.id;
@@ -506,210 +206,27 @@ export async function handleBookByUrlText(ctx: BotContext) {
506
  return false;
507
  }
508
 
509
- interface WebhookAccount {
510
- id: string;
511
- email: string;
512
- password: string;
513
- }
514
 
515
  async function handleBookingProcess(ctx: BotContext, withPayment: boolean) {
516
  const telegramId = ctx.from?.id;
517
  if (!telegramId) return;
518
-
519
  const state = bookByUrlStates.get(telegramId);
520
  if (!state || !state.eventUrl || !state.tickets) {
521
  await ctx.reply(messageManager.getMessage('webook_request_expired'), { parse_mode: 'HTML' });
522
  return;
523
  }
524
-
525
  state.step = 4;
526
  bookByUrlStates.set(telegramId, state);
527
-
528
- if (withPayment) {
529
- await ctx.reply(messageManager.getMessage('webook_processing_payment'), { parse_mode: 'HTML' });
530
- } else {
531
- await ctx.reply(messageManager.getMessage('webook_processing_manual'), { parse_mode: 'HTML' });
532
- }
533
-
534
- const { eventUrl, tickets: ticketsNeeded } = state;
535
- let ticketsObtained = 0;
536
- const paymentUrls: string[] = [];
537
-
538
- const { data: accounts, error } = await fetchDataFromTable<WebhookAccount>('account_webhook', 100, 0, { is_active: true });
539
-
540
- if (error || !accounts) {
541
- await ctx.reply(messageManager.getMessage('webook_error_fetching_accounts'), { parse_mode: 'HTML' });
542
- bookByUrlStates.delete(telegramId);
543
- return;
544
- }
545
-
546
- const availableAccounts = accounts.filter((a) => a.id && a.email && a.password);
547
-
548
- if (availableAccounts.length === 0) {
549
- await ctx.reply(messageManager.getMessage('webook_no_available_accounts'), { parse_mode: 'HTML' });
550
- bookByUrlStates.delete(telegramId);
551
- return;
552
- }
553
-
554
- const paymentType = withPayment ? messageManager.getMessage('webook_automatic_payment') : messageManager.getMessage('webook_manual_payment');
555
-
556
- // Process accounts in parallel with a limit to avoid overwhelming the system
557
- const MAX_CONCURRENT_ACCOUNTS = 3; // Adjust this number based on your system capacity
558
-
559
- await ctx.reply(
560
- messageManager.getMessage('webook_starting_process')
561
- .replace('{event_url}', eventUrl)
562
- .replace('{fixed_url}', state.fixedUrl || eventUrl)
563
- .replace('{tickets_requested}', ticketsNeeded.toString())
564
- .replace('{payment_type}', paymentType)
565
- .replace('{available_accounts}', availableAccounts.length.toString())
566
- .replace('{max_concurrent}', MAX_CONCURRENT_ACCOUNTS.toString()),
567
- { parse_mode: 'HTML' }
568
- );
569
-
570
- const accountChunks = [];
571
-
572
- for (let i = 0; i < availableAccounts.length; i += MAX_CONCURRENT_ACCOUNTS) {
573
- accountChunks.push(availableAccounts.slice(i, i + MAX_CONCURRENT_ACCOUNTS));
574
- }
575
-
576
- for (const accountChunk of accountChunks) {
577
- if (ticketsObtained >= ticketsNeeded) {
578
- await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' });
579
- break;
580
- }
581
-
582
- // Process this chunk of accounts in parallel
583
- const bookingPromises = accountChunk.map(async (account) => {
584
- if (ticketsObtained >= ticketsNeeded) {
585
- return null; // Skip if we already have enough tickets
586
- }
587
-
588
- let login: WeBookLogin | undefined;
589
- try {
590
- await ctx.reply(messageManager.getMessage('webook_logging_in').replace('{email}', account.email), { parse_mode: 'HTML' });
591
- login = new WeBookLogin(account.email, account.password, `${account.id}`);
592
- const page = await login.login();
593
-
594
- if (!page || typeof page !== 'object') {
595
- await ctx.reply(messageManager.getMessage('webook_login_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
596
- return null;
597
- }
598
-
599
- await ctx.reply(messageManager.getMessage('webook_attempting_booking').replace('{email}', account.email), { parse_mode: 'HTML' });
600
- const booking = new WeBookBooking(page);
601
- const bookingResult = await booking.bookEvent(eventUrl, withPayment);
602
-
603
- if (withPayment) {
604
- // Handle number result for automatic payment
605
- if (typeof bookingResult === 'number') {
606
- const bookedCount = bookingResult;
607
- if (bookedCount > 0) {
608
- ticketsObtained += bookedCount;
609
- await ctx.reply(messageManager.getMessage('webook_booking_success_auto')
610
- .replace('{tickets}', bookedCount.toString())
611
- .replace('{email}', account.email)
612
- .replace('{total_obtained}', ticketsObtained.toString())
613
- .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
614
- return { success: true, tickets: bookedCount, account: account.email };
615
- } else {
616
- await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
617
- return { success: false, account: account.email };
618
- }
619
- }
620
- } else {
621
- console.log("bookingResult", bookingResult)
622
- // Handle object result for manual payment
623
- if (typeof bookingResult === 'object' && bookingResult !== null && 'ticketsCount' in bookingResult) {
624
- const result = bookingResult as { ticketsCount: number, paymentUrl?: string };
625
- if (result.ticketsCount > 0) {
626
- ticketsObtained += result.ticketsCount;
627
- if (result.paymentUrl) {
628
- paymentUrls.push(result.paymentUrl);
629
-
630
- // Send payment button immediately after successful booking
631
- await ctx.reply(
632
- messageManager.getMessage('webook_booking_success_manual_with_payment')
633
- .replace('{tickets}', result.ticketsCount.toString())
634
- .replace('{email}', account.email)
635
- .replace('{total_obtained}', ticketsObtained.toString())
636
- .replace('{total_needed}', ticketsNeeded.toString()),
637
- {
638
- parse_mode: 'HTML',
639
- ...Markup.inlineKeyboard([
640
- [Markup.button.url('Complete Payment', result.paymentUrl)],
641
- [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]
642
- ])
643
- }
644
- );
645
- }
646
- await ctx.reply(messageManager.getMessage('webook_booking_success_manual')
647
- .replace('{tickets}', result.ticketsCount.toString())
648
- .replace('{email}', account.email)
649
- .replace('{total_obtained}', ticketsObtained.toString())
650
- .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
651
- return { success: true, tickets: result.ticketsCount, account: account.email, paymentUrl: result.paymentUrl };
652
- } else {
653
- await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
654
- return { success: false, account: account.email };
655
- }
656
- }
657
- }
658
- return { success: false, account: account.email };
659
- } catch (e: any) {
660
- if (e.message && e.message.includes('Tickets sold out')) {
661
- await ctx.reply(messageManager.getMessage('webook_tickets_sold_out').replace('{email}', account.email), { parse_mode: 'HTML' });
662
- } else {
663
- await ctx.reply(messageManager.getMessage('webook_booking_error').replace('{email}', account.email).replace('{error}', e.message), { parse_mode: 'HTML' });
664
- }
665
- return { success: false, account: account.email, error: e.message };
666
- } finally {
667
- if (login) {
668
- await login.close();
669
- }
670
- }
671
- });
672
-
673
- // Wait for all accounts in this chunk to complete
674
- const results = await Promise.all(bookingPromises);
675
-
676
- // Check if we have enough tickets
677
- if (ticketsObtained >= ticketsNeeded) {
678
- await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' });
679
- break;
680
- }
681
- }
682
-
683
- // Show final results
684
- let finalMessage = messageManager.getMessage('webook_booking_completed')
685
- .replace('{event_url}', eventUrl)
686
- .replace('{fixed_url}', state.fixedUrl || eventUrl)
687
- .replace('{tickets_requested}', ticketsNeeded.toString())
688
- .replace('{tickets_obtained}', ticketsObtained.toString())
689
- .replace('{payment_type}', paymentType)
690
- .replace('{accounts_used}', availableAccounts.length.toString())
691
- .replace('{max_concurrent}', MAX_CONCURRENT_ACCOUNTS.toString());
692
-
693
- if (!withPayment && ticketsObtained > 0 && paymentUrls.length > 0) {
694
- finalMessage += messageManager.getMessage('webook_payment_buttons_sent');
695
-
696
- await ctx.reply(
697
- finalMessage,
698
- {
699
- parse_mode: 'HTML',
700
- ...Markup.inlineKeyboard([
701
- [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]
702
- ])
703
- }
704
- );
705
- } else if (!withPayment && ticketsObtained > 0) {
706
- finalMessage += messageManager.getMessage('webook_manual_payment_required');
707
- await ctx.reply(finalMessage, { parse_mode: 'HTML' });
708
- } else {
709
- await ctx.reply(finalMessage, { parse_mode: 'HTML' });
710
- }
711
-
712
- bookByUrlStates.delete(telegramId);
713
  }
714
 
715
  // Handle Yes/No payment buttons
@@ -733,55 +250,4 @@ export function setupWeBookBookByUrlPaymentHandlers(bot: any) {
733
  }
734
  await handleBookingProcess(ctx, false);
735
  });
736
- }
737
-
738
- // Helper: Validate WeBook event URL
739
- function isValidWeBookEventUrl(url: string): boolean {
740
- try {
741
- const u = new URL(url);
742
- return u.hostname === 'webook.com' && /\/events\//.test(u.pathname);
743
- } catch {
744
- return false;
745
- }
746
- }
747
-
748
- // Helper: Fix WeBook event URL
749
- function fixWeBookEventUrl(url: string): string {
750
- let fixed = url.replace('/ar/', '/en/');
751
- if (!/\/en\//.test(fixed)) fixed = fixed.replace('/events/', '/en/events/');
752
- if (!/\/book$/.test(fixed)) fixed = fixed.replace(/\/?$/, '/book');
753
- return fixed;
754
- }
755
-
756
- // Real implementation: get max tickets allowed (clickCount) using Playwright
757
- async function getMaxTicketsAllowed(eventUrl: string): Promise<number> {
758
-
759
- const email = process.env.WEBOOK_EMAIL || 'mfoud444@gmail.com';
760
- const password = process.env.WEBOOK_PASSWORD || '009988Ppooii@@@@';
761
- try {
762
- return 5;
763
- // // Login and get a Playwright page
764
- // const login = new WeBookLogin(email, password);
765
- // const page = await login.login();
766
- // if (!page) throw new Error('Login failed');
767
- // // Use WeBookBooking to navigate and count max tickets
768
- // const booking = new WeBookBooking(page);
769
- // await page.goto(eventUrl);
770
-
771
- // // Check if tickets are available first
772
- // if (!(await booking.areTicketsAvailable())) {
773
- // console.warn('No tickets available for this event');
774
- // if (page.context()) await page.context().close();
775
- // return 0;
776
- // }
777
-
778
- // // Use the selectMaxTickets method and return the clickCount
779
- // const clickCount = await booking.selectMaxTickets();
780
- // if (page.context()) await page.context().close();
781
- // return clickCount;
782
- } catch (e) {
783
- console.error('Error in getMaxTicketsAllowed:', e);
784
- return 5; // fallback
785
-
786
- }
787
  }
 
5
  import { webookAPI, EventFilters } from '../../webook/webookApi';
6
  import { Markup } from "telegraf";
7
  import { getBackToMainMenuButton, getLoggedInMenuKeyboard } from "../utils/keyboardUtils";
8
+ import {
9
+ injectWeBookFilterDeps,
10
+ handleWeBookEventsAction,
11
+ handleWeBookPageAction
12
+ } from './webookFiltersHandlers';
13
+ import { orchestrateBookingProcess, BookingState, isValidWeBookEventUrl, fixWeBookEventUrl, getMaxTicketsAllowed } from '../services/WeBookBookingService';
14
 
15
  const logger = createLogger('WeBookHandlers');
16
 
17
+ export const userStates = new Map<number, {
 
18
  page: number;
19
  filters: EventFilters;
20
  totalEvents: number;
 
34
  export const setupWeBookHandlers = (bot: any) => {
35
  bot.action('webook_events', callbackReplyHandler(handleWeBookEventsAction));
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
42
+ injectWeBookFilterDeps({
43
+ userStates,
44
+ EVENTS_PER_PAGE
45
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  export const handleWeBookBackToMenuAction = async (ctx: BotContext) => {
48
  // Delete the previous message with buttons
 
83
  };
84
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  // Export a handler for booking-by-URL text input
87
  export async function handleBookByUrlText(ctx: BotContext) {
88
  const telegramId = ctx.from?.id;
 
206
  return false;
207
  }
208
 
209
+
 
 
 
 
210
 
211
  async function handleBookingProcess(ctx: BotContext, withPayment: boolean) {
212
  const telegramId = ctx.from?.id;
213
  if (!telegramId) return;
 
214
  const state = bookByUrlStates.get(telegramId);
215
  if (!state || !state.eventUrl || !state.tickets) {
216
  await ctx.reply(messageManager.getMessage('webook_request_expired'), { parse_mode: 'HTML' });
217
  return;
218
  }
 
219
  state.step = 4;
220
  bookByUrlStates.set(telegramId, state);
221
+ // Ensure state matches BookingState type
222
+ const bookingState: BookingState = {
223
+ eventUrl: state.eventUrl,
224
+ fixedUrl: state.fixedUrl,
225
+ tickets: state.tickets,
226
+ step: state.step,
227
+ lastBotMessageId: state.lastBotMessageId
228
+ };
229
+ await orchestrateBookingProcess(ctx, bookingState, withPayment, bookByUrlStates as Map<number, BookingState>);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  }
231
 
232
  // Handle Yes/No payment buttons
 
250
  }
251
  await handleBookingProcess(ctx, false);
252
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
src/bots/services/WeBookBookingService.ts ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { messageManager } from "../utils/messageManager";
3
+ import { WeBookBooking } from '../../webook/book';
4
+ import { WeBookLogin } from '../../webook/login';
5
+ import { fetchDataFromTable } from '../../db/supabaseHelper';
6
+ import { Markup } from "telegraf";
7
+
8
+ export interface WebhookAccount {
9
+ id: string;
10
+ email: string;
11
+ password: string;
12
+ }
13
+
14
+ export interface BookingState {
15
+ eventUrl: string;
16
+ fixedUrl?: string;
17
+ tickets: number;
18
+ step: number;
19
+ lastBotMessageId?: number;
20
+ }
21
+
22
+ export interface BookingResult {
23
+ success: boolean;
24
+ tickets?: number;
25
+ account: string;
26
+ paymentUrl?: string;
27
+ error?: string;
28
+ }
29
+
30
+ export function chunkAccounts<T>(accounts: T[], chunkSize: number): T[][] {
31
+ const chunks: T[][] = [];
32
+ for (let i = 0; i < accounts.length; i += chunkSize) {
33
+ chunks.push(accounts.slice(i, i + chunkSize));
34
+ }
35
+ return chunks;
36
+ }
37
+
38
+ export async function bookTicketsForAccount(
39
+ ctx: BotContext,
40
+ account: WebhookAccount,
41
+ eventUrl: string,
42
+ withPayment: boolean,
43
+ ticketsObtained: number,
44
+ ticketsNeeded: number,
45
+ isLogin: boolean = false
46
+ ): Promise<BookingResult> {
47
+ let login: WeBookLogin | undefined;
48
+ try {
49
+ if (isLogin) {
50
+ await ctx.reply(messageManager.getMessage('webook_logging_in').replace('{email}', account.email), { parse_mode: 'HTML' });
51
+ login = new WeBookLogin(account.email, account.password, `${account.id}`);
52
+ const page = await login.login();
53
+ if (!page || typeof page !== 'object') {
54
+ await ctx.reply(messageManager.getMessage('webook_login_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
55
+ return { success: false, account: account.email };
56
+ }
57
+ await ctx.reply(messageManager.getMessage('webook_attempting_booking').replace('{email}', account.email), { parse_mode: 'HTML' });
58
+ const booking = new WeBookBooking(page);
59
+ const bookingResult = await booking.bookEvent(eventUrl, withPayment);
60
+ if (withPayment) {
61
+ if (typeof bookingResult === 'number') {
62
+ const bookedCount = bookingResult;
63
+ if (bookedCount > 0) {
64
+ await ctx.reply(messageManager.getMessage('webook_booking_success_auto')
65
+ .replace('{tickets}', bookedCount.toString())
66
+ .replace('{email}', account.email)
67
+ .replace('{total_obtained}', (ticketsObtained + bookedCount).toString())
68
+ .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
69
+ return { success: true, tickets: bookedCount, account: account.email };
70
+ } else {
71
+ await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
72
+ return { success: false, account: account.email };
73
+ }
74
+ }
75
+ } else {
76
+ if (typeof bookingResult === 'object' && bookingResult !== null && 'ticketsCount' in bookingResult) {
77
+ const result = bookingResult as { ticketsCount: number, paymentUrl?: string };
78
+ if (result.ticketsCount > 0) {
79
+ if (result.paymentUrl) {
80
+ await ctx.reply(
81
+ messageManager.getMessage('webook_booking_success_manual_with_payment')
82
+ .replace('{tickets}', result.ticketsCount.toString())
83
+ .replace('{email}', account.email)
84
+ .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString())
85
+ .replace('{total_needed}', ticketsNeeded.toString()),
86
+ {
87
+ parse_mode: 'HTML',
88
+ ...Markup.inlineKeyboard([
89
+ [Markup.button.url('Complete Payment', result.paymentUrl)],
90
+ [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]
91
+ ])
92
+ }
93
+ );
94
+ }
95
+ await ctx.reply(messageManager.getMessage('webook_booking_success_manual')
96
+ .replace('{tickets}', result.ticketsCount.toString())
97
+ .replace('{email}', account.email)
98
+ .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString())
99
+ .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
100
+ return { success: true, tickets: result.ticketsCount, account: account.email, paymentUrl: result.paymentUrl };
101
+ } else {
102
+ await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
103
+ return { success: false, account: account.email };
104
+ }
105
+ }
106
+ }
107
+ return { success: false, account: account.email };
108
+ } else {
109
+ // No login, use direct booking
110
+ await ctx.reply(messageManager.getMessage('webook_attempting_booking').replace('{email}', account.email), { parse_mode: 'HTML' });
111
+ const bookingResult = await WeBookBooking.bookDirect(eventUrl, `${account.id}`, withPayment);
112
+ if (withPayment) {
113
+ if (typeof bookingResult === 'number') {
114
+ const bookedCount = bookingResult;
115
+ if (bookedCount > 0) {
116
+ await ctx.reply(messageManager.getMessage('webook_booking_success_auto')
117
+ .replace('{tickets}', bookedCount.toString())
118
+ .replace('{email}', account.email)
119
+ .replace('{total_obtained}', (ticketsObtained + bookedCount).toString())
120
+ .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
121
+ return { success: true, tickets: bookedCount, account: account.email };
122
+ } else {
123
+ await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
124
+ return { success: false, account: account.email };
125
+ }
126
+ }
127
+ } else {
128
+ if (typeof bookingResult === 'object' && bookingResult !== null && 'ticketsCount' in bookingResult) {
129
+ const result = bookingResult as { ticketsCount: number, paymentUrl?: string };
130
+ if (result.ticketsCount > 0) {
131
+ if (result.paymentUrl) {
132
+ await ctx.reply(
133
+ messageManager.getMessage('webook_booking_success_manual_with_payment')
134
+ .replace('{tickets}', result.ticketsCount.toString())
135
+ .replace('{email}', account.email)
136
+ .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString())
137
+ .replace('{total_needed}', ticketsNeeded.toString()),
138
+ {
139
+ parse_mode: 'HTML',
140
+ ...Markup.inlineKeyboard([
141
+ [Markup.button.url('Complete Payment', result.paymentUrl)],
142
+ [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]
143
+ ])
144
+ }
145
+ );
146
+ }
147
+ await ctx.reply(messageManager.getMessage('webook_booking_success_manual')
148
+ .replace('{tickets}', result.ticketsCount.toString())
149
+ .replace('{email}', account.email)
150
+ .replace('{total_obtained}', (ticketsObtained + result.ticketsCount).toString())
151
+ .replace('{total_needed}', ticketsNeeded.toString()), { parse_mode: 'HTML' });
152
+ return { success: true, tickets: result.ticketsCount, account: account.email, paymentUrl: result.paymentUrl };
153
+ } else {
154
+ await ctx.reply(messageManager.getMessage('webook_booking_failed').replace('{email}', account.email), { parse_mode: 'HTML' });
155
+ return { success: false, account: account.email };
156
+ }
157
+ }
158
+ }
159
+ return { success: false, account: account.email };
160
+ }
161
+ } catch (e: any) {
162
+ if (e.message && e.message.includes('Tickets sold out')) {
163
+ await ctx.reply(messageManager.getMessage('webook_tickets_sold_out').replace('{email}', account.email), { parse_mode: 'HTML' });
164
+ } else {
165
+ await ctx.reply(messageManager.getMessage('webook_booking_error').replace('{email}', account.email).replace('{error}', e.message), { parse_mode: 'HTML' });
166
+ }
167
+ return { success: false, account: account.email, error: e.message };
168
+ } finally {
169
+ if (login) {
170
+ await login.close();
171
+ }
172
+ }
173
+ }
174
+
175
+ export async function sendBookingSummary(
176
+ ctx: BotContext,
177
+ state: BookingState,
178
+ paymentType: string,
179
+ availableAccounts: number,
180
+ maxConcurrent: number
181
+ ) {
182
+ await ctx.reply(
183
+ messageManager.getMessage('webook_starting_process')
184
+ .replace('{event_url}', state.eventUrl)
185
+ .replace('{fixed_url}', state.fixedUrl || state.eventUrl)
186
+ .replace('{tickets_requested}', state.tickets.toString())
187
+ .replace('{payment_type}', paymentType)
188
+ .replace('{available_accounts}', availableAccounts.toString())
189
+ .replace('{max_concurrent}', maxConcurrent.toString()),
190
+ { parse_mode: 'HTML' }
191
+ );
192
+ }
193
+
194
+ export async function orchestrateBookingProcess(
195
+ ctx: BotContext,
196
+ state: BookingState,
197
+ withPayment: boolean,
198
+ bookByUrlStates: Map<number, BookingState>
199
+ ) {
200
+ const telegramId = ctx.from?.id;
201
+ if (!telegramId) return;
202
+ let ticketsObtained = 0;
203
+ const paymentUrls: string[] = [];
204
+ const { eventUrl, tickets: ticketsNeeded } = state;
205
+ const { data: accounts, error } = await fetchDataFromTable<WebhookAccount>('account_webhook', 100, 0, { is_active: true });
206
+ if (error || !accounts) {
207
+ await ctx.reply(messageManager.getMessage('webook_error_fetching_accounts'), { parse_mode: 'HTML' });
208
+ bookByUrlStates.delete(telegramId);
209
+ return;
210
+ }
211
+ const availableAccounts = accounts.filter((a) => a.id && a.email && a.password);
212
+ if (availableAccounts.length === 0) {
213
+ await ctx.reply(messageManager.getMessage('webook_no_available_accounts'), { parse_mode: 'HTML' });
214
+ bookByUrlStates.delete(telegramId);
215
+ return;
216
+ }
217
+ const paymentType = withPayment ? messageManager.getMessage('webook_automatic_payment') : messageManager.getMessage('webook_manual_payment');
218
+ const MAX_CONCURRENT_ACCOUNTS = 3;
219
+ await sendBookingSummary(ctx, state, paymentType, availableAccounts.length, MAX_CONCURRENT_ACCOUNTS);
220
+ const accountChunks = chunkAccounts(availableAccounts, MAX_CONCURRENT_ACCOUNTS);
221
+ for (const accountChunk of accountChunks) {
222
+ if (ticketsObtained >= ticketsNeeded) {
223
+ await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' });
224
+ break;
225
+ }
226
+ const bookingResults = await Promise.all(
227
+ accountChunk.map(account =>
228
+ ticketsObtained >= ticketsNeeded
229
+ ? Promise.resolve(null)
230
+ : bookTicketsForAccount(ctx, account, eventUrl, withPayment, ticketsObtained, ticketsNeeded)
231
+ )
232
+ );
233
+ for (const result of bookingResults) {
234
+ if (result && result.success && result.tickets) {
235
+ ticketsObtained += result.tickets;
236
+ if (!withPayment && result.paymentUrl) {
237
+ paymentUrls.push(result.paymentUrl);
238
+ }
239
+ }
240
+ }
241
+ if (ticketsObtained >= ticketsNeeded) {
242
+ await ctx.reply(messageManager.getMessage('webook_all_tickets_booked'), { parse_mode: 'HTML' });
243
+ break;
244
+ }
245
+ }
246
+ let finalMessage = messageManager.getMessage('webook_booking_completed')
247
+ .replace('{event_url}', eventUrl)
248
+ .replace('{fixed_url}', state.fixedUrl || eventUrl)
249
+ .replace('{tickets_requested}', ticketsNeeded.toString())
250
+ .replace('{tickets_obtained}', ticketsObtained.toString())
251
+ .replace('{payment_type}', paymentType)
252
+ .replace('{accounts_used}', availableAccounts.length.toString())
253
+ .replace('{max_concurrent}', MAX_CONCURRENT_ACCOUNTS.toString());
254
+ if (!withPayment && ticketsObtained > 0 && paymentUrls.length > 0) {
255
+ finalMessage += messageManager.getMessage('webook_payment_buttons_sent');
256
+ await ctx.reply(
257
+ finalMessage,
258
+ {
259
+ parse_mode: 'HTML',
260
+ ...Markup.inlineKeyboard([
261
+ [Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]
262
+ ])
263
+ }
264
+ );
265
+ } else if (!withPayment && ticketsObtained > 0) {
266
+ finalMessage += messageManager.getMessage('webook_manual_payment_required');
267
+ await ctx.reply(finalMessage, { parse_mode: 'HTML' });
268
+ } else {
269
+ await ctx.reply(finalMessage, { parse_mode: 'HTML' });
270
+ }
271
+ bookByUrlStates.delete(telegramId);
272
+ }
273
+
274
+ export function isValidWeBookEventUrl(url: string): boolean {
275
+ try {
276
+ const u = new URL(url);
277
+ return u.hostname === 'webook.com' && /\/events\//.test(u.pathname);
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
283
+
284
+ export function fixWeBookEventUrl(url: string): string {
285
+ try {
286
+ const u = new URL(url);
287
+ // Replace any language code in the path before /events/ with /en/
288
+ u.pathname = u.pathname.replace(/^\/(?:[a-zA-Z-]+)\/(events\/)/, '/en/$1');
289
+ // If /en/ is not present before /events/, insert it
290
+ if (!/\/en\/events\//.test(u.pathname)) {
291
+ u.pathname = u.pathname.replace(/\/events\//, '/en/events/');
292
+ }
293
+ // Ensure the path ends with /book
294
+ if (!/\/book$/.test(u.pathname)) {
295
+ u.pathname = u.pathname.replace(/\/?$/, '/book');
296
+ }
297
+ return u.toString();
298
+ } catch {
299
+ // fallback to previous logic if URL parsing fails
300
+ let fixed = url.replace(/\/[a-zA-Z-]+\/(events\/)/, '/en/$1');
301
+ if (!/\/en\//.test(fixed)) fixed = fixed.replace('/events/', '/en/events/');
302
+ if (!/\/book$/.test(fixed)) fixed = fixed.replace(/\/?$/, '/book');
303
+ return fixed;
304
+ }
305
+ }
306
+
307
+ export async function getMaxTicketsAllowed(eventUrl: string): Promise<number> {
308
+ const email = process.env.WEBOOK_EMAIL || 'mfoud444@gmail.com';
309
+ const password = process.env.WEBOOK_PASSWORD || '009988Ppooii@@@@';
310
+ try {
311
+ return 5;
312
+ // Real implementation commented out for now
313
+ // const login = new WeBookLogin(email, password);
314
+ // const page = await login.login();
315
+ // if (!page) throw new Error('Login failed');
316
+ // const booking = new WeBookBooking(page);
317
+ // await page.goto(eventUrl);
318
+ // if (!(await booking.areTicketsAvailable())) {
319
+ // if (page.context()) await page.context().close();
320
+ // return 0;
321
+ // }
322
+ // const clickCount = await booking.selectMaxTickets();
323
+ // if (page.context()) await page.context().close();
324
+ // return clickCount;
325
+ } catch (e) {
326
+ console.error('Error in getMaxTicketsAllowed:', e);
327
+ return 5;
328
+ }
329
+ }
src/bots/utils/keyboardUtils.ts CHANGED
@@ -13,9 +13,9 @@ export const getMainMenuKeyboard = () => {
13
  return Markup.inlineKeyboard([
14
  // [Markup.button.callback(messageManager.getMessage('btn_login'), 'login')],
15
  // [Markup.button.callback(messageManager.getMessage('btn_terms'), 'terms')],
16
- [Markup.button.callback(messageManager.getMessage('btn_webook_events'), 'webook_events'),
17
- // Markup.button.callback('🎟️ Book Tickets by Event URL', 'webook_book_by_url')
18
- ],
19
  [Markup.button.callback(messageManager.getMessage('btn_webook_book_by_url'), 'webook_book_by_url')],
20
  // [Markup.button.callback(messageManager.getMessage('btn_new_members'), 'new_members')],
21
  // [Markup.button.callback(messageManager.getMessage('btn_stats'), 'stats')],
 
13
  return Markup.inlineKeyboard([
14
  // [Markup.button.callback(messageManager.getMessage('btn_login'), 'login')],
15
  // [Markup.button.callback(messageManager.getMessage('btn_terms'), 'terms')],
16
+ // [Markup.button.callback(messageManager.getMessage('btn_webook_events'), 'webook_events'),
17
+ // // Markup.button.callback('🎟️ Book Tickets by Event URL', 'webook_book_by_url')
18
+ // ],
19
  [Markup.button.callback(messageManager.getMessage('btn_webook_book_by_url'), 'webook_book_by_url')],
20
  // [Markup.button.callback(messageManager.getMessage('btn_new_members'), 'new_members')],
21
  // [Markup.button.callback(messageManager.getMessage('btn_stats'), 'stats')],
src/index.ts CHANGED
@@ -39,4 +39,5 @@ startServer();
39
  // webookMain().catch(e => { console.error('WeBook Fatal error:', e); });
40
 
41
 
42
- // handleAddTelegrafBot("1e186256-fdd6-453f-8344-fd824660b8bd")
 
 
39
  // webookMain().catch(e => { console.error('WeBook Fatal error:', e); });
40
 
41
 
42
+
43
+ handleAddTelegrafBot("1e186256-fdd6-453f-8344-fd824660b8bd")
src/webook/book.ts CHANGED
@@ -3,10 +3,32 @@ import { WeBookBase } from './webookBase';
3
  import { fetchAllPaymentMethods } from '../db/supabaseHelper';
4
 
5
  export class WeBookBooking extends WeBookBase {
6
- constructor(page: Page, profileDir?: string) {
7
- super(profileDir, page);
8
- if (!this.page) {
9
- throw new Error('A Playwright page object must be provided to WeBookBooking');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
  }
12
 
 
3
  import { fetchAllPaymentMethods } from '../db/supabaseHelper';
4
 
5
  export class WeBookBooking extends WeBookBase {
6
+ constructor(pageOrProfileDir?: Page | string, profileDirIfPage?: string) {
7
+ if (pageOrProfileDir && typeof pageOrProfileDir === 'object') {
8
+ // page provided
9
+ super(profileDirIfPage, pageOrProfileDir as Page);
10
+ if (!this.page) {
11
+ throw new Error('A Playwright page object must be provided to WeBookBooking');
12
+ }
13
+ } else {
14
+ // only profileDir provided (or nothing)
15
+ super(pageOrProfileDir as string | undefined);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Static helper to book directly with just eventUrl and profileDir (no login)
21
+ * @param eventUrl string
22
+ * @param profileDir string (optional)
23
+ * @param withPayment boolean (default true)
24
+ */
25
+ static async bookDirect(eventUrl: string, profileDir?: string, withPayment: boolean = true): Promise<number | { ticketsCount: number, paymentUrl?: string }> {
26
+ const booking = new WeBookBooking(profileDir);
27
+ await booking.setupBrowser();
28
+ try {
29
+ return await booking.bookEvent(eventUrl, withPayment);
30
+ } finally {
31
+ await booking.close();
32
  }
33
  }
34
 
src/webook/login.ts CHANGED
@@ -64,10 +64,11 @@ export class WeBookLogin extends WeBookBase {
64
  if (!this.page) throw new Error('Page not initialized');
65
  try {
66
 
67
- // this.page.goto('https://webook.com/en');
68
- await this.page.goto('https://webook.com/en', { waitUntil: 'networkidle' });
69
  await this.handleCookies();
70
 
 
71
  await this.page.screenshot({ path: 'logs/login-check-failed.png', fullPage: true });
72
 
73
 
 
64
  if (!this.page) throw new Error('Page not initialized');
65
  try {
66
 
67
+ await this.page.goto('https://webook.com/en');
68
+ // await this.page.goto('https://webook.com/en', { waitUntil: 'networkidle' });
69
  await this.handleCookies();
70
 
71
+
72
  await this.page.screenshot({ path: 'logs/login-check-failed.png', fullPage: true });
73
 
74