// src/services/NotificationService.js const express = require('express'); const { createMatchRequestEmbed } = require('../utils/embedBuilders'); const { createMatchActionRow } = require('../utils/componentBuilders'); class NotificationService { constructor(bot, supabase) { this.bot = bot; this.supabase = supabase; this.app = express(); this.app.use(express.json()); this.setupRoutes(); } setupRoutes() { this.app.post('/api/match-notification', this.handleMatchNotification.bind(this)); } async start(port = 3000) { return new Promise((resolve, reject) => { try { this.server = this.app.listen(port, () => { console.log(`Notification service listening on port ${port}`); resolve(); }); this.server.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.log(`Port ${port} is in use, trying port ${port + 1}`); this.start(port + 1).then(resolve).catch(reject); } else { reject(error); } }); } catch (error) { reject(error); } }); } async stop() { if (this.server) { return new Promise((resolve) => { this.server.close(resolve); }); } } async handleMatchNotification(req, res) { try { const authToken = req.headers['x-webhook-token']; if (authToken !== process.env.WEBHOOK_SECRET) { return res.status(401).json({ error: 'Unauthorized' }); } const matchData = req.body; if (!this.validateMatchData(matchData)) { return res.status(400).json({ error: 'Invalid match data' }); } matchData.match_date = `${matchData.match_date}T${matchData.match_time}Z`; const { data: subscriptions, error } = await this.supabase .from('active_subscriptions') .select('*') .eq('game_name', matchData.game_name); if (error) { console.error('Error fetching subscriptions:', error); return res.status(500).json({ error: 'Failed to fetch subscriptions' }); } const results = []; for (const subscription of (subscriptions || [])) { try { const channel = await this.bot.client.channels.fetch(subscription.notification_channel_id); // Check if bot has permissions to send messages in this channel if (!channel.permissionsFor(this.bot.client.user).has(['SendMessages', 'ViewChannel'])) { console.warn(`Missing permissions for channel ${subscription.notification_channel_id} in server ${channel.guild.name}`); results.push({ channelId: subscription.notification_channel_id, status: 'failed', error: 'Missing permissions' }); continue; } const embed = createMatchRequestEmbed(matchData); const actionRow = createMatchActionRow(matchData.game_name); await channel.send({ embeds: [embed], components: [actionRow] }); results.push({ channelId: subscription.notification_channel_id, status: 'success' }); } catch (error) { console.error(`Error sending notification to channel ${subscription.notification_channel_id}:`, error); results.push({ channelId: subscription.notification_channel_id, status: 'failed', error: error.message }); } } res.json({ success: true, results, summary: { total: results.length, successful: results.filter(r => r.status === 'success').length, failed: results.filter(r => r.status === 'failed').length } }); } catch (error) { console.error('Error handling match notification:', error); res.status(500).json({ error: 'Internal server error' }); } } validateMatchData(matchData) { const requiredFields = [ 'type', 'game_name', 'game_id', 'created_by_user_id', 'status', 'team_size', 'match_type', 'region', 'match_date', 'match_time', 'match_class' ]; // Check if all required fields are present const hasAllFields = requiredFields.every(field => { const hasField = matchData.hasOwnProperty(field) && matchData[field] !== null && matchData[field] !== undefined; if (!hasField) { console.log(`Missing or null field: ${field}`); } return hasField; }); if (!hasAllFields) { return false; } // Validate team size format (should be a number or string containing a number) const teamSize = matchData.team_size.toString().replace(/[^0-9]/g, ''); if (!teamSize || isNaN(teamSize) || teamSize < 1) { console.log('Invalid team size format'); return false; } return true; } } module.exports = NotificationService;