feat: send notification for new entries with Gotify, ntfy or Pushover, configurable per feed.

This commit is contained in:
Louis POIROT--HATTERMANN
2026-02-15 17:19:43 +01:00
parent ca2c687f26
commit e54151d2eb
26 changed files with 974 additions and 30 deletions

View File

@@ -81,6 +81,31 @@ public class Settings implements Serializable {
@Schema(description = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings();
@Schema(description = "notification settings", required = true)
private NotificationSettings notificationSettings = new NotificationSettings();
@Schema(description = "User notification settings")
@Data
public static class NotificationSettings implements Serializable {
@Schema(required = true)
private boolean enabled;
@Schema(description = "notification provider type: ntfy, gotify, or pushover")
private String type;
@Schema(description = "server URL for ntfy or gotify")
private String serverUrl;
@Schema(description = "API token for gotify or pushover")
private String token;
@Schema(description = "user key for pushover")
private String userKey;
@Schema(description = "topic for ntfy")
private String topic;
}
@Schema(description = "User sharing settings")
@Data
public static class SharingSettings implements Serializable {

View File

@@ -65,6 +65,9 @@ public class Subscription implements Serializable {
@Schema(description = "JEXL legacy filter")
private String filterLegacy;
@Schema(description = "whether to send notifications for new entries of this feed", required = true)
private boolean notifyOnNewEntries;
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
FeedCategory category = subscription.getCategory();
Feed feed = subscription.getFeed();
@@ -85,6 +88,7 @@ public class Subscription implements Serializable {
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
sub.setFilter(subscription.getFilter());
sub.setFilterLegacy(subscription.getFilterLegacy());
sub.setNotifyOnNewEntries(subscription.isNotifyOnNewEntries());
return sub;
}

View File

@@ -31,4 +31,7 @@ public class FeedModificationRequest implements Serializable {
@Size(max = 4096)
private String filter;
@Schema(description = "whether to send notifications for new entries of this feed")
private Boolean notifyOnNewEntries;
}

View File

@@ -28,4 +28,7 @@ public class SubscribeRequest implements Serializable {
@Size(max = 128)
private String categoryId;
@Schema(description = "whether to send notifications for new entries of this feed")
private boolean notifyOnNewEntries = true;
}

View File

@@ -365,7 +365,8 @@ public class FeedREST {
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
User user = authenticationContext.getCurrentUser();
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category);
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0,
req.isNotifyOnNewEntries());
return Response.ok(subscriptionId).build();
} catch (Exception e) {
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
@@ -384,7 +385,7 @@ public class FeedREST {
Preconditions.checkNotNull(url);
FeedInfo info = fetchFeedInternal(prependHttp(url));
User user = authenticationContext.getCurrentUser();
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle());
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0, true);
} catch (Exception e) {
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
}
@@ -438,6 +439,10 @@ public class FeedREST {
subscription.setFilterLegacy(null);
}
if (req.getNotifyOnNewEntries() != null) {
subscription.setNotifyOnNewEntries(req.getNotifyOnNewEntries());
}
if (StringUtils.isNotBlank(req.getName())) {
subscription.setTitle(req.getName());
}

View File

@@ -43,6 +43,7 @@ import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.model.UserSettings.IconDisplayMode;
import com.commafeed.backend.model.UserSettings.NotificationType;
import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.model.UserSettings.ScrollMode;
@@ -125,6 +126,15 @@ public class UserREST {
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor());
s.getNotificationSettings().setEnabled(settings.isNotificationEnabled());
if (settings.getNotificationType() != null) {
s.getNotificationSettings().setType(settings.getNotificationType().name().toLowerCase());
}
s.getNotificationSettings().setServerUrl(settings.getNotificationServerUrl());
s.getNotificationSettings().setToken(settings.getNotificationToken());
s.getNotificationSettings().setUserKey(settings.getNotificationUserKey());
s.getNotificationSettings().setTopic(settings.getNotificationTopic());
} else {
s.setReadingMode(ReadingMode.UNREAD);
s.setReadingOrder(ReadingOrder.DESC);
@@ -190,6 +200,17 @@ public class UserREST {
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor());
s.setNotificationEnabled(settings.getNotificationSettings().isEnabled());
if (settings.getNotificationSettings().getType() != null) {
s.setNotificationType(NotificationType.valueOf(settings.getNotificationSettings().getType().toUpperCase()));
} else {
s.setNotificationType(null);
}
s.setNotificationServerUrl(settings.getNotificationSettings().getServerUrl());
s.setNotificationToken(settings.getNotificationSettings().getToken());
s.setNotificationUserKey(settings.getNotificationSettings().getUserKey());
s.setNotificationTopic(settings.getNotificationSettings().getTopic());
s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail());
s.setFacebook(settings.getSharingSettings().isFacebook());