Files
Jellystat/backend/classes/webhook-manager.js

418 lines
15 KiB
JavaScript

const axios = require('axios');
const dbInstance = require('../db');
const EventEmitter = require('events');
class WebhookManager {
constructor() {
if (WebhookManager.instance) {
return WebhookManager.instance;
}
this.eventEmitter = new EventEmitter();
this.setupEventListeners();
WebhookManager.instance = this;
}
setupEventListeners() {
// Adding event listeners for different events
this.eventEmitter.on('playback_started', async (data) => {
await this.triggerEventWebhooks('playback_started', data);
});
this.eventEmitter.on('playback_ended', async (data) => {
await this.triggerEventWebhooks('playback_ended', data);
});
this.eventEmitter.on('media_recently_added', async (data) => {
await this.triggerEventWebhooks('media_recently_added', data);
});
// If needed, add more event listeners here
}
async getWebhooksByEventType(eventType) {
return await dbInstance.query(
'SELECT * FROM webhooks WHERE trigger_type = $1 AND event_type = $2 AND enabled = true',
['event', eventType]
).then(res => res.rows);
}
async getScheduledWebhooks() {
return await dbInstance.query(
'SELECT * FROM webhooks WHERE trigger_type = $1 AND enabled = true',
['scheduled']
).then(res => res.rows);
}
async triggerEventWebhooks(eventType, data = {}) {
try {
const webhooks = await this.getWebhooksByEventType(eventType);
if (webhooks.length === 0) {
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
return;
}
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
const enrichedData = {
...data,
event: eventType,
triggeredAt: new Date().toISOString()
};
const promises = webhooks.map(webhook => {
return this.executeWebhook(webhook, enrichedData);
});
await Promise.all(promises);
return true;
} catch (error) {
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
return false;
}
}
async executeWebhook(webhook, data = {}) {
try {
let headers = {};
let payload = {};
const isDiscordWebhook = webhook.url.includes('discord.com/api/webhooks');
try {
headers = typeof webhook.headers === 'string'
? JSON.parse(webhook.headers || '{}')
: (webhook.headers || {});
payload = typeof webhook.payload === 'string'
? JSON.parse(webhook.payload || '{}')
: (webhook.payload || {});
} catch (e) {
console.error("[WEBHOOK] Error while parsing:", e);
return false;
}
if (isDiscordWebhook) {
console.log("[WEBHOOK] Webhook Discord detected");
await axios({
method: webhook.method || 'POST',
url: webhook.url,
headers: { 'Content-Type': 'application/json' },
data: payload,
timeout: 10000
});
console.log(`[WEBHOOK] Discord webhook ${webhook.name} send successfully`);
} else {
const compiledPayload = this.compileTemplate(payload, data);
await axios({
method: webhook.method || 'POST',
url: webhook.url,
headers,
data: compiledPayload,
timeout: 10000
});
console.log(`[WEBHOOK] Webhook ${webhook.name} send successfully`);
}
//Update the last triggered timestamp
await dbInstance.query(
'UPDATE webhooks SET last_triggered = NOW() WHERE id = $1',
[webhook.id]
);
return true;
} catch (error) {
console.error(`[WEBHOOK] Error triggering webhook ${webhook.name}:`, error.message);
if (error.response) {
console.error(`[WEBHOOK] Response status: ${error.response.status}`);
console.error(`[WEBHOOK] Response data:`, error.response.data);
}
return false;
}
}
compileTemplate(template, data) {
if (typeof template === 'object') {
return Object.keys(template).reduce((result, key) => {
result[key] = this.compileTemplate(template[key], data);
return result;
}, {});
} else if (typeof template === 'string') {
// Replace {{variable}} with the corresponding value from data
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
const keys = path.trim().split('.');
let value = data;
for (const key of keys) {
if (value === undefined) return match;
value = value[key];
}
return value !== undefined ? value : match;
});
}
return template;
}
async triggerEvent(eventType, eventData = {}) {
try {
const webhooks = this.eventWebhooks?.[eventType] || [];
if (webhooks.length === 0) {
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
return;
}
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
const promises = webhooks.map(webhook => {
return this.webhookManager.executeWebhook(webhook, {
...eventData,
event: eventType,
triggeredAt: new Date().toISOString()
});
});
await Promise.all(promises);
} catch (error) {
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
}
}
emitEvent(eventType, data) {
this.eventEmitter.emit(eventType, data);
}
async getTopWatchedContent(contentType, period = 'month', limit = 5) {
// Calculate period start date
const today = new Date();
let startDate;
if (period === 'month') {
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
} else if (period === 'week') {
const day = today.getDay();
startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - day - 7);
} else {
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
}
const formattedStartDate = startDate.toISOString().split('T')[0];
// SQL query to get top watched content
let query;
if (contentType === 'movie') {
query = `
SELECT
"NowPlayingItemName" as title,
COUNT(DISTINCT "UserId") as unique_viewers,
SUM("PlaybackDuration") / 60000 as total_minutes
FROM jf_playback_activity
WHERE "ActivityDateInserted" >= $1
AND "NowPlayingItemName" IS NOT NULL
AND "SeriesName" IS NULL
GROUP BY "NowPlayingItemName", "NowPlayingItemId"
ORDER BY total_minutes DESC
LIMIT $2
`;
} else if (contentType === 'series') {
query = `
SELECT
"SeriesName" as title,
COUNT(DISTINCT "UserId") as unique_viewers,
SUM("PlaybackDuration") / 60000 as total_minutes
FROM jf_playback_activity
WHERE "ActivityDateInserted" >= $1
AND "SeriesName" IS NOT NULL
GROUP BY "SeriesName"
ORDER BY total_minutes DESC
LIMIT $2
`;
}
try {
const result = await dbInstance.query(query, [formattedStartDate, limit]);
return result.rows || [];
} catch (error) {
console.error(`[WEBHOOK] SQL ERROR (${contentType}):`, error.message);
return [];
}
}
async getMonthlySummaryData() {
try {
// Get the top watched movies and series
const topMovies = await this.getTopWatchedContent('movie', 'month', 5);
const topSeries = await this.getTopWatchedContent('series', 'month', 5);
const prevMonth = new Date();
prevMonth.setMonth(prevMonth.getMonth() - 1);
const prevMonthStart = new Date(prevMonth.getFullYear(), prevMonth.getMonth(), 1);
const prevMonthEnd = new Date(prevMonth.getFullYear(), prevMonth.getMonth() + 1, 0);
const formattedStart = prevMonthStart.toISOString().split('T')[0];
const formattedEnd = prevMonthEnd.toISOString().split('T')[0];
// Get general statistics
const statsQuery = `
SELECT
COUNT(DISTINCT "UserId") as active_users,
COUNT(*) as total_plays,
SUM("PlaybackDuration") / 3600000 as total_hours
FROM jf_playback_activity
WHERE "ActivityDateInserted" BETWEEN $1 AND $2
`;
const statsResult = await dbInstance.query(statsQuery, [formattedStart, formattedEnd]);
const generalStats = statsResult.rows[0] || {
active_users: 0,
total_plays: 0,
total_hours: 0
};
return {
period: {
start: formattedStart,
end: formattedEnd,
name: prevMonth.toLocaleString('fr-FR', { month: 'long', year: 'numeric' })
},
topMovies,
topSeries,
stats: generalStats
};
} catch (error) {
console.error("[WEBHOOK] Error while getting data:", error.message);
throw error;
}
}
async triggerMonthlySummaryWebhook(webhookId) {
try {
// Get the webhook details
const result = await dbInstance.query(
'SELECT * FROM webhooks WHERE id = $1 AND enabled = true',
[webhookId]
);
if (result.rows.length === 0) {
console.error(`[WEBHOOK] Webhook ID ${webhookId} not found or disable`);
return false;
}
const webhook = result.rows[0];
// Generate the monthly summary data
try {
const data = await this.getMonthlySummaryData();
const moviesFields = data.topMovies.map((movie, index) => ({
name: `${index + 1}. ${movie.title}`,
value: `${Math.round(movie.total_minutes)} minutes • ${movie.unique_viewers} viewers`,
inline: false
}));
const seriesFields = data.topSeries.map((series, index) => ({
name: `${index + 1}. ${series.title}`,
value: `${Math.round(series.total_minutes)} minutes • ${series.unique_viewers} viewers`,
inline: false
}));
const monthlyPayload = {
content: `📊 **Monthly Report - ${data.period.name}**`,
embeds: [
{
title: "🎬 Most Watched Movies",
color: 15844367,
fields: moviesFields.length > 0 ? moviesFields : [{ name: "No data", value: "No movies watch this month" }]
},
{
title: "📺 Most Watched Series",
color: 5793266,
fields: seriesFields.length > 0 ? seriesFields : [{ name: "No data", value: "No Series watch this month" }]
},
{
title: "📈 General Statistics",
color: 5763719,
fields: [
{
name: "Active Users",
value: `${data.stats.active_users || 0}`,
inline: true
},
{
name: "Total Plays",
value: `${data.stats.total_plays || 0}`,
inline: true
},
{
name: "Total Hours Watched",
value: `${Math.round(data.stats.total_hours || 0)}`,
inline: true
}
],
footer: {
text: `Period: from ${new Date(data.period.start).toLocaleDateString('en-US')} to ${new Date(data.period.end).toLocaleDateString('en-US')}`
}
}
]
};
// Send the webhook
await axios({
method: webhook.method || 'POST',
url: webhook.url,
headers: { 'Content-Type': 'application/json' },
data: monthlyPayload,
timeout: 10000
});
console.log(`[WEBHOOK] Monthly report webhook ${webhook.name} sent successfully`);
// Update the last triggered timestamp
await dbInstance.query(
'UPDATE webhooks SET last_triggered = NOW() WHERE id = $1',
[webhook.id]
);
return true;
} catch (dataError) {
console.error(`[WEBHOOK] Error while preparing the data:`, dataError.message);
return false;
}
} catch (error) {
console.error(`[WEBHOOK] Error while sending the monthly report:`, error.message);
return false;
}
}
async executeDiscordWebhook(webhook, data) {
try {
console.log(`Execution of discord webhook: ${webhook.name}`);
const response = await axios.post(webhook.url, data, {
headers: {
'Content-Type': 'application/json'
}
});
console.log(`[WEBHOOK] Discord response: ${response.status}`);
return response.status >= 200 && response.status < 300;
} catch (error) {
console.error(`[WEBHOOK] Error with Discord webhook ${webhook.name}:`, error.message);
if (error.response) {
console.error('[WEBHOOK] Response status:', error.response.status);
console.error('[WEBHOOK] Response data:', error.response.data);
}
return false;
}
}
}
module.exports = WebhookManager;