mirror of
https://github.com/Athou/commafeed.git
synced 2026-10-26 15:08:51 +00:00
use google java formatter to avoid downloading eclipse during build
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@@ -15,27 +13,27 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class Category implements Serializable {
|
||||
|
||||
@Schema(description = "category id", required = true)
|
||||
private String id;
|
||||
@Schema(description = "category id", required = true)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "parent category id")
|
||||
private String parentId;
|
||||
@Schema(description = "parent category id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "parent category name")
|
||||
private String parentName;
|
||||
@Schema(description = "parent category name")
|
||||
private String parentName;
|
||||
|
||||
@Schema(description = "category id", required = true)
|
||||
private String name;
|
||||
@Schema(description = "category id", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "category children categories", required = true)
|
||||
private List<Category> children = new ArrayList<>();
|
||||
@Schema(description = "category children categories", required = true)
|
||||
private List<Category> children = new ArrayList<>();
|
||||
|
||||
@Schema(description = "category feeds", required = true)
|
||||
private List<Subscription> feeds = new ArrayList<>();
|
||||
@Schema(description = "category feeds", required = true)
|
||||
private List<Subscription> feeds = new ArrayList<>();
|
||||
|
||||
@Schema(description = "whether the category is expanded or collapsed", required = true)
|
||||
private boolean expanded;
|
||||
@Schema(description = "whether the category is expanded or collapsed", required = true)
|
||||
private boolean expanded;
|
||||
|
||||
@Schema(description = "position of the category in the list", required = true)
|
||||
private int position;
|
||||
}
|
||||
@Schema(description = "position of the category in the list", required = true)
|
||||
private int position;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "List of entries with some metadata")
|
||||
@@ -15,36 +13,38 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class Entries implements Serializable {
|
||||
|
||||
@Schema(description = "name of the feed or the category requested", required = true)
|
||||
private String name;
|
||||
@Schema(description = "name of the feed or the category requested", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "error or warning message")
|
||||
private String message;
|
||||
@Schema(description = "error or warning message")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "times the server tried to refresh the feed and failed", required = true)
|
||||
private int errorCount;
|
||||
@Schema(description = "times the server tried to refresh the feed and failed", required = true)
|
||||
private int errorCount;
|
||||
|
||||
@Schema(description = "URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries")
|
||||
private String feedLink;
|
||||
@Schema(
|
||||
description =
|
||||
"URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries")
|
||||
private String feedLink;
|
||||
|
||||
@Schema(description = "list generation timestamp", required = true)
|
||||
private long timestamp;
|
||||
@Schema(description = "list generation timestamp", required = true)
|
||||
private long timestamp;
|
||||
|
||||
@Schema(description = "if the query has more elements", required = true)
|
||||
private boolean hasMore;
|
||||
@Schema(description = "if the query has more elements", required = true)
|
||||
private boolean hasMore;
|
||||
|
||||
@Schema(description = "the requested offset")
|
||||
private int offset;
|
||||
@Schema(description = "the requested offset")
|
||||
private int offset;
|
||||
|
||||
@Schema(description = "the requested limit")
|
||||
private int limit;
|
||||
@Schema(description = "the requested limit")
|
||||
private int limit;
|
||||
|
||||
@Schema(description = "list of entries", required = true)
|
||||
private List<Entry> entries = new ArrayList<>();
|
||||
|
||||
@Schema(
|
||||
description = "if true, the unread flag was ignored in the request, all entries are returned regardless of their read status",
|
||||
required = true)
|
||||
private boolean ignoredReadStatus;
|
||||
@Schema(description = "list of entries", required = true)
|
||||
private List<Entry> entries = new ArrayList<>();
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"if true, the unread flag was ignored in the request, all entries are returned regardless of their read status",
|
||||
required = true)
|
||||
private boolean ignoredReadStatus;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@@ -24,123 +21,134 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class Entry implements Serializable {
|
||||
|
||||
@Schema(description = "entry id", required = true)
|
||||
private String id;
|
||||
@Schema(description = "entry id", required = true)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "entry guid", required = true)
|
||||
private String guid;
|
||||
@Schema(description = "entry guid", required = true)
|
||||
private String guid;
|
||||
|
||||
@Schema(description = "entry title", required = true)
|
||||
private String title;
|
||||
@Schema(description = "entry title", required = true)
|
||||
private String title;
|
||||
|
||||
@Schema(description = "entry content", required = true)
|
||||
private String content;
|
||||
@Schema(description = "entry content", required = true)
|
||||
private String content;
|
||||
|
||||
@Schema(description = "comma-separated list of categories")
|
||||
private String categories;
|
||||
@Schema(description = "comma-separated list of categories")
|
||||
private String categories;
|
||||
|
||||
@Schema(description = "whether entry content and title are rtl", required = true)
|
||||
private boolean rtl;
|
||||
@Schema(description = "whether entry content and title are rtl", required = true)
|
||||
private boolean rtl;
|
||||
|
||||
@Schema(description = "entry author")
|
||||
private String author;
|
||||
@Schema(description = "entry author")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "entry enclosure url, if any")
|
||||
private String enclosureUrl;
|
||||
@Schema(description = "entry enclosure url, if any")
|
||||
private String enclosureUrl;
|
||||
|
||||
@Schema(description = "entry enclosure mime type, if any")
|
||||
private String enclosureType;
|
||||
@Schema(description = "entry enclosure mime type, if any")
|
||||
private String enclosureType;
|
||||
|
||||
@Schema(description = "entry media description, if any")
|
||||
private String mediaDescription;
|
||||
@Schema(description = "entry media description, if any")
|
||||
private String mediaDescription;
|
||||
|
||||
@Schema(description = "entry media thumbnail url, if any")
|
||||
private String mediaThumbnailUrl;
|
||||
@Schema(description = "entry media thumbnail url, if any")
|
||||
private String mediaThumbnailUrl;
|
||||
|
||||
@Schema(description = "entry media thumbnail width, if any")
|
||||
private Integer mediaThumbnailWidth;
|
||||
@Schema(description = "entry media thumbnail width, if any")
|
||||
private Integer mediaThumbnailWidth;
|
||||
|
||||
@Schema(description = "entry media thumbnail height, if any")
|
||||
private Integer mediaThumbnailHeight;
|
||||
@Schema(description = "entry media thumbnail height, if any")
|
||||
private Integer mediaThumbnailHeight;
|
||||
|
||||
@Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true)
|
||||
private Instant date;
|
||||
@Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true)
|
||||
private Instant date;
|
||||
|
||||
@Schema(description = "entry insertion date in the database", type = SchemaType.INTEGER, required = true)
|
||||
private Instant insertedDate;
|
||||
@Schema(
|
||||
description = "entry insertion date in the database",
|
||||
type = SchemaType.INTEGER,
|
||||
required = true)
|
||||
private Instant insertedDate;
|
||||
|
||||
@Schema(description = "feed id", required = true)
|
||||
private String feedId;
|
||||
@Schema(description = "feed id", required = true)
|
||||
private String feedId;
|
||||
|
||||
@Schema(description = "feed name", required = true)
|
||||
private String feedName;
|
||||
@Schema(description = "feed name", required = true)
|
||||
private String feedName;
|
||||
|
||||
@Schema(description = "this entry's feed url", required = true)
|
||||
private String feedUrl;
|
||||
@Schema(description = "this entry's feed url", required = true)
|
||||
private String feedUrl;
|
||||
|
||||
@Schema(description = "this entry's website url", required = true)
|
||||
private String feedLink;
|
||||
@Schema(description = "this entry's website url", required = true)
|
||||
private String feedLink;
|
||||
|
||||
@Schema(description = "The favicon url to use for this feed", required = true)
|
||||
private String iconUrl;
|
||||
@Schema(description = "The favicon url to use for this feed", required = true)
|
||||
private String iconUrl;
|
||||
|
||||
@Schema(description = "entry url", required = true)
|
||||
private String url;
|
||||
@Schema(description = "entry url", required = true)
|
||||
private String url;
|
||||
|
||||
@Schema(description = "read status", required = true)
|
||||
private boolean read;
|
||||
@Schema(description = "read status", required = true)
|
||||
private boolean read;
|
||||
|
||||
@Schema(description = "starred status", required = true)
|
||||
private boolean starred;
|
||||
@Schema(description = "starred status", required = true)
|
||||
private boolean starred;
|
||||
|
||||
@Schema(description = "whether the entry is still markable (old entry statuses are discarded)", required = true)
|
||||
private boolean markable;
|
||||
@Schema(
|
||||
description = "whether the entry is still markable (old entry statuses are discarded)",
|
||||
required = true)
|
||||
private boolean markable;
|
||||
|
||||
@Schema(description = "tags", required = true)
|
||||
private List<String> tags;
|
||||
@Schema(description = "tags", required = true)
|
||||
private List<String> tags;
|
||||
|
||||
public static Entry build(FeedEntryStatus status, boolean proxyImages) {
|
||||
Entry entry = new Entry();
|
||||
public static Entry build(FeedEntryStatus status, boolean proxyImages) {
|
||||
Entry entry = new Entry();
|
||||
|
||||
FeedEntry feedEntry = status.getEntry();
|
||||
FeedSubscription sub = status.getSubscription();
|
||||
FeedEntryContent content = feedEntry.getContent();
|
||||
FeedEntry feedEntry = status.getEntry();
|
||||
FeedSubscription sub = status.getSubscription();
|
||||
FeedEntryContent content = feedEntry.getContent();
|
||||
|
||||
entry.setId(String.valueOf(feedEntry.getId()));
|
||||
entry.setGuid(feedEntry.getGuid());
|
||||
entry.setRead(status.isRead());
|
||||
entry.setStarred(status.isStarred());
|
||||
entry.setMarkable(status.isMarkable());
|
||||
entry.setDate(feedEntry.getPublished());
|
||||
entry.setInsertedDate(feedEntry.getInserted());
|
||||
entry.setUrl(feedEntry.getUrl());
|
||||
entry.setFeedName(sub.getTitle());
|
||||
entry.setFeedId(String.valueOf(sub.getId()));
|
||||
entry.setFeedUrl(sub.getFeed().getUrl());
|
||||
entry.setFeedLink(sub.getFeed().getLink());
|
||||
entry.setIconUrl(FeedUtils.getFaviconUrl(sub));
|
||||
entry.setTags(status.getTags().stream().map(FeedEntryTag::getName).toList());
|
||||
entry.setId(String.valueOf(feedEntry.getId()));
|
||||
entry.setGuid(feedEntry.getGuid());
|
||||
entry.setRead(status.isRead());
|
||||
entry.setStarred(status.isStarred());
|
||||
entry.setMarkable(status.isMarkable());
|
||||
entry.setDate(feedEntry.getPublished());
|
||||
entry.setInsertedDate(feedEntry.getInserted());
|
||||
entry.setUrl(feedEntry.getUrl());
|
||||
entry.setFeedName(sub.getTitle());
|
||||
entry.setFeedId(String.valueOf(sub.getId()));
|
||||
entry.setFeedUrl(sub.getFeed().getUrl());
|
||||
entry.setFeedLink(sub.getFeed().getLink());
|
||||
entry.setIconUrl(FeedUtils.getFaviconUrl(sub));
|
||||
entry.setTags(status.getTags().stream().map(FeedEntryTag::getName).toList());
|
||||
|
||||
if (content != null) {
|
||||
entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL);
|
||||
entry.setTitle(content.getTitle());
|
||||
entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent()) : content.getContent());
|
||||
entry.setAuthor(content.getAuthor());
|
||||
if (content != null) {
|
||||
entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL);
|
||||
entry.setTitle(content.getTitle());
|
||||
entry.setContent(
|
||||
proxyImages
|
||||
? FeedUtils.proxyImages(content.getContent())
|
||||
: content.getContent());
|
||||
entry.setAuthor(content.getAuthor());
|
||||
|
||||
entry.setEnclosureType(content.getEnclosureType());
|
||||
entry.setEnclosureUrl(proxyImages && Strings.CS.contains(content.getEnclosureType(), "image")
|
||||
? FeedUtils.proxyImage(content.getEnclosureUrl())
|
||||
: content.getEnclosureUrl());
|
||||
entry.setEnclosureType(content.getEnclosureType());
|
||||
entry.setEnclosureUrl(
|
||||
proxyImages && Strings.CS.contains(content.getEnclosureType(), "image")
|
||||
? FeedUtils.proxyImage(content.getEnclosureUrl())
|
||||
: content.getEnclosureUrl());
|
||||
|
||||
entry.setMediaDescription(content.getMediaDescription());
|
||||
entry.setMediaThumbnailUrl(proxyImages ? FeedUtils.proxyImage(content.getMediaThumbnailUrl()) : content.getMediaThumbnailUrl());
|
||||
entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth());
|
||||
entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight());
|
||||
entry.setMediaDescription(content.getMediaDescription());
|
||||
entry.setMediaThumbnailUrl(
|
||||
proxyImages
|
||||
? FeedUtils.proxyImage(content.getMediaThumbnailUrl())
|
||||
: content.getMediaThumbnailUrl());
|
||||
entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth());
|
||||
entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight());
|
||||
|
||||
entry.setCategories(content.getCategories());
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
entry.setCategories(content.getCategories());
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Feed details")
|
||||
@@ -13,10 +11,9 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class FeedInfo implements Serializable {
|
||||
|
||||
@Schema(description = "url", required = true)
|
||||
private String url;
|
||||
|
||||
@Schema(description = "title", required = true)
|
||||
private String title;
|
||||
@Schema(description = "url", required = true)
|
||||
private String url;
|
||||
|
||||
@Schema(description = "title", required = true)
|
||||
private String title;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Server infos")
|
||||
@@ -13,46 +11,44 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class ServerInfo implements Serializable {
|
||||
|
||||
@Schema
|
||||
private String announcement;
|
||||
@Schema private String announcement;
|
||||
|
||||
@Schema(required = true)
|
||||
private String version;
|
||||
@Schema(required = true)
|
||||
private String version;
|
||||
|
||||
@Schema(required = true)
|
||||
private String gitCommit;
|
||||
@Schema(required = true)
|
||||
private String gitCommit;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean allowRegistrations;
|
||||
@Schema(required = true)
|
||||
private boolean allowRegistrations;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean emailAddressRequired;
|
||||
@Schema(required = true)
|
||||
private boolean emailAddressRequired;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean smtpEnabled;
|
||||
@Schema(required = true)
|
||||
private boolean smtpEnabled;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean demoAccountEnabled;
|
||||
@Schema(required = true)
|
||||
private boolean demoAccountEnabled;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean websocketEnabled;
|
||||
@Schema(required = true)
|
||||
private boolean websocketEnabled;
|
||||
|
||||
@Schema(required = true)
|
||||
private long websocketPingInterval;
|
||||
@Schema(required = true)
|
||||
private long websocketPingInterval;
|
||||
|
||||
@Schema(required = true)
|
||||
private long treeReloadInterval;
|
||||
@Schema(required = true)
|
||||
private long treeReloadInterval;
|
||||
|
||||
@Schema(required = true)
|
||||
private long forceRefreshCooldownDuration;
|
||||
@Schema(required = true)
|
||||
private long forceRefreshCooldownDuration;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean initialSetupRequired;
|
||||
@Schema(required = true)
|
||||
private boolean initialSetupRequired;
|
||||
|
||||
@Schema(required = true)
|
||||
private int minimumPasswordLength;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean pushNotificationsEnabled;
|
||||
@Schema(required = true)
|
||||
private int minimumPasswordLength;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean pushNotificationsEnabled;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.backend.model.UserSettings.IconDisplayMode;
|
||||
import com.commafeed.backend.model.UserSettings.PushNotificationType;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.model.UserSettings.ScrollMode;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User settings")
|
||||
@@ -19,113 +16,128 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class Settings implements Serializable {
|
||||
|
||||
@Schema(description = "user's preferred language, english if none")
|
||||
private String language;
|
||||
@Schema(description = "user's preferred language, english if none")
|
||||
private String language;
|
||||
|
||||
@Schema(description = "user reads all entries or unread entries only", required = true)
|
||||
private ReadingMode readingMode;
|
||||
@Schema(description = "user reads all entries or unread entries only", required = true)
|
||||
private ReadingMode readingMode;
|
||||
|
||||
@Schema(description = "user reads entries in ascending or descending order", required = true)
|
||||
private ReadingOrder readingOrder;
|
||||
@Schema(description = "user reads entries in ascending or descending order", required = true)
|
||||
private ReadingOrder readingOrder;
|
||||
|
||||
@Schema(description = "user wants category and feeds with no unread entries shown", required = true)
|
||||
private boolean showRead;
|
||||
@Schema(
|
||||
description = "user wants category and feeds with no unread entries shown",
|
||||
required = true)
|
||||
private boolean showRead;
|
||||
|
||||
@Schema(description = "In expanded view, scroll through entries mark them as read", required = true)
|
||||
private boolean scrollMarks;
|
||||
@Schema(
|
||||
description = "In expanded view, scroll through entries mark them as read",
|
||||
required = true)
|
||||
private boolean scrollMarks;
|
||||
|
||||
@Schema(description = "user's custom css for the website")
|
||||
private String customCss;
|
||||
@Schema(description = "user's custom css for the website")
|
||||
private String customCss;
|
||||
|
||||
@Schema(description = "user's custom js for the website")
|
||||
private String customJs;
|
||||
@Schema(description = "user's custom js for the website")
|
||||
private String customJs;
|
||||
|
||||
@Schema(description = "user's preferred scroll speed when navigating between entries", required = true)
|
||||
private int scrollSpeed;
|
||||
@Schema(
|
||||
description = "user's preferred scroll speed when navigating between entries",
|
||||
required = true)
|
||||
private int scrollSpeed;
|
||||
|
||||
@Schema(description = "whether to scroll to the selected entry", required = true)
|
||||
private ScrollMode scrollMode;
|
||||
@Schema(description = "whether to scroll to the selected entry", required = true)
|
||||
private ScrollMode scrollMode;
|
||||
|
||||
@Schema(description = "number of entries to keep above the selected entry when scrolling", required = true)
|
||||
private int entriesToKeepOnTopWhenScrolling;
|
||||
@Schema(
|
||||
description = "number of entries to keep above the selected entry when scrolling",
|
||||
required = true)
|
||||
private int entriesToKeepOnTopWhenScrolling;
|
||||
|
||||
@Schema(description = "whether to show the star icon in the header of entries", required = true)
|
||||
private IconDisplayMode starIconDisplayMode;
|
||||
@Schema(description = "whether to show the star icon in the header of entries", required = true)
|
||||
private IconDisplayMode starIconDisplayMode;
|
||||
|
||||
@Schema(description = "whether to show the external link icon in the header of entries", required = true)
|
||||
private IconDisplayMode externalLinkIconDisplayMode;
|
||||
@Schema(
|
||||
description = "whether to show the external link icon in the header of entries",
|
||||
required = true)
|
||||
private IconDisplayMode externalLinkIconDisplayMode;
|
||||
|
||||
@Schema(description = "ask for confirmation when marking all entries as read", required = true)
|
||||
private boolean markAllAsReadConfirmation;
|
||||
@Schema(description = "ask for confirmation when marking all entries as read", required = true)
|
||||
private boolean markAllAsReadConfirmation;
|
||||
|
||||
@Schema(description = "navigate to the next unread category or feed after marking all entries as read", required = true)
|
||||
private boolean markAllAsReadNavigateToNextUnread;
|
||||
@Schema(
|
||||
description =
|
||||
"navigate to the next unread category or feed after marking all entries as read",
|
||||
required = true)
|
||||
private boolean markAllAsReadNavigateToNextUnread;
|
||||
|
||||
@Schema(description = "show commafeed's own context menu on right click", required = true)
|
||||
private boolean customContextMenu;
|
||||
@Schema(description = "show commafeed's own context menu on right click", required = true)
|
||||
private boolean customContextMenu;
|
||||
|
||||
@Schema(description = "on mobile, show action buttons at the bottom of the screen", required = true)
|
||||
private boolean mobileFooter;
|
||||
@Schema(
|
||||
description = "on mobile, show action buttons at the bottom of the screen",
|
||||
required = true)
|
||||
private boolean mobileFooter;
|
||||
|
||||
@Schema(description = "show unread count in the title", required = true)
|
||||
private boolean unreadCountTitle;
|
||||
@Schema(description = "show unread count in the title", required = true)
|
||||
private boolean unreadCountTitle;
|
||||
|
||||
@Schema(description = "show unread count in the favicon", required = true)
|
||||
private boolean unreadCountFavicon;
|
||||
@Schema(description = "show unread count in the favicon", required = true)
|
||||
private boolean unreadCountFavicon;
|
||||
|
||||
@Schema(description = "disable pull to refresh", required = true)
|
||||
private boolean disablePullToRefresh;
|
||||
@Schema(description = "disable pull to refresh", required = true)
|
||||
private boolean disablePullToRefresh;
|
||||
|
||||
@Schema(description = "primary theme color to use in the UI")
|
||||
private String primaryColor;
|
||||
@Schema(description = "primary theme color to use in the UI")
|
||||
private String primaryColor;
|
||||
|
||||
@Schema(description = "sharing settings", required = true)
|
||||
private SharingSettings sharingSettings = new SharingSettings();
|
||||
@Schema(description = "sharing settings", required = true)
|
||||
private SharingSettings sharingSettings = new SharingSettings();
|
||||
|
||||
@Schema(description = "push notification settings", required = true)
|
||||
private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings();
|
||||
@Schema(description = "push notification settings", required = true)
|
||||
private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings();
|
||||
|
||||
@Schema(description = "User notification settings")
|
||||
@Data
|
||||
public static class PushNotificationSettings implements Serializable {
|
||||
@Schema(description = "notification provider type")
|
||||
private PushNotificationType type;
|
||||
@Schema(description = "User notification settings")
|
||||
@Data
|
||||
public static class PushNotificationSettings implements Serializable {
|
||||
@Schema(description = "notification provider type")
|
||||
private PushNotificationType type;
|
||||
|
||||
@Schema(description = "server URL for ntfy or gotify")
|
||||
private String serverUrl;
|
||||
@Schema(description = "server URL for ntfy or gotify")
|
||||
private String serverUrl;
|
||||
|
||||
@Schema(description = "user Id")
|
||||
private String userId;
|
||||
@Schema(description = "user Id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "user secret for authentication with the service")
|
||||
private String userSecret;
|
||||
@Schema(description = "user secret for authentication with the service")
|
||||
private String userSecret;
|
||||
|
||||
@Schema(description = "topic")
|
||||
private String topic;
|
||||
}
|
||||
@Schema(description = "topic")
|
||||
private String topic;
|
||||
}
|
||||
|
||||
@Schema(description = "User sharing settings")
|
||||
@Data
|
||||
public static class SharingSettings implements Serializable {
|
||||
@Schema(required = true)
|
||||
private boolean email;
|
||||
@Schema(description = "User sharing settings")
|
||||
@Data
|
||||
public static class SharingSettings implements Serializable {
|
||||
@Schema(required = true)
|
||||
private boolean email;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean gmail;
|
||||
@Schema(required = true)
|
||||
private boolean gmail;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean facebook;
|
||||
@Schema(required = true)
|
||||
private boolean facebook;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean twitter;
|
||||
@Schema(required = true)
|
||||
private boolean twitter;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean tumblr;
|
||||
@Schema(required = true)
|
||||
private boolean tumblr;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean instapaper;
|
||||
@Schema(required = true)
|
||||
private boolean instapaper;
|
||||
|
||||
@Schema(required = true)
|
||||
private boolean buffer;
|
||||
}
|
||||
@Schema(required = true)
|
||||
private boolean buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@@ -20,80 +17,90 @@ import lombok.Data;
|
||||
@RegisterForReflection
|
||||
public class Subscription implements Serializable {
|
||||
|
||||
@Schema(description = "subscription id", required = true)
|
||||
private Long id;
|
||||
@Schema(description = "subscription id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "subscription name", required = true)
|
||||
private String name;
|
||||
@Schema(description = "subscription name", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "error message while fetching the feed")
|
||||
private String message;
|
||||
@Schema(description = "error message while fetching the feed")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "error count", required = true)
|
||||
private int errorCount;
|
||||
@Schema(description = "error count", required = true)
|
||||
private int errorCount;
|
||||
|
||||
@Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER)
|
||||
private Instant lastRefresh;
|
||||
@Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER)
|
||||
private Instant lastRefresh;
|
||||
|
||||
@Schema(description = "next time the feed refresh is planned, null if refresh is already queued", type = SchemaType.INTEGER)
|
||||
private Instant nextRefresh;
|
||||
@Schema(
|
||||
description =
|
||||
"next time the feed refresh is planned, null if refresh is already queued",
|
||||
type = SchemaType.INTEGER)
|
||||
private Instant nextRefresh;
|
||||
|
||||
@Schema(description = "this subscription's feed url", required = true)
|
||||
private String feedUrl;
|
||||
@Schema(description = "this subscription's feed url", required = true)
|
||||
private String feedUrl;
|
||||
|
||||
@Schema(description = "this subscription's website url", required = true)
|
||||
private String feedLink;
|
||||
@Schema(description = "this subscription's website url", required = true)
|
||||
private String feedLink;
|
||||
|
||||
@Schema(description = "The favicon url to use for this feed", required = true)
|
||||
private String iconUrl;
|
||||
@Schema(description = "The favicon url to use for this feed", required = true)
|
||||
private String iconUrl;
|
||||
|
||||
@Schema(description = "unread count", required = true)
|
||||
private long unread;
|
||||
@Schema(description = "unread count", required = true)
|
||||
private long unread;
|
||||
|
||||
@Schema(description = "category id")
|
||||
private String categoryId;
|
||||
@Schema(description = "category id")
|
||||
private String categoryId;
|
||||
|
||||
@Schema(description = "position of the subscription's in the list")
|
||||
private int position;
|
||||
@Schema(description = "position of the subscription's in the list")
|
||||
private int position;
|
||||
|
||||
@Schema(description = "date of the newest item", type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
@Schema(description = "date of the newest item", type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
private String filter;
|
||||
@Schema(
|
||||
description =
|
||||
"CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
private String filter;
|
||||
|
||||
@Schema(description = "JEXL legacy filter")
|
||||
private String filterLegacy;
|
||||
@Schema(description = "JEXL legacy filter")
|
||||
private String filterLegacy;
|
||||
|
||||
@Schema(description = "whether to send push notifications for new entries of this feed", required = true)
|
||||
private boolean pushNotificationsEnabled;
|
||||
@Schema(
|
||||
description = "whether to send push notifications for new entries of this feed",
|
||||
required = true)
|
||||
private boolean pushNotificationsEnabled;
|
||||
|
||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
@Schema(
|
||||
description =
|
||||
"automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
|
||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||
FeedCategory category = subscription.getCategory();
|
||||
Feed feed = subscription.getFeed();
|
||||
Subscription sub = new Subscription();
|
||||
sub.setId(subscription.getId());
|
||||
sub.setName(subscription.getTitle());
|
||||
sub.setPosition(subscription.getPosition());
|
||||
sub.setMessage(feed.getMessage());
|
||||
sub.setErrorCount(feed.getErrorCount());
|
||||
sub.setFeedUrl(feed.getUrl());
|
||||
sub.setFeedLink(feed.getLink());
|
||||
sub.setIconUrl(FeedUtils.getFaviconUrl(subscription));
|
||||
sub.setLastRefresh(feed.getLastUpdated());
|
||||
sub.setNextRefresh(
|
||||
(feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now())) ? null : feed.getDisabledUntil());
|
||||
sub.setUnread(unreadCount.getUnreadCount());
|
||||
sub.setNewestItemTime(unreadCount.getNewestItemTime());
|
||||
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
|
||||
sub.setFilter(subscription.getFilter());
|
||||
sub.setFilterLegacy(subscription.getFilterLegacy());
|
||||
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
|
||||
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
|
||||
return sub;
|
||||
}
|
||||
|
||||
}
|
||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||
FeedCategory category = subscription.getCategory();
|
||||
Feed feed = subscription.getFeed();
|
||||
Subscription sub = new Subscription();
|
||||
sub.setId(subscription.getId());
|
||||
sub.setName(subscription.getTitle());
|
||||
sub.setPosition(subscription.getPosition());
|
||||
sub.setMessage(feed.getMessage());
|
||||
sub.setErrorCount(feed.getErrorCount());
|
||||
sub.setFeedUrl(feed.getUrl());
|
||||
sub.setFeedLink(feed.getLink());
|
||||
sub.setIconUrl(FeedUtils.getFaviconUrl(subscription));
|
||||
sub.setLastRefresh(feed.getLastUpdated());
|
||||
sub.setNextRefresh(
|
||||
(feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now()))
|
||||
? null
|
||||
: feed.getDisabledUntil());
|
||||
sub.setUnread(unreadCount.getUnreadCount());
|
||||
sub.setNewestItemTime(unreadCount.getNewestItemTime());
|
||||
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
|
||||
sub.setFilter(subscription.getFilter());
|
||||
sub.setFilterLegacy(subscription.getFilterLegacy());
|
||||
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
|
||||
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Unread count")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UnreadCount implements Serializable {
|
||||
|
||||
@Schema
|
||||
private long feedId;
|
||||
@Schema private long feedId;
|
||||
|
||||
@Schema
|
||||
private long unreadCount;
|
||||
@Schema private long unreadCount;
|
||||
|
||||
@Schema(type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
@Schema(type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
|
||||
public UnreadCount() {
|
||||
}
|
||||
|
||||
public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) {
|
||||
this.feedId = feedId;
|
||||
this.unreadCount = unreadCount;
|
||||
this.newestItemTime = newestItemTime;
|
||||
}
|
||||
public UnreadCount() {}
|
||||
|
||||
public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) {
|
||||
this.feedId = feedId;
|
||||
this.unreadCount = unreadCount;
|
||||
this.newestItemTime = newestItemTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
package com.commafeed.frontend.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UserModel implements Serializable {
|
||||
|
||||
@Schema(description = "user id", required = true)
|
||||
private Long id;
|
||||
@Schema(description = "user id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "user name", required = true)
|
||||
private String name;
|
||||
@Schema(description = "user name", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "user email, if any")
|
||||
private String email;
|
||||
@Schema(description = "user email, if any")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "api key")
|
||||
private String apiKey;
|
||||
@Schema(description = "api key")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "user password, never returned by the api")
|
||||
private String password;
|
||||
@Schema(description = "user password, never returned by the api")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "account status", required = true)
|
||||
private boolean enabled;
|
||||
@Schema(description = "account status", required = true)
|
||||
private boolean enabled;
|
||||
|
||||
@Schema(description = "account creation date", type = SchemaType.INTEGER)
|
||||
private Instant created;
|
||||
@Schema(description = "account creation date", type = SchemaType.INTEGER)
|
||||
private Instant created;
|
||||
|
||||
@Schema(description = "last login date", type = SchemaType.INTEGER)
|
||||
private Instant lastLogin;
|
||||
@Schema(description = "last login date", type = SchemaType.INTEGER)
|
||||
private Instant lastLogin;
|
||||
|
||||
@Schema(description = "user is admin", required = true)
|
||||
private boolean admin;
|
||||
|
||||
@Schema(description = "user last force refresh", type = SchemaType.INTEGER)
|
||||
private Instant lastForceRefresh;
|
||||
@Schema(description = "user is admin", required = true)
|
||||
private boolean admin;
|
||||
|
||||
@Schema(description = "user last force refresh", type = SchemaType.INTEGER)
|
||||
private Instant lastForceRefresh;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Add Category Request")
|
||||
@Data
|
||||
public class AddCategoryRequest implements Serializable {
|
||||
|
||||
@Schema(description = "name", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "parent category id, if any")
|
||||
@Size(max = 128)
|
||||
private String parentId;
|
||||
@Schema(description = "name", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "parent category id, if any")
|
||||
@Size(max = 128)
|
||||
private String parentId;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Save User information")
|
||||
@Data
|
||||
public class AdminSaveUserRequest implements Serializable {
|
||||
|
||||
@Schema(description = "user id")
|
||||
private Long id;
|
||||
@Schema(description = "user id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "user name", required = true)
|
||||
private String name;
|
||||
@Schema(description = "user name", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "user email, if any")
|
||||
private String email;
|
||||
@Schema(description = "user email, if any")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "user password")
|
||||
@ValidPassword
|
||||
private String password;
|
||||
@Schema(description = "user password")
|
||||
@ValidPassword
|
||||
private String password;
|
||||
|
||||
@Schema(description = "account status", required = true)
|
||||
private boolean enabled;
|
||||
@Schema(description = "account status", required = true)
|
||||
private boolean enabled;
|
||||
|
||||
@Schema(description = "user is admin", required = true)
|
||||
private boolean admin;
|
||||
@Schema(description = "user is admin", required = true)
|
||||
private boolean admin;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Category modification request")
|
||||
@Data
|
||||
public class CategoryModificationRequest implements Serializable {
|
||||
|
||||
@Schema(description = "id", required = true)
|
||||
private Long id;
|
||||
@Schema(description = "id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "new name, null if not changed")
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
@Schema(description = "new name, null if not changed")
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "new parent category id")
|
||||
@Size(max = 128)
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
@Schema(description = "new parent category id")
|
||||
@Size(max = 128)
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Mark Request")
|
||||
@Data
|
||||
public class CollapseRequest implements Serializable {
|
||||
|
||||
@Schema(description = "category id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "collapse", required = true)
|
||||
private boolean collapse;
|
||||
@Schema(description = "category id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "collapse", required = true)
|
||||
private boolean collapse;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Feed information request")
|
||||
@Data
|
||||
public class FeedInfoRequest implements Serializable {
|
||||
|
||||
@Schema(description = "feed url", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 4096)
|
||||
private String url;
|
||||
|
||||
@Schema(description = "feed url", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 4096)
|
||||
private String url;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Feed modification request")
|
||||
@Data
|
||||
public class FeedModificationRequest implements Serializable {
|
||||
|
||||
@Schema(description = "id", required = true)
|
||||
private Long id;
|
||||
@Schema(description = "id", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "new name, null if not changed")
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
@Schema(description = "new name, null if not changed")
|
||||
@Size(max = 128)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "new parent category id")
|
||||
@Size(max = 128)
|
||||
private String categoryId;
|
||||
@Schema(description = "new parent category id")
|
||||
@Size(max = 128)
|
||||
private String categoryId;
|
||||
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Size(max = 4096)
|
||||
private String filter;
|
||||
@Schema(
|
||||
description =
|
||||
"CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Size(max = 4096)
|
||||
private String filter;
|
||||
|
||||
@Schema(description = "whether to send push notifications for new entries of this feed")
|
||||
private boolean pushNotificationsEnabled;
|
||||
|
||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
@Schema(description = "whether to send push notifications for new entries of this feed")
|
||||
private boolean pushNotificationsEnabled;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema
|
||||
@Data
|
||||
public class IDRequest implements Serializable {
|
||||
|
||||
@Schema(required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(required = true)
|
||||
private Long id;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Initial admin account setup request")
|
||||
@Data
|
||||
public class InitialSetupRequest implements Serializable {
|
||||
|
||||
@Schema(description = "admin username", required = true)
|
||||
private String name;
|
||||
@Schema(description = "admin username", required = true)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "admin password", required = true)
|
||||
@ValidPassword
|
||||
private String password;
|
||||
@Schema(description = "admin password", required = true)
|
||||
@ValidPassword
|
||||
private String password;
|
||||
|
||||
@Schema(description = "admin email")
|
||||
private String email;
|
||||
@Schema(description = "admin email")
|
||||
private String email;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Mark Request")
|
||||
@Data
|
||||
public class MarkRequest implements Serializable {
|
||||
|
||||
@Schema(description = "entry id, category id, 'all' or 'starred'", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String id;
|
||||
@Schema(description = "entry id, category id, 'all' or 'starred'", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "mark as read or unread", required = true)
|
||||
private boolean read;
|
||||
@Schema(description = "mark as read or unread", required = true)
|
||||
private boolean read;
|
||||
|
||||
@Schema(description = "mark only entries older than this")
|
||||
private Long olderThan;
|
||||
@Schema(description = "mark only entries older than this")
|
||||
private Long olderThan;
|
||||
|
||||
@Schema(
|
||||
description = "pass the timestamp you got from the entry list to avoid marking entries that may have been fetched in the mean time and never displayed")
|
||||
private Long insertedBefore;
|
||||
@Schema(
|
||||
description =
|
||||
"pass the timestamp you got from the entry list to avoid marking entries that may have been fetched in the mean time and never displayed")
|
||||
private Long insertedBefore;
|
||||
|
||||
@Schema(description = "only mark read if a feed has these keywords in the title or rss content")
|
||||
@Size(max = 128)
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "if marking a category or 'all', exclude those subscriptions from the marking")
|
||||
private List<Long> excludedSubscriptions;
|
||||
@Schema(description = "only mark read if a feed has these keywords in the title or rss content")
|
||||
@Size(max = 128)
|
||||
private String keywords;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"if marking a category or 'all', exclude those subscriptions from the marking")
|
||||
private List<Long> excludedSubscriptions;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Multiple Mark Request")
|
||||
@Data
|
||||
public class MultipleMarkRequest implements Serializable {
|
||||
|
||||
@Schema(description = "list of mark requests", required = true)
|
||||
private List<@Valid MarkRequest> requests;
|
||||
|
||||
@Schema(description = "list of mark requests", required = true)
|
||||
private List<@Valid MarkRequest> requests;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Schema
|
||||
public class PasswordResetConfirmationRequest implements Serializable {
|
||||
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
|
||||
@Schema(description = "password recovery token", required = true)
|
||||
@NotEmpty
|
||||
private String token;
|
||||
@Schema(description = "password recovery token", required = true)
|
||||
@NotEmpty
|
||||
private String token;
|
||||
|
||||
@Schema(description = "new password", required = true)
|
||||
@NotEmpty
|
||||
@ValidPassword
|
||||
private String password;
|
||||
@Schema(description = "new password", required = true)
|
||||
@NotEmpty
|
||||
@ValidPassword
|
||||
private String password;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Schema
|
||||
public class PasswordResetRequest implements Serializable {
|
||||
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Profile modification request")
|
||||
@Data
|
||||
public class ProfileModificationRequest implements Serializable {
|
||||
@Schema(description = "current user password, required to change profile data", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String currentPassword;
|
||||
@Schema(description = "current user password, required to change profile data", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String currentPassword;
|
||||
|
||||
@Schema(description = "changes email of the user, if specified")
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
@Schema(description = "changes email of the user, if specified")
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
|
||||
@Schema(description = "changes password of the user, if specified")
|
||||
@ValidPassword
|
||||
private String newPassword;
|
||||
|
||||
@Schema(description = "generate a new api key")
|
||||
private boolean newApiKey;
|
||||
@Schema(description = "changes password of the user, if specified")
|
||||
@ValidPassword
|
||||
private String newPassword;
|
||||
|
||||
@Schema(description = "generate a new api key")
|
||||
private boolean newApiKey;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,31 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Schema
|
||||
public class RegistrationRequest implements Serializable {
|
||||
|
||||
@Schema(description = "username, between 3 and 32 characters", required = true)
|
||||
@NotEmpty
|
||||
@Size(min = 3, max = 32)
|
||||
private String name;
|
||||
@Schema(description = "username, between 3 and 32 characters", required = true)
|
||||
@NotEmpty
|
||||
@Size(min = 3, max = 32)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "password", required = true)
|
||||
@NotEmpty
|
||||
@ValidPassword
|
||||
private String password;
|
||||
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
@Schema(description = "password", required = true)
|
||||
@NotEmpty
|
||||
@ValidPassword
|
||||
private String password;
|
||||
|
||||
@Schema(description = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
@Size(max = 255)
|
||||
private String email;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Star Request")
|
||||
@Data
|
||||
public class StarRequest implements Serializable {
|
||||
|
||||
@Schema(description = "id", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String id;
|
||||
@Schema(description = "id", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "feed id", required = true)
|
||||
private Long feedId;
|
||||
|
||||
@Schema(description = "starred or not", required = true)
|
||||
private boolean starred;
|
||||
@Schema(description = "feed id", required = true)
|
||||
private Long feedId;
|
||||
|
||||
@Schema(description = "starred or not", required = true)
|
||||
private boolean starred;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Subscription request")
|
||||
@Data
|
||||
public class SubscribeRequest implements Serializable {
|
||||
|
||||
@Schema(description = "url of the feed", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 4096)
|
||||
private String url;
|
||||
@Schema(description = "url of the feed", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 4096)
|
||||
private String url;
|
||||
|
||||
@Schema(description = "name of the feed for the user", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String title;
|
||||
@Schema(description = "name of the feed for the user", required = true)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String title;
|
||||
|
||||
@Schema(description = "id of the user category to place the feed in")
|
||||
@Size(max = 128)
|
||||
private String categoryId;
|
||||
@Schema(description = "id of the user category to place the feed in")
|
||||
@Size(max = 128)
|
||||
private String categoryId;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,17 @@ package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Tag Request")
|
||||
@Data
|
||||
public class TagRequest implements Serializable {
|
||||
|
||||
@Schema(description = "entry id", required = true)
|
||||
private Long entryId;
|
||||
|
||||
@Schema(description = "tags", required = true)
|
||||
private List<String> tags;
|
||||
@Schema(description = "entry id", required = true)
|
||||
private Long entryId;
|
||||
|
||||
@Schema(description = "tags", required = true)
|
||||
private List<String> tags;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
@@ -40,8 +15,29 @@ import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
@Path("/rest/admin")
|
||||
@RolesAllowed(Roles.ADMIN)
|
||||
@@ -52,138 +48,156 @@ import lombok.RequiredArgsConstructor;
|
||||
@Tag(name = "Admin")
|
||||
public class AdminREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final MetricRegistry metrics;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final MetricRegistry metrics;
|
||||
|
||||
@Path("/user/save")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Save or update a user",
|
||||
description = "Save or update a user. If the id is not specified, a new user will be created")
|
||||
public Response adminSaveUser(@Valid @Parameter(required = true) AdminSaveUserRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
@Path("/user/save")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Save or update a user",
|
||||
description =
|
||||
"Save or update a user. If the id is not specified, a new user will be created")
|
||||
public Response adminSaveUser(@Valid @Parameter(required = true) AdminSaveUserRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
|
||||
Long id = req.getId();
|
||||
if (id == null) {
|
||||
Preconditions.checkNotNull(req.getPassword());
|
||||
Long id = req.getId();
|
||||
if (id == null) {
|
||||
Preconditions.checkNotNull(req.getPassword());
|
||||
|
||||
Set<Role> roles = Sets.newHashSet(Role.USER);
|
||||
if (req.isAdmin()) {
|
||||
roles.add(Role.ADMIN);
|
||||
}
|
||||
try {
|
||||
id = userService.register(req.getName(), req.getPassword(), req.getEmail(), roles, true).getId();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
||||
}
|
||||
} else {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
|
||||
}
|
||||
Set<Role> roles = Sets.newHashSet(Role.USER);
|
||||
if (req.isAdmin()) {
|
||||
roles.add(Role.ADMIN);
|
||||
}
|
||||
try {
|
||||
id =
|
||||
userService
|
||||
.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
roles,
|
||||
true)
|
||||
.getId();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
||||
}
|
||||
} else {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("You cannot disable your own account.")
|
||||
.build();
|
||||
}
|
||||
|
||||
User u = userDAO.findById(id);
|
||||
u.setName(req.getName());
|
||||
if (StringUtils.isNotBlank(req.getPassword())) {
|
||||
u.setPassword(encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt()));
|
||||
}
|
||||
u.setEmail(req.getEmail());
|
||||
u.setDisabled(!req.isEnabled());
|
||||
User u = userDAO.findById(id);
|
||||
u.setName(req.getName());
|
||||
if (StringUtils.isNotBlank(req.getPassword())) {
|
||||
u.setPassword(
|
||||
encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt()));
|
||||
}
|
||||
u.setEmail(req.getEmail());
|
||||
u.setDisabled(!req.isEnabled());
|
||||
|
||||
Set<Role> roles = userRoleDAO.findRoles(u);
|
||||
if (req.isAdmin() && !roles.contains(Role.ADMIN)) {
|
||||
userRoleDAO.persist(new UserRole(u, Role.ADMIN));
|
||||
} else if (!req.isAdmin() && roles.contains(Role.ADMIN)) {
|
||||
if (userRoleDAO.countAdmins() == 1) {
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the last admin user.").build();
|
||||
}
|
||||
for (UserRole userRole : userRoleDAO.findAll(u)) {
|
||||
if (userRole.getRole() == Role.ADMIN) {
|
||||
userRoleDAO.delete(userRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Role> roles = userRoleDAO.findRoles(u);
|
||||
if (req.isAdmin() && !roles.contains(Role.ADMIN)) {
|
||||
userRoleDAO.persist(new UserRole(u, Role.ADMIN));
|
||||
} else if (!req.isAdmin() && roles.contains(Role.ADMIN)) {
|
||||
if (userRoleDAO.countAdmins() == 1) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("You cannot remove the admin role from the last admin user.")
|
||||
.build();
|
||||
}
|
||||
for (UserRole userRole : userRoleDAO.findAll(u)) {
|
||||
if (userRole.getRole() == Role.ADMIN) {
|
||||
userRoleDAO.delete(userRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Response.ok(id).build();
|
||||
}
|
||||
|
||||
}
|
||||
return Response.ok(id).build();
|
||||
@Path("/user/get/{id}")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get user information", description = "Get user information")
|
||||
public UserModel adminGetUser(
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User u = userDAO.findById(id);
|
||||
UserModel userModel = new UserModel();
|
||||
userModel.setId(u.getId());
|
||||
userModel.setName(u.getName());
|
||||
userModel.setEmail(u.getEmail());
|
||||
userModel.setEnabled(!u.isDisabled());
|
||||
userModel.setAdmin(
|
||||
userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN));
|
||||
return userModel;
|
||||
}
|
||||
|
||||
}
|
||||
@Path("/user/getAll")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get all users", description = "Get all users")
|
||||
public List<UserModel> adminGetUsers() {
|
||||
Map<Long, UserModel> users = new HashMap<>();
|
||||
for (UserRole role : userRoleDAO.findAll()) {
|
||||
User u = role.getUser();
|
||||
UserModel userModel =
|
||||
users.computeIfAbsent(
|
||||
u.getId(),
|
||||
k -> {
|
||||
UserModel um = new UserModel();
|
||||
um.setId(u.getId());
|
||||
um.setName(u.getName());
|
||||
um.setEmail(u.getEmail());
|
||||
um.setEnabled(!u.isDisabled());
|
||||
um.setCreated(u.getCreated());
|
||||
um.setLastLogin(u.getLastLogin());
|
||||
return um;
|
||||
});
|
||||
|
||||
@Path("/user/get/{id}")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get user information", description = "Get user information")
|
||||
public UserModel adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User u = userDAO.findById(id);
|
||||
UserModel userModel = new UserModel();
|
||||
userModel.setId(u.getId());
|
||||
userModel.setName(u.getName());
|
||||
userModel.setEmail(u.getEmail());
|
||||
userModel.setEnabled(!u.isDisabled());
|
||||
userModel.setAdmin(userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN));
|
||||
return userModel;
|
||||
}
|
||||
if (role.getRole() == Role.ADMIN) {
|
||||
userModel.setAdmin(true);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
|
||||
@Path("/user/getAll")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get all users", description = "Get all users")
|
||||
public List<UserModel> adminGetUsers() {
|
||||
Map<Long, UserModel> users = new HashMap<>();
|
||||
for (UserRole role : userRoleDAO.findAll()) {
|
||||
User u = role.getUser();
|
||||
UserModel userModel = users.computeIfAbsent(u.getId(), k -> {
|
||||
UserModel um = new UserModel();
|
||||
um.setId(u.getId());
|
||||
um.setName(u.getName());
|
||||
um.setEmail(u.getEmail());
|
||||
um.setEnabled(!u.isDisabled());
|
||||
um.setCreated(u.getCreated());
|
||||
um.setLastLogin(u.getLastLogin());
|
||||
return um;
|
||||
});
|
||||
@Path("/user/delete")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions")
|
||||
public Response adminDeleteUser(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
if (role.getRole() == Role.ADMIN) {
|
||||
userModel.setAdmin(true);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
User u = userDAO.findById(req.getId());
|
||||
if (u == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
@Path("/user/delete")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions")
|
||||
public Response adminDeleteUser(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User u = userDAO.findById(req.getId());
|
||||
if (u == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user.getId().equals(u.getId())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build();
|
||||
}
|
||||
userService.unregister(u);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/metrics")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve server metrics")
|
||||
public Response getMetrics() {
|
||||
return Response.ok(metrics).build();
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user.getId().equals(u.getId())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("You cannot delete your own user.")
|
||||
.build();
|
||||
}
|
||||
userService.unregister(u);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/metrics")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve server metrics")
|
||||
public Response getMetrics() {
|
||||
return Response.ok(metrics).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
@@ -67,9 +31,41 @@ import com.google.common.collect.Lists;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.io.StringWriter;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
@Path("/rest/category")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@@ -81,376 +77,472 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Tag(name = "Feed categories")
|
||||
public class CategoryREST {
|
||||
|
||||
public static final String ALL = "all";
|
||||
public static final String STARRED = "starred";
|
||||
public static final String ALL = "all";
|
||||
public static final String STARRED = "starred";
|
||||
|
||||
private static final Comparator<Category> CATEGORY_COMPARATOR = Comparator.comparing(Category::getPosition)
|
||||
.thenComparing(Category::getName);
|
||||
private static final Comparator<Subscription> SUBSCRIPTION_COMPARATOR = Comparator.comparing(Subscription::getPosition)
|
||||
.thenComparing(Subscription::getName);
|
||||
private static final Comparator<Category> CATEGORY_COMPARATOR =
|
||||
Comparator.comparing(Category::getPosition).thenComparing(Category::getName);
|
||||
private static final Comparator<Subscription> SUBSCRIPTION_COMPARATOR =
|
||||
Comparator.comparing(Subscription::getPosition).thenComparing(Subscription::getName);
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get category entries", description = "Get a list of category entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class)) })
|
||||
@APIResponse(responseCode = "404", description = "category not found")
|
||||
public Response getCategoryEntries(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords,
|
||||
@Parameter(
|
||||
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get category entries", description = "Get a list of category entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Entries.class))
|
||||
})
|
||||
@APIResponse(responseCode = "404", description = "category not found")
|
||||
public Response getCategoryEntries(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true)
|
||||
@QueryParam("id")
|
||||
String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("unread")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@QueryParam("keywords")
|
||||
String keywords,
|
||||
@Parameter(description = "comma-separated list of excluded subscription ids")
|
||||
@QueryParam("excludedSubscriptionIds")
|
||||
String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag")
|
||||
String tag) {
|
||||
|
||||
Preconditions.checkNotNull(readType);
|
||||
Preconditions.checkNotNull(readType);
|
||||
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
List<FeedEntryKeyword> entryKeywords =
|
||||
FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
|
||||
Entries entries = new Entries();
|
||||
entries.setOffset(offset);
|
||||
entries.setLimit(limit);
|
||||
boolean unreadOnly = readType == ReadingMode.UNREAD;
|
||||
if (StringUtils.isBlank(id)) {
|
||||
id = ALL;
|
||||
}
|
||||
Entries entries = new Entries();
|
||||
entries.setOffset(offset);
|
||||
entries.setLimit(limit);
|
||||
boolean unreadOnly = readType == ReadingMode.UNREAD;
|
||||
if (StringUtils.isBlank(id)) {
|
||||
id = ALL;
|
||||
}
|
||||
|
||||
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
|
||||
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
|
||||
|
||||
List<Long> excludedIds = null;
|
||||
if (StringUtils.isNotEmpty(excludedSubscriptionIds)) {
|
||||
excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
List<Long> excludedIds = null;
|
||||
if (StringUtils.isNotEmpty(excludedSubscriptionIds)) {
|
||||
excludedIds =
|
||||
Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(id)) {
|
||||
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(id)) {
|
||||
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
||||
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
||||
offset, limit + 1, order, true, tag, null, null);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subs,
|
||||
unreadOnly,
|
||||
entryKeywords,
|
||||
newerThanDate,
|
||||
offset,
|
||||
limit + 1,
|
||||
order,
|
||||
true,
|
||||
tag,
|
||||
null,
|
||||
null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
|
||||
} else if (STARRED.equals(id)) {
|
||||
entries.setName("Starred");
|
||||
List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred(user, entryKeywords, newerThanDate, offset, limit + 1, order,
|
||||
true);
|
||||
for (FeedEntryStatus status : starred) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id));
|
||||
if (parent != null) {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
||||
offset, limit + 1, order, true, tag, null, null);
|
||||
} else if (STARRED.equals(id)) {
|
||||
entries.setName("Starred");
|
||||
List<FeedEntryStatus> starred =
|
||||
feedEntryStatusDAO.findStarred(
|
||||
user, entryKeywords, newerThanDate, offset, limit + 1, order, true);
|
||||
for (FeedEntryStatus status : starred) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id));
|
||||
if (parent != null) {
|
||||
List<FeedCategory> categories =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs =
|
||||
feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subs,
|
||||
unreadOnly,
|
||||
entryKeywords,
|
||||
newerThanDate,
|
||||
offset,
|
||||
limit + 1,
|
||||
order,
|
||||
true,
|
||||
tag,
|
||||
null,
|
||||
null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
|
||||
}
|
||||
}
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND)
|
||||
.entity("<message>category not found</message>")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
if (hasMore) {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().removeLast();
|
||||
}
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
if (hasMore) {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().removeLast();
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get category entries as feed", description = "Get a feed of category entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getCategoryEntriesAsFeed(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords,
|
||||
@Parameter(
|
||||
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get category entries as feed",
|
||||
description = "Get a feed of category entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getCategoryEntriesAsFeed(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true)
|
||||
@QueryParam("id")
|
||||
String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("all")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@QueryParam("keywords")
|
||||
String keywords,
|
||||
@Parameter(description = "comma-separated list of excluded subscription ids")
|
||||
@QueryParam("excludedSubscriptionIds")
|
||||
String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag")
|
||||
String tag) {
|
||||
|
||||
Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
Response response =
|
||||
getCategoryEntries(
|
||||
id,
|
||||
readType,
|
||||
newerThan,
|
||||
offset,
|
||||
limit,
|
||||
order,
|
||||
keywords,
|
||||
excludedSubscriptionIds,
|
||||
tag);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(uri.getBaseUri().toString());
|
||||
feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList());
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(uri.getBaseUri().toString());
|
||||
feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList());
|
||||
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
output.output(feed, writer);
|
||||
} catch (Exception e) {
|
||||
writer.write("Could not get feed information");
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return Response.ok(writer.toString()).build();
|
||||
}
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
output.output(feed, writer);
|
||||
} catch (Exception e) {
|
||||
writer.write("Could not get feed information");
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return Response.ok(writer.toString()).build();
|
||||
}
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read")
|
||||
public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark category entries",
|
||||
description = "Mark feed entries of this category as read")
|
||||
public Response markCategoryEntries(
|
||||
@Valid @Parameter(description = "category id, or 'all'", required = true)
|
||||
MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
Instant olderThan =
|
||||
req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore =
|
||||
req.getInsertedBefore() == null
|
||||
? null
|
||||
: Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(req.getId())) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
} else if (STARRED.equals(req.getId())) {
|
||||
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(req.getId()));
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(req.getId())) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
} else if (STARRED.equals(req.getId())) {
|
||||
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(req.getId()));
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
|
||||
if (CollectionUtils.isNotEmpty(excludedIds)) {
|
||||
subs.removeIf(sub -> excludedIds.contains(sub.getId()));
|
||||
}
|
||||
}
|
||||
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
|
||||
if (CollectionUtils.isNotEmpty(excludedIds)) {
|
||||
subs.removeIf(sub -> excludedIds.contains(sub.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/add")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Add a category", description = "Add a new feed category")
|
||||
public Long addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
@Path("/add")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Add a category", description = "Add a new feed category")
|
||||
public Long addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
FeedCategory cat = new FeedCategory();
|
||||
cat.setName(req.getName());
|
||||
cat.setUser(user);
|
||||
cat.setPosition(0);
|
||||
String parentId = req.getParentId();
|
||||
if (parentId != null && !ALL.equals(parentId)) {
|
||||
FeedCategory parent = new FeedCategory();
|
||||
parent.setId(Long.valueOf(parentId));
|
||||
cat.setParent(parent);
|
||||
}
|
||||
feedCategoryDAO.persist(cat);
|
||||
return cat.getId();
|
||||
}
|
||||
FeedCategory cat = new FeedCategory();
|
||||
cat.setName(req.getName());
|
||||
cat.setUser(user);
|
||||
cat.setPosition(0);
|
||||
String parentId = req.getParentId();
|
||||
if (parentId != null && !ALL.equals(parentId)) {
|
||||
FeedCategory parent = new FeedCategory();
|
||||
parent.setId(Long.valueOf(parentId));
|
||||
cat.setParent(parent);
|
||||
}
|
||||
feedCategoryDAO.persist(cat);
|
||||
return cat.getId();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/delete")
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a category", description = "Delete an existing feed category")
|
||||
public Response deleteCategory(@Parameter(required = true) IDRequest req) {
|
||||
@POST
|
||||
@Path("/delete")
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a category", description = "Delete an existing feed category")
|
||||
public Response deleteCategory(@Parameter(required = true) IDRequest req) {
|
||||
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory cat = feedCategoryDAO.findById(user, req.getId());
|
||||
if (cat != null) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(user, cat);
|
||||
for (FeedSubscription sub : subs) {
|
||||
sub.setCategory(null);
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory cat = feedCategoryDAO.findById(user, req.getId());
|
||||
if (cat != null) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(user, cat);
|
||||
for (FeedSubscription sub : subs) {
|
||||
sub.setCategory(null);
|
||||
}
|
||||
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, cat);
|
||||
for (FeedCategory child : categories) {
|
||||
if (!child.getId().equals(cat.getId()) && child.getParent().getId().equals(cat.getId())) {
|
||||
child.setParent(null);
|
||||
}
|
||||
}
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, cat);
|
||||
for (FeedCategory child : categories) {
|
||||
if (!child.getId().equals(cat.getId())
|
||||
&& child.getParent().getId().equals(cat.getId())) {
|
||||
child.setParent(null);
|
||||
}
|
||||
}
|
||||
|
||||
feedCategoryDAO.delete(cat);
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
feedCategoryDAO.delete(cat);
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a category", description = "Modify an existing feed category")
|
||||
public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a category", description = "Modify an existing feed category")
|
||||
public Response modifyCategory(
|
||||
@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
category.setName(req.getName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
category.setName(req.getName());
|
||||
}
|
||||
|
||||
FeedCategory parent = null;
|
||||
if (req.getParentId() != null && !ALL.equals(req.getParentId())
|
||||
&& !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) {
|
||||
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId()));
|
||||
}
|
||||
category.setParent(parent);
|
||||
FeedCategory parent = null;
|
||||
if (req.getParentId() != null
|
||||
&& !ALL.equals(req.getParentId())
|
||||
&& !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) {
|
||||
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId()));
|
||||
}
|
||||
category.setParent(parent);
|
||||
|
||||
if (req.getPosition() != null) {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findByParent(user, parent);
|
||||
categories.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||
if (req.getPosition() != null) {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findByParent(user, parent);
|
||||
categories.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||
|
||||
int existingIndex = -1;
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
if (Objects.equals(categories.get(i).getId(), category.getId())) {
|
||||
existingIndex = i;
|
||||
}
|
||||
}
|
||||
if (existingIndex != -1) {
|
||||
categories.remove(existingIndex);
|
||||
}
|
||||
int existingIndex = -1;
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
if (Objects.equals(categories.get(i).getId(), category.getId())) {
|
||||
existingIndex = i;
|
||||
}
|
||||
}
|
||||
if (existingIndex != -1) {
|
||||
categories.remove(existingIndex);
|
||||
}
|
||||
|
||||
categories.add(Math.min(req.getPosition(), categories.size()), category);
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
categories.get(i).setPosition(i);
|
||||
}
|
||||
}
|
||||
categories.add(Math.min(req.getPosition(), categories.size()), category);
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
categories.get(i).setPosition(i);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/collapse")
|
||||
@Transactional
|
||||
@Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category")
|
||||
public Response collapseCategory(@Parameter(required = true) CollapseRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
@POST
|
||||
@Path("/collapse")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Collapse a category",
|
||||
description = "Save collapsed or expanded status for a category")
|
||||
public Response collapseCategory(@Parameter(required = true) CollapseRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
if (category == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
category.setCollapsed(req.isCollapse());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
if (category == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
category.setCollapsed(req.isCollapse());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unreadCount")
|
||||
@Transactional
|
||||
@Operation(summary = "Get unread count for feed subscriptions")
|
||||
public List<UnreadCount> getUnreadCount() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Map<Long, UnreadCount> unreadCount = feedSubscriptionService.getUnreadCount(user);
|
||||
return Lists.newArrayList(unreadCount.values());
|
||||
}
|
||||
@GET
|
||||
@Path("/unreadCount")
|
||||
@Transactional
|
||||
@Operation(summary = "Get unread count for feed subscriptions")
|
||||
public List<UnreadCount> getUnreadCount() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Map<Long, UnreadCount> unreadCount = feedSubscriptionService.getUnreadCount(user);
|
||||
return Lists.newArrayList(unreadCount.values());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get")
|
||||
@Transactional
|
||||
@Operation(summary = "Get root category", description = "Get all categories and subscriptions of the user")
|
||||
public Category getRootCategory() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
@GET
|
||||
@Path("/get")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get root category",
|
||||
description = "Get all categories and subscriptions of the user")
|
||||
public Category getRootCategory() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||
Map<Long, UnreadCount> unreadCount = feedSubscriptionService.getUnreadCount(user);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||
Map<Long, UnreadCount> unreadCount = feedSubscriptionService.getUnreadCount(user);
|
||||
|
||||
Category root = buildCategory(null, categories, subscriptions, unreadCount);
|
||||
root.setId("all");
|
||||
root.setName("All");
|
||||
Category root = buildCategory(null, categories, subscriptions, unreadCount);
|
||||
root.setId("all");
|
||||
root.setName("All");
|
||||
|
||||
return root;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private Category buildCategory(Long id, List<FeedCategory> categories, List<FeedSubscription> subscriptions,
|
||||
Map<Long, UnreadCount> unreadCount) {
|
||||
Category category = new Category();
|
||||
category.setId(String.valueOf(id));
|
||||
category.setExpanded(true);
|
||||
private Category buildCategory(
|
||||
Long id,
|
||||
List<FeedCategory> categories,
|
||||
List<FeedSubscription> subscriptions,
|
||||
Map<Long, UnreadCount> unreadCount) {
|
||||
Category category = new Category();
|
||||
category.setId(String.valueOf(id));
|
||||
category.setExpanded(true);
|
||||
|
||||
for (FeedCategory c : categories) {
|
||||
if (id == null && c.getParent() == null || c.getParent() != null && Objects.equals(c.getParent().getId(), id)) {
|
||||
Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount);
|
||||
child.setId(String.valueOf(c.getId()));
|
||||
child.setName(c.getName());
|
||||
child.setPosition(c.getPosition());
|
||||
if (c.getParent() != null && c.getParent().getId() != null) {
|
||||
child.setParentId(String.valueOf(c.getParent().getId()));
|
||||
child.setParentName(c.getParent().getName());
|
||||
}
|
||||
child.setExpanded(!c.isCollapsed());
|
||||
category.getChildren().add(child);
|
||||
}
|
||||
}
|
||||
category.getChildren().sort(CATEGORY_COMPARATOR);
|
||||
for (FeedCategory c : categories) {
|
||||
if (id == null && c.getParent() == null
|
||||
|| c.getParent() != null && Objects.equals(c.getParent().getId(), id)) {
|
||||
Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount);
|
||||
child.setId(String.valueOf(c.getId()));
|
||||
child.setName(c.getName());
|
||||
child.setPosition(c.getPosition());
|
||||
if (c.getParent() != null && c.getParent().getId() != null) {
|
||||
child.setParentId(String.valueOf(c.getParent().getId()));
|
||||
child.setParentName(c.getParent().getName());
|
||||
}
|
||||
child.setExpanded(!c.isCollapsed());
|
||||
category.getChildren().add(child);
|
||||
}
|
||||
}
|
||||
category.getChildren().sort(CATEGORY_COMPARATOR);
|
||||
|
||||
for (FeedSubscription subscription : subscriptions) {
|
||||
if (id == null && subscription.getCategory() == null
|
||||
|| subscription.getCategory() != null && Objects.equals(subscription.getCategory().getId(), id)) {
|
||||
UnreadCount uc = unreadCount.get(subscription.getId());
|
||||
Subscription sub = Subscription.build(subscription, uc);
|
||||
category.getFeeds().add(sub);
|
||||
}
|
||||
}
|
||||
category.getFeeds().sort(SUBSCRIPTION_COMPARATOR);
|
||||
|
||||
return category;
|
||||
}
|
||||
for (FeedSubscription subscription : subscriptions) {
|
||||
if (id == null && subscription.getCategory() == null
|
||||
|| subscription.getCategory() != null
|
||||
&& Objects.equals(subscription.getCategory().getId(), id)) {
|
||||
UnreadCount uc = unreadCount.get(subscription.getId());
|
||||
Subscription sub = Subscription.build(subscription, uc);
|
||||
category.getFeeds().add(sub);
|
||||
}
|
||||
}
|
||||
category.getFeeds().sort(SUBSCRIPTION_COMPARATOR);
|
||||
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
@@ -29,8 +11,22 @@ import com.commafeed.frontend.model.request.TagRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
@Path("/rest/entry")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@@ -41,78 +37,87 @@ import lombok.RequiredArgsConstructor;
|
||||
@Tag(name = "Feed entries")
|
||||
public class EntryREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedEntryTagService feedEntryTagService;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedEntryTagService feedEntryTagService;
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response markEntry(
|
||||
@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead());
|
||||
return Response.ok().build();
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead());
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/markMultiple")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread")
|
||||
public Response markEntries(@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getRequests());
|
||||
@Path("/markMultiple")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark multiple feed entries",
|
||||
description = "Mark feed entries as read/unread")
|
||||
public Response markEntries(
|
||||
@Valid @Parameter(description = "Multiple Mark Request", required = true)
|
||||
MultipleMarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getRequests());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
for (MarkRequest r : req.getRequests()) {
|
||||
Preconditions.checkNotNull(r.getId());
|
||||
feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead());
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
for (MarkRequest r : req.getRequests()) {
|
||||
Preconditions.checkNotNull(r.getId());
|
||||
feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead());
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/star")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
Preconditions.checkNotNull(req.getFeedId());
|
||||
@Path("/star")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response starEntry(
|
||||
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
Preconditions.checkNotNull(req.getFeedId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.starEntry(
|
||||
user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/tags")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user")
|
||||
public Response getTags() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
List<String> tags = feedEntryTagDAO.findByUser(user);
|
||||
return Response.ok(tags).build();
|
||||
}
|
||||
@Path("/tags")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get list of tags for the user",
|
||||
description = "Get list of tags for the user")
|
||||
public Response getTags() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
List<String> tags = feedEntryTagDAO.findByUser(user);
|
||||
return Response.ok(tags).build();
|
||||
}
|
||||
|
||||
@Path("/tag")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Set feed entry tags")
|
||||
public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getEntryId());
|
||||
@Path("/tag")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Set feed entry tags")
|
||||
public Response tagEntry(
|
||||
@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getEntryId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,5 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.resteasy.reactive.Cache;
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConstants;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
@@ -85,9 +47,43 @@ import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
import com.rometools.rome.io.WireFeedOutput;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.resteasy.reactive.Cache;
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
|
||||
@Path("/rest/feed")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@@ -99,422 +95,518 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Tag(name = "Feeds")
|
||||
public class FeedREST {
|
||||
|
||||
private static final FeedEntry TEST_ENTRY = initTestEntry();
|
||||
private static final FeedEntry TEST_ENTRY = initTestEntry();
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedFetcher feedFetcher;
|
||||
private final FeedFaviconService feedFaviconService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||
private final OPMLImporter opmlImporter;
|
||||
private final OPMLExporter opmlExporter;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedFetcher feedFetcher;
|
||||
private final FeedFaviconService feedFaviconService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||
private final OPMLImporter opmlImporter;
|
||||
private final OPMLExporter opmlExporter;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
|
||||
private static FeedEntry initTestEntry() {
|
||||
FeedEntry entry = new FeedEntry();
|
||||
entry.setUrl("https://github.com/Athou/commafeed");
|
||||
private static FeedEntry initTestEntry() {
|
||||
FeedEntry entry = new FeedEntry();
|
||||
entry.setUrl("https://github.com/Athou/commafeed");
|
||||
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setAuthor("Athou");
|
||||
content.setTitle("Merge pull request #662 from Athou/dw8");
|
||||
content.setContent("Merge pull request #662 from Athou/dw8");
|
||||
entry.setContent(content);
|
||||
return entry;
|
||||
}
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setAuthor("Athou");
|
||||
content.setTitle("Merge pull request #662 from Athou/dw8");
|
||||
content.setContent("Merge pull request #662 from Athou/dw8");
|
||||
entry.setContent(content);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get feed entries", description = "Get a list of feed entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class)) })
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeedEntries(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
|
||||
description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords) {
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get feed entries", description = "Get a list of feed entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Entries.class))
|
||||
})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeedEntries(
|
||||
@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("unread")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@QueryParam("keywords")
|
||||
String keywords) {
|
||||
|
||||
Preconditions.checkNotNull(id);
|
||||
Preconditions.checkNotNull(readType);
|
||||
Preconditions.checkNotNull(id);
|
||||
Preconditions.checkNotNull(readType);
|
||||
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
List<FeedEntryKeyword> entryKeywords =
|
||||
FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
|
||||
Entries entries = new Entries();
|
||||
entries.setOffset(offset);
|
||||
entries.setLimit(limit);
|
||||
Entries entries = new Entries();
|
||||
entries.setOffset(offset);
|
||||
entries.setLimit(limit);
|
||||
|
||||
boolean unreadOnly = readType == ReadingMode.UNREAD;
|
||||
boolean unreadOnly = readType == ReadingMode.UNREAD;
|
||||
|
||||
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
|
||||
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id));
|
||||
if (subscription != null) {
|
||||
entries.setName(subscription.getTitle());
|
||||
entries.setMessage(subscription.getFeed().getMessage());
|
||||
entries.setErrorCount(subscription.getFeed().getErrorCount());
|
||||
entries.setFeedLink(subscription.getFeed().getLink());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id));
|
||||
if (subscription != null) {
|
||||
entries.setName(subscription.getTitle());
|
||||
entries.setMessage(subscription.getFeed().getMessage());
|
||||
entries.setErrorCount(subscription.getFeed().getErrorCount());
|
||||
entries.setFeedLink(subscription.getFeed().getLink());
|
||||
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly,
|
||||
entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null);
|
||||
List<FeedEntryStatus> list =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
Collections.singletonList(subscription),
|
||||
unreadOnly,
|
||||
entryKeywords,
|
||||
newerThanDate,
|
||||
offset,
|
||||
limit + 1,
|
||||
order,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
if (hasMore) {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().removeLast();
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>feed not found</message>").build();
|
||||
}
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
if (hasMore) {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().removeLast();
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND)
|
||||
.entity("<message>feed not found</message>")
|
||||
.build();
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(keywords != null);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
entries.setIgnoredReadStatus(keywords != null);
|
||||
return Response.ok(entries).build();
|
||||
}
|
||||
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getFeedEntriesAsFeed(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
|
||||
description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords) {
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getFeedEntriesAsFeed(
|
||||
@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("all")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@QueryParam("keywords")
|
||||
String keywords) {
|
||||
|
||||
Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(uri.getBaseUri().toString());
|
||||
feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList());
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(uri.getBaseUri().toString());
|
||||
feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList());
|
||||
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
output.output(feed, writer);
|
||||
} catch (Exception e) {
|
||||
writer.write("Could not get feed information");
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return Response.ok(writer.toString()).build();
|
||||
}
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
StringWriter writer = new StringWriter();
|
||||
try {
|
||||
output.output(feed, writer);
|
||||
} catch (Exception e) {
|
||||
writer.write("Could not get feed information");
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return Response.ok(writer.toString()).build();
|
||||
}
|
||||
|
||||
private FeedInfo fetchFeedInternal(String url) {
|
||||
FeedInfo info;
|
||||
url = StringUtils.trimToEmpty(url);
|
||||
url = prependHttp(url);
|
||||
try {
|
||||
FeedFetcherResult feedFetcherResult = feedFetcher.fetch(url, true, null, null, null, null);
|
||||
info = new FeedInfo();
|
||||
info.setUrl(feedFetcherResult.urlAfterRedirect());
|
||||
info.setTitle(feedFetcherResult.feed().title());
|
||||
private FeedInfo fetchFeedInternal(String url) {
|
||||
FeedInfo info;
|
||||
url = StringUtils.trimToEmpty(url);
|
||||
url = prependHttp(url);
|
||||
try {
|
||||
FeedFetcherResult feedFetcherResult =
|
||||
feedFetcher.fetch(url, true, null, null, null, null);
|
||||
info = new FeedInfo();
|
||||
info.setUrl(feedFetcherResult.urlAfterRedirect());
|
||||
info.setTitle(feedFetcherResult.feed().title());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug(e.getMessage(), e);
|
||||
throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug(e.getMessage(), e);
|
||||
throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/fetch")
|
||||
@Transactional
|
||||
@Operation(summary = "Fetch a feed", description = "Fetch a feed by its url")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = { @Content(mediaType = "application/json", schema = @Schema(implementation = FeedInfo.class)) })
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
@POST
|
||||
@Path("/fetch")
|
||||
@Transactional
|
||||
@Operation(summary = "Fetch a feed", description = "Fetch a feed by its url")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = FeedInfo.class))
|
||||
})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response fetchFeed(
|
||||
@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
|
||||
FeedInfo info;
|
||||
try {
|
||||
info = fetchFeedInternal(req.getUrl());
|
||||
} catch (Exception e) {
|
||||
Throwable cause = Throwables.getRootCause(e);
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(cause.getMessage()).type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
return Response.ok(info).build();
|
||||
}
|
||||
FeedInfo info;
|
||||
try {
|
||||
info = fetchFeedInternal(req.getUrl());
|
||||
} catch (Exception e) {
|
||||
Throwable cause = Throwables.getRootCause(e);
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(cause.getMessage())
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
return Response.ok(info).build();
|
||||
}
|
||||
|
||||
@Path("/refreshAll")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue")
|
||||
public Response queueAllForRefresh() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
try {
|
||||
feedSubscriptionService.refreshAll(user);
|
||||
return Response.ok().build();
|
||||
} catch (ForceFeedRefreshTooSoonException e) {
|
||||
return Response.status(HttpStatus.SC_TOO_MANY_REQUESTS).build();
|
||||
}
|
||||
@Path("/refreshAll")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Queue all feeds of the user for refresh",
|
||||
description = "Manually add all feeds of the user to the refresh queue")
|
||||
public Response queueAllForRefresh() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
try {
|
||||
feedSubscriptionService.refreshAll(user);
|
||||
return Response.ok().build();
|
||||
} catch (ForceFeedRefreshTooSoonException e) {
|
||||
return Response.status(HttpStatus.SC_TOO_MANY_REQUESTS).build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark feed entries",
|
||||
description = "Mark feed entries as read (unread is not supported)")
|
||||
public Response markFeedEntries(
|
||||
@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)")
|
||||
public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
Instant olderThan =
|
||||
req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore =
|
||||
req.getInsertedBefore() == null
|
||||
? null
|
||||
: Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription =
|
||||
feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
|
||||
if (subscription != null) {
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user,
|
||||
Collections.singletonList(subscription),
|
||||
olderThan,
|
||||
insertedBefore,
|
||||
entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
|
||||
if (subscription != null) {
|
||||
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, insertedBefore,
|
||||
entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
@GET
|
||||
@Path("/get/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "get feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Subscription.class))
|
||||
})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeed(
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
@GET
|
||||
@Path("/get/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "get feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class)) })
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, id);
|
||||
if (sub == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
UnreadCount unreadCount = feedSubscriptionService.getUnreadCount(user).get(id);
|
||||
return Response.ok(Subscription.build(sub, unreadCount)).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, id);
|
||||
if (sub == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
UnreadCount unreadCount = feedSubscriptionService.getUnreadCount(user).get(id);
|
||||
return Response.ok(Subscription.build(sub, unreadCount)).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/favicon/{id}")
|
||||
@Cache(maxAge = 2592000)
|
||||
@Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon")
|
||||
public Response getFeedFavicon(
|
||||
@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
@GET
|
||||
@Path("/favicon/{id}")
|
||||
@Cache(maxAge = 2592000)
|
||||
@Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon")
|
||||
public Response getFeedFavicon(@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||
if (subscription == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||
if (subscription == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
Feed feed = subscription.getFeed();
|
||||
if (feed.getLastUpdated() == null) {
|
||||
// the feed has never been fetched yet so the iconUrl field hasn't been updated yet, and
|
||||
// the
|
||||
// icon can't be fetched
|
||||
// this can happen for newly subscribed feeds:
|
||||
// the feed has been added in the tree in the web client and the favicon is being
|
||||
// fetched,
|
||||
// but the feed has not been fetched yet
|
||||
// the client is configured to retry in that case
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE)
|
||||
.entity("Feed has not been fetched yet, please retry in a bit")
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
|
||||
Feed feed = subscription.getFeed();
|
||||
if (feed.getLastUpdated() == null) {
|
||||
// the feed has never been fetched yet so the iconUrl field hasn't been updated yet, and the icon can't be fetched
|
||||
// this can happen for newly subscribed feeds:
|
||||
// the feed has been added in the tree in the web client and the favicon is being fetched,
|
||||
// but the feed has not been fetched yet
|
||||
// the client is configured to retry in that case
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE)
|
||||
.entity("Feed has not been fetched yet, please retry in a bit")
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
Favicon icon = feedFaviconService.fetchFavicon(feed);
|
||||
return Response.ok(icon.icon(), icon.mediaType()).build();
|
||||
}
|
||||
|
||||
Favicon icon = feedFaviconService.fetchFavicon(feed);
|
||||
return Response.ok(icon.icon(), icon.mediaType()).build();
|
||||
}
|
||||
@POST
|
||||
@Path("/subscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))
|
||||
})
|
||||
public Response subscribe(
|
||||
@Valid @Parameter(description = "subscription request", required = true)
|
||||
SubscribeRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getTitle());
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
|
||||
@POST
|
||||
@Path("/subscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Long.class)) })
|
||||
public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getTitle());
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
try {
|
||||
FeedCategory category = null;
|
||||
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
|
||||
category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
|
||||
}
|
||||
|
||||
try {
|
||||
FeedCategory category = null;
|
||||
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
|
||||
category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
|
||||
}
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
long subscriptionId =
|
||||
feedSubscriptionService.subscribe(
|
||||
user, info.getUrl(), req.getTitle(), category, 0);
|
||||
return Response.ok(subscriptionId).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE)
|
||||
.entity("Failed to subscribe to URL " + req.getUrl() + ": " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0);
|
||||
return Response.ok(subscriptionId).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE)
|
||||
.entity("Failed to subscribe to URL " + req.getUrl() + ": " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@GET
|
||||
@Path("/subscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
public Response subscribeFromUrl(
|
||||
@Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
|
||||
try {
|
||||
Preconditions.checkNotNull(url);
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(url));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0);
|
||||
} catch (Exception e) {
|
||||
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
|
||||
}
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/subscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
public Response subscribeFromUrl(@Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
|
||||
try {
|
||||
Preconditions.checkNotNull(url);
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(url));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0);
|
||||
} catch (Exception e) {
|
||||
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
|
||||
}
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).build();
|
||||
}
|
||||
private String prependHttp(String url) {
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private String prependHttp(String url) {
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@POST
|
||||
@Path("/unsubscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed")
|
||||
public Response unsubscribe(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@POST
|
||||
@Path("/unsubscribe")
|
||||
@Transactional
|
||||
@Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed")
|
||||
public Response unsubscribe(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId());
|
||||
if (deleted) {
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId());
|
||||
if (deleted) {
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a subscription", description = "Modify a feed subscription")
|
||||
public Response modifyFeed(
|
||||
@Valid @Parameter(description = "subscription id", required = true)
|
||||
FeedModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a subscription", description = "Modify a feed subscription")
|
||||
public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
try {
|
||||
feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY);
|
||||
} catch (FeedEntryFilterException e) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY);
|
||||
} catch (FeedEntryFilterException e) {
|
||||
return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
|
||||
subscription.setFilter(req.getFilter());
|
||||
if (StringUtils.isNotBlank(subscription.getFilter())) {
|
||||
// if the new filter is filled, remove the legacy filter
|
||||
subscription.setFilterLegacy(null);
|
||||
}
|
||||
|
||||
subscription.setFilter(req.getFilter());
|
||||
if (StringUtils.isNotBlank(subscription.getFilter())) {
|
||||
// if the new filter is filled, remove the legacy filter
|
||||
subscription.setFilterLegacy(null);
|
||||
}
|
||||
subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled());
|
||||
subscription.setAutoMarkAsReadAfterDays(req.getAutoMarkAsReadAfterDays());
|
||||
|
||||
subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled());
|
||||
subscription.setAutoMarkAsReadAfterDays(req.getAutoMarkAsReadAfterDays());
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
subscription.setTitle(req.getName());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
subscription.setTitle(req.getName());
|
||||
}
|
||||
FeedCategory parent = null;
|
||||
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
|
||||
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getCategoryId()));
|
||||
}
|
||||
subscription.setCategory(parent);
|
||||
|
||||
FeedCategory parent = null;
|
||||
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
|
||||
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getCategoryId()));
|
||||
}
|
||||
subscription.setCategory(parent);
|
||||
if (req.getPosition() != null) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(user, parent);
|
||||
subs.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||
|
||||
if (req.getPosition() != null) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(user, parent);
|
||||
subs.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||
int existingIndex = -1;
|
||||
for (int i = 0; i < subs.size(); i++) {
|
||||
if (Objects.equals(subs.get(i).getId(), subscription.getId())) {
|
||||
existingIndex = i;
|
||||
}
|
||||
}
|
||||
if (existingIndex != -1) {
|
||||
subs.remove(existingIndex);
|
||||
}
|
||||
|
||||
int existingIndex = -1;
|
||||
for (int i = 0; i < subs.size(); i++) {
|
||||
if (Objects.equals(subs.get(i).getId(), subscription.getId())) {
|
||||
existingIndex = i;
|
||||
}
|
||||
}
|
||||
if (existingIndex != -1) {
|
||||
subs.remove(existingIndex);
|
||||
}
|
||||
subs.add(Math.min(req.getPosition(), subs.size()), subscription);
|
||||
for (int i = 0; i < subs.size(); i++) {
|
||||
subs.get(i).setPosition(i);
|
||||
}
|
||||
}
|
||||
|
||||
subs.add(Math.min(req.getPosition(), subs.size()), subscription);
|
||||
for (int i = 0; i < subs.size(); i++) {
|
||||
subs.get(i).setPosition(i);
|
||||
}
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
@POST
|
||||
@Path("/import")
|
||||
@Transactional
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(
|
||||
summary = "OPML import",
|
||||
description = "Import an OPML file, posted as a FORM with the 'file' name")
|
||||
public Response importOpml(
|
||||
@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("Import is disabled for the demo account")
|
||||
.build();
|
||||
}
|
||||
try {
|
||||
// opml will be encoded in the default JVM encoding, bu we want UTF-8
|
||||
opmlImporter.importOpml(
|
||||
user,
|
||||
new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/import")
|
||||
@Transactional
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name")
|
||||
public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
|
||||
}
|
||||
try {
|
||||
// opml will be encoded in the default JVM encoding, bu we want UTF-8
|
||||
opmlImporter.importOpml(user, new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/export")
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions")
|
||||
public Response exportOpml() throws FeedException {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Opml opml = opmlExporter.export(user);
|
||||
|
||||
WireFeedOutput output = new WireFeedOutput();
|
||||
String opmlString = output.outputString(opml);
|
||||
return Response.ok(opmlString).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/export")
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Operation(
|
||||
summary = "OPML export",
|
||||
description = "Export an OPML file of the user's subscriptions")
|
||||
public Response exportOpml() throws FeedException {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Opml opml = opmlExporter.export(user);
|
||||
|
||||
WireFeedOutput output = new WireFeedOutput();
|
||||
String opmlString = output.outputString(opml);
|
||||
return Response.ok(opmlString).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import jakarta.ws.rs.core.Application;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
|
||||
import org.eclipse.microprofile.openapi.annotations.info.Info;
|
||||
@@ -10,9 +9,8 @@ import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
|
||||
import org.eclipse.microprofile.openapi.annotations.servers.Server;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(title = "CommaFeed API", version = "1.0.0"),
|
||||
servers = { @Server(description = "CommaFeed API", url = "/") },
|
||||
security = { @SecurityRequirement(name = "basicAuth") })
|
||||
info = @Info(title = "CommaFeed API", version = "1.0.0"),
|
||||
servers = {@Server(description = "CommaFeed API", url = "/")},
|
||||
security = {@SecurityRequirement(name = "basicAuth")})
|
||||
@SecurityScheme(securitySchemeName = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic")
|
||||
public class OpenAPI extends Application {
|
||||
}
|
||||
public class OpenAPI extends Application {}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedVersion;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.feed.ImageProxyUrl;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
import com.commafeed.frontend.model.ServerInfo;
|
||||
import com.commafeed.security.Roles;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
@@ -12,24 +20,12 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedVersion;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.feed.ImageProxyUrl;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
import com.commafeed.frontend.model.ServerInfo;
|
||||
import com.commafeed.security.Roles;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/rest/server")
|
||||
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor
|
||||
@@ -37,52 +33,54 @@ import lombok.RequiredArgsConstructor;
|
||||
@Tag(name = "Server")
|
||||
public class ServerREST {
|
||||
|
||||
private final HttpGetter httpGetter;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CommaFeedVersion version;
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final HttpGetter httpGetter;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CommaFeedVersion version;
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
|
||||
@Path("/get")
|
||||
@GET
|
||||
@PermitAll
|
||||
@Transactional
|
||||
@Operation(summary = "Get server infos", description = "Get server infos")
|
||||
public ServerInfo getServerInfos() {
|
||||
ServerInfo infos = new ServerInfo();
|
||||
infos.setAnnouncement(config.announcement().orElse(null));
|
||||
infos.setVersion(version.getVersion());
|
||||
infos.setGitCommit(version.getGitCommit());
|
||||
infos.setAllowRegistrations(config.users().allowRegistrations());
|
||||
infos.setEmailAddressRequired(config.users().emailAddressRequired());
|
||||
infos.setSmtpEnabled(config.passwordRecoveryEnabled());
|
||||
infos.setDemoAccountEnabled(config.users().createDemoAccount());
|
||||
infos.setWebsocketEnabled(config.websocket().enabled());
|
||||
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
||||
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
|
||||
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
|
||||
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
|
||||
infos.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||
infos.setPushNotificationsEnabled(config.pushNotifications().enabled());
|
||||
return infos;
|
||||
}
|
||||
@Path("/get")
|
||||
@GET
|
||||
@PermitAll
|
||||
@Transactional
|
||||
@Operation(summary = "Get server infos", description = "Get server infos")
|
||||
public ServerInfo getServerInfos() {
|
||||
ServerInfo infos = new ServerInfo();
|
||||
infos.setAnnouncement(config.announcement().orElse(null));
|
||||
infos.setVersion(version.getVersion());
|
||||
infos.setGitCommit(version.getGitCommit());
|
||||
infos.setAllowRegistrations(config.users().allowRegistrations());
|
||||
infos.setEmailAddressRequired(config.users().emailAddressRequired());
|
||||
infos.setSmtpEnabled(config.passwordRecoveryEnabled());
|
||||
infos.setDemoAccountEnabled(config.users().createDemoAccount());
|
||||
infos.setWebsocketEnabled(config.websocket().enabled());
|
||||
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
||||
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
|
||||
infos.setForceRefreshCooldownDuration(
|
||||
config.feedRefresh().forceRefreshCooldownDuration().toMillis());
|
||||
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
|
||||
infos.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||
infos.setPushNotificationsEnabled(config.pushNotifications().enabled());
|
||||
return infos;
|
||||
}
|
||||
|
||||
@Path("/proxy")
|
||||
@GET
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Transactional
|
||||
@Operation(summary = "proxy image")
|
||||
@Produces("image/png")
|
||||
public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) {
|
||||
if (!config.imageProxyEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
@Path("/proxy")
|
||||
@GET
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Transactional
|
||||
@Operation(summary = "proxy image")
|
||||
@Produces("image/png")
|
||||
public Response getProxiedImage(
|
||||
@Parameter(description = "image url", required = true) @QueryParam("u") String url) {
|
||||
if (!config.imageProxyEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
url = ImageProxyUrl.decode(url);
|
||||
try {
|
||||
HttpResult result = httpGetter.get(url);
|
||||
return Response.ok(result.content()).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
url = ImageProxyUrl.decode(url);
|
||||
try {
|
||||
HttpResult result = httpGetter.get(url);
|
||||
return Response.ok(result.content()).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConstants;
|
||||
import com.commafeed.backend.Digests;
|
||||
@@ -67,9 +36,36 @@ import com.commafeed.frontend.model.request.RegistrationRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
@Path("/rest/user")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@@ -81,371 +77,410 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Tag(name = "Users")
|
||||
public class UserREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final MailService mailService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
private final PushNotificationService pushNotificationService;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final MailService mailService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final UriInfo uri;
|
||||
private final PushNotificationService pushNotificationService;
|
||||
|
||||
@Path("/settings")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve user settings", description = "Retrieve user settings")
|
||||
public Settings getUserSettings() {
|
||||
Settings s = new Settings();
|
||||
@Path("/settings")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve user settings", description = "Retrieve user settings")
|
||||
public Settings getUserSettings() {
|
||||
Settings s = new Settings();
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings != null) {
|
||||
s.setReadingMode(settings.getReadingMode());
|
||||
s.setReadingOrder(settings.getReadingOrder());
|
||||
s.setShowRead(settings.isShowRead());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings != null) {
|
||||
s.setReadingMode(settings.getReadingMode());
|
||||
s.setReadingOrder(settings.getReadingOrder());
|
||||
s.setShowRead(settings.isShowRead());
|
||||
|
||||
s.getSharingSettings().setEmail(settings.isEmail());
|
||||
s.getSharingSettings().setGmail(settings.isGmail());
|
||||
s.getSharingSettings().setFacebook(settings.isFacebook());
|
||||
s.getSharingSettings().setTwitter(settings.isTwitter());
|
||||
s.getSharingSettings().setTumblr(settings.isTumblr());
|
||||
s.getSharingSettings().setInstapaper(settings.isInstapaper());
|
||||
s.getSharingSettings().setBuffer(settings.isBuffer());
|
||||
s.getSharingSettings().setEmail(settings.isEmail());
|
||||
s.getSharingSettings().setGmail(settings.isGmail());
|
||||
s.getSharingSettings().setFacebook(settings.isFacebook());
|
||||
s.getSharingSettings().setTwitter(settings.isTwitter());
|
||||
s.getSharingSettings().setTumblr(settings.isTumblr());
|
||||
s.getSharingSettings().setInstapaper(settings.isInstapaper());
|
||||
s.getSharingSettings().setBuffer(settings.isBuffer());
|
||||
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setCustomJs(settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setScrollMode(settings.getScrollMode());
|
||||
s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling());
|
||||
s.setStarIconDisplayMode(settings.getStarIconDisplayMode());
|
||||
s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode());
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setCustomJs(settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setScrollMode(settings.getScrollMode());
|
||||
s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling());
|
||||
s.setStarIconDisplayMode(settings.getStarIconDisplayMode());
|
||||
s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode());
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
|
||||
if (settings.getPushNotifications() != null) {
|
||||
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
||||
s.getPushNotificationSettings().setServerUrl(settings.getPushNotifications().getServerUrl());
|
||||
s.getPushNotificationSettings().setUserId(settings.getPushNotifications().getUserId());
|
||||
s.getPushNotificationSettings().setUserSecret(settings.getPushNotifications().getUserSecret());
|
||||
s.getPushNotificationSettings().setTopic(settings.getPushNotifications().getTopic());
|
||||
}
|
||||
} else {
|
||||
s.setReadingMode(ReadingMode.UNREAD);
|
||||
s.setReadingOrder(ReadingOrder.DESC);
|
||||
s.setShowRead(true);
|
||||
if (settings.getPushNotifications() != null) {
|
||||
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
||||
s.getPushNotificationSettings()
|
||||
.setServerUrl(settings.getPushNotifications().getServerUrl());
|
||||
s.getPushNotificationSettings()
|
||||
.setUserId(settings.getPushNotifications().getUserId());
|
||||
s.getPushNotificationSettings()
|
||||
.setUserSecret(settings.getPushNotifications().getUserSecret());
|
||||
s.getPushNotificationSettings()
|
||||
.setTopic(settings.getPushNotifications().getTopic());
|
||||
}
|
||||
} else {
|
||||
s.setReadingMode(ReadingMode.UNREAD);
|
||||
s.setReadingOrder(ReadingOrder.DESC);
|
||||
s.setShowRead(true);
|
||||
|
||||
s.getSharingSettings().setEmail(true);
|
||||
s.getSharingSettings().setGmail(true);
|
||||
s.getSharingSettings().setFacebook(true);
|
||||
s.getSharingSettings().setTwitter(true);
|
||||
s.getSharingSettings().setTumblr(true);
|
||||
s.getSharingSettings().setInstapaper(true);
|
||||
s.getSharingSettings().setBuffer(true);
|
||||
s.getSharingSettings().setEmail(true);
|
||||
s.getSharingSettings().setGmail(true);
|
||||
s.getSharingSettings().setFacebook(true);
|
||||
s.getSharingSettings().setTwitter(true);
|
||||
s.getSharingSettings().setTumblr(true);
|
||||
s.getSharingSettings().setInstapaper(true);
|
||||
s.getSharingSettings().setBuffer(true);
|
||||
|
||||
s.setScrollMarks(true);
|
||||
s.setScrollSpeed(400);
|
||||
s.setScrollMode(ScrollMode.IF_NEEDED);
|
||||
s.setEntriesToKeepOnTopWhenScrolling(1);
|
||||
s.setStarIconDisplayMode(IconDisplayMode.ON_DESKTOP);
|
||||
s.setExternalLinkIconDisplayMode(IconDisplayMode.ON_DESKTOP);
|
||||
s.setMarkAllAsReadConfirmation(true);
|
||||
s.setMarkAllAsReadNavigateToNextUnread(false);
|
||||
s.setCustomContextMenu(true);
|
||||
s.setMobileFooter(false);
|
||||
s.setUnreadCountTitle(false);
|
||||
s.setUnreadCountFavicon(true);
|
||||
s.setDisablePullToRefresh(false);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
s.setScrollMarks(true);
|
||||
s.setScrollSpeed(400);
|
||||
s.setScrollMode(ScrollMode.IF_NEEDED);
|
||||
s.setEntriesToKeepOnTopWhenScrolling(1);
|
||||
s.setStarIconDisplayMode(IconDisplayMode.ON_DESKTOP);
|
||||
s.setExternalLinkIconDisplayMode(IconDisplayMode.ON_DESKTOP);
|
||||
s.setMarkAllAsReadConfirmation(true);
|
||||
s.setMarkAllAsReadNavigateToNextUnread(false);
|
||||
s.setCustomContextMenu(true);
|
||||
s.setMobileFooter(false);
|
||||
s.setUnreadCountTitle(false);
|
||||
s.setUnreadCountFavicon(true);
|
||||
s.setDisablePullToRefresh(false);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@Path("/settings")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Save user settings", description = "Save user settings")
|
||||
public Response saveUserSettings(@Parameter(required = true) Settings settings) {
|
||||
Preconditions.checkNotNull(settings);
|
||||
@Path("/settings")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Save user settings", description = "Save user settings")
|
||||
public Response saveUserSettings(@Parameter(required = true) Settings settings) {
|
||||
Preconditions.checkNotNull(settings);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings s = userSettingsDAO.findByUser(user);
|
||||
if (s == null) {
|
||||
s = new UserSettings();
|
||||
s.setUser(user);
|
||||
}
|
||||
s.setReadingMode(settings.getReadingMode());
|
||||
s.setReadingOrder(settings.getReadingOrder());
|
||||
s.setShowRead(settings.isShowRead());
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setCustomJs(CommaFeedConstants.USERNAME_DEMO.equals(user.getName()) ? "" : settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setScrollMode(settings.getScrollMode());
|
||||
s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling());
|
||||
s.setStarIconDisplayMode(settings.getStarIconDisplayMode());
|
||||
s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode());
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings s = userSettingsDAO.findByUser(user);
|
||||
if (s == null) {
|
||||
s = new UserSettings();
|
||||
s.setUser(user);
|
||||
}
|
||||
s.setReadingMode(settings.getReadingMode());
|
||||
s.setReadingOrder(settings.getReadingOrder());
|
||||
s.setShowRead(settings.isShowRead());
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setCustomJs(
|
||||
CommaFeedConstants.USERNAME_DEMO.equals(user.getName())
|
||||
? ""
|
||||
: settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setScrollMode(settings.getScrollMode());
|
||||
s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling());
|
||||
s.setStarIconDisplayMode(settings.getStarIconDisplayMode());
|
||||
s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode());
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
s.setUnreadCountTitle(settings.isUnreadCountTitle());
|
||||
s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
|
||||
s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
|
||||
s.setPrimaryColor(settings.getPrimaryColor());
|
||||
|
||||
PushNotificationUserSettings ps = new PushNotificationUserSettings();
|
||||
ps.setType(settings.getPushNotificationSettings().getType());
|
||||
ps.setServerUrl(settings.getPushNotificationSettings().getServerUrl());
|
||||
ps.setUserId(settings.getPushNotificationSettings().getUserId());
|
||||
ps.setUserSecret(settings.getPushNotificationSettings().getUserSecret());
|
||||
ps.setTopic(settings.getPushNotificationSettings().getTopic());
|
||||
s.setPushNotifications(ps);
|
||||
PushNotificationUserSettings ps = new PushNotificationUserSettings();
|
||||
ps.setType(settings.getPushNotificationSettings().getType());
|
||||
ps.setServerUrl(settings.getPushNotificationSettings().getServerUrl());
|
||||
ps.setUserId(settings.getPushNotificationSettings().getUserId());
|
||||
ps.setUserSecret(settings.getPushNotificationSettings().getUserSecret());
|
||||
ps.setTopic(settings.getPushNotificationSettings().getTopic());
|
||||
s.setPushNotifications(ps);
|
||||
|
||||
s.setEmail(settings.getSharingSettings().isEmail());
|
||||
s.setGmail(settings.getSharingSettings().isGmail());
|
||||
s.setFacebook(settings.getSharingSettings().isFacebook());
|
||||
s.setTwitter(settings.getSharingSettings().isTwitter());
|
||||
s.setTumblr(settings.getSharingSettings().isTumblr());
|
||||
s.setInstapaper(settings.getSharingSettings().isInstapaper());
|
||||
s.setBuffer(settings.getSharingSettings().isBuffer());
|
||||
s.setEmail(settings.getSharingSettings().isEmail());
|
||||
s.setGmail(settings.getSharingSettings().isGmail());
|
||||
s.setFacebook(settings.getSharingSettings().isFacebook());
|
||||
s.setTwitter(settings.getSharingSettings().isTwitter());
|
||||
s.setTumblr(settings.getSharingSettings().isTumblr());
|
||||
s.setInstapaper(settings.getSharingSettings().isInstapaper());
|
||||
s.setBuffer(settings.getSharingSettings().isBuffer());
|
||||
|
||||
userSettingsDAO.merge(s);
|
||||
return Response.ok().build();
|
||||
userSettingsDAO.merge(s);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
@Path("/pushNotificationTest")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Send a test push notification")
|
||||
public Response sendTestPushNotification(
|
||||
@Parameter(required = true) PushNotificationSettings settings) {
|
||||
FeedSubscription sub = new FeedSubscription();
|
||||
sub.setTitle("CommaFeed Test Feed");
|
||||
sub.setFeed(new Feed());
|
||||
|
||||
@Path("/pushNotificationTest")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Send a test push notification")
|
||||
public Response sendTestPushNotification(@Parameter(required = true) PushNotificationSettings settings) {
|
||||
FeedSubscription sub = new FeedSubscription();
|
||||
sub.setTitle("CommaFeed Test Feed");
|
||||
sub.setFeed(new Feed());
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setTitle("Test Entry");
|
||||
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setTitle("Test Entry");
|
||||
FeedEntry entry = new FeedEntry();
|
||||
entry.setContent(content);
|
||||
|
||||
FeedEntry entry = new FeedEntry();
|
||||
entry.setContent(content);
|
||||
PushNotificationUserSettings pushSettings = new PushNotificationUserSettings();
|
||||
pushSettings.setType(settings.getType());
|
||||
pushSettings.setServerUrl(settings.getServerUrl());
|
||||
pushSettings.setUserId(settings.getUserId());
|
||||
pushSettings.setUserSecret(settings.getUserSecret());
|
||||
pushSettings.setTopic(settings.getTopic());
|
||||
|
||||
PushNotificationUserSettings pushSettings = new PushNotificationUserSettings();
|
||||
pushSettings.setType(settings.getType());
|
||||
pushSettings.setServerUrl(settings.getServerUrl());
|
||||
pushSettings.setUserId(settings.getUserId());
|
||||
pushSettings.setUserSecret(settings.getUserSecret());
|
||||
pushSettings.setTopic(settings.getTopic());
|
||||
try {
|
||||
pushNotificationService.notify(pushSettings, sub, entry);
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(e.getCause().getMessage())
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
pushNotificationService.notify(pushSettings, sub, entry);
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
@Path("/profile")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve user's profile")
|
||||
public UserModel getUserProfile() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
@Path("/profile")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve user's profile")
|
||||
public UserModel getUserProfile() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserModel userModel = new UserModel();
|
||||
userModel.setId(user.getId());
|
||||
userModel.setName(user.getName());
|
||||
userModel.setEmail(user.getEmail());
|
||||
userModel.setEnabled(!user.isDisabled());
|
||||
userModel.setApiKey(user.getApiKey());
|
||||
userModel.setLastForceRefresh(user.getLastForceRefresh());
|
||||
for (UserRole role : userRoleDAO.findAll(user)) {
|
||||
if (role.getRole() == Role.ADMIN) {
|
||||
userModel.setAdmin(true);
|
||||
}
|
||||
}
|
||||
return userModel;
|
||||
}
|
||||
|
||||
UserModel userModel = new UserModel();
|
||||
userModel.setId(user.getId());
|
||||
userModel.setName(user.getName());
|
||||
userModel.setEmail(user.getEmail());
|
||||
userModel.setEnabled(!user.isDisabled());
|
||||
userModel.setApiKey(user.getApiKey());
|
||||
userModel.setLastForceRefresh(user.getLastForceRefresh());
|
||||
for (UserRole role : userRoleDAO.findAll(user)) {
|
||||
if (role.getRole() == Role.ADMIN) {
|
||||
userModel.setAdmin(true);
|
||||
}
|
||||
}
|
||||
return userModel;
|
||||
}
|
||||
@Path("/profile")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Save user's profile")
|
||||
public Response saveUserProfile(
|
||||
@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("the profile of the demo account cannot be modified")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Path("/profile")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Save user's profile")
|
||||
public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("the profile of the demo account cannot be modified").build();
|
||||
}
|
||||
Optional<User> login = userService.login(user.getName(), request.getCurrentPassword());
|
||||
if (login.isEmpty()) {
|
||||
throw new BadRequestException("invalid password");
|
||||
}
|
||||
|
||||
Optional<User> login = userService.login(user.getName(), request.getCurrentPassword());
|
||||
if (login.isEmpty()) {
|
||||
throw new BadRequestException("invalid password");
|
||||
}
|
||||
String email = StringUtils.trimToNull(request.getEmail());
|
||||
if (StringUtils.isNotBlank(email)) {
|
||||
User u = userDAO.findByEmail(email);
|
||||
if (u != null && !user.getId().equals(u.getId())) {
|
||||
throw new BadRequestException("email already taken");
|
||||
}
|
||||
user.setEmail(email);
|
||||
}
|
||||
|
||||
String email = StringUtils.trimToNull(request.getEmail());
|
||||
if (StringUtils.isNotBlank(email)) {
|
||||
User u = userDAO.findByEmail(email);
|
||||
if (u != null && !user.getId().equals(u.getId())) {
|
||||
throw new BadRequestException("email already taken");
|
||||
}
|
||||
user.setEmail(email);
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getNewPassword())) {
|
||||
byte[] password =
|
||||
encryptionService.getEncryptedPassword(
|
||||
request.getNewPassword(), user.getSalt());
|
||||
user.setPassword(password);
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(request.getNewPassword())) {
|
||||
byte[] password = encryptionService.getEncryptedPassword(request.getNewPassword(), user.getSalt());
|
||||
user.setPassword(password);
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
if (request.isNewApiKey()) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
|
||||
if (request.isNewApiKey()) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
userDAO.merge(user);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
userDAO.merge(user);
|
||||
return Response.ok().build();
|
||||
}
|
||||
@Path("/register")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Register a new account")
|
||||
public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) {
|
||||
try {
|
||||
userService.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
Collections.singletonList(Role.USER));
|
||||
return Response.ok().build();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/register")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Register a new account")
|
||||
public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) {
|
||||
try {
|
||||
userService.register(req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER));
|
||||
return Response.ok().build();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
}
|
||||
@Path("/initialSetup")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Create the initial admin account",
|
||||
description = "This endpoint is only available when no users exist in the database")
|
||||
public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) {
|
||||
boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired();
|
||||
if (!initialSetupRequired) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity("Initial setup has already been completed")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Path("/initialSetup")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Create the initial admin account",
|
||||
description = "This endpoint is only available when no users exist in the database")
|
||||
public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) {
|
||||
boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired();
|
||||
if (!initialSetupRequired) {
|
||||
return Response.status(Status.BAD_REQUEST).entity("Initial setup has already been completed").build();
|
||||
}
|
||||
userService.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
List.of(Role.ADMIN, Role.USER),
|
||||
true);
|
||||
|
||||
userService.register(req.getName(), req.getPassword(), req.getEmail(), List.of(Role.ADMIN, Role.USER), true);
|
||||
if (config.users().createDemoAccount()) {
|
||||
User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
|
||||
if (demo == null) {
|
||||
userService.createDemoUser();
|
||||
}
|
||||
}
|
||||
|
||||
if (config.users().createDemoAccount()) {
|
||||
User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
|
||||
if (demo == null) {
|
||||
userService.createDemoUser();
|
||||
}
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
@Path("/passwordReset")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "send a password reset email")
|
||||
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
||||
if (!config.passwordRecoveryEnabled()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Password recovery is not enabled on this CommaFeed instance");
|
||||
}
|
||||
|
||||
@Path("/passwordReset")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "send a password reset email")
|
||||
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
||||
if (!config.passwordRecoveryEnabled()) {
|
||||
throw new IllegalArgumentException("Password recovery is not enabled on this CommaFeed instance");
|
||||
}
|
||||
User user = userDAO.findByEmail(req.getEmail());
|
||||
if (user == null) {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
User user = userDAO.findByEmail(req.getEmail());
|
||||
if (user == null) {
|
||||
return Response.ok().build();
|
||||
}
|
||||
try {
|
||||
user.setRecoverPasswordToken(Digests.sha1Hex(UUID.randomUUID().toString()));
|
||||
user.setRecoverPasswordTokenDate(Instant.now());
|
||||
|
||||
try {
|
||||
user.setRecoverPasswordToken(Digests.sha1Hex(UUID.randomUUID().toString()));
|
||||
user.setRecoverPasswordTokenDate(Instant.now());
|
||||
mailService.sendMail(user, "Password recovery", buildEmailContent(user));
|
||||
return Response.ok().build();
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("could not send email")
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
mailService.sendMail(user, "Password recovery", buildEmailContent(user));
|
||||
return Response.ok().build();
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity("could not send email").type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
}
|
||||
private String buildEmailContent(User user) throws URISyntaxException {
|
||||
String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString());
|
||||
return String.format(
|
||||
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||
user.getName(), callbackUrl(user, publicUrl));
|
||||
}
|
||||
|
||||
private String buildEmailContent(User user) throws URISyntaxException {
|
||||
String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString());
|
||||
return String.format(
|
||||
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||
user.getName(), callbackUrl(user, publicUrl));
|
||||
}
|
||||
private String callbackUrl(User user, String publicUrl) throws URISyntaxException {
|
||||
URIBuilder queryBuilder = new URIBuilder();
|
||||
queryBuilder.addParameter("email", user.getEmail());
|
||||
queryBuilder.addParameter("token", user.getRecoverPasswordToken());
|
||||
String queryString = queryBuilder.build().getRawQuery();
|
||||
return publicUrl + "/#/passwordReset?" + queryString;
|
||||
}
|
||||
|
||||
private String callbackUrl(User user, String publicUrl) throws URISyntaxException {
|
||||
URIBuilder queryBuilder = new URIBuilder();
|
||||
queryBuilder.addParameter("email", user.getEmail());
|
||||
queryBuilder.addParameter("token", user.getRecoverPasswordToken());
|
||||
String queryString = queryBuilder.build().getRawQuery();
|
||||
return publicUrl + "/#/passwordReset?" + queryString;
|
||||
}
|
||||
@Path("/passwordResetCallback")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "confirm password reset with new password")
|
||||
public Response passwordRecoveryCallback(
|
||||
@Valid @Parameter(required = true) PasswordResetConfirmationRequest req) {
|
||||
String email = req.getEmail();
|
||||
String token = req.getToken();
|
||||
String password = req.getPassword();
|
||||
|
||||
@Path("/passwordResetCallback")
|
||||
@PermitAll
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "confirm password reset with new password")
|
||||
public Response passwordRecoveryCallback(@Valid @Parameter(required = true) PasswordResetConfirmationRequest req) {
|
||||
String email = req.getEmail();
|
||||
String token = req.getToken();
|
||||
String password = req.getPassword();
|
||||
Preconditions.checkNotNull(email);
|
||||
Preconditions.checkNotNull(token);
|
||||
Preconditions.checkNotNull(password);
|
||||
|
||||
Preconditions.checkNotNull(email);
|
||||
Preconditions.checkNotNull(token);
|
||||
Preconditions.checkNotNull(password);
|
||||
User user = userDAO.findByEmail(email);
|
||||
if (user == null
|
||||
|| user.getRecoverPasswordToken() == null
|
||||
|| !user.getRecoverPasswordToken().equals(token)) {
|
||||
return Response.status(Status.UNAUTHORIZED)
|
||||
.entity("Email not found or invalid token.")
|
||||
.build();
|
||||
}
|
||||
if (ChronoUnit.MINUTES.between(user.getRecoverPasswordTokenDate(), Instant.now()) >= 30) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build();
|
||||
}
|
||||
|
||||
User user = userDAO.findByEmail(email);
|
||||
if (user == null || user.getRecoverPasswordToken() == null || !user.getRecoverPasswordToken().equals(token)) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Email not found or invalid token.").build();
|
||||
}
|
||||
if (ChronoUnit.MINUTES.between(user.getRecoverPasswordTokenDate(), Instant.now()) >= 30) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build();
|
||||
}
|
||||
byte[] encryptedPassword = encryptionService.getEncryptedPassword(password, user.getSalt());
|
||||
user.setPassword(encryptedPassword);
|
||||
if (StringUtils.isNotBlank(user.getApiKey())) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
user.setRecoverPasswordToken(null);
|
||||
user.setRecoverPasswordTokenDate(null);
|
||||
|
||||
byte[] encryptedPassword = encryptionService.getEncryptedPassword(password, user.getSalt());
|
||||
user.setPassword(encryptedPassword);
|
||||
if (StringUtils.isNotBlank(user.getApiKey())) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
user.setRecoverPasswordToken(null);
|
||||
user.setRecoverPasswordTokenDate(null);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
@Path("/profile/deleteAccount")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Delete the user account")
|
||||
public Response deleteUser() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("the demo account cannot be deleted")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Path("/profile/deleteAccount")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Delete the user account")
|
||||
public Response deleteUser() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("the demo account cannot be deleted").build();
|
||||
}
|
||||
Set<Role> roles = userRoleDAO.findRoles(user);
|
||||
if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("The last admin account cannot be deleted")
|
||||
.build();
|
||||
}
|
||||
|
||||
Set<Role> roles = userRoleDAO.findRoles(user);
|
||||
if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) {
|
||||
return Response.status(Status.FORBIDDEN).entity("The last admin account cannot be deleted").build();
|
||||
}
|
||||
|
||||
userService.unregister(userDAO.findById(user.getId()));
|
||||
return Response.ok().build();
|
||||
}
|
||||
userService.unregister(userDAO.findById(user.getId()));
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
package com.commafeed.frontend.resource.fever;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.jboss.resteasy.reactive.server.multipart.FormValue;
|
||||
import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
|
||||
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
@@ -52,18 +20,46 @@ import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeed;
|
||||
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup;
|
||||
import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup;
|
||||
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
|
||||
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.jboss.resteasy.reactive.server.multipart.FormValue;
|
||||
import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
|
||||
|
||||
/**
|
||||
* Fever-compatible API
|
||||
*
|
||||
*
|
||||
* <ul>
|
||||
* <li>url: /rest/fever/user/${userId}</li>
|
||||
* <li>login: username</li>
|
||||
* <li>password: api key</li>
|
||||
* <li>url: /rest/fever/user/${userId}
|
||||
* <li>login: username
|
||||
* <li>password: api key
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* See https://feedafever.com/api
|
||||
*/
|
||||
@Path("/rest/fever")
|
||||
@@ -73,264 +69,336 @@ import lombok.RequiredArgsConstructor;
|
||||
@Singleton
|
||||
public class FeverREST {
|
||||
|
||||
private static final String PATH = "/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}";
|
||||
private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000;
|
||||
private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000;
|
||||
private static final int ITEMS_BATCH_SIZE = 200;
|
||||
private static final String PATH =
|
||||
"/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}";
|
||||
private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000;
|
||||
private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000;
|
||||
private static final int ITEMS_BATCH_SIZE = 200;
|
||||
|
||||
private final UserService userService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedFaviconService feedFaviconService;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final UserService userService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedFaviconService feedFaviconService;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
|
||||
// expected Fever API
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap<String, String> form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
// expected Fever API
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formUrlencoded(
|
||||
@Context UriInfo uri,
|
||||
@PathParam("userId") Long userId,
|
||||
MultivaluedMap<String, String> form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
// workaround for some readers that post data without any media type, and all params in the url
|
||||
// e.g. FeedMe
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
// workaround for some readers that post data without any media type, and all params in the url
|
||||
// e.g. FeedMe
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
// workaround for some readers that use GET instead of POST
|
||||
// e.g. Unread
|
||||
@Path(PATH)
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
// workaround for some readers that use GET instead of POST
|
||||
// e.g. Unread
|
||||
@Path(PATH)
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
// workaround for some readers that post data using MultiPart FormData instead of the classic POST
|
||||
// e.g. Raven Reader
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
|
||||
return handle(userId, params);
|
||||
}
|
||||
// workaround for some readers that post data using MultiPart FormData instead of the classic
|
||||
// POST
|
||||
// e.g. Raven Reader
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formData(
|
||||
@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.getValues()
|
||||
.forEach(
|
||||
(k, v) ->
|
||||
params.put(
|
||||
k,
|
||||
v.stream()
|
||||
.map(FormValue::getValue)
|
||||
.findFirst()
|
||||
.orElse(null)));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
public FeverResponse handle(long userId, Map<String, String> params) {
|
||||
User user = auth(userId, params.get("api_key")).orElse(null);
|
||||
if (user == null) {
|
||||
FeverResponse resp = new FeverResponse();
|
||||
resp.setAuth(false);
|
||||
return resp;
|
||||
}
|
||||
public FeverResponse handle(long userId, Map<String, String> params) {
|
||||
User user = auth(userId, params.get("api_key")).orElse(null);
|
||||
if (user == null) {
|
||||
FeverResponse resp = new FeverResponse();
|
||||
resp.setAuth(false);
|
||||
return resp;
|
||||
}
|
||||
|
||||
FeverResponse resp = new FeverResponse();
|
||||
resp.setAuth(true);
|
||||
FeverResponse resp = new FeverResponse();
|
||||
resp.setAuth(true);
|
||||
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||
resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions));
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
|
||||
resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions));
|
||||
|
||||
if (params.containsKey("groups") || params.containsKey("feeds")) {
|
||||
resp.setFeedsGroups(buildFeedsGroups(subscriptions));
|
||||
if (params.containsKey("groups") || params.containsKey("feeds")) {
|
||||
resp.setFeedsGroups(buildFeedsGroups(subscriptions));
|
||||
|
||||
if (params.containsKey("groups")) {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
||||
resp.setGroups(buildGroups(categories));
|
||||
}
|
||||
if (params.containsKey("groups")) {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
|
||||
resp.setGroups(buildGroups(categories));
|
||||
}
|
||||
|
||||
if (params.containsKey("feeds")) {
|
||||
resp.setFeeds(buildFeeds(subscriptions));
|
||||
}
|
||||
}
|
||||
if (params.containsKey("feeds")) {
|
||||
resp.setFeeds(buildFeeds(subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
if (params.containsKey("unread_item_ids")) {
|
||||
resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions));
|
||||
}
|
||||
if (params.containsKey("unread_item_ids")) {
|
||||
resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions));
|
||||
}
|
||||
|
||||
if (params.containsKey("saved_item_ids")) {
|
||||
resp.setSavedItemIds(buildSavedItemIds(user));
|
||||
}
|
||||
if (params.containsKey("saved_item_ids")) {
|
||||
resp.setSavedItemIds(buildSavedItemIds(user));
|
||||
}
|
||||
|
||||
if (params.containsKey("items")) {
|
||||
if (params.containsKey("with_ids")) {
|
||||
String withIds = params.get("with_ids");
|
||||
List<String> entryIds = Stream.of(withIds.split(",")).map(String::trim).toList();
|
||||
resp.setItems(buildItems(user, subscriptions, entryIds));
|
||||
} else {
|
||||
Long sinceId = Optional.ofNullable(params.get("since_id")).filter(StringUtils::isNotBlank).map(Long::valueOf).orElse(null);
|
||||
Long maxId = Optional.ofNullable(params.get("max_id")).filter(StringUtils::isNotBlank).map(Long::valueOf).orElse(null);
|
||||
resp.setItems(buildItems(user, subscriptions, sinceId, maxId));
|
||||
}
|
||||
}
|
||||
if (params.containsKey("items")) {
|
||||
if (params.containsKey("with_ids")) {
|
||||
String withIds = params.get("with_ids");
|
||||
List<String> entryIds = Stream.of(withIds.split(",")).map(String::trim).toList();
|
||||
resp.setItems(buildItems(user, subscriptions, entryIds));
|
||||
} else {
|
||||
Long sinceId =
|
||||
Optional.ofNullable(params.get("since_id"))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(Long::valueOf)
|
||||
.orElse(null);
|
||||
Long maxId =
|
||||
Optional.ofNullable(params.get("max_id"))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(Long::valueOf)
|
||||
.orElse(null);
|
||||
resp.setItems(buildItems(user, subscriptions, sinceId, maxId));
|
||||
}
|
||||
}
|
||||
|
||||
if (params.containsKey("favicons")) {
|
||||
resp.setFavicons(buildFavicons(subscriptions));
|
||||
}
|
||||
if (params.containsKey("favicons")) {
|
||||
resp.setFavicons(buildFavicons(subscriptions));
|
||||
}
|
||||
|
||||
if (params.containsKey("links")) {
|
||||
resp.setLinks(Collections.emptyList());
|
||||
}
|
||||
if (params.containsKey("links")) {
|
||||
resp.setLinks(Collections.emptyList());
|
||||
}
|
||||
|
||||
if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) {
|
||||
long id = Long.parseLong(params.get("id"));
|
||||
String before = params.get("before");
|
||||
Instant insertedBefore = before == null ? null : Instant.ofEpochSecond(Long.parseLong(before));
|
||||
mark(user, params.get("mark"), id, params.get("as"), insertedBefore);
|
||||
}
|
||||
if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) {
|
||||
long id = Long.parseLong(params.get("id"));
|
||||
String before = params.get("before");
|
||||
Instant insertedBefore =
|
||||
before == null ? null : Instant.ofEpochSecond(Long.parseLong(before));
|
||||
mark(user, params.get("mark"), id, params.get("as"), insertedBefore);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
private Optional<User> auth(Long userId, String feverApiKey) {
|
||||
return userService.login(userId, feverApiKey);
|
||||
}
|
||||
private Optional<User> auth(Long userId, String feverApiKey) {
|
||||
return userService.login(userId, feverApiKey);
|
||||
}
|
||||
|
||||
private long buildLastRefreshedOnTime(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.map(FeedSubscription::getFeed)
|
||||
.map(Feed::getLastUpdated)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Comparator.naturalOrder())
|
||||
.map(Instant::getEpochSecond)
|
||||
.orElse(0L);
|
||||
}
|
||||
private long buildLastRefreshedOnTime(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.map(FeedSubscription::getFeed)
|
||||
.map(Feed::getLastUpdated)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Comparator.naturalOrder())
|
||||
.map(Instant::getEpochSecond)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
private List<FeverFeedGroup> buildFeedsGroups(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.collect(Collectors.groupingBy(s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> {
|
||||
FeverFeedGroup fg = new FeverFeedGroup();
|
||||
fg.setGroupId(e.getKey());
|
||||
fg.setFeedIds(e.getValue().stream().map(FeedSubscription::getId).toList());
|
||||
return fg;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
private List<FeverFeedGroup> buildFeedsGroups(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(
|
||||
e -> {
|
||||
FeverFeedGroup fg = new FeverFeedGroup();
|
||||
fg.setGroupId(e.getKey());
|
||||
fg.setFeedIds(
|
||||
e.getValue().stream().map(FeedSubscription::getId).toList());
|
||||
return fg;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
|
||||
return categories.stream().map(c -> {
|
||||
FeverGroup g = new FeverGroup();
|
||||
g.setId(c.getId());
|
||||
g.setTitle(c.getName());
|
||||
return g;
|
||||
}).toList();
|
||||
}
|
||||
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
|
||||
return categories.stream()
|
||||
.map(
|
||||
c -> {
|
||||
FeverGroup g = new FeverGroup();
|
||||
g.setId(c.getId());
|
||||
g.setTitle(c.getName());
|
||||
return g;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream().map(s -> {
|
||||
FeverFeed f = new FeverFeed();
|
||||
f.setId(s.getId());
|
||||
f.setFaviconId(s.getId());
|
||||
f.setTitle(s.getTitle());
|
||||
f.setUrl(s.getFeed().getUrl());
|
||||
f.setSiteUrl(s.getFeed().getLink());
|
||||
f.setSpark(false);
|
||||
f.setLastUpdatedOnTime(s.getFeed().getLastUpdated() == null ? 0 : s.getFeed().getLastUpdated().getEpochSecond());
|
||||
return f;
|
||||
}).toList();
|
||||
}
|
||||
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.map(
|
||||
s -> {
|
||||
FeverFeed f = new FeverFeed();
|
||||
f.setId(s.getId());
|
||||
f.setFaviconId(s.getId());
|
||||
f.setTitle(s.getTitle());
|
||||
f.setUrl(s.getFeed().getUrl());
|
||||
f.setSiteUrl(s.getFeed().getLink());
|
||||
f.setSpark(false);
|
||||
f.setLastUpdatedOnTime(
|
||||
s.getFeed().getLastUpdated() == null
|
||||
? 0
|
||||
: s.getFeed().getLastUpdated().getEpochSecond());
|
||||
return f;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0,
|
||||
UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC, false, null, null, null);
|
||||
return statuses.stream().map(s -> s.getEntry().getId()).toList();
|
||||
}
|
||||
private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subscriptions,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
UNREAD_ITEM_IDS_BATCH_SIZE,
|
||||
ReadingOrder.DESC,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
return statuses.stream().map(s -> s.getEntry().getId()).toList();
|
||||
}
|
||||
|
||||
private List<Long> buildSavedItemIds(User user) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC,
|
||||
false);
|
||||
return statuses.stream().map(s -> s.getEntry().getId()).toList();
|
||||
}
|
||||
private List<Long> buildSavedItemIds(User user) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findStarred(
|
||||
user, null, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC, false);
|
||||
return statuses.stream().map(s -> s.getEntry().getId()).toList();
|
||||
}
|
||||
|
||||
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
|
||||
List<FeverItem> items = new ArrayList<>();
|
||||
private List<FeverItem> buildItems(
|
||||
User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
|
||||
List<FeverItem> items = new ArrayList<>();
|
||||
|
||||
Map<Long, FeedSubscription> subscriptionsByFeedId = subscriptions.stream()
|
||||
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s));
|
||||
for (String entryId : entryIds) {
|
||||
FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId));
|
||||
FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId());
|
||||
if (sub != null) {
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
items.add(mapStatus(status));
|
||||
}
|
||||
}
|
||||
Map<Long, FeedSubscription> subscriptionsByFeedId =
|
||||
subscriptions.stream().collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s));
|
||||
for (String entryId : entryIds) {
|
||||
FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId));
|
||||
FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId());
|
||||
if (sub != null) {
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
items.add(mapStatus(status));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE,
|
||||
ReadingOrder.DESC, false, null, sinceId, maxId);
|
||||
return statuses.stream().map(this::mapStatus).toList();
|
||||
}
|
||||
private List<FeverItem> buildItems(
|
||||
User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subscriptions,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
ITEMS_BATCH_SIZE,
|
||||
ReadingOrder.DESC,
|
||||
false,
|
||||
null,
|
||||
sinceId,
|
||||
maxId);
|
||||
return statuses.stream().map(this::mapStatus).toList();
|
||||
}
|
||||
|
||||
private FeverItem mapStatus(FeedEntryStatus s) {
|
||||
FeverItem i = new FeverItem();
|
||||
i.setId(s.getEntry().getId());
|
||||
i.setFeedId(s.getSubscription().getId());
|
||||
i.setTitle(s.getEntry().getContent().getTitle());
|
||||
i.setAuthor(s.getEntry().getContent().getAuthor());
|
||||
i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse(""));
|
||||
i.setUrl(s.getEntry().getUrl());
|
||||
i.setSaved(s.isStarred());
|
||||
i.setRead(s.isRead());
|
||||
i.setCreatedOnTime(s.getEntryPublished().getEpochSecond());
|
||||
return i;
|
||||
}
|
||||
private FeverItem mapStatus(FeedEntryStatus s) {
|
||||
FeverItem i = new FeverItem();
|
||||
i.setId(s.getEntry().getId());
|
||||
i.setFeedId(s.getSubscription().getId());
|
||||
i.setTitle(s.getEntry().getContent().getTitle());
|
||||
i.setAuthor(s.getEntry().getContent().getAuthor());
|
||||
i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse(""));
|
||||
i.setUrl(s.getEntry().getUrl());
|
||||
i.setSaved(s.isStarred());
|
||||
i.setRead(s.isRead());
|
||||
i.setCreatedOnTime(s.getEntryPublished().getEpochSecond());
|
||||
return i;
|
||||
}
|
||||
|
||||
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream().map(s -> {
|
||||
Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed());
|
||||
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.map(
|
||||
s -> {
|
||||
Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed());
|
||||
|
||||
FeverFavicon f = new FeverFavicon();
|
||||
f.setId(s.getFeed().getId());
|
||||
f.setData(String.format("data:%s;base64,%s", favicon.mediaType(), Base64.getEncoder().encodeToString(favicon.icon())));
|
||||
return f;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void mark(User user, String source, long id, String action, Instant insertedBefore) {
|
||||
if ("item".equals(source)) {
|
||||
if ("read".equals(action) || "unread".equals(action)) {
|
||||
feedEntryService.markEntry(user, id, "read".equals(action));
|
||||
} else if ("saved".equals(action) || "unsaved".equals(action)) {
|
||||
FeedEntry entry = feedEntryDAO.findById(id);
|
||||
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
||||
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
|
||||
}
|
||||
} else if ("feed".equals(source)) {
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), null, insertedBefore, null);
|
||||
} else if ("group".equals(source)) {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, id);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
feedEntryService.markSubscriptionEntries(user, subscriptions, null, insertedBefore, null);
|
||||
}
|
||||
}
|
||||
FeverFavicon f = new FeverFavicon();
|
||||
f.setId(s.getFeed().getId());
|
||||
f.setData(
|
||||
String.format(
|
||||
"data:%s;base64,%s",
|
||||
favicon.mediaType(),
|
||||
Base64.getEncoder().encodeToString(favicon.icon())));
|
||||
return f;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void mark(User user, String source, long id, String action, Instant insertedBefore) {
|
||||
if ("item".equals(source)) {
|
||||
if ("read".equals(action) || "unread".equals(action)) {
|
||||
feedEntryService.markEntry(user, id, "read".equals(action));
|
||||
} else if ("saved".equals(action) || "unsaved".equals(action)) {
|
||||
FeedEntry entry = feedEntryDAO.findById(id);
|
||||
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
||||
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
|
||||
}
|
||||
} else if ("feed".equals(source)) {
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, Collections.singletonList(subscription), null, insertedBefore, null);
|
||||
} else if ("group".equals(source)) {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, id);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subscriptions =
|
||||
feedSubscriptionDAO.findByCategories(user, categories);
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subscriptions, null, insertedBefore, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package com.commafeed.frontend.resource.fever;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
@@ -18,161 +13,168 @@ import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.Data;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@Data
|
||||
public class FeverResponse {
|
||||
|
||||
@JsonProperty("api_version")
|
||||
private int apiVersion = 3;
|
||||
@JsonProperty("api_version")
|
||||
private int apiVersion = 3;
|
||||
|
||||
@JsonProperty("auth")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean auth;
|
||||
@JsonProperty("auth")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean auth;
|
||||
|
||||
@JsonProperty("last_refreshed_on_time")
|
||||
private Long lastRefreshedOnTime;
|
||||
@JsonProperty("last_refreshed_on_time")
|
||||
private Long lastRefreshedOnTime;
|
||||
|
||||
@JsonProperty("groups")
|
||||
private List<FeverGroup> groups;
|
||||
@JsonProperty("groups")
|
||||
private List<FeverGroup> groups;
|
||||
|
||||
@JsonProperty("feeds")
|
||||
private List<FeverFeed> feeds;
|
||||
@JsonProperty("feeds")
|
||||
private List<FeverFeed> feeds;
|
||||
|
||||
@JsonProperty("feeds_groups")
|
||||
private List<FeverFeedGroup> feedsGroups;
|
||||
@JsonProperty("feeds_groups")
|
||||
private List<FeverFeedGroup> feedsGroups;
|
||||
|
||||
@JsonProperty("unread_item_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> unreadItemIds;
|
||||
@JsonProperty("unread_item_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> unreadItemIds;
|
||||
|
||||
@JsonProperty("saved_item_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> savedItemIds;
|
||||
@JsonProperty("saved_item_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> savedItemIds;
|
||||
|
||||
@JsonProperty("items")
|
||||
private List<FeverItem> items;
|
||||
@JsonProperty("items")
|
||||
private List<FeverItem> items;
|
||||
|
||||
@JsonProperty("favicons")
|
||||
private List<FeverFavicon> favicons;
|
||||
@JsonProperty("favicons")
|
||||
private List<FeverFavicon> favicons;
|
||||
|
||||
@JsonProperty("links")
|
||||
private List<FeverLink> links;
|
||||
@JsonProperty("links")
|
||||
private List<FeverLink> links;
|
||||
|
||||
@Data
|
||||
public static class FeverGroup {
|
||||
@Data
|
||||
public static class FeverGroup {
|
||||
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
}
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FeverFeed {
|
||||
@Data
|
||||
public static class FeverFeed {
|
||||
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
|
||||
@JsonProperty("favicon_id")
|
||||
private long faviconId;
|
||||
@JsonProperty("favicon_id")
|
||||
private long faviconId;
|
||||
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("site_url")
|
||||
private String siteUrl;
|
||||
@JsonProperty("site_url")
|
||||
private String siteUrl;
|
||||
|
||||
@JsonProperty("is_spark")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean spark;
|
||||
@JsonProperty("is_spark")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean spark;
|
||||
|
||||
@JsonProperty("last_updated_on_time")
|
||||
private long lastUpdatedOnTime;
|
||||
}
|
||||
@JsonProperty("last_updated_on_time")
|
||||
private long lastUpdatedOnTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FeverFeedGroup {
|
||||
@Data
|
||||
public static class FeverFeedGroup {
|
||||
|
||||
@JsonProperty("group_id")
|
||||
private long groupId;
|
||||
@JsonProperty("group_id")
|
||||
private long groupId;
|
||||
|
||||
@JsonProperty("feed_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> feedIds;
|
||||
}
|
||||
@JsonProperty("feed_ids")
|
||||
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
|
||||
@JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class)
|
||||
private List<Long> feedIds;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FeverItem {
|
||||
@Data
|
||||
public static class FeverItem {
|
||||
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
|
||||
@JsonProperty("feed_id")
|
||||
private long feedId;
|
||||
@JsonProperty("feed_id")
|
||||
private long feedId;
|
||||
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@JsonProperty("author")
|
||||
private String author;
|
||||
@JsonProperty("author")
|
||||
private String author;
|
||||
|
||||
@JsonProperty("html")
|
||||
private String html;
|
||||
@JsonProperty("html")
|
||||
private String html;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("is_saved")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean saved;
|
||||
@JsonProperty("is_saved")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean saved;
|
||||
|
||||
@JsonProperty("is_read")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean read;
|
||||
@JsonProperty("is_read")
|
||||
@JsonFormat(shape = Shape.NUMBER)
|
||||
private boolean read;
|
||||
|
||||
@JsonProperty("created_on_time")
|
||||
private long createdOnTime;
|
||||
@JsonProperty("created_on_time")
|
||||
private long createdOnTime;
|
||||
}
|
||||
|
||||
}
|
||||
@Data
|
||||
public static class FeverFavicon {
|
||||
|
||||
@Data
|
||||
public static class FeverFavicon {
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
|
||||
@JsonProperty("id")
|
||||
private long id;
|
||||
@JsonProperty("data")
|
||||
private String data;
|
||||
}
|
||||
|
||||
@JsonProperty("data")
|
||||
private String data;
|
||||
}
|
||||
@Data
|
||||
public static class FeverLink {}
|
||||
|
||||
@Data
|
||||
public static class FeverLink {
|
||||
public static class LongListToCommaSeparatedStringSerializer
|
||||
extends JsonSerializer<List<Long>> {
|
||||
@Override
|
||||
public void serialize(
|
||||
List<Long> input,
|
||||
JsonGenerator jsonGenerator,
|
||||
SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
String output = input.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
jsonGenerator.writeObject(output);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer<List<Long>> {
|
||||
@Override
|
||||
public void serialize(List<Long> input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
|
||||
String output = input.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
jsonGenerator.writeObject(output);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommaSeparatedStringToLongListDeserializer extends JsonDeserializer<List<Long>> {
|
||||
@Override
|
||||
public List<Long> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
String value = ctxt.readValue(p, String.class);
|
||||
return Stream.of(value.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
}
|
||||
public static class CommaSeparatedStringToLongListDeserializer
|
||||
extends JsonDeserializer<List<Long>> {
|
||||
@Override
|
||||
public List<Long> deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException {
|
||||
String value = ctxt.readValue(p, String.class);
|
||||
return Stream.of(value.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
@Path("/custom_css.css")
|
||||
@Produces("text/css")
|
||||
@@ -21,24 +18,23 @@ import lombok.RequiredArgsConstructor;
|
||||
@Singleton
|
||||
public class CustomCssServlet {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return settings.getCustomCss();
|
||||
}
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return settings.getCustomCss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
@Path("/custom_js.js")
|
||||
@Produces("application/javascript")
|
||||
@@ -21,24 +18,23 @@ import lombok.RequiredArgsConstructor;
|
||||
@Singleton
|
||||
public class CustomJsServlet {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return settings.getCustomJs();
|
||||
}
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return settings.getCustomJs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import com.commafeed.security.CookieService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -7,12 +8,8 @@ import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.NewCookie;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
import com.commafeed.security.CookieService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Path("/logout")
|
||||
@@ -20,13 +17,13 @@ import lombok.RequiredArgsConstructor;
|
||||
@Singleton
|
||||
public class LogoutServlet {
|
||||
|
||||
private final UriInfo uri;
|
||||
private final CookieService cookieService;
|
||||
private final UriInfo uri;
|
||||
private final CookieService cookieService;
|
||||
|
||||
@GET
|
||||
@Operation(hidden = true)
|
||||
public Response get() {
|
||||
NewCookie removeCookie = cookieService.buildLogoutCookie();
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).cookie(removeCookie).build();
|
||||
}
|
||||
@GET
|
||||
@Operation(hidden = true)
|
||||
public Response get() {
|
||||
NewCookie removeCookie = cookieService.buildLogoutCookie();
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).cookie(removeCookie).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
@@ -26,51 +11,79 @@ import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.frontend.resource.CategoryREST;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
@Path("/next")
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class NextUnreadServlet {
|
||||
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UriInfo uri;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UriInfo uri;
|
||||
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).build();
|
||||
}
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public Response get(
|
||||
@QueryParam("category") String categoryId,
|
||||
@QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).build();
|
||||
}
|
||||
|
||||
FeedEntryStatus s = null;
|
||||
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, null,
|
||||
null, null);
|
||||
s = statuses.stream().findFirst().orElse(null);
|
||||
} else {
|
||||
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
|
||||
if (category != null) {
|
||||
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order,
|
||||
true, null, null, null);
|
||||
s = statuses.stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
if (s != null) {
|
||||
feedEntryService.markEntry(user, s.getEntry().getId(), true);
|
||||
}
|
||||
FeedEntryStatus s = null;
|
||||
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user, subs, true, null, null, 0, 1, order, true, null, null, null);
|
||||
s = statuses.stream().findFirst().orElse(null);
|
||||
} else {
|
||||
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
|
||||
if (category != null) {
|
||||
List<FeedCategory> children =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions =
|
||||
feedSubscriptionDAO.findByCategories(user, children);
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subscriptions,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
order,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
s = statuses.stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
if (s != null) {
|
||||
feedEntryService.markEntry(user, s.getEntry().getId(), true);
|
||||
}
|
||||
|
||||
String url = s == null ? uri.getBaseUri().toString() : s.getEntry().getUrl();
|
||||
return Response.temporaryRedirect(URI.create(url)).build();
|
||||
}
|
||||
String url = s == null ? uri.getBaseUri().toString() : s.getEntry().getUrl();
|
||||
return Response.temporaryRedirect(URI.create(url)).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -7,14 +8,10 @@ import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/robots.txt")
|
||||
@PermitAll
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@@ -22,15 +19,15 @@ import lombok.RequiredArgsConstructor;
|
||||
@Singleton
|
||||
public class RobotsTxtDisallowAllServlet {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@GET
|
||||
@Operation(hidden = true)
|
||||
public Response get() {
|
||||
if (config.hideFromWebCrawlers()) {
|
||||
return Response.ok("User-agent: *\nDisallow: /").build();
|
||||
} else {
|
||||
return Response.status(HttpStatus.SC_NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@GET
|
||||
@Operation(hidden = true)
|
||||
public Response get() {
|
||||
if (config.hideFromWebCrawlers()) {
|
||||
return Response.ok("User-agent: *\nDisallow: /").build();
|
||||
} else {
|
||||
return Response.status(HttpStatus.SC_NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.commafeed.frontend.ws;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.websocket.CloseReason;
|
||||
import jakarta.websocket.CloseReason.CloseCodes;
|
||||
@@ -10,11 +11,7 @@ import jakarta.websocket.OnMessage;
|
||||
import jakarta.websocket.OnOpen;
|
||||
import jakarta.websocket.Session;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -24,37 +21,36 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketEndpoint {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final WebSocketSessions sessions;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final WebSocketSessions sessions;
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session) throws IOException {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
reject(session);
|
||||
return;
|
||||
}
|
||||
@OnOpen
|
||||
public void onOpen(Session session) throws IOException {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
reject(session);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("created websocket session for user '{}'", user.getName());
|
||||
sessions.add(user.getId(), session);
|
||||
session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000);
|
||||
}
|
||||
log.debug("created websocket session for user '{}'", user.getName());
|
||||
sessions.add(user.getId(), session);
|
||||
session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000);
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
if ("ping".equals(message)) {
|
||||
session.getAsyncRemote().sendText("pong");
|
||||
}
|
||||
}
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
if ("ping".equals(message)) {
|
||||
session.getAsyncRemote().sendText("pong");
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session) {
|
||||
sessions.remove(session);
|
||||
}
|
||||
|
||||
private void reject(Session session) throws IOException {
|
||||
session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "unauthorized"));
|
||||
}
|
||||
@OnClose
|
||||
public void onClose(Session session) {
|
||||
sessions.remove(session);
|
||||
}
|
||||
|
||||
private void reject(Session session) throws IOException {
|
||||
session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "unauthorized"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.commafeed.frontend.ws;
|
||||
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class WebSocketMessageBuilder {
|
||||
|
||||
public static String newFeedEntries(FeedSubscription subscription, long count) {
|
||||
return String.format("%s:%s:%s", "new-feed-entries", subscription.getId(), count);
|
||||
}
|
||||
|
||||
public static String newFeedEntries(FeedSubscription subscription, long count) {
|
||||
return String.format("%s:%s:%s", "new-feed-entries", subscription.getId(), count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
package com.commafeed.frontend.ws;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.websocket.Session;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.websocket.Session;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class WebSocketSessions {
|
||||
|
||||
// a user may have multiple sessions (two tabs, two devices, ...)
|
||||
private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>();
|
||||
// a user may have multiple sessions (two tabs, two devices, ...)
|
||||
private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public WebSocketSessions(MetricRegistry metrics) {
|
||||
metrics.register(MetricRegistry.name(getClass(), "users"),
|
||||
(Gauge<Long>) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
|
||||
metrics.register(MetricRegistry.name(getClass(), "sessions"),
|
||||
(Gauge<Long>) () -> sessions.values().stream().mapToLong(Set::size).sum());
|
||||
}
|
||||
public WebSocketSessions(MetricRegistry metrics) {
|
||||
metrics.register(
|
||||
MetricRegistry.name(getClass(), "users"),
|
||||
(Gauge<Long>) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
|
||||
metrics.register(
|
||||
MetricRegistry.name(getClass(), "sessions"),
|
||||
(Gauge<Long>) () -> sessions.values().stream().mapToLong(Set::size).sum());
|
||||
}
|
||||
|
||||
public void add(Long userId, Session session) {
|
||||
sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session);
|
||||
}
|
||||
public void add(Long userId, Session session) {
|
||||
sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session);
|
||||
}
|
||||
|
||||
public void remove(Session session) {
|
||||
sessions.values().forEach(v -> v.remove(session));
|
||||
}
|
||||
public void remove(Session session) {
|
||||
sessions.values().forEach(v -> v.remove(session));
|
||||
}
|
||||
|
||||
public void sendMessage(User user, String text) {
|
||||
Set<Session> userSessions = sessions.get(user.getId());
|
||||
if (userSessions != null && !userSessions.isEmpty()) {
|
||||
log.debug("sending '{}' to user {} via websocket ({} sessions)", text, user.getId(), userSessions.size());
|
||||
for (Session userSession : userSessions) {
|
||||
if (userSession.isOpen()) {
|
||||
userSession.getAsyncRemote().sendText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void sendMessage(User user, String text) {
|
||||
Set<Session> userSessions = sessions.get(user.getId());
|
||||
if (userSessions != null && !userSessions.isEmpty()) {
|
||||
log.debug(
|
||||
"sending '{}' to user {} via websocket ({} sessions)",
|
||||
text,
|
||||
user.getId(),
|
||||
userSessions.size());
|
||||
for (Session userSession : userSessions) {
|
||||
if (userSession.isOpen()) {
|
||||
userSession.getAsyncRemote().sendText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user