Mohammed Foud commited on
Commit
1029f50
·
1 Parent(s): 5c9af5b
WEBHOOK_EVENTS_FEATURE.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WeBook Events Feature
2
+
3
+ ## Overview
4
+ The WeBook Events feature allows users to browse and search for events from the WeBook platform directly through the Telegram bot. Users can filter events by date, price, and other criteria.
5
+
6
+ ## Features
7
+
8
+ ### 🎭 Event Browsing
9
+ - Browse events from WeBook platform
10
+ - View event details including title, date, location, price, and venue
11
+ - Paginated results (5 events per page)
12
+
13
+ ### 🔍 Filtering Options
14
+ - **Date Filters:**
15
+ - Today's events
16
+ - Tomorrow's events
17
+ - This week's events
18
+
19
+ - **Price Filters:**
20
+ - Free events only
21
+ - Paid events only
22
+
23
+ - **Clear Filters:** Reset all applied filters
24
+
25
+ ### 📱 User Interface
26
+ - Inline keyboard navigation
27
+ - Pagination controls (Previous/Next)
28
+ - Active filters display
29
+ - Back to main menu option
30
+
31
+ ## Technical Implementation
32
+
33
+ ### Files Modified/Created
34
+
35
+ 1. **`src/bots/handlers/webookHandlers.ts`** - Main handler for WeBook events
36
+ 2. **`src/bots/utils/keyboardUtils.ts`** - Updated to include WeBook Events button
37
+ 3. **`src/bots/handlers/index.ts`** - Added WeBook handlers setup
38
+ 4. **`src/bots/utils/messageManager.ts`** - Added error messages
39
+
40
+ ### Key Components
41
+
42
+ #### WeBook API Integration
43
+ - Uses the existing `WeBookAPI` class from `src/webook/webookApi.ts`
44
+ - Fetches events with filtering and pagination
45
+ - Handles API errors gracefully
46
+
47
+ #### User State Management
48
+ - Stores user pagination state and filters
49
+ - Maintains state per user (telegram ID)
50
+ - Resets to first page when applying new filters
51
+
52
+ #### Event Display Format
53
+ ```
54
+ 🎭 WeBook Events
55
+
56
+ 📊 Showing 1-5 of 25 events
57
+
58
+ 🔍 Active Filters: Today, Free Events
59
+
60
+ 1. Event Title
61
+ Event Subtitle
62
+ 📅 Mon, Jan 15, 2024
63
+ 📍 Riyadh
64
+ 💰 Free
65
+ 🎪 Venue Name
66
+
67
+ 2. Another Event
68
+ ...
69
+ ```
70
+
71
+ ### Keyboard Layout
72
+
73
+ #### Main Menu (Logged In)
74
+ ```
75
+ 🔍 Browse Services
76
+ 🎭 WeBook Events
77
+ [Profile] [Change Language]
78
+ [Top Up Balance] [History]
79
+ [Back]
80
+ ```
81
+
82
+ #### Events Browser
83
+ ```
84
+ [📅 Today] [📅 Tomorrow] [📅 This Week]
85
+ [🆓 Free] [💰 Paid] [🧹 Clear Filters]
86
+ [⬅️ Previous] [📄 1/5] [Next ➡️]
87
+ [🔙 Back to Menu]
88
+ ```
89
+
90
+ ## Usage
91
+
92
+ 1. **Access Events:** Click "🎭 WeBook Events" from the main menu
93
+ 2. **Browse Events:** View events with pagination
94
+ 3. **Apply Filters:** Use filter buttons to narrow results
95
+ 4. **Clear Filters:** Reset to show all events
96
+ 5. **Navigate:** Use Previous/Next buttons to browse pages
97
+
98
+ ## Error Handling
99
+
100
+ - **API Errors:** Graceful fallback with user-friendly messages
101
+ - **Invalid States:** Automatic state reset and recovery
102
+ - **Network Issues:** Retry mechanism and error notifications
103
+
104
+ ## Configuration
105
+
106
+ ### Default Settings
107
+ - **Country Code:** SA (Saudi Arabia)
108
+ - **Events Per Page:** 5
109
+ - **Default Language:** Arabic (ar-SA)
110
+
111
+ ### Customization
112
+ - Modify `EVENTS_PER_PAGE` constant in `webookHandlers.ts`
113
+ - Update default filters in `handleWeBookEventsAction`
114
+ - Add new filter types in `handleWeBookFilterAction`
115
+
116
+ ## Future Enhancements
117
+
118
+ - [ ] Category-based filtering
119
+ - [ ] Location-based filtering
120
+ - [ ] Event booking integration
121
+ - [ ] Event reminders
122
+ - [ ] Favorite events
123
+ - [ ] Event sharing
124
+ - [ ] Advanced search with text input
125
+
126
+ ## Dependencies
127
+
128
+ - `telegraf` - Telegram bot framework
129
+ - `axios` - HTTP client for API calls
130
+ - `src/webook/webookApi.ts` - WeBook API integration
131
+
132
+ ## Testing
133
+
134
+ To test the feature:
135
+
136
+ 1. Start the bot
137
+ 2. Login to the bot
138
+ 3. Click "🎭 WeBook Events" from the main menu
139
+ 4. Test pagination and filtering
140
+ 5. Verify error handling with network issues
141
+
142
+ ## Troubleshooting
143
+
144
+ ### Common Issues
145
+
146
+ 1. **No events showing:** Check WeBook API connectivity
147
+ 2. **Filters not working:** Verify filter logic in handlers
148
+ 3. **Pagination issues:** Check user state management
149
+ 4. **Keyboard not responding:** Verify action handlers setup
150
+
151
+ ### Debug Steps
152
+
153
+ 1. Check bot logs for errors
154
+ 2. Verify WeBook API responses
155
+ 3. Test individual handler functions
156
+ 4. Check user state persistence
db/full_backup.sql ADDED
The diff for this file is too large to render. See raw diff
 
