From 9364082395cdad5751666917c1434e15c73a57b1 Mon Sep 17 00:00:00 2001 From: Aerya Date: Wed, 17 Jun 2026 11:46:56 +0200 Subject: [PATCH] Ajoute les notifications Discord et Apprise + bouton Schedules Notifications : - Discord : une action de notification par webhook (champ Webhook URL). - Apprise : notification via un serveur Apprise API (caronc/apprise-api), avec l'URL du serveur et une ou plusieurs URLs Apprise cibles. Les deux s'ajoutent dans la section Notifications de l'editeur de schedule, au meme titre que Pushbullet et Amazon SNS. Cablage complet : modeles web, conversion vers/depuis ActionModel (types "discord" et "apprise"), instanciation runtime dans ScheduleConfigurationFactory, fragments HTML, endpoints de fragment, validation et payload JavaScript. Navigation : - Ajoute une entree "Schedules" toujours visible dans la barre de navigation, pour revenir en un clic a la liste des schedules (et donc au schedule en cours avec sa progression) depuis n'importe quelle page. Passe la version a 2.4.0 et met a jour le changelog. --- README.md | 6 ++ conf/local/application.properties | 2 +- conf/release/application.properties | 2 +- .../davos/converters/ScheduleConverter.java | 46 +++++++++++ .../ScheduleConfigurationFactory.java | 8 ++ .../workflow/actions/AppriseNotifyAction.java | 76 +++++++++++++++++++ .../workflow/actions/DiscordNotifyAction.java | 61 +++++++++++++++ .../io/linuxserver/davos/web/Apprise.java | 40 ++++++++++ .../io/linuxserver/davos/web/Discord.java | 31 ++++++++ .../linuxserver/davos/web/Notifications.java | 18 +++++ .../davos/web/controller/APIController.java | 4 +- .../web/controller/FragmentController.java | 12 ++- src/main/resources/static/js/davos.js | 31 +++++++- .../templates/fragments/apprise.html | 25 ++++++ .../templates/fragments/discord.html | 18 +++++ .../resources/templates/fragments/header.html | 3 + .../resources/templates/v2/edit-schedule.html | 49 +++++++++++- version.txt | 2 +- 18 files changed, 426 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/linuxserver/davos/schedule/workflow/actions/AppriseNotifyAction.java create mode 100644 src/main/java/io/linuxserver/davos/schedule/workflow/actions/DiscordNotifyAction.java create mode 100644 src/main/java/io/linuxserver/davos/web/Apprise.java create mode 100644 src/main/java/io/linuxserver/davos/web/Discord.java create mode 100644 src/main/resources/templates/fragments/apprise.html create mode 100644 src/main/resources/templates/fragments/discord.html diff --git a/README.md b/README.md index 6849a46..c16b033 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ docker run -d \ The web UI is then available on [http://localhost:8080](http://localhost:8080). # Changelog +- **2.4.0** + - Added **Discord** and **Apprise** notifications, alongside the existing Pushbullet and Amazon SNS options, in the schedule editor's Notifications section. + - Discord: paste a channel webhook URL. + - Apprise: point to an [Apprise API](https://github.com/caronc/apprise-api) server and provide one or more Apprise notification URLs. + - Added a **Schedules** entry in the top navigation bar so the running schedules (with their live progress) are reachable in one click from any page. + - **2.3.1** - Static assets (`davos.js`, `davos.css`) are now versioned in the page URLs so browsers always reload them after an upgrade, instead of serving a stale cached copy. This fixes the schedule **Browse** buttons appearing to do nothing when an old `davos.js` was still cached. diff --git a/conf/local/application.properties b/conf/local/application.properties index 24e5275..a248044 100644 --- a/conf/local/application.properties +++ b/conf/local/application.properties @@ -1 +1 @@ -davos.version=2.3.1 +davos.version=2.4.0 diff --git a/conf/release/application.properties b/conf/release/application.properties index 4618ea0..f3bb7ee 100644 --- a/conf/release/application.properties +++ b/conf/release/application.properties @@ -8,4 +8,4 @@ spring.jpa.hibernate.ddl-auto=update # Directory field. Mount your download volume here (see Dockerfile). davos.local.downloadRoot=/download -davos.version=2.3.1 +davos.version=2.4.0 diff --git a/src/main/java/io/linuxserver/davos/converters/ScheduleConverter.java b/src/main/java/io/linuxserver/davos/converters/ScheduleConverter.java index 90078ac..b78548e 100644 --- a/src/main/java/io/linuxserver/davos/converters/ScheduleConverter.java +++ b/src/main/java/io/linuxserver/davos/converters/ScheduleConverter.java @@ -13,6 +13,8 @@ import io.linuxserver.davos.persistence.model.ScheduleModel; import io.linuxserver.davos.transfer.ftp.FileTransferType; import io.linuxserver.davos.web.API; +import io.linuxserver.davos.web.Apprise; +import io.linuxserver.davos.web.Discord; import io.linuxserver.davos.web.Filter; import io.linuxserver.davos.web.Pushbullet; import io.linuxserver.davos.web.SNS; @@ -78,6 +80,23 @@ public Schedule convertTo(ScheduleModel source) { sns.setSecretAccessKey(action.f4); schedule.getNotifications().getSns().add(sns); + + } else if ("discord".equals(action.actionType)) { + + Discord discord = new Discord(); + discord.setId(action.id); + discord.setWebhookUrl(action.f1); + + schedule.getNotifications().getDiscord().add(discord); + + } else if ("apprise".equals(action.actionType)) { + + Apprise apprise = new Apprise(); + apprise.setId(action.id); + apprise.setServerUrl(action.f1); + apprise.setUrls(action.f2); + + schedule.getNotifications().getApprise().add(apprise); } } @@ -152,6 +171,33 @@ public ScheduleModel convertFrom(Schedule source) { model.actions.add(actionModel); } + for (Discord action : source.getNotifications().getDiscord()) { + + LOGGER.debug("Converting Discord to internal action: {}", action.getWebhookUrl()); + + ActionModel actionModel = new ActionModel(); + actionModel.id = action.getId(); + actionModel.actionType = "discord"; + actionModel.f1 = action.getWebhookUrl(); + actionModel.schedule = model; + + model.actions.add(actionModel); + } + + for (Apprise action : source.getNotifications().getApprise()) { + + LOGGER.debug("Converting Apprise to internal action: {}", action.getServerUrl()); + + ActionModel actionModel = new ActionModel(); + actionModel.id = action.getId(); + actionModel.actionType = "apprise"; + actionModel.f1 = action.getServerUrl(); + actionModel.f2 = action.getUrls(); + actionModel.schedule = model; + + model.actions.add(actionModel); + } + for (API action : source.getApis()) { LOGGER.debug("Converting API to internal action: {}", action.getUrl()); diff --git a/src/main/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactory.java b/src/main/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactory.java index c6bb8e1..6d8a48b 100644 --- a/src/main/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactory.java +++ b/src/main/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactory.java @@ -6,6 +6,8 @@ import io.linuxserver.davos.persistence.model.FilterModel; import io.linuxserver.davos.persistence.model.HostModel; import io.linuxserver.davos.persistence.model.ScheduleModel; +import io.linuxserver.davos.schedule.workflow.actions.AppriseNotifyAction; +import io.linuxserver.davos.schedule.workflow.actions.DiscordNotifyAction; import io.linuxserver.davos.schedule.workflow.actions.HttpAPICallAction; import io.linuxserver.davos.schedule.workflow.actions.MoveFileAction; import io.linuxserver.davos.schedule.workflow.actions.PushbulletNotifyAction; @@ -51,6 +53,12 @@ private static void addActions(ScheduleModel model, ScheduleConfiguration config if ("sns".equals(action.actionType)) config.getActions().add(new SNSNotifyAction(action.f2, action.f1, action.f3, action.f4)); + if ("discord".equals(action.actionType)) + config.getActions().add(new DiscordNotifyAction(action.f1)); + + if ("apprise".equals(action.actionType)) + config.getActions().add(new AppriseNotifyAction(action.f1, action.f2)); + if ("api".equals(action.actionType)) config.getActions().add(new HttpAPICallAction(action.f1, action.f2, action.f3, action.f4)); } diff --git a/src/main/java/io/linuxserver/davos/schedule/workflow/actions/AppriseNotifyAction.java b/src/main/java/io/linuxserver/davos/schedule/workflow/actions/AppriseNotifyAction.java new file mode 100644 index 0000000..2aecd86 --- /dev/null +++ b/src/main/java/io/linuxserver/davos/schedule/workflow/actions/AppriseNotifyAction.java @@ -0,0 +1,76 @@ +package io.linuxserver.davos.schedule.workflow.actions; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + * Notifies through an Apprise + * API server. The stateless {@code /notify} endpoint is used: davos posts + * the target Apprise URLs along with the message, so any service Apprise + * supports (Discord, Telegram, Gotify, email, ...) can be reached. + */ +public class AppriseNotifyAction implements PostDownloadAction { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppriseNotifyAction.class); + + private RestTemplate restTemplate = new RestTemplate(); + private String serverUrl; + private String urls; + + public AppriseNotifyAction(String serverUrl, String urls) { + this.serverUrl = serverUrl; + this.urls = urls; + } + + @Override + public void execute(PostDownloadExecution execution) { + + AppriseRequest body = new AppriseRequest(); + body.urls = urls; + body.title = "A new file has been downloaded"; + body.body = execution.fileName; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String endpoint = StringUtils.removeEnd(StringUtils.trimToEmpty(serverUrl), "/") + "/notify"; + + try { + + LOGGER.info("Sending notification to Apprise for {}", execution.fileName); + LOGGER.debug("Apprise endpoint: {}, urls: {}", endpoint, urls); + HttpEntity httpEntity = new HttpEntity(body, headers); + restTemplate.exchange(endpoint, HttpMethod.POST, httpEntity, Object.class); + + } catch (RestClientException | HttpMessageConversionException e) { + + LOGGER.debug("Full stacktrace", e); + LOGGER.error("Unable to complete notification to Apprise. Given error: {}", e.getMessage()); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + class AppriseRequest { + + public String urls; + public String title; + public String body; + + @Override + public String toString() { + return "AppriseRequest [urls=" + urls + ", title=" + title + ", body=" + body + "]"; + } + } +} diff --git a/src/main/java/io/linuxserver/davos/schedule/workflow/actions/DiscordNotifyAction.java b/src/main/java/io/linuxserver/davos/schedule/workflow/actions/DiscordNotifyAction.java new file mode 100644 index 0000000..767b546 --- /dev/null +++ b/src/main/java/io/linuxserver/davos/schedule/workflow/actions/DiscordNotifyAction.java @@ -0,0 +1,61 @@ +package io.linuxserver.davos.schedule.workflow.actions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +public class DiscordNotifyAction implements PostDownloadAction { + + private static final Logger LOGGER = LoggerFactory.getLogger(DiscordNotifyAction.class); + + private RestTemplate restTemplate = new RestTemplate(); + private String webhookUrl; + + public DiscordNotifyAction(String webhookUrl) { + this.webhookUrl = webhookUrl; + } + + @Override + public void execute(PostDownloadExecution execution) { + + DiscordRequest body = new DiscordRequest(); + body.content = "A new file has been downloaded: " + execution.fileName; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + try { + + LOGGER.info("Sending notification to Discord for {}", execution.fileName); + LOGGER.debug("Webhook URL: {}", webhookUrl); + HttpEntity httpEntity = new HttpEntity(body, headers); + restTemplate.exchange(webhookUrl, HttpMethod.POST, httpEntity, Object.class); + + } catch (RestClientException | HttpMessageConversionException e) { + + LOGGER.debug("Full stacktrace", e); + LOGGER.error("Unable to complete notification to Discord. Given error: {}", e.getMessage()); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + class DiscordRequest { + + public String content; + + @Override + public String toString() { + return "DiscordRequest [content=" + content + "]"; + } + } +} diff --git a/src/main/java/io/linuxserver/davos/web/Apprise.java b/src/main/java/io/linuxserver/davos/web/Apprise.java new file mode 100644 index 0000000..054fab5 --- /dev/null +++ b/src/main/java/io/linuxserver/davos/web/Apprise.java @@ -0,0 +1,40 @@ +package io.linuxserver.davos.web; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class Apprise { + + private Long id; + private String serverUrl; + private String urls; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public String getUrls() { + return urls; + } + + public void setUrls(String urls) { + this.urls = urls; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/src/main/java/io/linuxserver/davos/web/Discord.java b/src/main/java/io/linuxserver/davos/web/Discord.java new file mode 100644 index 0000000..47c14d9 --- /dev/null +++ b/src/main/java/io/linuxserver/davos/web/Discord.java @@ -0,0 +1,31 @@ +package io.linuxserver.davos.web; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class Discord { + + private Long id; + private String webhookUrl; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getWebhookUrl() { + return webhookUrl; + } + + public void setWebhookUrl(String webhookUrl) { + this.webhookUrl = webhookUrl; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/src/main/java/io/linuxserver/davos/web/Notifications.java b/src/main/java/io/linuxserver/davos/web/Notifications.java index 0fa02fe..eaa3cc7 100644 --- a/src/main/java/io/linuxserver/davos/web/Notifications.java +++ b/src/main/java/io/linuxserver/davos/web/Notifications.java @@ -7,6 +7,8 @@ public class Notifications { private List pushbullet = new ArrayList(); private List sns = new ArrayList(); + private List discord = new ArrayList(); + private List apprise = new ArrayList(); public List getPushbullet() { return pushbullet; @@ -16,6 +18,14 @@ public List getSns() { return sns; } + public List getDiscord() { + return discord; + } + + public List getApprise() { + return apprise; + } + public void setPushbullet(List pushbullet) { this.pushbullet = pushbullet; } @@ -23,4 +33,12 @@ public void setPushbullet(List pushbullet) { public void setSns(List sns) { this.sns = sns; } + + public void setDiscord(List discord) { + this.discord = discord; + } + + public void setApprise(List apprise) { + this.apprise = apprise; + } } diff --git a/src/main/java/io/linuxserver/davos/web/controller/APIController.java b/src/main/java/io/linuxserver/davos/web/controller/APIController.java index 9891d93..73312b2 100644 --- a/src/main/java/io/linuxserver/davos/web/controller/APIController.java +++ b/src/main/java/io/linuxserver/davos/web/controller/APIController.java @@ -77,10 +77,12 @@ private boolean isSchedulePostPayloadValid(Schedule schedule) { boolean hasPushbulletIds = schedule.getNotifications().getPushbullet().stream().anyMatch(pb -> pb.getId() != null); boolean hasSnsIds = schedule.getNotifications().getSns().stream().anyMatch(pb -> pb.getId() != null); + boolean hasDiscordIds = schedule.getNotifications().getDiscord().stream().anyMatch(d -> d.getId() != null); + boolean hasAppriseIds = schedule.getNotifications().getApprise().stream().anyMatch(a -> a.getId() != null); boolean hasFilterIds = schedule.getFilters().stream().anyMatch(f -> f.getId() != null); boolean hasApiIds = schedule.getApis().stream().anyMatch(a -> a.getId() != null); - if (null != schedule.getId() || hasPushbulletIds || hasSnsIds || hasFilterIds || hasApiIds) + if (null != schedule.getId() || hasPushbulletIds || hasSnsIds || hasDiscordIds || hasAppriseIds || hasFilterIds || hasApiIds) return false; return true; diff --git a/src/main/java/io/linuxserver/davos/web/controller/FragmentController.java b/src/main/java/io/linuxserver/davos/web/controller/FragmentController.java index b4cbd2c..b52b269 100644 --- a/src/main/java/io/linuxserver/davos/web/controller/FragmentController.java +++ b/src/main/java/io/linuxserver/davos/web/controller/FragmentController.java @@ -44,7 +44,17 @@ public String notificationPushbullet() { public String notificationSns() { return "fragments/sns"; } - + + @RequestMapping("/notification/discord") + public String notificationDiscord() { + return "fragments/discord"; + } + + @RequestMapping("/notification/apprise") + public String notificationApprise() { + return "fragments/apprise"; + } + @RequestMapping("/api") public String api() { return "fragments/api"; diff --git a/src/main/resources/static/js/davos.js b/src/main/resources/static/js/davos.js index dace9e6..f0b9e42 100644 --- a/src/main/resources/static/js/davos.js +++ b/src/main/resources/static/js/davos.js @@ -92,6 +92,14 @@ var fragments = (function($) { $('#notifications').append($("
").load("/fragments/notification/sns")); }); + $('#newDiscord').on('click', function() { + $('#notifications').append($("
").load("/fragments/notification/discord")); + }); + + $('#newApprise').on('click', function() { + $('#notifications').append($("
").load("/fragments/notification/apprise")); + }); + $('#addFilter').on('click', function() { if ($.trim($('#newFilter').val()).length > 0) { @@ -172,7 +180,9 @@ var schedule = (function($, settings) { filters: [], notifications: { pushbullet: [], - sns: [] + sns: [], + discord: [], + apprise: [] }, apis: [] }; @@ -194,7 +204,7 @@ var schedule = (function($, settings) { }); $('#notifications .notification.sns').each(function() { - + postData.notifications.sns.push({ "id": cleanId($(this).attr('data-notification-id')), "topicArn": $(this).find('.topicArn').val(), @@ -204,6 +214,23 @@ var schedule = (function($, settings) { }); }); + $('#notifications .notification.discord').each(function() { + + postData.notifications.discord.push({ + "id": cleanId($(this).attr('data-notification-id')), + "webhookUrl": $(this).find('.webhookUrl').val() + }); + }); + + $('#notifications .notification.apprise').each(function() { + + postData.notifications.apprise.push({ + "id": cleanId($(this).attr('data-notification-id')), + "serverUrl": $(this).find('.serverUrl').val(), + "urls": $(this).find('.urls').val() + }); + }); + $('#apis .api').each(function() { postData.apis.push({ diff --git a/src/main/resources/templates/fragments/apprise.html b/src/main/resources/templates/fragments/apprise.html new file mode 100644 index 0000000..8669c95 --- /dev/null +++ b/src/main/resources/templates/fragments/apprise.html @@ -0,0 +1,25 @@ +
+ +

Apprise

+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
diff --git a/src/main/resources/templates/fragments/discord.html b/src/main/resources/templates/fragments/discord.html new file mode 100644 index 0000000..196fe28 --- /dev/null +++ b/src/main/resources/templates/fragments/discord.html @@ -0,0 +1,18 @@ +
+ +

Discord

+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 5aeb627..569e456 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -14,6 +14,9 @@
- + +
+ +
+ +

Discord

+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +

Apprise

+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
@@ -314,6 +359,8 @@

Amazon SNS

diff --git a/version.txt b/version.txt index 2bf1c1c..197c4d5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.3.1 +2.4.0