Add:Notification settings, notification manager trigger #996

This commit is contained in:
advplyr 2022-09-21 18:01:10 -05:00
parent 9a7503cde2
commit ff04eb8d5e
9 changed files with 239 additions and 5 deletions

View File

@ -82,6 +82,11 @@ export default {
id: 'config-log',
title: 'Logs',
path: '/config/log'
},
{
id: 'config-notifications',
title: 'Notifications',
path: '/config/notifications'
}
]

View File

@ -0,0 +1,73 @@
<template>
<div>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-8 mb-2 max-w-3xl mx-auto">
<h2 class="text-xl font-semibold mb-2">Apprise Notification Settings</h2>
<p class="mb-6">Insert some text here describing this feature</p>
<form @submit.prevent="submitForm">
<ui-text-input-with-label v-model="appriseApiUrl" label="Apprise API Url" />
<div class="flex items-center justify-end pt-4">
<ui-btn type="submit">Save</ui-btn>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
appriseApiUrl: null,
notifications: [],
notificationSettings: null
}
},
computed: {},
methods: {
submitForm() {
if (this.notificationSettings && this.notificationSettings.appriseApiUrl == this.appriseApiUrl) {
return
}
// TODO: Validate apprise api url
const updatePayload = {
appriseApiUrl: this.appriseApiUrl || null
}
this.loading = true
this.$axios
.$patch('/api/notifications', updatePayload)
.then(() => {
this.$toast.success('Notification settings updated')
})
.catch((error) => {
console.error('Failed to update notification settings', error)
this.$toast.error('Failed to update notification settings')
})
.finally(() => {
this.loading = false
})
},
async init() {
this.loading = true
const notificationSettings = await this.$axios.$get('/api/notifications').catch((error) => {
console.error('Failed to get notification settings', error)
this.$toast.error('Failed to load notification settings')
return null
})
this.loading = false
if (!notificationSettings) {
return
}
this.notificationSettings = notificationSettings
this.appriseApiUrl = notificationSettings.appriseApiUrl
this.notifications = notificationSettings.notifications || []
}
},
mounted() {
this.init()
}
}
</script>

View File

@ -9,8 +9,8 @@ const Library = require('./objects/Library')
const Author = require('./objects/entities/Author')
const Series = require('./objects/entities/Series')
const ServerSettings = require('./objects/settings/ServerSettings')
const NotificationSettings = require('./objects/settings/NotificationSettings')
const PlaybackSession = require('./objects/PlaybackSession')
const Feed = require('./objects/Feed')
class Db {
constructor() {
@ -43,6 +43,7 @@ class Db {
this.series = []
this.serverSettings = null
this.notificationSettings = null
// Stores previous version only if upgraded
this.previousVersion = null
@ -125,6 +126,10 @@ class Db {
this.serverSettings = new ServerSettings()
await this.insertEntity('settings', this.serverSettings)
}
if (!this.notificationSettings) {
this.notificationSettings = new NotificationSettings()
await this.insertEntity('settings', this.notificationSettings)
}
global.ServerSettings = this.serverSettings.toJSON()
}
@ -166,6 +171,11 @@ class Db {
}
}
}
var notificationSettings = this.settings.find(s => s.id === 'notification-settings')
if (notificationSettings) {
this.notificationSettings = new NotificationSettings(notificationSettings)
}
}
})
var p5 = this.collectionsDb.select(() => true).then((results) => {

View File

@ -0,0 +1,25 @@
const Logger = require('../Logger')
class NotificationController {
constructor() { }
get(req, res) {
res.json(this.db.notificationSettings)
}
async update(req, res) {
const updated = this.db.notificationSettings.update(req.body)
if (updated) {
await this.db.updateEntity('settings', this.db.notificationSettings)
}
res.sendStatus(200)
}
middleware(req, res, next) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(404)
}
next()
}
}
module.exports = new NotificationController()

View File

