diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 050e7e2f..f2d634b2 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -2,6 +2,7 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') const Auth = require('./Auth') +const NotificationManager = require('./managers/NotificationManager') /** * @typedef SocketClient @@ -15,11 +16,25 @@ class SocketAuthority { constructor() { this.Server = null this.socketIoServers = [] + this.emittedNotifications = new Set(['item_created', 'item_updated']); /** @type {Object.} */ this.clients = {} } + /** + * Fires a notification if enabled and the event is whitelisted + * @param {string} event - The event name fired. Needs to be whitelisted in this.emitted_notifications + * @param {any} payload - The payload to send with the event. For user-specific events, this includes the userId + */ + _fireNotification(event, payload) { + // Should be O(1) so no real performance hit + if (!this.emittedNotifications.has(event)) return + Logger.debug(`[SocketAuthority] fireNotification - ${event}`) + + NotificationManager.fireNotificationFromSocket(event, payload) + } + /** * returns an array of User.toJSONForPublic with `connections` for the # of socket connections * a user can have many socket connections @@ -53,6 +68,7 @@ class SocketAuthority { * @param {Function} [filter] optional filter function to only send event to specific users */ emitter(evt, data, filter = null) { + void this._fireNotification(evt, data) for (const socketId in this.clients) { if (this.clients[socketId].user) { if (filter && !filter(this.clients[socketId].user)) continue @@ -64,6 +80,7 @@ class SocketAuthority { // Emits event to all clients for a specific user clientEmitter(userId, evt, data) { + void this._fireNotification(evt, data) const clients = this.getClientsForUser(userId) if (!clients.length) { return Logger.debug(`[SocketAuthority] clientEmitter - no clients found for user ${userId}`) @@ -77,6 +94,7 @@ class SocketAuthority { // Emits event to all admin user clients adminEmitter(evt, data) { + void this._fireNotification(evt, data); for (const socketId in this.clients) { if (this.clients[socketId].user?.isAdminOrUp) { this.clients[socketId].socket.emit(evt, data) @@ -92,6 +110,7 @@ class SocketAuthority { * @param {import('./models/LibraryItem')} libraryItem */ libraryItemEmitter(evt, libraryItem) { + void this._fireNotification(evt, libraryItem) for (const socketId in this.clients) { if (this.clients[socketId].user?.checkCanAccessLibraryItem(libraryItem)) { this.clients[socketId].socket.emit(evt, libraryItem.toOldJSONExpanded()) @@ -107,6 +126,7 @@ class SocketAuthority { * @param {import('./models/LibraryItem')[]} libraryItems */ libraryItemsEmitter(evt, libraryItems) { + void this._fireNotification(evt, libraryItems) for (const socketId in this.clients) { if (this.clients[socketId].user) { const libraryItemsAccessibleToUser = libraryItems.filter((li) => this.clients[socketId].user.checkCanAccessLibraryItem(li)) diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js index 8edcf428..fec77c4a 100644 --- a/server/managers/NotificationManager.js +++ b/server/managers/NotificationManager.js @@ -90,6 +90,14 @@ class NotificationManager { this.triggerNotification('onBackupFailed', eventData) } + /** + * @param + */ + async onItemUpdated(libraryItem) { + console.log('onItemUpdated', libraryItem) + this.triggerNotification('onItemUpdated', libraryItem) + } + onTest() { this.triggerNotification('onTest') } @@ -124,7 +132,7 @@ class NotificationManager { } await Database.updateSetting(Database.notificationSettings) - SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON()) + //SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON()) this.notificationFinished() } @@ -184,5 +192,26 @@ class NotificationManager { return false }) } + + fireNotificationFromSocket(eventName, eventData) { + if (!Database.notificationSettings.isUseable) return + + const eventNameModified = eventName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); + const eventKey = `on${eventNameModified.charAt(0).toUpperCase()}${eventNameModified.slice(1)}`; + + if (!Database.notificationSettings.getHasActiveNotificationsForEvent(eventKey)) { + // No logging to prevent console spam + Logger.debug(`[NotificationManager] fireSocketNotification: No active notifications`) + return + } + + Logger.debug(`[NotificationManager] fireNotificationFromSocket: ${eventKey} event fired`) + + switch (eventKey) { + case 'onItemUpdated': + void this.onItemUpdated(eventData) + break + } + } } module.exports = new NotificationManager() diff --git a/server/utils/notifications.js b/server/utils/notifications.js index 7a3e1198..8295a503 100644 --- a/server/utils/notifications.js +++ b/server/utils/notifications.js @@ -1,4 +1,61 @@ const { version } = require('../../package.json') +const LibraryItem = require('../models/LibraryItem') + +const libraryItemVariables = [ + 'id', + 'ino', + 'path', + 'relPath', + 'mediaId', + 'mediaType', + 'isFile', + 'isMissing', + 'isInvalid', + 'mtime', + 'ctime', + 'birthtime', + 'size', + 'lastScan', + 'lastScanVersion', + 'libraryFiles', + 'extraData', + 'title', + 'titleIgnorePrefix', + 'authorNamesFirstLast', + 'authorNamesLastFirst', + 'createdAt', + 'updatedAt', + 'libraryId', + 'libraryFolderId', +] + +const libraryItemTestData = { + id: '123e4567-e89b-12d3-a456-426614174000', + ino: '9876543', + path: '/audiobooks/Frank Herbert/Dune', + relPath: 'Frank Herbert/Dune', + mediaId: 'abcdef12-3456-7890-abcd-ef1234567890', + mediaType: 'book', + isFile: true, + isMissing: false, + isInvalid: false, + mtime: new Date('2023-11-15T10:20:30.400Z'), + ctime: new Date('2023-11-15T10:20:30.400Z'), + birthtime: new Date('2023-11-15T10:20:30.390Z'), + size: 987654321, + lastScan: new Date('2024-01-10T08:15:00.000Z'), + lastScanVersion: '3.2.0', + title: 'Dune', + titleIgnorePrefix: 'Dune', + authorNamesFirstLast: 'Frank Herbert', + authorNamesLastFirst: 'Herbert, Frank', + createdAt: new Date('2023-11-15T10:21:00.000Z'), + updatedAt: new Date('2024-05-15T18:30:36.940Z'), + libraryId: 'fedcba98-7654-3210-fedc-ba9876543210', + libraryFolderId: '11223344-5566-7788-99aa-bbccddeeff00' +}; + + module.exports.notificationData = { events: [ @@ -60,6 +117,22 @@ module.exports.notificationData = { errorMsg: 'Example error message' } }, + // Sockets - Silently crying because not using typescript + + { + name: 'onItemUpdated', + requiresLibrary: true, + description: 'Triggered when an item is updated', + descriptionKey: 'NotificationOnItemUpdatedDescription', + variables: libraryItemVariables, + defaults: { + title: 'Item Updated: {{title}}', + body: 'Item {{title}} has been updated.\n\nPath: {{path}}\nSize: {{size}} bytes\nLast Scan: {{lastScan}}\nLibrary ID: {{libraryId}}\nLibrary Folder ID: {{libraryFolderId}}' + }, + testData: libraryItemTestData + }, + + // Test { name: 'onTest', requiresLibrary: false,