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