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