Add ntfy.sh as notification provider alongside Apprise

Support sending notifications via ntfy.sh or self-hosted ntfy servers
as an alternative to Apprise. Each notification can be configured to
use either provider via a dropdown in the create/edit modal.

Server changes:
- Add ntfyUrl and ntfyToken fields to NotificationSettings
- Add ntfyTopic field to Notification model with getNtfyPayload()
- Route notifications to Apprise or ntfy based on ntfyTopic presence
- Add null guard for appriseApiUrl before sending
- Add undefined guards in update() to prevent partial PATCH from
  resetting unrelated fields

Client changes:
- Add ntfy.sh settings section on the notifications config page
- Add provider dropdown (Apprise/ntfy) in notification edit modal
- Extract shared saveSettings() helper to deduplicate form submissions
- Use i18n for all user-visible strings
This commit is contained in:
Mikkel Mikkelsen 2026-04-15 11:02:02 +02:00 committed by Mikkel Mikkelsen
parent 88667d00a1
commit 9391a940f5
8 changed files with 150 additions and 36 deletions

View file

@ -14,7 +14,7 @@
<ui-icon-btn bg-color="bg-error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" /> <ui-icon-btn bg-color="bg-error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
</div> </div>
<div class="pt-4"> <div class="pt-4">
<p class="text-gray-300 text-xs md:text-sm mb-2">{{ notification.urls.join(', ') }}</p> <p class="text-gray-300 text-xs md:text-sm mb-2">{{ notification.ntfyTopic ? $getString('LabelNotificationNtfyTopicPrefix', [notification.ntfyTopic]) : notification.urls.join(', ') }}</p>
<p v-if="lastFiredAt && lastAttemptFailed" class="text-red-300 text-xs">Last attempt failed {{ $dateDistanceFromNow(lastFiredAt) }} ({{ numConsecutiveFailedAttempts }} attempt{{ numConsecutiveFailedAttempts === 1 ? '' : 's' }})</p> <p v-if="lastFiredAt && lastAttemptFailed" class="text-red-300 text-xs">Last attempt failed {{ $dateDistanceFromNow(lastFiredAt) }} ({{ numConsecutiveFailedAttempts }} attempt{{ numConsecutiveFailedAttempts === 1 ? '' : 's' }})</p>
<p v-else-if="lastFiredAt" class="text-gray-400 text-xs">Last fired {{ $dateDistanceFromNow(lastFiredAt) }}</p> <p v-else-if="lastFiredAt" class="text-gray-400 text-xs">Last fired {{ $dateDistanceFromNow(lastFiredAt) }}</p>

View file

