mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
@@ -19,8 +19,12 @@ class WebhookManager {
|
|||||||
await this.triggerEventWebhooks('playback_started', data);
|
await this.triggerEventWebhooks('playback_started', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.on('user_login', async (data) => {
|
this.eventEmitter.on('playback_ended', async (data) => {
|
||||||
await this.triggerEventWebhooks('user_login', 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
|
// If needed, add more event listeners here
|
||||||
@@ -40,11 +44,33 @@ class WebhookManager {
|
|||||||
).then(res => res.rows);
|
).then(res => res.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
async triggerEventWebhooks(eventType, data) {
|
async triggerEventWebhooks(eventType, data = {}) {
|
||||||
const webhooks = await this.getWebhooksByEventType(eventType);
|
try {
|
||||||
|
const webhooks = await this.getWebhooksByEventType(eventType);
|
||||||
for (const webhook of webhooks) {
|
|
||||||
await this.executeWebhook(webhook, data);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +161,31 @@ class WebhookManager {
|
|||||||
return template;
|
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) {
|
emitEvent(eventType, data) {
|
||||||
this.eventEmitter.emit(eventType, data);
|
this.eventEmitter.emit(eventType, data);
|
||||||
}
|
}
|
||||||
@@ -340,6 +391,28 @@ class WebhookManager {
|
|||||||
return false;
|
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;
|
module.exports = WebhookManager;
|
||||||
@@ -35,6 +35,54 @@ class WebhookScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadEventWebhooks() {
|
||||||
|
try {
|
||||||
|
const eventWebhooks = await this.webhookManager.getEventWebhooks();
|
||||||
|
if (eventWebhooks && eventWebhooks.length > 0) {
|
||||||
|
this.eventWebhooks = {};
|
||||||
|
|
||||||
|
eventWebhooks.forEach(webhook => {
|
||||||
|
if (!this.eventWebhooks[webhook.eventType]) {
|
||||||
|
this.eventWebhooks[webhook.eventType] = [];
|
||||||
|
}
|
||||||
|
this.eventWebhooks[webhook.eventType].push(webhook);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[WEBHOOK] Loaded ${eventWebhooks.length} event-based webhooks`);
|
||||||
|
} else {
|
||||||
|
console.log('[WEBHOOK] No event-based webhooks found');
|
||||||
|
this.eventWebhooks = {};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WEBHOOK] Failed to load event-based webhooks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
||||||
|
event: eventType,
|
||||||
|
data: eventData,
|
||||||
|
triggeredAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scheduleWebhook(webhook) {
|
scheduleWebhook(webhook) {
|
||||||
try {
|
try {
|
||||||
this.cronJobs[webhook.id] = cron.schedule(webhook.schedule, async () => {
|
this.cronJobs[webhook.id] = cron.schedule(webhook.schedule, async () => {
|
||||||
@@ -50,6 +98,7 @@ class WebhookScheduler {
|
|||||||
|
|
||||||
async refreshSchedule() {
|
async refreshSchedule() {
|
||||||
await this.loadScheduledWebhooks();
|
await this.loadScheduledWebhooks();
|
||||||
|
await this.loadEventWebhooks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -824,6 +824,8 @@ async function partialSync(triggertype) {
|
|||||||
const config = await new configClass().getConfig();
|
const config = await new configClass().getConfig();
|
||||||
|
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
|
|
||||||
|
const newItems = []; // Array to track newly added items during the sync process
|
||||||
|
|
||||||
syncTask = { loggedData: [], uuid: uuid, wsKey: "PartialSyncTask", taskName: taskName.partialsync };
|
syncTask = { loggedData: [], uuid: uuid, wsKey: "PartialSyncTask", taskName: taskName.partialsync };
|
||||||
try {
|
try {
|
||||||
@@ -833,7 +835,7 @@ async function partialSync(triggertype) {
|
|||||||
if (config.error) {
|
if (config.error) {
|
||||||
syncTask.loggedData.push({ Message: config.error });
|
syncTask.loggedData.push({ Message: config.error });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
return;
|
return { success: false, error: config.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraries = await API.getLibraries();
|
const libraries = await API.getLibraries();
|
||||||
@@ -842,7 +844,7 @@ async function partialSync(triggertype) {
|
|||||||
syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." });
|
syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " " + taskName.fullsync + " Completed" });
|
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " " + taskName.fullsync + " Completed" });
|
||||||
return;
|
return { success: false, error: "No libraries found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const excluded_libraries = config.settings.ExcludedLibraries || [];
|
const excluded_libraries = config.settings.ExcludedLibraries || [];
|
||||||
@@ -850,10 +852,10 @@ async function partialSync(triggertype) {
|
|||||||
const filtered_libraries = libraries.filter((library) => !excluded_libraries.includes(library.Id));
|
const filtered_libraries = libraries.filter((library) => !excluded_libraries.includes(library.Id));
|
||||||
const existing_excluded_libraries = libraries.filter((library) => excluded_libraries.includes(library.Id));
|
const existing_excluded_libraries = libraries.filter((library) => excluded_libraries.includes(library.Id));
|
||||||
|
|
||||||
// //syncUserData
|
// syncUserData
|
||||||
await syncUserData();
|
await syncUserData();
|
||||||
|
|
||||||
// //syncLibraryFolders
|
// syncLibraryFolders
|
||||||
await syncLibraryFolders(filtered_libraries, existing_excluded_libraries);
|
await syncLibraryFolders(filtered_libraries, existing_excluded_libraries);
|
||||||
|
|
||||||
//item sync counters
|
//item sync counters
|
||||||
@@ -956,7 +958,7 @@ async function partialSync(triggertype) {
|
|||||||
insertEpisodeInfoCount += Number(infoCount.insertEpisodeInfoCount);
|
insertEpisodeInfoCount += Number(infoCount.insertEpisodeInfoCount);
|
||||||
updateEpisodeInfoCount += Number(infoCount.updateEpisodeInfoCount);
|
updateEpisodeInfoCount += Number(infoCount.updateEpisodeInfoCount);
|
||||||
|
|
||||||
//clear data from memory as its no longer needed
|
//clear data from memory as it's no longer needed
|
||||||
library_items = null;
|
library_items = null;
|
||||||
seasons = null;
|
seasons = null;
|
||||||
episodes = null;
|
episodes = null;
|
||||||
@@ -1023,10 +1025,22 @@ async function partialSync(triggertype) {
|
|||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.SUCCESS);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.SUCCESS);
|
||||||
|
|
||||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " Sync Completed" });
|
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " Sync Completed" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
newItems: newItems,
|
||||||
|
stats: {
|
||||||
|
itemsAdded: insertedItemsCount,
|
||||||
|
episodesAdded: insertedEpisodeCount,
|
||||||
|
seasonsAdded: insertedSeasonsCount
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
syncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
|
syncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
sendUpdate(syncTask.wsKey, { type: "Error", message: triggertype + " Sync Halted with Errors" });
|
sendUpdate(syncTask.wsKey, { type: "Error", message: triggertype + " Sync Halted with Errors" });
|
||||||
|
|
||||||
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,18 +185,129 @@ router.post('/:id/test', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webhook = result.rows[0];
|
const webhook = result.rows[0];
|
||||||
const testData = req.body || {};
|
let testData = req.body || {};
|
||||||
|
let success = false;
|
||||||
|
|
||||||
const success = await webhookManager.executeWebhook(webhook, testData);
|
// Traitement spécial pour les webhooks Discord
|
||||||
|
if (webhook.url.includes('discord.com/api/webhooks')) {
|
||||||
|
console.log('Discord webhook détecté, préparation du payload spécifique');
|
||||||
|
|
||||||
|
// Format spécifique pour Discord
|
||||||
|
testData = {
|
||||||
|
content: "Test de webhook depuis Jellystat",
|
||||||
|
embeds: [{
|
||||||
|
title: "Test de notification Discord",
|
||||||
|
description: "Ceci est un test de notification via webhook Discord",
|
||||||
|
color: 3447003, // Bleu
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Type de webhook",
|
||||||
|
value: webhook.trigger_type || "Non spécifié",
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ID",
|
||||||
|
value: webhook.id,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bypass du traitement normal pour Discord
|
||||||
|
success = await webhookManager.executeDiscordWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
// Comportement existant pour les autres types de webhook
|
||||||
|
else if (webhook.trigger_type === 'event' && webhook.event_type) {
|
||||||
|
const eventType = webhook.event_type;
|
||||||
|
|
||||||
|
let eventData = {};
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case 'playback_started':
|
||||||
|
eventData = {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: "test-user-id",
|
||||||
|
deviceId: "test-device-id",
|
||||||
|
deviceName: "Test Device",
|
||||||
|
clientName: "Test Client",
|
||||||
|
isPaused: false,
|
||||||
|
mediaType: "Movie",
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
startTime: new Date().toISOString()
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
username: "Test User",
|
||||||
|
userImageTag: "test-image-tag"
|
||||||
|
},
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: "test-item-id",
|
||||||
|
episodeId: null,
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
seasonName: null,
|
||||||
|
seriesName: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'playback_ended':
|
||||||
|
eventData = {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: "test-user-id",
|
||||||
|
deviceId: "test-device-id",
|
||||||
|
deviceName: "Test Device",
|
||||||
|
clientName: "Test Client",
|
||||||
|
mediaType: "Movie",
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
startTime: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
endTime: new Date().toISOString(),
|
||||||
|
playbackDuration: 3600
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
username: "Test User",
|
||||||
|
userImageTag: "test-image-tag"
|
||||||
|
},
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: "test-item-id",
|
||||||
|
episodeId: null,
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
seasonName: null,
|
||||||
|
seriesName: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'media_recently_added':
|
||||||
|
eventData = {
|
||||||
|
mediaItem: {
|
||||||
|
id: "test-item-id",
|
||||||
|
name: "Test Media",
|
||||||
|
type: "Movie",
|
||||||
|
overview: "This is a test movie for webhook testing",
|
||||||
|
addedDate: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
success = await webhookManager.executeWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
success = await webhookManager.executeWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.json({ message: 'Webhook executed successfully' });
|
res.json({ message: 'Webhook exécuté avec succès' });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ error: 'Webhook execution failed' });
|
res.status(500).json({ error: 'Échec de l\'exécution du webhook' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error testing webhook:', error);
|
console.error('Error testing webhook:', error);
|
||||||
res.status(500).json({ error: 'Failed to test webhook' });
|
res.status(500).json({ error: 'Failed to test webhook: ' + error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,10 +316,110 @@ router.post('/:id/trigger-monthly', async (req, res) => {
|
|||||||
const success = await webhookManager.triggerMonthlySummaryWebhook(req.params.id);
|
const success = await webhookManager.triggerMonthlySummaryWebhook(req.params.id);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.status(200).json({ message: "Rapport mensuel envoyé avec succès" });
|
res.status(200).json({ message: "Monthly report sent successfully" });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Échec de l'envoi du rapport mensuel" });
|
res.status(500).json({ message: "Failed to send monthly report" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
// Get status of event webhooks
|
||||||
|
router.get('/event-status', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const eventTypes = ['playback_started', 'playback_ended', 'media_recently_added'];
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (const eventType of eventTypes) {
|
||||||
|
const webhooks = await dbInstance.query(
|
||||||
|
'SELECT id, name, enabled FROM webhooks WHERE trigger_type = $1 AND event_type = $2',
|
||||||
|
['event', eventType]
|
||||||
|
);
|
||||||
|
|
||||||
|
result[eventType] = {
|
||||||
|
exists: webhooks.rows.length > 0,
|
||||||
|
enabled: webhooks.rows.some(webhook => webhook.enabled),
|
||||||
|
webhooks: webhooks.rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching webhook status:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch webhook status' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle all webhooks of a specific event type
|
||||||
|
router.post('/toggle-event/:eventType', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { eventType } = req.params;
|
||||||
|
const { enabled } = req.body;
|
||||||
|
|
||||||
|
if (!['playback_started', 'playback_ended', 'media_recently_added'].includes(eventType)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid event type' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
return res.status(400).json({ error: 'Enabled parameter must be a boolean' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour tous les webhooks de ce type d'événement
|
||||||
|
const result = await dbInstance.query(
|
||||||
|
'UPDATE webhooks SET enabled = $1 WHERE trigger_type = $2 AND event_type = $3 RETURNING id',
|
||||||
|
[enabled, 'event', eventType]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si aucun webhook n'existe pour ce type, en créer un de base
|
||||||
|
if (result.rows.length === 0 && enabled) {
|
||||||
|
const defaultWebhook = {
|
||||||
|
name: `Webhook pour ${eventType}`,
|
||||||
|
url: req.body.url || '',
|
||||||
|
method: 'POST',
|
||||||
|
trigger_type: 'event',
|
||||||
|
event_type: eventType,
|
||||||
|
enabled: true,
|
||||||
|
headers: '{}',
|
||||||
|
payload: JSON.stringify({
|
||||||
|
event: `{{event}}`,
|
||||||
|
data: `{{data}}`,
|
||||||
|
timestamp: `{{triggeredAt}}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!defaultWebhook.url) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'URL parameter is required when creating a new webhook',
|
||||||
|
needsUrl: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbInstance.query(
|
||||||
|
`INSERT INTO webhooks (name, url, method, trigger_type, event_type, enabled, headers, payload)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
|
[
|
||||||
|
defaultWebhook.name,
|
||||||
|
defaultWebhook.url,
|
||||||
|
defaultWebhook.method,
|
||||||
|
defaultWebhook.trigger_type,
|
||||||
|
defaultWebhook.event_type,
|
||||||
|
defaultWebhook.enabled,
|
||||||
|
defaultWebhook.headers,
|
||||||
|
defaultWebhook.payload
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rafraîchir le planificateur de webhooks
|
||||||
|
await webhookScheduler.refreshSchedule();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Webhooks for ${eventType} ${enabled ? 'enabled' : 'disabled'}`,
|
||||||
|
affectedCount: result.rows.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling webhooks:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to toggle webhooks' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ const configClass = require("../classes/config");
|
|||||||
const API = require("../classes/api-loader");
|
const API = require("../classes/api-loader");
|
||||||
const { sendUpdate } = require("../ws");
|
const { sendUpdate } = require("../ws");
|
||||||
const { isNumber } = require("@mui/x-data-grid/internals");
|
const { isNumber } = require("@mui/x-data-grid/internals");
|
||||||
|
const WebhookManager = require("../classes/webhook-manager");
|
||||||
|
|
||||||
const MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK = process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK
|
const MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK = process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK
|
||||||
? Number(process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK)
|
? Number(process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK)
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
|
const webhookManager = new WebhookManager();
|
||||||
|
|
||||||
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||||
let existingData = await WatchdogData.filter((wdData) => {
|
let existingData = await WatchdogData.filter((wdData) => {
|
||||||
return SessionData.some((sessionData) => {
|
return SessionData.some((sessionData) => {
|
||||||
@@ -146,6 +150,42 @@ async function ActivityMonitor(interval) {
|
|||||||
//filter fix if table is empty
|
//filter fix if table is empty
|
||||||
|
|
||||||
if (WatchdogDataToInsert.length > 0) {
|
if (WatchdogDataToInsert.length > 0) {
|
||||||
|
for (const session of WatchdogDataToInsert) {
|
||||||
|
let userData = {};
|
||||||
|
try {
|
||||||
|
const userInfo = await API.getUserById(session.UserId);
|
||||||
|
if (userInfo) {
|
||||||
|
userData = {
|
||||||
|
username: userInfo.Name,
|
||||||
|
userImageTag: userInfo.PrimaryImageTag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webhookManager.triggerEventWebhooks('playback_started', {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: session.UserId,
|
||||||
|
deviceId: session.DeviceId,
|
||||||
|
deviceName: session.DeviceName,
|
||||||
|
clientName: session.ClientName,
|
||||||
|
isPaused: session.IsPaused,
|
||||||
|
mediaType: session.MediaType,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
startTime: session.ActivityDateInserted
|
||||||
|
},
|
||||||
|
userData,
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: session.NowPlayingItemId,
|
||||||
|
episodeId: session.EpisodeId,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
seasonName: session.SeasonName,
|
||||||
|
seriesName: session.SeriesName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//insert new rows where not existing items
|
//insert new rows where not existing items
|
||||||
// console.log("Inserted " + WatchdogDataToInsert.length + " wd playback records");
|
// console.log("Inserted " + WatchdogDataToInsert.length + " wd playback records");
|
||||||
db.insertBulk("jf_activity_watchdog", WatchdogDataToInsert, jf_activity_watchdog_columns);
|
db.insertBulk("jf_activity_watchdog", WatchdogDataToInsert, jf_activity_watchdog_columns);
|
||||||
@@ -158,11 +198,46 @@ async function ActivityMonitor(interval) {
|
|||||||
console.log("Existing Data Updated: ", WatchdogDataToUpdate.length);
|
console.log("Existing Data Updated: ", WatchdogDataToUpdate.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataToRemove.length > 0) {
|
||||||
|
for (const session of dataToRemove) {
|
||||||
|
let userData = {};
|
||||||
|
try {
|
||||||
|
const userInfo = await API.getUserById(session.UserId);
|
||||||
|
if (userInfo) {
|
||||||
|
userData = {
|
||||||
|
username: userInfo.Name,
|
||||||
|
userImageTag: userInfo.PrimaryImageTag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webhookManager.triggerEventWebhooks('playback_ended', {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: session.UserId,
|
||||||
|
deviceId: session.DeviceId,
|
||||||
|
deviceName: session.DeviceName,
|
||||||
|
clientName: session.ClientName,
|
||||||
|
playbackDuration: session.PlaybackDuration,
|
||||||
|
endTime: session.ActivityDateInserted
|
||||||
|
},
|
||||||
|
userData,
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: session.NowPlayingItemId,
|
||||||
|
episodeId: session.EpisodeId,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
seasonName: session.SeasonName,
|
||||||
|
seriesName: session.SeriesName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
||||||
|
|
||||||
//delete from db no longer in session data and insert into stats db
|
//delete from db no longer in session data and insert into stats db
|
||||||
//Bulk delete from db thats no longer on api
|
//Bulk delete from db thats no longer on api
|
||||||
|
|
||||||
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
|
||||||
|
|
||||||
let playbackToInsert = dataToRemove;
|
let playbackToInsert = dataToRemove;
|
||||||
|
|
||||||
if (playbackToInsert.length == 0 && toDeleteIds.length == 0) {
|
if (playbackToInsert.length == 0 && toDeleteIds.length == 0) {
|
||||||
@@ -248,7 +323,9 @@ async function ActivityMonitor(interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
} catch (error) {
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
if (error?.code === "ECONNREFUSED") {
|
if (error?.code === "ECONNREFUSED") {
|
||||||
console.error("Error: Unable to connect to API"); //TO-DO Change this to correct API name
|
console.error("Error: Unable to connect to API"); //TO-DO Change this to correct API name
|
||||||
} else if (error?.code === "ERR_BAD_RESPONSE") {
|
} else if (error?.code === "ERR_BAD_RESPONSE") {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { parentPort } = require("worker_threads");
|
const { parentPort } = require("worker_threads");
|
||||||
const triggertype = require("../logging/triggertype");
|
const triggertype = require("../logging/triggertype");
|
||||||
const sync = require("../routes/sync");
|
const sync = require("../routes/sync");
|
||||||
|
const WebhookManager = require("../classes/webhook-manager");
|
||||||
|
|
||||||
async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
||||||
try {
|
try {
|
||||||
@@ -17,12 +18,25 @@ async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
|||||||
});
|
});
|
||||||
parentPort.postMessage({ type: "log", message: formattedArgs.join(" ") });
|
parentPort.postMessage({ type: "log", message: formattedArgs.join(" ") });
|
||||||
};
|
};
|
||||||
await sync.partialSync(triggerType);
|
|
||||||
|
const syncResults = await sync.partialSync(triggerType);
|
||||||
|
|
||||||
|
const webhookManager = new WebhookManager();
|
||||||
|
|
||||||
|
const newMediaCount = syncResults?.newItems?.length || 0;
|
||||||
|
|
||||||
|
if (newMediaCount > 0) {
|
||||||
|
await webhookManager.triggerEventWebhooks('media_recently_added', {
|
||||||
|
count: newMediaCount,
|
||||||
|
items: syncResults.newItems,
|
||||||
|
syncDate: new Date().toISOString(),
|
||||||
|
triggerType: triggerType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
parentPort.postMessage({ status: "complete" });
|
parentPort.postMessage({ status: "complete" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
parentPort.postMessage({ status: "error", message: error.message });
|
parentPort.postMessage({ status: "error", message: error.message });
|
||||||
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jfstat",
|
"name": "jfstat",
|
||||||
"version": "1.1.5",
|
"version": "1.1.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
|||||||
@@ -226,7 +226,12 @@
|
|||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"TYPE": "Type",
|
"TYPE": "Type",
|
||||||
"TRIGGER": "Trigger",
|
"TRIGGER": "Trigger",
|
||||||
"STATUS": "Status"
|
"STATUS": "Status",
|
||||||
|
"EVENT_WEBHOOKS": "Event notifications",
|
||||||
|
"EVENT_WEBHOOKS_TOOLTIP": "Enable or disable event notifications",
|
||||||
|
"PLAYBACK_STARTED": "Playback Started",
|
||||||
|
"PLAYBACK_ENDED": "Playback Stopped",
|
||||||
|
"MEDIA_ADDED": "Media Added"
|
||||||
},
|
},
|
||||||
"TASK_TYPE": {
|
"TASK_TYPE": {
|
||||||
"JOB": "Job",
|
"JOB": "Job",
|
||||||
|
|||||||
@@ -213,7 +213,25 @@
|
|||||||
},
|
},
|
||||||
"SELECT_LIBRARIES_TO_IMPORT": "Sélectionner les médiathèques à importer",
|
"SELECT_LIBRARIES_TO_IMPORT": "Sélectionner les médiathèques à importer",
|
||||||
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'activité du contenu de ces médiathèques est toujours suivie, même s'ils ne sont pas importés.",
|
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'activité du contenu de ces médiathèques est toujours suivie, même s'ils ne sont pas importés.",
|
||||||
"DATE_ADDED": "Date d'ajout"
|
"DATE_ADDED": "Date d'ajout",
|
||||||
|
"WEBHOOKS": "Webhooks",
|
||||||
|
"WEBHOOK_TYPE": "Type de webhook",
|
||||||
|
"TEST_NOW": "Tester maintenant",
|
||||||
|
"WEBHOOKS_CONFIGURATION": "Configuration des webhooks",
|
||||||
|
"WEBHOOKS_TOOLTIP": "L'URL des webhooks utiliser pour envoyer des notifications à Discord ou à d'autres services",
|
||||||
|
"WEBHOOK_SAVED": "Webhook sauvegardé",
|
||||||
|
"WEBHOOK_NAME": "Nom du webhook",
|
||||||
|
"DISCORD_WEBHOOK_URL": "URL du webhook Discord",
|
||||||
|
"ENABLE_WEBHOOK": "Activer le webhook",
|
||||||
|
"URL": "URL",
|
||||||
|
"TYPE": "Type",
|
||||||
|
"TRIGGER": "Déclencheur",
|
||||||
|
"STATUS": "Status",
|
||||||
|
"EVENT_WEBHOOKS": "Notifications d'événements",
|
||||||
|
"EVENT_WEBHOOKS_TOOLTIP": "Activez ou désactivez les notifications pour différents événements du système",
|
||||||
|
"PLAYBACK_STARTED": "Lecture commencée",
|
||||||
|
"PLAYBACK_ENDED": "Lecture arrêtée",
|
||||||
|
"MEDIA_ADDED": "Média ajouté"
|
||||||
},
|
},
|
||||||
"TASK_TYPE": {
|
"TASK_TYPE": {
|
||||||
"JOB": "Job",
|
"JOB": "Job",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import axios from "../../../lib/axios_instance";
|
|||||||
import { Form, Row, Col, Button, Spinner, Alert } from "react-bootstrap";
|
import { Form, Row, Col, Button, Spinner, Alert } from "react-bootstrap";
|
||||||
import InformationLineIcon from "remixicon-react/InformationLineIcon";
|
import InformationLineIcon from "remixicon-react/InformationLineIcon";
|
||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
@@ -17,6 +18,7 @@ import ErrorBoundary from "../general/ErrorBoundary";
|
|||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Modification du composant WebhookRow pour passer l'objet webhook complet
|
||||||
function WebhookRow(props) {
|
function WebhookRow(props) {
|
||||||
const { webhook, onEdit, onTest } = props;
|
const { webhook, onEdit, onTest } = props;
|
||||||
|
|
||||||
@@ -28,16 +30,16 @@ function WebhookRow(props) {
|
|||||||
<TableCell>{webhook.webhook_type || 'generic'}</TableCell>
|
<TableCell>{webhook.webhook_type || 'generic'}</TableCell>
|
||||||
<TableCell>{webhook.trigger_type}</TableCell>
|
<TableCell>{webhook.trigger_type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`badge ${webhook.enabled ? 'bg-success' : 'bg-secondary'}`}>
|
<span className={`badge ${webhook.enabled ? 'bg-success' : 'bg-secondary'}`}>
|
||||||
{webhook.enabled ? <Trans i18nKey={"ENABLED"} /> : <Trans i18nKey={"DISABLED"} />}
|
{webhook.enabled ? <Trans i18nKey={"ENABLED"} /> : <Trans i18nKey={"DISABLED"} />}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="d-flex justify-content-end gap-2">
|
<div className="d-flex justify-content-end gap-2">
|
||||||
<Button size="sm" variant="outline-primary" onClick={() => onEdit(webhook)}>
|
<Button size="sm" variant="outline-primary" onClick={() => onEdit(webhook)}>
|
||||||
<Trans i18nKey={"EDIT"} />
|
<Trans i18nKey={"EDIT"} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline-secondary" onClick={() => onTest(webhook.id)}>
|
<Button size="sm" variant="outline-secondary" onClick={() => onTest(webhook)}>
|
||||||
<Trans i18nKey={"SETTINGS_PAGE.TEST_NOW"} />
|
<Trans i18nKey={"SETTINGS_PAGE.TEST_NOW"} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +49,19 @@ function WebhookRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebhookRow.propTypes = {
|
||||||
|
webhook: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
webhook_type: PropTypes.string,
|
||||||
|
trigger_type: PropTypes.string.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired,
|
||||||
|
onTest: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
function WebhooksSettings() {
|
function WebhooksSettings() {
|
||||||
const [webhooks, setWebhooks] = useState([]);
|
const [webhooks, setWebhooks] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -63,6 +78,12 @@ function WebhooksSettings() {
|
|||||||
webhook_type: 'discord'
|
webhook_type: 'discord'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [eventWebhooks, setEventWebhooks] = useState({
|
||||||
|
playback_started: { exists: false, enabled: false },
|
||||||
|
playback_ended: { exists: false, enabled: false },
|
||||||
|
media_recently_added: { exists: false, enabled: false }
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchWebhooks = async () => {
|
const fetchWebhooks = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -73,18 +94,19 @@ function WebhooksSettings() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data != webhooks) {
|
if (response.data !== webhooks) {
|
||||||
setWebhooks(response.data);
|
setWebhooks(response.data);
|
||||||
}
|
await loadEventWebhooks();
|
||||||
|
}
|
||||||
if (loading) {
|
|
||||||
|
if (loading) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading webhooks:", err);
|
console.error("Error loading webhooks:", err);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +114,7 @@ function WebhooksSettings() {
|
|||||||
|
|
||||||
const intervalId = setInterval(fetchWebhooks, 1000 * 10);
|
const intervalId = setInterval(fetchWebhooks, 1000 * 10);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, []);
|
}, [webhooks.length]);
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
@@ -111,7 +133,13 @@ function WebhooksSettings() {
|
|||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
if (!currentWebhook.url) {
|
if (!currentWebhook.url) {
|
||||||
setError("Discord webhook URL is required");
|
setError("Webhook URL is required");
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWebhook.trigger_type === 'event' && !currentWebhook.event_type) {
|
||||||
|
setError("Event type is required for an event based webhook");
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -134,6 +162,17 @@ function WebhooksSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webhooksResponse = await axios.get('/webhooks', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebhooks(webhooksResponse.data);
|
||||||
|
|
||||||
|
await loadEventWebhooks();
|
||||||
|
|
||||||
setCurrentWebhook({
|
setCurrentWebhook({
|
||||||
name: 'New Webhook',
|
name: 'New Webhook',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -143,10 +182,11 @@ function WebhooksSettings() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
webhook_type: 'discord'
|
webhook_type: 'discord'
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccess("Webhook saved successfully!");
|
setSuccess("Webhook saved successfully!");
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Error during webhook saving: " + (err.response?.data?.error || err.message));
|
setError("Error while saving webhook " + (err.response?.data?.error || err.message));
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -155,9 +195,10 @@ function WebhooksSettings() {
|
|||||||
setCurrentWebhook(webhook);
|
setCurrentWebhook(webhook);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTest = async (webhookId) => {
|
const handleTest = async (webhook) => {
|
||||||
if (!webhookId) {
|
if (!webhook || !webhook.id) {
|
||||||
setError("Impossible to test the webhook: no ID provided");
|
setError("Impossible to test the webhook: no webhook provided");
|
||||||
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,14 +206,20 @@ function WebhooksSettings() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
await axios.post(`/webhooks/${webhookId}/trigger-monthly`, {}, {
|
let endpoint = `/webhooks/${webhook.id}/test`;
|
||||||
|
|
||||||
|
if (webhook.trigger_type === 'scheduled' && webhook.schedule && webhook.schedule.includes('1 * *')) {
|
||||||
|
endpoint = `/webhooks/${webhook.id}/trigger-monthly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(endpoint, {}, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccess("Webhook test triggered successfully!");
|
setSuccess(`Webhook ${webhook.name} test triggered successfully!`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Error during the test of the webhook: " + (err.response?.data?.message || err.message));
|
setError("Error during the test of the webhook: " + (err.response?.data?.message || err.message));
|
||||||
@@ -180,6 +227,107 @@ function WebhooksSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEventWebhookStatus = (eventType) => {
|
||||||
|
return eventWebhooks[eventType]?.enabled || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEventWebhooks = async () => {
|
||||||
|
try {
|
||||||
|
const eventTypes = ['playback_started', 'playback_ended', 'media_recently_added'];
|
||||||
|
const status = {};
|
||||||
|
|
||||||
|
eventTypes.forEach(eventType => {
|
||||||
|
const matchingWebhooks = webhooks.filter(
|
||||||
|
webhook => webhook.trigger_type === 'event' && webhook.event_type === eventType
|
||||||
|
);
|
||||||
|
|
||||||
|
status[eventType] = {
|
||||||
|
exists: matchingWebhooks.length > 0,
|
||||||
|
enabled: matchingWebhooks.some(webhook => webhook.enabled)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setEventWebhooks(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading event webhook status:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEventWebhook = async (eventType) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const isCurrentlyEnabled = getEventWebhookStatus(eventType);
|
||||||
|
const matchingWebhooks = webhooks.filter(
|
||||||
|
webhook => webhook.trigger_type === 'event' && webhook.event_type === eventType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingWebhooks.length === 0 && !isCurrentlyEnabled) {
|
||||||
|
const newWebhook = {
|
||||||
|
name: `Notification - ${getEventDisplayName(eventType)}`,
|
||||||
|
url: '',
|
||||||
|
enabled: true,
|
||||||
|
trigger_type: 'event',
|
||||||
|
event_type: eventType,
|
||||||
|
method: 'POST',
|
||||||
|
webhook_type: 'discord'
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentWebhook(newWebhook);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const webhook of matchingWebhooks) {
|
||||||
|
await axios.put(`/webhooks/${webhook.id}`,
|
||||||
|
{ ...webhook, enabled: !isCurrentlyEnabled },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventWebhooks(prev => ({
|
||||||
|
...prev,
|
||||||
|
[eventType]: {
|
||||||
|
...prev[eventType],
|
||||||
|
enabled: !isCurrentlyEnabled
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await axios.get('/webhooks', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebhooks(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
setSuccess(`Webhook for ${getEventDisplayName(eventType)} ${!isCurrentlyEnabled ? 'enabled' : 'disabled'} with success!`);
|
||||||
|
} catch (error) {
|
||||||
|
setError("Error while editing webhook: " + (error.response?.data?.error || error.message));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventDisplayName = (eventType) => {
|
||||||
|
switch(eventType) {
|
||||||
|
case 'playback_started':
|
||||||
|
return 'Playback started';
|
||||||
|
case 'playback_ended':
|
||||||
|
return 'Playback ended';
|
||||||
|
case 'media_recently_added':
|
||||||
|
return 'New media added';
|
||||||
|
default:
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading && !webhooks.length) {
|
if (loading && !webhooks.length) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -273,7 +421,72 @@ function WebhooksSettings() {
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
{/* Ajout de la section pour les webhooks événementiels */}
|
||||||
|
<div className="event-webhooks mt-4 mb-4">
|
||||||
|
<h3 className="my-3">
|
||||||
|
<Trans i18nKey={"SETTINGS_PAGE.EVENT_WEBHOOKS"} />
|
||||||
|
<Tooltip title={<Trans i18nKey={"SETTINGS_PAGE.EVENT_WEBHOOKS_TOOLTIP"} />}>
|
||||||
|
<span className="ms-2">
|
||||||
|
<InformationLineIcon />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Row className="g-4">
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.PLAYBACK_STARTED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="playback-started-enabled"
|
||||||
|
checked={getEventWebhookStatus('playback_started')}
|
||||||
|
onChange={() => toggleEventWebhook('playback_started')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when a user starts watching a media
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.PLAYBACK_ENDED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="playback-ended-enabled"
|
||||||
|
checked={getEventWebhookStatus('playback_ended')}
|
||||||
|
onChange={() => toggleEventWebhook('playback_ended')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when a user finishes watching a media
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.MEDIA_ADDED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="media-recently-added-enabled"
|
||||||
|
checked={getEventWebhookStatus('media_recently_added')}
|
||||||
|
onChange={() => toggleEventWebhook('media_recently_added')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when new media is added to the library
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TableContainer className='rounded-2 mt-4'>
|
<TableContainer className='rounded-2 mt-4'>
|
||||||
<Table aria-label="webhooks table">
|
<Table aria-label="webhooks table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -305,7 +518,7 @@ function WebhooksSettings() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user