@ -1,10 +1,55 @@
const axios = require('axios')
const Logger = require("../Logger")
class NotificationManager {
constructor() { }
constructor(db) {
this.db = db
onNewPodcastEpisode(libraryItem, episode) {
Logger.debug(`[NotificationManager] onNewPodcastEpisode: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
this.notificationFailedMap = {}
}
onPodcastEpisodeDownloaded(libraryItem, episode) {
if (!this.db.notificationSettings.isUseable) return
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
this.triggerNotification('onPodcastEpisodeDownloaded', { libraryItem, episode })
}
onTest() {
this.triggerNotification('onTest')
}
async triggerNotification(eventName, eventData) {
if (!this.db.notificationSettings.isUseable) return
const notifications = this.db.notificationSettings.getNotificationsForEvent(eventName)
for (const notification of notifications) {
Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`)
const success = await this.sendNotification(notification, eventData)
if (!success) { // Failed notification
if (!this.notificationFailedMap[notification.id]) this.notificationFailedMap[notification.id] = 1
else this.notificationFailedMap[notification.id]++
if (this.notificationFailedMap[notification.id] > 2) {
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
// TODO: Do something like disable the notification
}
} else { // Successful notification
delete this.notificationFailedMap[notification.id]
}
}
}
sendNotification(notification, eventData) {
const payload = notification.getApprisePayload(eventData)
return axios.post(`${this.db.notificationSettings.appriseApiUrl}/notify`, payload, { timeout: 6000 }).then((data) => {
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=${data}`)
return true
}).catch((error) => {
Logger.error(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} error=`, error)
return false
})
}
}
module.exports = NotificationManager

View File

@ -136,7 +136,7 @@ class PodcastManager {
this.emitter('item_updated', libraryItem.toJSONExpanded())
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
this.notificationManager.onNewPodcastEpisode(libraryItem, podcastEpisode)
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
}
return true

View File

@ -1,10 +1,12 @@
class Notification {
constructor(notification = null) {
this.id = null
this.libraryId = null
this.eventName = ''
this.urls = []
this.titleTemplate = ''
this.bodyTemplate = ''
this.type = 'info'
this.enabled = false
this.createdAt = null
@ -16,10 +18,12 @@ class Notification {
construct(notification) {
this.id = notification.id
this.libraryId = notification.libraryId || null
this.eventName = notification.eventName
this.urls = notification.urls || []
this.titleTemplate = notification.titleTemplate || ''
this.bodyTemplate = notification.bodyTemplate || ''
this.type = notification.type || 'info'
this.enabled = !!notification.enabled
this.createdAt = notification.createdAt
}
@ -27,13 +31,33 @@ class Notification {
toJSON() {
return {
id: this.id,
libraryId: this.libraryId,
eventName: this.eventName,
urls: this.urls,
titleTemplate: this.titleTemplate,
bodyTemplate: this.bodyTemplate,
enabled: this.enabled,
type: this.type,
createdAt: this.createdAt
}
}
parseTitleTemplate(data) {
// TODO: Implement template parsing
return 'Test Title'
}
parseBodyTemplate(data) {
// TODO: Implement template parsing
return 'Test Body'
}
getApprisePayload(data) {
return {
urls: this.urls,
title: this.parseTitleTemplate(data),
body: this.parseBodyTemplate(data)
}
}
}
module.exports = Notification

View File

@ -0,0 +1,45 @@
class NotificationSettings {
constructor(settings = null) {
this.id = 'notification-settings'
this.appriseType = 'api'
this.appriseApiUrl = null
this.notifications = []
if (settings) {
this.construct(settings)
}
}
construct(settings) {
this.appriseType = settings.appriseType
this.appriseApiUrl = settings.appriseApiUrl || null
this.notifications = (settings.notifications || []).map(n => ({ ...n }))
}
toJSON() {
return {
id: this.id,
appriseType: this.appriseType,
appriseApiUrl: this.appriseApiUrl,
notifications: this.notifications.map(n => n.toJSON())
}
}
get isUseable() {
return !!this.appriseApiUrl
}
getNotificationsForEvent(eventName) {
return this.notifications.filter(n => n.eventName === eventName)
}
update(payload) {
if (!payload) return false
if (payload.appriseApiUrl !== this.appriseApiUrl) {
this.appriseApiUrl = payload.appriseApiUrl || null
return true
}
return false
}
}
module.exports = NotificationSettings

View File

@ -14,6 +14,7 @@ const SeriesController = require('../controllers/SeriesController')
const AuthorController = require('../controllers/AuthorController')
const SessionController = require('../controllers/SessionController')
const PodcastController = require('../controllers/PodcastController')
const NotificationController = require('../controllers/NotificationController')
const MiscController = require('../controllers/MiscController')
const BookFinder = require('../finders/BookFinder')
@ -199,6 +200,12 @@ class ApiRouter {
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))
//
// Notification Routes
//
this.router.get('/notifications', NotificationController.middleware.bind(this), NotificationController.get.bind(this))
this.router.patch('/notifications', NotificationController.middleware.bind(this), NotificationController.update.bind(this))
//
// Misc Routes
//