db/get.sh ADDED
@@ -0,0 +1 @@
 
 
1
+ pg_dump "postgresql://postgres.bxfyodnvfroddubzxgyj:009988Ppooii@aws-0-us-west-1.pooler.supabase.com:5432/postgres" --no-owner --format=plain --file=full_backup.sql
db/push.sh ADDED
@@ -0,0 +1 @@
 
 
1
+ psql "postgresql://postgres.aoptpqvwquunkmpihygt:009988Ppooii@aws-0-eu-north-1.pooler.supabase.com:5432/postgres" < full_backup.sql
src/bots/handlers/index.ts CHANGED
@@ -5,6 +5,7 @@ import { setupHistoryHandlers } from './historyHandlers';
5
  import { setupBalanceHandlers } from './balanceHandlers';
6
  import { setupLanguageHandlers } from './languageHandlers';
7
  import { setupProfileHandlers } from './profileHandlers';
 
8
  import { handleGiftAmountInput, handleGiftEmailInput, giftStates, handleGiftBalanceAction } from './giftHandlers';
9
  import { BotContext } from '../types/botTypes';
10
  import type { ParseMode } from 'telegraf/types';
@@ -17,6 +18,7 @@ export const setupCallbackHandlers = (bot: any) => {
17
  setupBalanceHandlers(bot);
18
  setupLanguageHandlers(bot);
19
  setupProfileHandlers(bot);
 
20
 
21
  bot.action('gift_balance', async (ctx: BotContext) => {
22
  const result = await handleGiftBalanceAction(ctx);
 
5
  import { setupBalanceHandlers } from './balanceHandlers';
6
  import { setupLanguageHandlers } from './languageHandlers';
7
  import { setupProfileHandlers } from './profileHandlers';
8
+ import { setupWeBookHandlers } from './webookHandlers';
9
  import { handleGiftAmountInput, handleGiftEmailInput, giftStates, handleGiftBalanceAction } from './giftHandlers';
10
  import { BotContext } from '../types/botTypes';
11
  import type { ParseMode } from 'telegraf/types';
 
18
  setupBalanceHandlers(bot);
19
  setupLanguageHandlers(bot);
20
  setupProfileHandlers(bot);
21
+ setupWeBookHandlers(bot);
22
 
23
  bot.action('gift_balance', async (ctx: BotContext) => {
24
  const result = await handleGiftBalanceAction(ctx);
src/bots/handlers/webookHandlers.ts ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { createLogger } from '../../utils/logger';
3
+ import { callbackReplyHandler } from "../utils/handlerUtils";
4
+ 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
+
9
+ const logger = createLogger('WeBookHandlers');
10
+
11
+ // Store user states for pagination and filters
12
+ const userStates = new Map<number, {
13
+ page: number;
14
+ filters: EventFilters;
15
+ totalEvents: number;
16
+ }>();
17
+
18
+ const EVENTS_PER_PAGE = 5;
19
+
20
+ export const setupWeBookHandlers = (bot: any) => {
21
+ bot.action('webook_events', callbackReplyHandler(handleWeBookEventsAction));
22
+ bot.action(/^webook_page_(\d+)$/, callbackReplyHandler(handleWeBookPageAction));
23
+ bot.action(/^webook_filter_(.+)$/, callbackReplyHandler(handleWeBookFilterAction));
24
+ bot.action('webook_back_to_menu', callbackReplyHandler(handleWeBookBackToMenuAction));
25
+ bot.action('webook_clear_filters', callbackReplyHandler(handleWeBookClearFiltersAction));
26
+ };
27
+
28
+ export const handleWeBookEventsAction = async (ctx: BotContext) => {
29
+ try {
30
+ const telegramId = ctx.from?.id;
31
+ if (!telegramId) {
32
+ return {
33
+ message: messageManager.getMessage('error_user_not_found'),
34
+ options: getBackToMainMenuButton()
35
+ };
36
+ }
37
+
38
+ // Initialize user state
39
+ userStates.set(telegramId, {
40
+ page: 0,
41
+ filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
42
+ totalEvents: 0
43
+ });
44
+
45
+ return await fetchAndDisplayEvents(telegramId, 0);
46
+ } catch (error: any) {
47
+ logger.error(`Error in WeBook events action: ${error.message}`);
48
+ return {
49
+ message: messageManager.getMessage('error_general'),
50
+ options: getBackToMainMenuButton()
51
+ };
52
+ }
53
+ };
54
+
55
+ export const handleWeBookPageAction = async (ctx: BotContext) => {
56
+ try {
57
+ const telegramId = ctx.from?.id;
58
+ if (!telegramId) {
59
+ return {
60
+ message: messageManager.getMessage('error_user_not_found'),
61
+ options: getBackToMainMenuButton()
62
+ };
63
+ }
64
+
65
+ const match = ctx.match;
66
+ if (!match) {
67
+ return {
68
+ message: messageManager.getMessage('error_invalid_page'),
69
+ options: getBackToMainMenuButton()
70
+ };
71
+ }
72
+
73
+ const page = parseInt(match[1]);
74
+ return await fetchAndDisplayEvents(telegramId, page);
75
+ } catch (error: any) {
76
+ logger.error(`Error in WeBook page action: ${error.message}`);
77
+ return {
78
+ message: messageManager.getMessage('error_general'),
79
+ options: getBackToMainMenuButton()
80
+ };
81
+ }
82
+ };
83
+
84
+ export const handleWeBookFilterAction = async (ctx: BotContext) => {
85
+ try {
86
+ const telegramId = ctx.from?.id;
87
+ if (!telegramId) {
88
+ return {
89
+ message: messageManager.getMessage('error_user_not_found'),
90
+ options: getBackToMainMenuButton()
91
+ };
92
+ }
93
+
94
+ const match = ctx.match;
95
+ if (!match) {
96
+ return {
97
+ message: messageManager.getMessage('error_invalid_filter'),
98
+ options: getBackToMainMenuButton()
99
+ };
100
+ }
101
+
102
+ const filterType = match[1];
103
+ const userState = userStates.get(telegramId);
104
+ if (!userState) {
105
+ return {
106
+ message: messageManager.getMessage('error_user_state_not_found'),
107
+ options: getBackToMainMenuButton()
108
+ };
109
+ }
110
+
111
+ // Apply filter based on type
112
+ switch (filterType) {
113
+ case 'today':
114
+ userState.filters.date_filter = 'today';
115
+ break;
116
+ case 'tomorrow':
117
+ userState.filters.date_filter = 'tomorrow';
118
+ break;
119
+ case 'this_week':
120
+ userState.filters.date_filter = 'this_week';
121
+ break;
122
+ case 'free':
123
+ userState.filters.price_max = 0;
124
+ break;
125
+ case 'paid':
126
+ userState.filters.price_min = 1;
127
+ break;
128
+ default:
129
+ // Handle category filters
130
+ if (filterType.startsWith('cat_')) {
131
+ const category = filterType.replace('cat_', '');
132
+ userState.filters.category = category;
133
+ }
134
+ }
135
+
136
+ userState.page = 0; // Reset to first page when applying filter
137
+ userStates.set(telegramId, userState);
138
+
139
+ return await fetchAndDisplayEvents(telegramId, 0);
140
+ } catch (error: any) {
141
+ logger.error(`Error in WeBook filter action: ${error.message}`);
142
+ return {
143
+ message: messageManager.getMessage('error_general'),
144
+ options: getBackToMainMenuButton()
145
+ };
146
+ }
147
+ };
148
+
149
+ export const handleWeBookClearFiltersAction = async (ctx: BotContext) => {
150
+ try {
151
+ const telegramId = ctx.from?.id;
152
+ if (!telegramId) {
153
+ return {
154
+ message: messageManager.getMessage('error_user_not_found'),
155
+ options: getBackToMainMenuButton()
156
+ };
157
+ }
158
+
159
+ // Reset user state to default
160
+ userStates.set(telegramId, {
161
+ page: 0,
162
+ filters: { country_code: 'SA', limit: EVENTS_PER_PAGE },
163
+ totalEvents: 0
164
+ });
165
+
166
+ return await fetchAndDisplayEvents(telegramId, 0);
167
+ } catch (error: any) {
168
+ logger.error(`Error in WeBook clear filters action: ${error.message}`);
169
+ return {
170
+ message: messageManager.getMessage('error_general'),
171
+ options: getBackToMainMenuButton()
172
+ };
173
+ }
174
+ };
175
+
176
+ export const handleWeBookBackToMenuAction = async (ctx: BotContext) => {
177
+ return {
178
+ message: messageManager.getMessage('main_menu_welcome_back').replace('{name}', ctx.from?.first_name || 'User'),
179
+ options: getLoggedInMenuKeyboard()
180
+ };
181
+ };
182
+
183
+ async function fetchAndDisplayEvents(telegramId: number, page: number) {
184
+ try {
185
+ const userState = userStates.get(telegramId);
186
+ if (!userState) {
187
+ return {
188
+ message: messageManager.getMessage('error_user_state_not_found'),
189
+ options: getBackToMainMenuButton()
190
+ };
191
+ }
192
+
193
+ // Update page and calculate skip
194
+ userState.page = page;
195
+ const skip = page * EVENTS_PER_PAGE;
196
+ const filters = { ...userState.filters, skip, limit: EVENTS_PER_PAGE };
197
+
198
+ // Fetch events from WeBook API
199
+ const response = await webookAPI.getEvents(filters);
200
+ const events = response.data.eventCollection.items;
201
+ const total = response.data.eventCollection.total;
202
+
203
+ userState.totalEvents = total;
204
+ userStates.set(telegramId, userState);
205
+
206
+ if (events.length === 0) {
207
+ return {
208
+ message: '🎭 No events found with the current filters.\n\nTry adjusting your search criteria or clearing filters.',
209
+ options: getWeBookEventsKeyboard(page, total, userState.filters)
210
+ };
211
+ }
212
+
213
+ // Build events message
214
+ let message = `🎭 <b>WeBook Events</b>\n\n`;
215
+ message += `📊 Showing ${skip + 1}-${Math.min(skip + events.length, total)} of ${total} events\n\n`;
216
+
217
+ // Add active filters info
218
+ const activeFilters = getActiveFiltersText(userState.filters);
219
+ if (activeFilters) {
220
+ message += `🔍 <b>Active Filters:</b> ${activeFilters}\n\n`;
221
+ }
222
+
223
+ // Display events
224
+ events.forEach((event, index) => {
225
+ const eventNumber = skip + index + 1;
226
+ message += `${eventNumber}. <b>${event.title || 'Untitled Event'}</b>\n`;
227
+
228
+ if (event.subtitle) {
229
+ message += ` ${event.subtitle}\n`;
230
+ }
231
+
232
+ if (event.schedule?.openDateTime) {
233
+ const eventDate = new Date(event.schedule.openDateTime).toLocaleDateString('en-US', {
234
+ weekday: 'short',
235
+ year: 'numeric',
236
+ month: 'short',
237
+ day: 'numeric'
238
+ });
239
+ message += ` 📅 ${eventDate}\n`;
240
+ }
241
+
242
+ if (event.location?.city) {
243
+ message += ` 📍 ${event.location.city}\n`;
244
+ }
245
+
246
+ if (event.startingPrice !== undefined) {
247
+ const priceText = event.startingPrice === 0 ? 'Free' : `${event.startingPrice} ${event.currencyCode || 'SAR'}`;
248
+ message += ` 💰 ${priceText}\n`;
249
+ }
250
+
251
+ if (event.zone?.title) {
252
+ message += ` 🎪 ${event.zone.title}\n`;
253
+ }
254
+
255
+ message += '\n';
256
+ });
257
+
258
+ return {
259
+ message,
260
+ options: getWeBookEventsKeyboard(page, total, userState.filters)
261
+ };
262
+ } catch (error: any) {
263
+ logger.error(`Error fetching WeBook events: ${error.message}`);
264
+ return {
265
+ message: '❌ Error fetching events. Please try again later.',
266
+ options: getBackToMainMenuButton()
267
+ };
268
+ }
269
+ }
270
+
271
+ function getWeBookEventsKeyboard(page: number, total: number, filters: EventFilters) {
272
+ const buttons = [];
273
+ const totalPages = Math.ceil(total / EVENTS_PER_PAGE);
274
+
275
+ // Filter buttons
276
+ const filterRow = [];
277
+ filterRow.push(Markup.button.callback('📅 Today', 'webook_filter_today'));
278
+ filterRow.push(Markup.button.callback('📅 Tomorrow', 'webook_filter_tomorrow'));
279
+ filterRow.push(Markup.button.callback('📅 This Week', 'webook_filter_this_week'));
280
+ buttons.push(filterRow);
281
+
282
+ const priceRow = [];
283
+ priceRow.push(Markup.button.callback('🆓 Free', 'webook_filter_free'));
284
+ priceRow.push(Markup.button.callback('💰 Paid', 'webook_filter_paid'));
285
+ priceRow.push(Markup.button.callback('🧹 Clear Filters', 'webook_clear_filters'));
286
+ buttons.push(priceRow);
287
+
288
+ // Pagination buttons
289
+ const paginationRow = [];
290
+ if (page > 0) {
291
+ paginationRow.push(Markup.button.callback('⬅️ Previous', `webook_page_${page - 1}`));
292
+ }
293
+
294
+ paginationRow.push(Markup.button.callback(`📄 ${page + 1}/${totalPages}`, 'noop'));
295
+
296
+ if (page < totalPages - 1) {
297
+ paginationRow.push(Markup.button.callback('Next ➡️', `webook_page_${page + 1}`));
298
+ }
299
+
300
+ if (paginationRow.length > 0) {
301
+ buttons.push(paginationRow);
302
+ }
303
+
304
+ // Back button
305
+ buttons.push([Markup.button.callback('🔙 Back to Menu', 'webook_back_to_menu')]);
306
+
307
+ return Markup.inlineKeyboard(buttons);
308
+ }
309
+
310
+ function getActiveFiltersText(filters: EventFilters): string {
311
+ const activeFilters = [];
312
+
313
+ if (filters.date_filter) {
314
+ const dateMap = {
315
+ 'today': 'Today',
316
+ 'tomorrow': 'Tomorrow',
317
+ 'this_week': 'This Week'
318
+ };
319
+ activeFilters.push(dateMap[filters.date_filter]);
320
+ }
321
+
322
+ if (filters.price_min !== undefined && filters.price_min > 0) {
323
+ activeFilters.push('Paid Events');
324
+ }
325
+
326
+ if (filters.price_max !== undefined && filters.price_max === 0) {
327
+ activeFilters.push('Free Events');
328
+ }
329
+
330
+ if (filters.category) {
331
+ const category = Array.isArray(filters.category) ? filters.category[0] : filters.category;
332
+ activeFilters.push(`Category: ${category}`);
333
+ }
334
+
335
+ return activeFilters.length > 0 ? activeFilters.join(', ') : '';
336
+ }
src/bots/utils/keyboardUtils.ts CHANGED
@@ -22,6 +22,7 @@ export const getMainMenuKeyboard = () => {
22
  export const getLoggedInMenuKeyboard = () => {
23
  return Markup.inlineKeyboard([
24
  [Markup.button.callback('🔍 Browse Services', 'browse_services')],
 
25
  [
26
  Markup.button.callback(messageManager.getMessage('btn_profile'), 'profile'),
27
  Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')
 
22
  export const getLoggedInMenuKeyboard = () => {
23
  return Markup.inlineKeyboard([
24
  [Markup.button.callback('🔍 Browse Services', 'browse_services')],
25
+ [Markup.button.callback('🎭 WeBook Events', 'webook_events')],
26
  [
27
  Markup.button.callback(messageManager.getMessage('btn_profile'), 'profile'),
28
  Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')
src/bots/utils/messageManager.ts CHANGED
@@ -98,6 +98,26 @@ class MessageManager {
98
  'no_affordable_products': {
99
  ar_value: 'عذراً، لا توجد منتجات يمكنك شراؤها برصيدك الحالي ({balance}).',
100
  en_value: 'Sorry, no products can be bought with your current balance ({balance}).'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
  };
103
 
 
98
  'no_affordable_products': {
99
  ar_value: 'عذراً، لا توجد منتجات يمكنك شراؤها برصيدك الحالي ({balance}).',
100
  en_value: 'Sorry, no products can be bought with your current balance ({balance}).'
101
+ },
102
+ 'error_user_not_found': {
103
+ ar_value: '❌ لم يتم العثور على معلومات المستخدم.',
104
+ en_value: '❌ User information not found.'
105
+ },
106
+ 'error_invalid_page': {
107
+ ar_value: '❌ رقم الصفحة غير صحيح.',
108
+ en_value: '❌ Invalid page number.'
109
+ },
110
+ 'error_invalid_filter': {
111
+ ar_value: '❌ نوع الفلتر غير صحيح.',
112
+ en_value: '❌ Invalid filter type.'
113
+ },
114
+ 'error_user_state_not_found': {
115
+ ar_value: '❌ لم يتم العثور على حالة المستخدم.',
116
+ en_value: '❌ User state not found.'
117
+ },
118
+ 'error_general': {
119
+ ar_value: '❌ حدث خطأ. يرجى المحاولة مرة أخرى.',
120
+ en_value: '❌ An error occurred. Please try again.'
121
  }
122
  };
123
 
src/index.ts CHANGED
@@ -37,4 +37,4 @@ startServer();
37
  // To use the WeBook main function, uncomment the following line:
38
  webookMain().catch(e => { console.error('WeBook Fatal error:', e); });
39
 
40
- // handleAddTelegrafBot("049a92c4-7654-43f6-8e6f-7ff5cce78995")
 
37
  // To use the WeBook main function, uncomment the following line:
38
  webookMain().catch(e => { console.error('WeBook Fatal error:', e); });
39
 
40
+ // handleAddTelegrafBot("1e186256-fdd6-453f-8344-fd824660b8bd")
src/webook/main.ts CHANGED
@@ -4,7 +4,7 @@ import { WeBookBooking } from './book';
4
  export async function main() {
5
  const email = 'mfoud444@gmail.com'; // Replace with your email
6
  const password = '009988Ppooii@@@@'; // Replace with your password
7
- const eventUrl = 'https://webook.com/en/event/your-event-id'; // Replace with your event URL
8
 
9
  const login = new WeBookLogin(email, password);
10
  const page = await login.login();
@@ -13,11 +13,11 @@ export async function main() {
13
  process.exit(1);
14
  }
15
 
16
- // const booking = new WeBookBooking(page);
17
- // const booked = await booking.bookEvent(eventUrl);
18
- // if (booked) {
19
- // console.info('Booking succeeded!');
20
- // } else {
21
- // console.error('Booking failed.');
22
- // }
23
  }
 
4
  export async function main() {
5
  const email = 'mfoud444@gmail.com'; // Replace with your email
6
  const password = '009988Ppooii@@@@'; // Replace with your password
7
+ const eventUrl = 'https://webook.com/en/events/ewc-opening-ceremony-2025-tickets/book'; // Replace with your event URL
8
 
9
  const login = new WeBookLogin(email, password);
10
  const page = await login.login();
 
13
  process.exit(1);
14
  }
15
 
16
+ const booking = new WeBookBooking(page);
17
+ const booked = await booking.bookEvent(eventUrl);
18
+ if (booked) {
19
+ console.info('Booking succeeded!');
20
+ } else {
21
+ console.error('Booking failed.');
22
+ }
23
  }
src/webook/webookApi.ts ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, { AxiosResponse } from 'axios';
2
+
3
+ // Type definitions
4
+ export interface EventFilter {
5
+ visibility_not?: string;
6
+ AND?: any[];
7
+ OR?: any[];
8
+ }
9
+
10
+ export interface EventSchedule {
11
+ title?: string;
12
+ openTitle?: string;
13
+ openDateTime?: string;
14
+ closeDateTime?: string;
15
+ openScheduleText?: string;
16
+ }
17
+
18
+ export interface EventLocation {
19
+ title?: string;
20
+ address?: string;
21
+ city?: string;
22
+ countryCode?: string;
23
+ seactionHeader?: string;
24
+ location?: {
25
+ lat: number;
26
+ lon: number;
27
+ };
28
+ banner?: {
29
+ title?: string;
30
+ sys?: {
31
+ id: string;
32
+ publishedAt: string;
33
+ };
34
+ url?: string;
35
+ width?: number;
36
+ height?: number;
37
+ contentType?: string;
38
+ };
39
+ accessibility?: any;
40
+ }
41
+
42
+ export interface EventZone {
43
+ id?: string;
44
+ slug?: string;
45
+ title?: string;
46
+ zoneLogo?: {
47
+ title?: string;
48
+ sys?: {
49
+ id: string;
50
+ publishedAt: string;
51
+ };
52
+ url?: string;
53
+ width?: number;
54
+ height?: number;
55
+ contentType?: string;
56
+ };
57
+ sponsorLogo?: {
58
+ title?: string;
59
+ sys?: {
60
+ id: string;
61
+ publishedAt: string;
62
+ };
63
+ url?: string;
64
+ width?: number;
65
+ height?: number;
66
+ contentType?: string;
67
+ };
68
+ }
69
+
70
+ export interface EventCategory {
71
+ id?: string;
72
+ title?: string;
73
+ slug?: string;
74
+ }
75
+
76
+ export interface EventImage {
77
+ title?: string;
78
+ sys?: {
79
+ id: string;
80
+ publishedAt: string;
81
+ };
82
+ url?: string;
83
+ width?: number;
84
+ height?: number;
85
+ contentType?: string;
86
+ }
87
+
88
+ export interface Event {
89
+ __typename?: string;
90
+ sys?: {
91
+ id: string;
92
+ };
93
+ id?: string;
94
+ title?: string;
95
+ subtitle?: string;
96
+ slug?: string;
97
+ ticketingUrlSlug?: string;
98
+ image11?: EventImage;
99
+ image31?: EventImage;
100
+ startingPrice?: number;
101
+ currencyCode?: string;
102
+ schedule?: EventSchedule;
103
+ isStreamingEvent?: boolean;
104
+ zoneEntryIncluded?: boolean;
105
+ streamingUrl?: string;
106
+ buttonLabel?: string;
107
+ cardButtonLabel?: string;
108
+ eventType?: string;
109
+ buttonLink?: string;
110
+ zone?: EventZone;
111
+ location?: EventLocation;
112
+ category?: EventCategory;
113
+ isComingSoon?: boolean;
114
+ organizationSlug?: string;
115
+ carousalCollection?: {
116
+ items?: EventImage[];
117
+ };
118
+ }
119
+
120
+ export interface EventCollection {
121
+ total: number;
122
+ items: Event[];
123
+ }
124
+
125
+ export interface GraphQLResponse {
126
+ data: {
127
+ eventCollection: EventCollection;
128
+ };
129
+ }
130
+
131
+ export interface EventFilters {
132
+ category?: string | string[];
133
+ date_filter?: 'today' | 'tomorrow' | 'this_week';
134
+ price_min?: number;
135
+ price_max?: number;
136
+ zones?: string[];
137
+ country_code?: string;
138
+ limit?: number;
139
+ skip?: number;
140
+ }
141
+
142
+ export class WeBookAPI {
143
+ private url: string = "https://cdn.webook.com/";
144
+ private headers = {
145
+ "Accept": "*/*",
146
+ "Accept-Encoding": "gzip, deflate, br, zstd",
147
+ "Accept-Language": "en-US,en;q=0.9,ar;q=0.8",
148
+ "Content-Type": "application/json",
149
+ "Origin": "https://webook.com",
150
+ "Referer": "https://webook.com/",
151
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
152
+ };
153
+
154
+ private getEventsPayload(filters: EventFilters = {}): any {
155
+ const {
156
+ category,
157
+ date_filter,
158
+ price_min,
159
+ price_max,
160
+ zones,
161
+ country_code = "SA",
162
+ limit = 20,
163
+ skip = 0
164
+ } = filters;
165
+
166
+ const now = new Date();
167
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
168
+ const endOfWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
169
+
170
+ // Construct Date Filter Clause
171
+ const dateScheduleOrClauses: any[] = [];
172
+
173
+ // Add clause for events without close date or ending from now onwards
174
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
175
+ dateScheduleOrClauses.push({
176
+ "OR": [
177
+ { "schedule": { "closeDateTime_exists": false } },
178
+ { "schedule": { "closeDateTime_gte": startOfToday.toISOString() } }
179
+ ]
180
+ });
181
+
182
+ // Add specific date range clause based on date_filter
183
+ let dateRangeAndClause: any = {};
184
+ if (date_filter === 'today') {
185
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
186
+ const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
187
+ dateRangeAndClause = {
188
+ "AND": [
189
+ { "schedule": { "openDateTime_lte": endOfDay.toISOString() } },
190
+ { "schedule": { "closeDateTime_gte": startOfDay.toISOString() } }
191
+ ]
192
+ };
193
+ } else if (date_filter === 'tomorrow') {
194
+ const tomorrowStart = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate());
195
+ const tomorrowEnd = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate(), 23, 59, 59, 999);
196
+ dateRangeAndClause = {
197
+ "AND": [
198
+ { "schedule": { "openDateTime_lte": tomorrowEnd.toISOString() } },
199
+ { "schedule": { "closeDateTime_gte": tomorrowStart.toISOString() } }
200
+ ]
201
+ };
202
+ } else if (date_filter === 'this_week') {
203
+ const startOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate());
204
+ const endOfWeekDate = new Date(endOfWeek.getFullYear(), endOfWeek.getMonth(), endOfWeek.getDate(), 23, 59, 59, 999);
205
+ dateRangeAndClause = {
206
+ "AND": [
207
+ { "schedule": { "openDateTime_lte": endOfWeekDate.toISOString() } },
208
+ { "schedule": { "closeDateTime_gte": startOfWeek.toISOString() } }
209
+ ]
210
+ };
211
+ }
212
+
213
+ if (Object.keys(dateRangeAndClause).length > 0) {
214
+ dateScheduleOrClauses.push(dateRangeAndClause);
215
+ }
216
+
217
+ // Construct Category Filter Clause
218
+ let categoryClause: any = {};
219
+ if (category) {
220
+ const categoryArray = Array.isArray(category) ? category : [category];
221
+ categoryClause = { "OR": [{ "category": { "slug_in": categoryArray } }] };
222
+ }
223
+
224
+ // Construct Price Filter Clause
225
+ let priceClause: any = {};
226
+ const priceAndClauses: any[] = [];
227
+ if (price_min !== undefined) {
228
+ priceAndClauses.push({ "startingPrice_gte": price_min });
229
+ }
230
+ if (price_max !== undefined) {
231
+ priceAndClauses.push({ "startingPrice_lte": price_max });
232
+ }
233
+ if (priceAndClauses.length > 0) {
234
+ priceClause = { "AND": priceAndClauses };
235
+ }
236
+
237
+ // Construct Zone Filter Clause
238
+ let zoneClause: any = {};
239
+ if (zones && zones.length > 0) {
240
+ zoneClause = { "OR": [{ "zone": { "slug_in": zones } }] };
241
+ }
242
+
243
+ // Construct Country Filter Clause
244
+ let countryClause: any = {};
245
+ if (country_code) {
246
+ countryClause = { "OR": [{ "location": { "countryCode": country_code } }] };
247
+ }
248
+
249
+ // Build the main 'AND' array for the 'where' clause
250
+ const andClauses: any[] = [];
251
+
252
+ if (Object.keys(zoneClause).length > 0) {
253
+ andClauses.push(zoneClause);
254
+ }
255
+
256
+ if (Object.keys(categoryClause).length > 0) {
257
+ andClauses.push(categoryClause);
258
+ }
259
+
260
+ andClauses.push({}); // Placeholder
261
+
262
+ if (Object.keys(priceClause).length > 0) {
263
+ andClauses.push(priceClause);
264
+ }
265
+
266
+ andClauses.push({}); // Placeholder
267
+
268
+ // Add the combined date schedule OR clause
269
+ if (dateScheduleOrClauses.length > 0) {
270
+ if (dateScheduleOrClauses.length === 1 && "OR" in dateScheduleOrClauses[0]) {
271
+ andClauses.push(dateScheduleOrClauses[0]);
272
+ } else {
273
+ andClauses.push({ "OR": dateScheduleOrClauses });
274
+ }
275
+ }
276
+
277
+ if (Object.keys(countryClause).length > 0) {
278
+ andClauses.push(countryClause);
279
+ }
280
+
281
+ // Construct the final 'where' clause
282
+ const whereClause: EventFilter = {
283
+ "visibility_not": "private",
284
+ };
285
+
286
+ if (andClauses.length > 0) {
287
+ whereClause.AND = andClauses;
288
+ }
289
+
290
+ return {
291
+ "query": `query getEventListing($lang:String,$limit:Int,$skip:Int,$where:EventFilter,$order:[EventOrder]){
292
+ eventCollection(locale:$lang,limit:$limit,skip:$skip,where:$where,order:$order){
293
+ total items{
294
+ __typename sys{id}id title subtitle slug ticketingUrlSlug
295
+ image11{title sys{id publishedAt}url width height contentType}
296
+ image31{title sys{id publishedAt}url width height contentType}
297
+ startingPrice currencyCode
298
+ schedule{title openTitle openDateTime closeDateTime openScheduleText}
299
+ isStreamingEvent zoneEntryIncluded streamingUrl buttonLabel cardButtonLabel
300
+ eventType buttonLink
301
+ zone{id slug title zoneLogo{title sys{id publishedAt}url width height contentType}
302
+ sponsorLogo{title sys{id publishedAt}url width height contentType}}
303
+ location{title address city countryCode seactionHeader location{lat lon}
304
+ banner{title sys{id publishedAt}url width height contentType} accessibility}
305
+ category{id title slug} isComingSoon organizationSlug
306
+ carousalCollection(limit:10){
307
+ items{title sys{id publishedAt}url width height contentType}
308
+ }
309
+ }
310
+ }
311
+ }`,
312
+ "variables": {
313
+ "order": ["sys_publishedAt_DESC"],
314
+ "lang": "ar-SA",
315
+ "limit": limit,
316
+ "skip": skip,
317
+ "where": whereClause,
318
+ },
319
+ "operationName": "getEventListing"
320
+ };
321
+ }
322
+
323
+ async getEvents(filters: EventFilters = {}): Promise<GraphQLResponse> {
324
+ try {
325
+ const payload = this.getEventsPayload(filters);
326
+ const response: AxiosResponse<GraphQLResponse> = await axios.post(
327
+ this.url,
328
+ payload,
329
+ { headers: this.headers }
330
+ );
331
+ return response.data;
332
+ } catch (error) {
333
+ console.error('Error fetching events:', error);
334
+ throw error;
335
+ }
336
+ }
337
+
338
+ async makeRequest(payload: any): Promise<any> {
339
+ try {
340
+ const response = await axios.post(this.url, payload, { headers: this.headers });
341
+ return response.data;
342
+ } catch (error) {
343
+ console.error('Error making request:', error);
344
+ throw error;
345
+ }
346
+ }
347
+ }
348
+
349
+ // Export a default instance
350
+ export const webookAPI = new WeBookAPI();
src/webook/webookBase.ts CHANGED
@@ -32,7 +32,7 @@ export class WeBookBase {
32
  if (!this.page) {
33
  const headless = process.env.PLAYWRIGHT_HEADLESS !== 'false';
34
  this.browser = await chromium.launchPersistentContext(this.profileDir, {
35
- headless: false,
36
  args: [],
37
  });
38
  this.page = this.browser.pages()[0] || await this.browser.newPage();
 
32
  if (!this.page) {
33
  const headless = process.env.PLAYWRIGHT_HEADLESS !== 'false';
34
  this.browser = await chromium.launchPersistentContext(this.profileDir, {
35
+ headless: headless,
36
  args: [],
37
  });
38
  this.page = this.browser.pages()[0] || await this.browser.newPage();