@ -10,7 +10,11 @@
<div class="w-full px-3 py-5 md:p-12"> <div class="w-full px-3 py-5 md:p-12">
<ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" /> <ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
<ui-multi-select ref="urlsInput" v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" /> <ui-dropdown v-model="selectedProvider" :label="$strings.LabelNotificationProvider" :items="providerOptions" class="mb-4" />
<ui-multi-select v-if="selectedProvider === 'apprise'" ref="urlsInput" v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
<ui-text-input-with-label v-if="selectedProvider === 'ntfy'" v-model="newNotification.ntfyTopic" :label="$strings.LabelNotificationNtfyTopic" class="mb-2" />
<ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" /> <ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
@ -51,7 +55,8 @@ export default {
return { return {
processing: false, processing: false,
newNotification: {}, newNotification: {},
isNew: true isNew: true,
selectedProvider: 'apprise'
} }
}, },
watch: { watch: {
@ -76,6 +81,12 @@ export default {
if (!this.notificationData) return [] if (!this.notificationData) return []
return this.notificationData.events || [] return this.notificationData.events || []
}, },
providerOptions() {
return [
{ value: 'apprise', text: 'Apprise' },
{ value: 'ntfy', text: 'ntfy.sh' }
]
},
eventOptions() { eventOptions() {
return this.notificationEvents.map((e) => { return this.notificationEvents.map((e) => {
return { return {
@ -109,12 +120,20 @@ export default {
if (this.$refs.modal) this.$refs.modal.setHide() if (this.$refs.modal) this.$refs.modal.setHide()
}, },
submitForm() { submitForm() {
if (this.selectedProvider === 'apprise') {
this.$refs.urlsInput?.forceBlur() this.$refs.urlsInput?.forceBlur()
if (!this.newNotification.urls.length) { if (!this.newNotification.urls.length) {
this.$toast.error(this.$strings.ToastAppriseUrlRequired) this.$toast.error(this.$strings.ToastAppriseUrlRequired)
return return
} }
this.newNotification.ntfyTopic = null
} else {
if (!this.newNotification.ntfyTopic) {
this.$toast.error(this.$strings.ToastNtfyTopicRequired)
return
}
this.newNotification.urls = []
}
if (this.isNew) { if (this.isNew) {
this.submitCreate() this.submitCreate()
@ -168,17 +187,20 @@ export default {
init() { init() {
this.isNew = !this.notification this.isNew = !this.notification
if (this.notification) { if (this.notification) {
this.selectedProvider = this.notification.ntfyTopic ? 'ntfy' : 'apprise'
this.newNotification = { this.newNotification = {
id: this.notification.id, id: this.notification.id,
libraryId: this.notification.libraryId, libraryId: this.notification.libraryId,
eventName: this.notification.eventName, eventName: this.notification.eventName,
urls: [...this.notification.urls], urls: [...(this.notification.urls || [])],
titleTemplate: this.notification.titleTemplate, titleTemplate: this.notification.titleTemplate,
bodyTemplate: this.notification.bodyTemplate, bodyTemplate: this.notification.bodyTemplate,
enabled: this.notification.enabled, enabled: this.notification.enabled,
type: this.notification.type type: this.notification.type,
ntfyTopic: this.notification.ntfyTopic || null
} }
} else { } else {
this.selectedProvider = 'apprise'
this.newNotification = { this.newNotification = {
libraryId: null, libraryId: null,
eventName: 'onTest', eventName: 'onTest',
@ -186,7 +208,8 @@ export default {
titleTemplate: '', titleTemplate: '',
bodyTemplate: '', bodyTemplate: '',
enabled: true, enabled: true,
type: null type: null,
ntfyTopic: null
} }
this.eventOptionUpdated() this.eventOptionUpdated()
} }

View file

@ -24,11 +24,22 @@
<ui-btn :loading="savingSettings" type="submit">{{ $strings.ButtonSave }}</ui-btn> <ui-btn :loading="savingSettings" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</form> </form>
</app-settings-content>
<div class="w-full h-px bg-white/10 my-6" /> <app-settings-content :header-text="$strings.HeaderNtfyNotificationSettings" :description="$strings.MessageNtfyDescription">
<form @submit.prevent="submitNtfyForm">
<ui-text-input-with-label ref="ntfyUrlInput" v-model="ntfyUrl" :disabled="savingSettings" label="ntfy Server URL" placeholder="https://ntfy.sh" class="mb-2" />
<ui-text-input-with-label ref="ntfyTokenInput" v-model="ntfyToken" :disabled="savingSettings" label="ntfy Auth Token" type="password" class="mb-2" />
<div class="flex items-center justify-end pt-4">
<ui-btn :loading="savingSettings" type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</form>
</app-settings-content>
<app-settings-content :header-text="$strings.HeaderNotifications">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold">{{ $strings.HeaderNotifications }}</h2>
<ui-btn small color="bg-success" class="flex items-center" @click="clickCreate">{{ $strings.ButtonCreate }} <span class="material-symbols text-lg pl-2">add</span></ui-btn> <ui-btn small color="bg-success" class="flex items-center" @click="clickCreate">{{ $strings.ButtonCreate }} <span class="material-symbols text-lg pl-2">add</span></ui-btn>
</div> </div>
@ -56,6 +67,8 @@ export default {
loading: false, loading: false,
savingSettings: false, savingSettings: false,
appriseApiUrl: null, appriseApiUrl: null,
ntfyUrl: null,
ntfyToken: null,
maxNotificationQueue: 0, maxNotificationQueue: 0,
maxFailedAttempts: 0, maxFailedAttempts: 0,
notifications: [], notifications: [],
@ -116,17 +129,10 @@ export default {
return true return true
}, },
submitForm() { saveSettings(payload) {
if (!this.validateForm()) return
const updatePayload = {
appriseApiUrl: this.appriseApiUrl || null,
maxNotificationQueue: Number(this.maxNotificationQueue),
maxFailedAttempts: Number(this.maxFailedAttempts)
}
this.savingSettings = true this.savingSettings = true
this.$axios this.$axios
.$patch('/api/notifications', updatePayload) .$patch('/api/notifications', payload)
.then(() => { .then(() => {
this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess) this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess)
}) })
@ -138,6 +144,21 @@ export default {
this.savingSettings = false this.savingSettings = false
}) })
}, },
submitForm() {
if (!this.validateForm()) return
this.saveSettings({
appriseApiUrl: this.appriseApiUrl || null,
maxNotificationQueue: Number(this.maxNotificationQueue),
maxFailedAttempts: Number(this.maxFailedAttempts)
})
},
submitNtfyForm() {
this.saveSettings({
ntfyUrl: this.ntfyUrl || null,
ntfyToken: this.ntfyToken || null
})
},
async init() { async init() {
this.loading = true this.loading = true
const notificationResponse = await this.$axios.$get('/api/notifications').catch((error) => { const notificationResponse = await this.$axios.$get('/api/notifications').catch((error) => {
@ -155,6 +176,8 @@ export default {
setNotificationSettings(notificationSettings) { setNotificationSettings(notificationSettings) {
this.notificationSettings = notificationSettings this.notificationSettings = notificationSettings
this.appriseApiUrl = notificationSettings.appriseApiUrl this.appriseApiUrl = notificationSettings.appriseApiUrl
this.ntfyUrl = notificationSettings.ntfyUrl || null
this.ntfyToken = notificationSettings.ntfyToken || null
this.maxNotificationQueue = notificationSettings.maxNotificationQueue this.maxNotificationQueue = notificationSettings.maxNotificationQueue
this.maxFailedAttempts = notificationSettings.maxFailedAttempts this.maxFailedAttempts = notificationSettings.maxFailedAttempts
this.notifications = notificationSettings.notifications || [] this.notifications = notificationSettings.notifications || []

View file

@ -123,6 +123,7 @@
"HeaderAdvanced": "Advanced", "HeaderAdvanced": "Advanced",
"HeaderApiKeys": "API Keys", "HeaderApiKeys": "API Keys",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderNtfyNotificationSettings": "ntfy.sh Notification Settings",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
"HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "Authentication",
@ -489,6 +490,9 @@
"LabelNotificationAvailableVariables": "Available variables", "LabelNotificationAvailableVariables": "Available variables",
"LabelNotificationBodyTemplate": "Body Template", "LabelNotificationBodyTemplate": "Body Template",
"LabelNotificationEvent": "Notification Event", "LabelNotificationEvent": "Notification Event",
"LabelNotificationNtfyTopicPrefix": "ntfy: {0}",
"LabelNotificationNtfyTopic": "ntfy Topic",
"LabelNotificationProvider": "Provider",
"LabelNotificationTitleTemplate": "Title Template", "LabelNotificationTitleTemplate": "Title Template",
"LabelNotificationsMaxFailedAttempts": "Max failed attempts", "LabelNotificationsMaxFailedAttempts": "Max failed attempts",
"LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times", "LabelNotificationsMaxFailedAttemptsHelp": "Notifications are disabled once they fail to send this many times",
@ -736,6 +740,7 @@
"LabelYourProgress": "Your Progress", "LabelYourProgress": "Your Progress",
"MessageAddToPlayerQueue": "Add to player queue", "MessageAddToPlayerQueue": "Add to player queue",
"MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.",
"MessageNtfyDescription": "Send notifications using <a href=\"https://ntfy.sh\" target=\"_blank\">ntfy.sh</a> or a self-hosted ntfy server. The server URL defaults to <code>https://ntfy.sh</code>. Set a topic for each notification when creating it.",
"MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.", "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 <a href=\"/config/api-keys\">API Keys</a> instead.", "MessageAuthenticationLegacyTokenWarning": "Legacy API tokens will be removed in the future. Use <a href=\"/config/api-keys\">API Keys</a> instead.",
"MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.", "MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.",
@ -983,6 +988,7 @@
"StatsYearInReview": "YEAR IN REVIEW", "StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateSuccess": "Account updated", "ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL", "ToastAppriseUrlRequired": "Must enter an Apprise URL",
"ToastNtfyTopicRequired": "Must enter an ntfy topic",
"ToastAsinRequired": "ASIN is required", "ToastAsinRequired": "ASIN is required",
"ToastAuthorImageRemoveSuccess": "Author image removed", "ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found", "ToastAuthorNotFound": "Author \"{0}\" not found",

View file

@ -120,7 +120,7 @@ class NotificationController {
* @param {Response} res * @param {Response} res
*/ */
async sendNotificationTest(req, 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) const success = await NotificationManager.sendTestNotification(req.notification)
if (success) res.sendStatus(200) if (success) res.sendStatus(200)

View file

@ -220,15 +220,45 @@ class NotificationManager {
} }
sendNotification(notification, eventData) { 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) const payload = notification.getApprisePayload(eventData)
return axios return axios
.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }) .post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 })
.then((response) => { .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 return true
}) })
.catch((error) => { .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 return false
}) })
} }

View file

@ -9,6 +9,7 @@ class Notification {
this.titleTemplate = '' this.titleTemplate = ''
this.bodyTemplate = '' this.bodyTemplate = ''
this.type = 'info' this.type = 'info'
this.ntfyTopic = null
this.enabled = false this.enabled = false
this.lastFiredAt = null this.lastFiredAt = null
@ -30,6 +31,7 @@ class Notification {
this.titleTemplate = notification.titleTemplate || '' this.titleTemplate = notification.titleTemplate || ''
this.bodyTemplate = notification.bodyTemplate || '' this.bodyTemplate = notification.bodyTemplate || ''
this.type = notification.type || 'info' this.type = notification.type || 'info'
this.ntfyTopic = notification.ntfyTopic || null
this.enabled = !!notification.enabled this.enabled = !!notification.enabled
this.lastFiredAt = notification.lastFiredAt || null this.lastFiredAt = notification.lastFiredAt || null
this.lastAttemptFailed = !!notification.lastAttemptFailed this.lastAttemptFailed = !!notification.lastAttemptFailed
@ -48,6 +50,7 @@ class Notification {
bodyTemplate: this.bodyTemplate, bodyTemplate: this.bodyTemplate,
enabled: this.enabled, enabled: this.enabled,
type: this.type, type: this.type,
ntfyTopic: this.ntfyTopic,
lastFiredAt: this.lastFiredAt, lastFiredAt: this.lastFiredAt,
lastAttemptFailed: this.lastAttemptFailed, lastAttemptFailed: this.lastAttemptFailed,
numConsecutiveFailedAttempts: this.numConsecutiveFailedAttempts, numConsecutiveFailedAttempts: this.numConsecutiveFailedAttempts,
@ -60,11 +63,12 @@ class Notification {
this.id = uuidv4() this.id = uuidv4()
this.libraryId = payload.libraryId || null this.libraryId = payload.libraryId || null
this.eventName = payload.eventName this.eventName = payload.eventName
this.urls = payload.urls this.urls = payload.urls || []
this.titleTemplate = payload.titleTemplate this.titleTemplate = payload.titleTemplate
this.bodyTemplate = payload.bodyTemplate this.bodyTemplate = payload.bodyTemplate
this.enabled = !!payload.enabled this.enabled = !!payload.enabled
this.type = payload.type || null this.type = payload.type || null
this.ntfyTopic = payload.ntfyTopic || null
this.createdAt = Date.now() this.createdAt = Date.now()
} }
@ -76,7 +80,7 @@ class Notification {
this.numConsecutiveFailedAttempts = 0 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 var hasUpdated = false
for (const key of keysToUpdate) { for (const key of keysToUpdate) {
if (payload[key] !== undefined) { if (payload[key] !== undefined) {
@ -129,5 +133,13 @@ class Notification {
body: this.parseBodyTemplate(data) body: this.parseBodyTemplate(data)
} }
} }
getNtfyPayload(data) {
return {
topic: this.ntfyTopic,
title: this.parseTitleTemplate(data),
message: this.parseBodyTemplate(data)
}
}
} }
module.exports = Notification module.exports = Notification

View file

@ -7,6 +7,8 @@ class NotificationSettings {
this.id = 'notification-settings' this.id = 'notification-settings'
this.appriseType = 'api' this.appriseType = 'api'
this.appriseApiUrl = null this.appriseApiUrl = null
this.ntfyUrl = null
this.ntfyToken = null
this.notifications = [] this.notifications = []
this.maxFailedAttempts = 5 this.maxFailedAttempts = 5
this.maxNotificationQueue = 20 // once reached events will be ignored this.maxNotificationQueue = 20 // once reached events will be ignored
@ -20,6 +22,8 @@ class NotificationSettings {
construct(settings) { construct(settings) {
this.appriseType = settings.appriseType this.appriseType = settings.appriseType
this.appriseApiUrl = settings.appriseApiUrl || null 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.notifications = (settings.notifications || []).map((n) => new Notification(n))
this.maxFailedAttempts = settings.maxFailedAttempts || 5 this.maxFailedAttempts = settings.maxFailedAttempts || 5
this.maxNotificationQueue = settings.maxNotificationQueue || 20 this.maxNotificationQueue = settings.maxNotificationQueue || 20
@ -31,6 +35,8 @@ class NotificationSettings {
id: this.id, id: this.id,
appriseType: this.appriseType, appriseType: this.appriseType,
appriseApiUrl: this.appriseApiUrl, appriseApiUrl: this.appriseApiUrl,
ntfyUrl: this.ntfyUrl,
ntfyToken: this.ntfyToken,
notifications: this.notifications.map((n) => n.toJSON()), notifications: this.notifications.map((n) => n.toJSON()),
maxFailedAttempts: this.maxFailedAttempts, maxFailedAttempts: this.maxFailedAttempts,
maxNotificationQueue: this.maxNotificationQueue, maxNotificationQueue: this.maxNotificationQueue,
@ -39,7 +45,7 @@ class NotificationSettings {
} }
get isUseable() { get isUseable() {
return !!this.appriseApiUrl return !!this.appriseApiUrl || !!this.ntfyUrl
} }
/** /**
@ -74,29 +80,43 @@ class NotificationSettings {
if (!payload) return false if (!payload) return false
var hasUpdates = false var hasUpdates = false
if (payload.appriseApiUrl !== this.appriseApiUrl) { if (payload.appriseApiUrl !== undefined && payload.appriseApiUrl !== this.appriseApiUrl) {
this.appriseApiUrl = payload.appriseApiUrl || null this.appriseApiUrl = payload.appriseApiUrl || null
hasUpdates = true hasUpdates = true
} }
if (payload.ntfyUrl !== undefined && payload.ntfyUrl !== this.ntfyUrl) {
this.ntfyUrl = payload.ntfyUrl || null
hasUpdates = true
}
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) const _maxFailedAttempts = isNullOrNaN(payload.maxFailedAttempts) ? 5 : Number(payload.maxFailedAttempts)
if (_maxFailedAttempts !== this.maxFailedAttempts) { if (_maxFailedAttempts !== this.maxFailedAttempts) {
this.maxFailedAttempts = _maxFailedAttempts this.maxFailedAttempts = _maxFailedAttempts
hasUpdates = true hasUpdates = true
} }
}
if (payload.maxNotificationQueue !== undefined) {
const _maxNotificationQueue = isNullOrNaN(payload.maxNotificationQueue) ? 20 : Number(payload.maxNotificationQueue) const _maxNotificationQueue = isNullOrNaN(payload.maxNotificationQueue) ? 20 : Number(payload.maxNotificationQueue)
if (_maxNotificationQueue !== this.maxNotificationQueue) { if (_maxNotificationQueue !== this.maxNotificationQueue) {
this.maxNotificationQueue = _maxNotificationQueue this.maxNotificationQueue = _maxNotificationQueue
hasUpdates = true hasUpdates = true
} }
}
return hasUpdates return hasUpdates
} }
createNotification(payload) { createNotification(payload) {
if (!payload) return false 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() const notification = new Notification()
notification.setData(payload) notification.setData(payload)