Introduces a new interactive help command with button navigation, adds a detailed DEPLOYMENT.md guide, and improves server setup validation and error handling. Updates command registration to include all 9 games, adds version reporting, enhances Docker deployment with a multi-platform script, and removes local .env files from the repo. Also refactors bot startup for better diagnostics and graceful shutdown.
167 lines
5.8 KiB
JavaScript
167 lines
5.8 KiB
JavaScript
// 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; |