From d7b0d572c1a6caecdbf304528ff4d1c610b60465 Mon Sep 17 00:00:00 2001 From: Athou Date: Thu, 17 Aug 2023 17:25:27 +0200 Subject: [PATCH] add fever-compatible api --- .../com/commafeed/CommaFeedApplication.java | 2 + .../backend/dao/FeedEntryStatusDAO.java | 17 +- .../backend/service/FeedEntryService.java | 2 +- .../backend/service/UserService.java | 22 ++ .../frontend/resource/CategoryREST.java | 4 +- .../commafeed/frontend/resource/FeedREST.java | 2 +- .../frontend/resource/fever/FeverREST.java | 284 ++++++++++++++++++ .../resource/fever/FeverResponse.java | 164 ++++++++++ .../frontend/servlet/NextUnreadServlet.java | 4 +- 9 files changed, 491 insertions(+), 10 deletions(-) create mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java create mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index a37b215e..86dd24f7 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -42,6 +42,7 @@ import com.commafeed.frontend.resource.FeedREST; import com.commafeed.frontend.resource.PubSubHubbubCallbackREST; import com.commafeed.frontend.resource.ServerREST; import com.commafeed.frontend.resource.UserREST; +import com.commafeed.frontend.resource.fever.FeverREST; import com.commafeed.frontend.servlet.AnalyticsServlet; import com.commafeed.frontend.servlet.CustomCssServlet; import com.commafeed.frontend.servlet.CustomJsServlet; @@ -163,6 +164,7 @@ public class CommaFeedApplication extends Application { environment.jersey().register(injector.getInstance(PubSubHubbubCallbackREST.class)); environment.jersey().register(injector.getInstance(ServerREST.class)); environment.jersey().register(injector.getInstance(UserREST.class)); + environment.jersey().register(injector.getInstance(FeverREST.class)); // Servlets environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next"); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java index cb2e4d4c..7bf8045f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java @@ -117,7 +117,7 @@ public class FeedEntryStatusDAO extends GenericDAO { } private JPAQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List keywords, - Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag) { + Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, Long maxEntryId) { JPAQuery query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed())); @@ -159,6 +159,14 @@ public class FeedEntryStatusDAO extends GenericDAO { query.where(entry.inserted.goe(newerThan)); } + if (minEntryId != null) { + query.where(entry.id.gt(minEntryId)); + } + + if (maxEntryId != null) { + query.where(entry.id.lt(maxEntryId)); + } + if (last != null) { if (order == ReadingOrder.desc) { query.where(entry.updated.gt(last.getEntryUpdated())); @@ -189,7 +197,7 @@ public class FeedEntryStatusDAO extends GenericDAO { public List findBySubscriptions(User user, List subs, boolean unreadOnly, List keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, - boolean onlyIds, String tag) { + boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) { int capacity = offset + limit; Comparator comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC; @@ -197,7 +205,8 @@ public class FeedEntryStatusDAO extends GenericDAO { FixedSizeSortedSet set = new FixedSizeSortedSet<>(capacity, comparator); for (FeedSubscription sub : subs) { FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null; - JPAQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag); + JPAQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId, + maxEntryId); List tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch(); for (Tuple tuple : tuples) { @@ -250,7 +259,7 @@ public class FeedEntryStatusDAO extends GenericDAO { public UnreadCount getUnreadCount(User user, FeedSubscription subscription) { UnreadCount uc = null; - JPAQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null); + JPAQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null); List tuples = query.select(entry.count(), entry.updated.max()).fetch(); for (Tuple tuple : tuples) { Long count = tuple.get(entry.count()); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java index 47b9c815..66431830 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java @@ -111,7 +111,7 @@ public class FeedEntryService { public void markSubscriptionEntries(User user, List subscriptions, Date olderThan, List keywords) { List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, - false, false, null); + false, false, null, null, null); markList(statuses, olderThan); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUserRootCategory(user); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java index 58c658cc..54432691 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java @@ -82,6 +82,28 @@ public class UserService { return Optional.empty(); } + /** + * try to log in with given fever api key + */ + public Optional login(long userId, String feverApiKey) { + if (feverApiKey == null) { + return Optional.empty(); + } + + User user = userDAO.findById(userId); + if (user == null || user.isDisabled() || user.getApiKey() == null) { + return Optional.empty(); + } + + String computedFeverApiKey = DigestUtils.md5Hex(user.getName() + ":" + user.getApiKey()); + if (!computedFeverApiKey.equals(feverApiKey)) { + return Optional.empty(); + } + + performPostLoginActivities(user); + return Optional.of(user); + } + /** * should triggers after successful login */ diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java index 5935d325..fb45a95d 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java @@ -141,7 +141,7 @@ public class CategoryREST { List subs = feedSubscriptionDAO.findAll(user); removeExcludedSubscriptions(subs, excludedIds); List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, - offset, limit + 1, order, true, onlyIds, tag); + offset, limit + 1, order, true, onlyIds, tag, null, null); for (FeedEntryStatus status : list) { entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); @@ -160,7 +160,7 @@ public class CategoryREST { List subs = feedSubscriptionDAO.findByCategories(user, categories); removeExcludedSubscriptions(subs, excludedIds); List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, - offset, limit + 1, order, true, onlyIds, tag); + offset, limit + 1, order, true, onlyIds, tag, null, null); for (FeedEntryStatus status : list) { entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java index 6e67fc4a..6425994b 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java @@ -172,7 +172,7 @@ public class FeedREST { entries.setFeedLink(subscription.getFeed().getLink()); List list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly, - entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null); + entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null, null, null); for (FeedEntryStatus status : list) { entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java new file mode 100644 index 00000000..803e730a --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java @@ -0,0 +1,284 @@ +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.Date; +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 javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; + +import com.codahale.metrics.annotation.Timed; +import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.dao.FeedEntryDAO; +import com.commafeed.backend.dao.FeedEntryStatusDAO; +import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon; +import com.commafeed.backend.model.Feed; +import com.commafeed.backend.model.FeedCategory; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryStatus; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserSettings.ReadingOrder; +import com.commafeed.backend.service.FeedEntryService; +import com.commafeed.backend.service.FeedService; +import com.commafeed.backend.service.UserService; +import com.commafeed.frontend.resource.fever.FeverResponse.FeverFavicon; +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 io.dropwizard.hibernate.UnitOfWork; +import lombok.RequiredArgsConstructor; + +/** + * Fever-compatible API + * + *
    + *
  • url: http://localhost:8082/rest/fever/user/${userId}
  • + *
  • login: username
  • + *
  • password: api key
  • + *
+ * + * See https://feedafever.com/api + */ +@Path("/fever") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_FORM_URLENCODED) +@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Singleton +public class FeverREST { + + 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 FeedService feedService; + private final FeedEntryDAO feedEntryDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + + @Path("/user/{userId}") + @POST + @UnitOfWork + @Timed + public FeverResponse handle(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap form) { + MultivaluedMap query = uri.getQueryParameters(); + + User user = auth(userId, form.getFirst("api_key")).orElse(null); + if (user == null) { + FeverResponse resp = new FeverResponse(); + resp.setAuth(false); + return resp; + } + + FeverResponse resp = new FeverResponse(); + resp.setAuth(true); + + List subscriptions = feedSubscriptionDAO.findAll(user); + resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions)); + + if (query.containsKey("groups") || query.containsKey("feeds")) { + resp.setFeedsGroups(buildFeedsGroups(subscriptions)); + + if (query.containsKey("groups")) { + List categories = feedCategoryDAO.findAll(user); + resp.setGroups(buildGroups(categories)); + } + + if (query.containsKey("feeds")) { + resp.setFeeds(buildFeeds(subscriptions)); + } + } + + if (query.containsKey("unread_item_ids")) { + resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions)); + } + + if (query.containsKey("saved_item_ids")) { + resp.setSavedItemIds(buildSavedItemIds(user)); + } + + if (query.containsKey("items")) { + if (query.containsKey("with_ids")) { + String withIds = query.getFirst("with_ids"); + List entryIds = Stream.of(withIds.split(",")).collect(Collectors.toList()); + resp.setItems(buildItems(user, subscriptions, entryIds)); + } else { + Long sinceId = query.containsKey("since_id") ? Long.valueOf(query.getFirst("since_id")) : null; + Long maxId = query.containsKey("max_id") ? Long.valueOf(query.getFirst("max_id")) : null; + resp.setItems(buildItems(user, subscriptions, sinceId, maxId)); + } + } + + if (query.containsKey("favicons")) { + resp.setFavicons(buildFavicons(subscriptions)); + } + + if (query.containsKey("links")) { + resp.setLinks(Collections.emptyList()); + } + + if (form.containsKey("mark") && form.containsKey("id") && form.containsKey("as")) { + long id = Long.parseLong(form.getFirst("id")); + String before = form.getFirst("before"); + Date olderThan = before == null ? null : Date.from(Instant.ofEpochSecond(Long.parseLong(before))); + mark(user, form.getFirst("mark"), id, form.getFirst("as"), olderThan); + } + + return resp; + } + + private Optional auth(Long userId, String feverApiKey) { + return userService.login(userId, feverApiKey); + } + + private long buildLastRefreshedOnTime(List subscriptions) { + return subscriptions.stream() + .map(FeedSubscription::getFeed) + .map(Feed::getLastUpdated) + .filter(Objects::nonNull) + .max(Comparator.naturalOrder()) + .map(d -> d.toInstant().getEpochSecond()) + .orElse(0L); + } + + private List buildFeedsGroups(List 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).collect(Collectors.toList())); + return fg; + }) + .collect(Collectors.toList()); + } + + private List buildGroups(List categories) { + return categories.stream().map(c -> { + FeverGroup g = new FeverGroup(); + g.setId(c.getId()); + g.setTitle(c.getName()); + return g; + }).collect(Collectors.toList()); + } + + private List buildFeeds(List 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().toInstant().getEpochSecond()); + return f; + }).collect(Collectors.toList()); + } + + private List buildUnreadItemIds(User user, List subscriptions) { + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, + UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false, true, null, null, null); + return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList()); + } + + private List buildSavedItemIds(User user) { + List statuses = feedEntryStatusDAO.findStarred(user, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false); + return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList()); + } + + private List buildItems(User user, List subscriptions, List entryIds) { + List items = new ArrayList<>(); + + Map 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; + } + + private List buildItems(User user, List subscriptions, Long sinceId, Long maxId) { + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE, + ReadingOrder.desc, false, false, null, sinceId, maxId); + return statuses.stream().map(this::mapStatus).collect(Collectors.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(s.getEntry().getContent().getContent()); + i.setUrl(s.getEntry().getUrl()); + i.setSaved(s.isStarred()); + i.setRead(s.isRead()); + i.setCreatedOnTime(s.getEntryUpdated().toInstant().getEpochSecond()); + return i; + } + + private List buildFavicons(List subscriptions) { + return subscriptions.stream().map(s -> { + Favicon favicon = feedService.fetchFavicon(s.getFeed()); + + FeverFavicon f = new FeverFavicon(); + f.setId(s.getFeed().getId()); + f.setData(String.format("data:%s;base64,%s", favicon.getMediaType(), Base64.getEncoder().encodeToString(favicon.getIcon()))); + return f; + }).collect(Collectors.toList()); + } + + private void mark(User user, String source, long id, String action, Date olderThan) { + 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), olderThan, null); + } else if ("group".equals(source)) { + FeedCategory parent = feedCategoryDAO.findById(user, id); + List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); + List subscriptions = feedSubscriptionDAO.findByCategories(user, categories); + feedEntryService.markSubscriptionEntries(user, subscriptions, olderThan, null); + } + } + +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java new file mode 100644 index 00000000..5dd91136 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java @@ -0,0 +1,164 @@ +package com.commafeed.frontend.resource.fever; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import lombok.Data; + +@JsonInclude(Include.NON_NULL) +@Data +public class FeverResponse { + + @JsonProperty("api_version") + private int apiVersion = 3; + + @JsonProperty("auth") + @JsonFormat(shape = Shape.NUMBER) + private boolean auth; + + @JsonProperty("last_refreshed_on_time") + private long lastRefreshedOnTime; + + @JsonProperty("groups") + private List groups; + + @JsonProperty("feeds") + private List feeds; + + @JsonProperty("feeds_groups") + private List feedsGroups; + + @JsonProperty("unread_item_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + private List unreadItemIds; + + @JsonProperty("saved_item_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + private List savedItemIds; + + @JsonProperty("items") + private List items; + + @JsonProperty("favicons") + private List favicons; + + @JsonProperty("links") + private List links; + + @Data + public static class FeverGroup { + + @JsonProperty("id") + private long id; + + @JsonProperty("title") + private String title; + } + + @Data + public static class FeverFeed { + + @JsonProperty("id") + private long id; + + @JsonProperty("favicon_id") + private long faviconId; + + @JsonProperty("title") + private String title; + + @JsonProperty("url") + private String url; + + @JsonProperty("site_url") + private String siteUrl; + + @JsonProperty("is_spark") + @JsonFormat(shape = Shape.NUMBER) + private boolean spark; + + @JsonProperty("last_updated_on_time") + private long lastUpdatedOnTime; + } + + @Data + public static class FeverFeedGroup { + + @JsonProperty("group_id") + private long groupId; + + @JsonProperty("feed_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + private List feedIds; + } + + @Data + public static class FeverItem { + + @JsonProperty("id") + private long id; + + @JsonProperty("feed_id") + private long feedId; + + @JsonProperty("title") + private String title; + + @JsonProperty("author") + private String author; + + @JsonProperty("html") + private String html; + + @JsonProperty("url") + private String url; + + @JsonProperty("is_saved") + @JsonFormat(shape = Shape.NUMBER) + private boolean saved; + + @JsonProperty("is_read") + @JsonFormat(shape = Shape.NUMBER) + private boolean read; + + @JsonProperty("created_on_time") + private long createdOnTime; + + } + + @Data + public static class FeverFavicon { + + @JsonProperty("id") + private long id; + + @JsonProperty("data") + private String data; + } + + @Data + public static class FeverLink { + + } + + public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer> { + + @Override + public void serialize(List input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + String output = input.stream().map(String::valueOf).collect(Collectors.joining(",")); + jsonGenerator.writeObject(output); + } + + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java index fcfc7c90..bcf47bea 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java @@ -69,7 +69,7 @@ public class NextUnreadServlet extends HttpServlet { if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { List subs = feedSubscriptionDAO.findAll(user.get()); List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, - true, false, null); + true, false, null, null, null); s = Iterables.getFirst(statuses, null); } else { FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); @@ -77,7 +77,7 @@ public class NextUnreadServlet extends HttpServlet { List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, - 1, order, true, false, null); + 1, order, true, false, null, null, null); s = Iterables.getFirst(statuses, null); } }