Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion conf/local/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
davos.version=2.3.1
davos.version=2.4.0
2 changes: 1 addition & 1 deletion conf/release/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/caronc/apprise-api">Apprise
* API</a> 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<AppriseRequest> httpEntity = new HttpEntity<AppriseRequest>(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 + "]";
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DiscordRequest> httpEntity = new HttpEntity<DiscordRequest>(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 + "]";
}
}
}
40 changes: 40 additions & 0 deletions src/main/java/io/linuxserver/davos/web/Apprise.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
31 changes: 31 additions & 0 deletions src/main/java/io/linuxserver/davos/web/Discord.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions src/main/java/io/linuxserver/davos/web/Notifications.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class Notifications {

private List<Pushbullet> pushbullet = new ArrayList<Pushbullet>();
private List<SNS> sns = new ArrayList<SNS>();
private List<Discord> discord = new ArrayList<Discord>();
private List<Apprise> apprise = new ArrayList<Apprise>();

public List<Pushbullet> getPushbullet() {
return pushbullet;
Expand All @@ -16,11 +18,27 @@ public List<SNS> getSns() {
return sns;
}

public List<Discord> getDiscord() {
return discord;
}

public List<Apprise> getApprise() {
return apprise;
}

public void setPushbullet(List<Pushbullet> pushbullet) {
this.pushbullet = pushbullet;
}

public void setSns(List<SNS> sns) {
this.sns = sns;
}

public void setDiscord(List<Discord> discord) {
this.discord = discord;
}

public void setApprise(List<Apprise> apprise) {
this.apprise = apprise;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading