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 @@
+
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 @@
-
+
+
+
+
+
+
+
+
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