diff --git a/client/components/cards/NotificationCard.vue b/client/components/cards/NotificationCard.vue
index b336e5f78..d91934df8 100644
--- a/client/components/cards/NotificationCard.vue
+++ b/client/components/cards/NotificationCard.vue
@@ -14,7 +14,7 @@
{{ notification.urls.join(', ') }}
+{{ notification.ntfyTopic ? $getString('LabelNotificationNtfyTopicPrefix', [notification.ntfyTopic]) : notification.urls.join(', ') }}
Last attempt failed {{ $dateDistanceFromNow(lastFiredAt) }} ({{ numConsecutiveFailedAttempts }} attempt{{ numConsecutiveFailedAttempts === 1 ? '' : 's' }})
Last fired {{ $dateDistanceFromNow(lastFiredAt) }}
diff --git a/client/components/modals/notification/NotificationEditModal.vue b/client/components/modals/notification/NotificationEditModal.vue index d69566344..14742ecc8 100644 --- a/client/components/modals/notification/NotificationEditModal.vue +++ b/client/components/modals/notification/NotificationEditModal.vue @@ -10,7 +10,11 @@http://192.168.1.1:8337 then you would put http://192.168.1.1:8337/notify.",
+ "MessageNtfyDescription": "Send notifications using ntfy.sh or a self-hosted ntfy server. The server URL defaults to https://ntfy.sh. Set a topic for each notification when creating it.",
"MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Legacy API tokens will be removed in the future. Use API Keys instead.",
"MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.",
@@ -983,6 +988,7 @@
"StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL",
+ "ToastNtfyTopicRequired": "Must enter an ntfy topic",
"ToastAsinRequired": "ASIN is required",
"ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found",
diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js
index b3fd72400..d89686bbe 100644
--- a/server/controllers/NotificationController.js
+++ b/server/controllers/NotificationController.js
@@ -120,7 +120,7 @@ class NotificationController {
* @param {Response} res
*/
async sendNotificationTest(req, res) {
- if (!Database.notificationSettings.isUseable) return res.status(400).send('Apprise is not configured')
+ if (!Database.notificationSettings.isUseable) return res.status(400).send('No notification service is configured')
const success = await NotificationManager.sendTestNotification(req.notification)
if (success) res.sendStatus(200)
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index 567da38ec..5d605fbac 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -220,15 +220,45 @@ class NotificationManager {
}
sendNotification(notification, eventData) {
+ if (notification.ntfyTopic) {
+ return this.sendNtfyNotification(notification, eventData)
+ }
+ return this.sendAppriseNotification(notification, eventData)
+ }
+
+ sendAppriseNotification(notification, eventData) {
+ if (!Database.notificationSettings.appriseApiUrl) {
+ Logger.error(`[NotificationManager] sendAppriseNotification: Apprise API URL is not configured`)
+ return Promise.resolve(false)
+ }
const payload = notification.getApprisePayload(eventData)
return axios
.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 })
.then((response) => {
- Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
+ Logger.debug(`[NotificationManager] sendAppriseNotification: ${notification.eventName}/${notification.id} response=`, response.data)
return true
})
.catch((error) => {
- Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
+ Logger.error(`[NotificationManager] sendAppriseNotification: ${notification.eventName}/${notification.id} error=`, error)
+ return false
+ })
+ }
+
+ sendNtfyNotification(notification, eventData) {
+ const payload = notification.getNtfyPayload(eventData)
+ const ntfyUrl = Database.notificationSettings.ntfyUrl || 'https://ntfy.sh'
+ const headers = { 'Content-Type': 'application/json' }
+ if (Database.notificationSettings.ntfyToken) {
+ headers['Authorization'] = `Bearer ${Database.notificationSettings.ntfyToken}`
+ }
+ return axios
+ .post(ntfyUrl, payload, { timeout: 6000, headers })
+ .then((response) => {
+ Logger.debug(`[NotificationManager] sendNtfyNotification: ${notification.eventName}/${notification.id} response=`, response.data)
+ return true
+ })
+ .catch((error) => {
+ Logger.error(`[NotificationManager] sendNtfyNotification: ${notification.eventName}/${notification.id} error=`, error)
return false
})
}
diff --git a/server/objects/Notification.js b/server/objects/Notification.js
index d075e1011..19f5762ce 100644
--- a/server/objects/Notification.js
+++ b/server/objects/Notification.js
@@ -9,6 +9,7 @@ class Notification {
this.titleTemplate = ''
this.bodyTemplate = ''
this.type = 'info'
+ this.ntfyTopic = null
this.enabled = false
this.lastFiredAt = null
@@ -30,6 +31,7 @@ class Notification {
this.titleTemplate = notification.titleTemplate || ''
this.bodyTemplate = notification.bodyTemplate || ''
this.type = notification.type || 'info'
+ this.ntfyTopic = notification.ntfyTopic || null
this.enabled = !!notification.enabled
this.lastFiredAt = notification.lastFiredAt || null
this.lastAttemptFailed = !!notification.lastAttemptFailed
@@ -48,6 +50,7 @@ class Notification {
bodyTemplate: this.bodyTemplate,
enabled: this.enabled,
type: this.type,
+ ntfyTopic: this.ntfyTopic,
lastFiredAt: this.lastFiredAt,
lastAttemptFailed: this.lastAttemptFailed,
numConsecutiveFailedAttempts: this.numConsecutiveFailedAttempts,
@@ -60,11 +63,12 @@ class Notification {
this.id = uuidv4()
this.libraryId = payload.libraryId || null
this.eventName = payload.eventName
- this.urls = payload.urls
+ this.urls = payload.urls || []
this.titleTemplate = payload.titleTemplate
this.bodyTemplate = payload.bodyTemplate
this.enabled = !!payload.enabled
this.type = payload.type || null
+ this.ntfyTopic = payload.ntfyTopic || null
this.createdAt = Date.now()
}
@@ -76,7 +80,7 @@ class Notification {
this.numConsecutiveFailedAttempts = 0
}
- const keysToUpdate = ['libraryId', 'eventName', 'urls', 'titleTemplate', 'bodyTemplate', 'enabled', 'type']
+ const keysToUpdate = ['libraryId', 'eventName', 'urls', 'titleTemplate', 'bodyTemplate', 'enabled', 'type', 'ntfyTopic']
var hasUpdated = false
for (const key of keysToUpdate) {
if (payload[key] !== undefined) {
@@ -129,5 +133,13 @@ class Notification {
body: this.parseBodyTemplate(data)
}
}
+
+ getNtfyPayload(data) {
+ return {
+ topic: this.ntfyTopic,
+ title: this.parseTitleTemplate(data),
+ message: this.parseBodyTemplate(data)
+ }
+ }
}
module.exports = Notification
\ No newline at end of file
diff --git a/server/objects/settings/NotificationSettings.js b/server/objects/settings/NotificationSettings.js
index aab8f3e4a..4491ffa03 100644
--- a/server/objects/settings/NotificationSettings.js
+++ b/server/objects/settings/NotificationSettings.js
@@ -7,6 +7,8 @@ class NotificationSettings {
this.id = 'notification-settings'
this.appriseType = 'api'
this.appriseApiUrl = null
+ this.ntfyUrl = null
+ this.ntfyToken = null
this.notifications = []
this.maxFailedAttempts = 5
this.maxNotificationQueue = 20 // once reached events will be ignored
@@ -20,6 +22,8 @@ class NotificationSettings {
construct(settings) {
this.appriseType = settings.appriseType
this.appriseApiUrl = settings.appriseApiUrl || null
+ this.ntfyUrl = settings.ntfyUrl || null
+ this.ntfyToken = settings.ntfyToken || null
this.notifications = (settings.notifications || []).map((n) => new Notification(n))
this.maxFailedAttempts = settings.maxFailedAttempts || 5
this.maxNotificationQueue = settings.maxNotificationQueue || 20
@@ -31,6 +35,8 @@ class NotificationSettings {
id: this.id,
appriseType: this.appriseType,
appriseApiUrl: this.appriseApiUrl,
+ ntfyUrl: this.ntfyUrl,
+ ntfyToken: this.ntfyToken,
notifications: this.notifications.map((n) => n.toJSON()),
maxFailedAttempts: this.maxFailedAttempts,
maxNotificationQueue: this.maxNotificationQueue,
@@ -39,7 +45,7 @@ class NotificationSettings {
}
get isUseable() {
- return !!this.appriseApiUrl
+ return !!this.appriseApiUrl || !!this.ntfyUrl
}
/**
@@ -74,29 +80,43 @@ class NotificationSettings {
if (!payload) return false
var hasUpdates = false
- if (payload.appriseApiUrl !== this.appriseApiUrl) {
+ if (payload.appriseApiUrl !== undefined && payload.appriseApiUrl !== this.appriseApiUrl) {
this.appriseApiUrl = payload.appriseApiUrl || null
hasUpdates = true
}
- const _maxFailedAttempts = isNullOrNaN(payload.maxFailedAttempts) ? 5 : Number(payload.maxFailedAttempts)
- if (_maxFailedAttempts !== this.maxFailedAttempts) {
- this.maxFailedAttempts = _maxFailedAttempts
+ if (payload.ntfyUrl !== undefined && payload.ntfyUrl !== this.ntfyUrl) {
+ this.ntfyUrl = payload.ntfyUrl || null
hasUpdates = true
}
- const _maxNotificationQueue = isNullOrNaN(payload.maxNotificationQueue) ? 20 : Number(payload.maxNotificationQueue)
- if (_maxNotificationQueue !== this.maxNotificationQueue) {
- this.maxNotificationQueue = _maxNotificationQueue
+ if (payload.ntfyToken !== undefined && payload.ntfyToken !== this.ntfyToken) {
+ this.ntfyToken = payload.ntfyToken || null
hasUpdates = true
}
+ if (payload.maxFailedAttempts !== undefined) {
+ const _maxFailedAttempts = isNullOrNaN(payload.maxFailedAttempts) ? 5 : Number(payload.maxFailedAttempts)
+ if (_maxFailedAttempts !== this.maxFailedAttempts) {
+ this.maxFailedAttempts = _maxFailedAttempts
+ hasUpdates = true
+ }
+ }
+
+ if (payload.maxNotificationQueue !== undefined) {
+ const _maxNotificationQueue = isNullOrNaN(payload.maxNotificationQueue) ? 20 : Number(payload.maxNotificationQueue)
+ if (_maxNotificationQueue !== this.maxNotificationQueue) {
+ this.maxNotificationQueue = _maxNotificationQueue
+ hasUpdates = true
+ }
+ }
+
return hasUpdates
}
createNotification(payload) {
if (!payload) return false
- if (!payload.eventName || !payload.urls.length) return false
+ if (!payload.eventName || (!payload.urls?.length && !payload.ntfyTopic)) return false
const notification = new Notification()
notification.setData(payload)