diff options
author | Frédéric Guillot <fred@miniflux.net> | 2018-11-11 09:52:12 -0800 |
---|---|---|
committer | Frédéric Guillot <fred@miniflux.net> | 2018-11-11 09:54:32 -0800 |
commit | a9f98adb0739f78495af745d30839a8aaf2f6979 (patch) | |
tree | 2edf0d16e6052079c832ba92dc3f08d5d4ceff4b /fever/handler.go | |
parent | 25c12053a6253f4a129016f8b5ef2638395f960f (diff) |
Move Fever middleware and routes to fever package
Diffstat (limited to 'fever/handler.go')
-rw-r--r-- | fever/handler.go | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/fever/handler.go b/fever/handler.go new file mode 100644 index 0000000..95d0a44 --- /dev/null +++ b/fever/handler.go @@ -0,0 +1,516 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package fever // import "miniflux.app/fever" + +import ( + "net/http" + "strconv" + "strings" + "time" + + "miniflux.app/config" + "miniflux.app/http/request" + "miniflux.app/http/response/json" + "miniflux.app/integration" + "miniflux.app/logger" + "miniflux.app/model" + "miniflux.app/storage" + + "github.com/gorilla/mux" +) + +// Serve handles Fever API calls. +func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) { + handler := &handler{cfg, store} + + sr := router.PathPrefix("/fever").Subrouter() + sr.Use(newMiddleware(store).serve) + sr.HandleFunc("/", handler.serve).Name("feverEndpoint") +} + +type handler struct { + cfg *config.Config + store *storage.Storage +} + +func (h *handler) serve(w http.ResponseWriter, r *http.Request) { + switch { + case request.HasQueryParam(r, "groups"): + h.handleGroups(w, r) + case request.HasQueryParam(r, "feeds"): + h.handleFeeds(w, r) + case request.HasQueryParam(r, "favicons"): + h.handleFavicons(w, r) + case request.HasQueryParam(r, "unread_item_ids"): + h.handleUnreadItems(w, r) + case request.HasQueryParam(r, "saved_item_ids"): + h.handleSavedItems(w, r) + case request.HasQueryParam(r, "items"): + h.handleItems(w, r) + case r.FormValue("mark") == "item": + h.handleWriteItems(w, r) + case r.FormValue("mark") == "feed": + h.handleWriteFeeds(w, r) + case r.FormValue("mark") == "group": + h.handleWriteGroups(w, r) + default: + json.OK(w, r, newBaseResponse()) + } +} + +/* +A request with the groups argument will return two additional members: + + groups contains an array of group objects + feeds_groups contains an array of feeds_group objects + +A group object has the following members: + + id (positive integer) + title (utf-8 string) + +The feeds_group object is documented under “Feeds/Groups Relationships.” + +The “Kindling” super group is not included in this response and is composed of all feeds with +an is_spark equal to 0. + +The “Sparks” super group is not included in this response and is composed of all feeds with an +is_spark equal to 1. + +*/ +func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Fetching groups for userID=%d", userID) + + categories, err := h.store.Categories(userID) + if err != nil { + json.ServerError(w, r, err) + return + } + + feeds, err := h.store.Feeds(userID) + if err != nil { + json.ServerError(w, r, err) + return + } + + var result groupsResponse + for _, category := range categories { + result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title}) + } + + result.FeedsGroups = h.buildFeedGroups(feeds) + result.SetCommonValues() + json.OK(w, r, result) +} + +/* +A request with the feeds argument will return two additional members: + + feeds contains an array of group objects + feeds_groups contains an array of feeds_group objects + +A feed object has the following members: + + id (positive integer) + favicon_id (positive integer) + title (utf-8 string) + url (utf-8 string) + site_url (utf-8 string) + is_spark (boolean integer) + last_updated_on_time (Unix timestamp/integer) + +The feeds_group object is documented under “Feeds/Groups Relationships.” + +The “All Items” super feed is not included in this response and is composed of all items from all feeds +that belong to a given group. For the “Kindling” super group and all user created groups the items +should be limited to feeds with an is_spark equal to 0. + +For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1. +*/ +func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Fetching feeds for userID=%d", userID) + + feeds, err := h.store.Feeds(userID) + if err != nil { + json.ServerError(w, r, err) + return + } + + var result feedsResponse + result.Feeds = make([]feed, 0) + for _, f := range feeds { + subscripion := feed{ + ID: f.ID, + Title: f.Title, + URL: f.FeedURL, + SiteURL: f.SiteURL, + IsSpark: 0, + LastUpdated: f.CheckedAt.Unix(), + } + + if f.Icon != nil { + subscripion.FaviconID = f.Icon.IconID + } + + result.Feeds = append(result.Feeds, subscripion) + } + + result.FeedsGroups = h.buildFeedGroups(feeds) + result.SetCommonValues() + json.OK(w, r, result) +} + +/* +A request with the favicons argument will return one additional member: + + favicons contains an array of favicon objects + +A favicon object has the following members: + + id (positive integer) + data (base64 encoded image data; prefixed by image type) + +An example data value: + + image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw== + +The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML. +A PHP/HTML example: + + echo '<img src="data:'.$favicon['data'].'">'; +*/ +func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Fetching favicons for userID=%d", userID) + + icons, err := h.store.Icons(userID) + if err != nil { + json.ServerError(w, r, err) + return + } + + var result faviconsResponse + for _, i := range icons { + result.Favicons = append(result.Favicons, favicon{ + ID: i.ID, + Data: i.DataURL(), + }) + } + + result.SetCommonValues() + json.OK(w, r, result) +} + +/* +A request with the items argument will return two additional members: + + items contains an array of item objects + total_items contains the total number of items stored in the database (added in API version 2) + +An item object has the following members: + + id (positive integer) + feed_id (positive integer) + title (utf-8 string) + author (utf-8 string) + html (utf-8 string) + url (utf-8 string) + is_saved (boolean integer) + is_read (boolean integer) + created_on_time (Unix timestamp/integer) + +Most servers won’t have enough memory allocated to PHP to dump all items at once. +Three optional arguments control determine the items included in the response. + + Use the since_id argument with the highest id of locally cached items to request 50 additional items. + Repeat until the items array in the response is empty. + + Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items. + Repeat until the items array in the response is empty. (added in API version 2) + + Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items. + (added in API version 2) + +*/ +func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) { + var result itemsResponse + + userID := request.UserID(r) + logger.Debug("[Fever] Fetching items for userID=%d", userID) + + builder := h.store.NewEntryQueryBuilder(userID) + builder.WithoutStatus(model.EntryStatusRemoved) + builder.WithLimit(50) + builder.WithOrder("id") + builder.WithDirection(model.DefaultSortingDirection) + + sinceID := request.QueryIntParam(r, "since_id", 0) + if sinceID > 0 { + builder.AfterEntryID(int64(sinceID)) + } + + maxID := request.QueryIntParam(r, "max_id", 0) + if maxID > 0 { + builder.WithOffset(maxID) + } + + csvItemIDs := request.QueryStringParam(r, "with_ids", "") + if csvItemIDs != "" { + var itemIDs []int64 + + for _, strItemID := range strings.Split(csvItemIDs, ",") { + strItemID = strings.TrimSpace(strItemID) + itemID, _ := strconv.Atoi(strItemID) + itemIDs = append(itemIDs, int64(itemID)) + } + + builder.WithEntryIDs(itemIDs) + } + + entries, err := builder.GetEntries() + if err != nil { + json.ServerError(w, r, err) + return + } + + builder = h.store.NewEntryQueryBuilder(userID) + builder.WithoutStatus(model.EntryStatusRemoved) + result.Total, err = builder.CountEntries() + if err != nil { + json.ServerError(w, r, err) + return + } + + result.Items = make([]item, 0) + for _, entry := range entries { + isRead := 0 + if entry.Status == model.EntryStatusRead { + isRead = 1 + } + + isSaved := 0 + if entry.Starred { + isSaved = 1 + } + + result.Items = append(result.Items, item{ + ID: entry.ID, + FeedID: entry.FeedID, + Title: entry.Title, + Author: entry.Author, + HTML: entry.Content, + URL: entry.URL, + IsSaved: isSaved, + IsRead: isRead, + CreatedAt: entry.Date.Unix(), + }) + } + + result.SetCommonValues() + json.OK(w, r, result) +} + +/* +The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced +with the remote Fever installation. + +A request with the unread_item_ids argument will return one additional member: + unread_item_ids (string/comma-separated list of positive integers) +*/ +func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Fetching unread items for userID=%d", userID) + + builder := h.store.NewEntryQueryBuilder(userID) + builder.WithStatus(model.EntryStatusUnread) + entries, err := builder.GetEntries() + if err != nil { + json.ServerError(w, r, err) + return + } + + var itemIDs []string + for _, entry := range entries { + itemIDs = append(itemIDs, strconv.FormatInt(entry.ID, 10)) + } + + var result unreadResponse + result.ItemIDs = strings.Join(itemIDs, ",") + result.SetCommonValues() + json.OK(w, r, result) +} + +/* +The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced +with the remote Fever installation. + + A request with the saved_item_ids argument will return one additional member: + + saved_item_ids (string/comma-separated list of positive integers) +*/ +func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Fetching saved items for userID=%d", userID) + + builder := h.store.NewEntryQueryBuilder(userID) + builder.WithStarred() + + entryIDs, err := builder.GetEntryIDs() + if err != nil { + json.ServerError(w, r, err) + return + } + + var itemsIDs []string + for _, entryID := range entryIDs { + itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10)) + } + + result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")} + result.SetCommonValues() + json.OK(w, r, result) +} + +/* + mark=item + as=? where ? is replaced with read, saved or unsaved + id=? where ? is replaced with the id of the item to modify +*/ +func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID) + + entryID := request.FormInt64Value(r, "id") + if entryID <= 0 { + return + } + + builder := h.store.NewEntryQueryBuilder(userID) + builder.WithEntryID(entryID) + builder.WithoutStatus(model.EntryStatusRemoved) + + entry, err := builder.GetEntry() + if err != nil { + json.ServerError(w, r, err) + return + } + + if entry == nil { + return + } + + switch r.FormValue("as") { + case "read": + logger.Debug("[Fever] Mark entry #%d as read", entryID) + h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead) + case "unread": + logger.Debug("[Fever] Mark entry #%d as unread", entryID) + h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread) + case "saved", "unsaved": + logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID) + if err := h.store.ToggleBookmark(userID, entryID); err != nil { + json.ServerError(w, r, err) + return + } + + settings, err := h.store.Integration(userID) + if err != nil { + json.ServerError(w, r, err) + return + } + + go func() { + integration.SendEntry(h.cfg, entry, settings) + }() + } + + json.OK(w, r, newBaseResponse()) +} + +/* + mark=feed + as=read + id=? where ? is replaced with the id of the feed or group to modify + before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request +*/ +func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + feedID := request.FormInt64Value(r, "id") + before := time.Unix(request.FormInt64Value(r, "before"), 0) + + logger.Debug("[Fever] mark=feed, userID=%d, feedID=%d, before=%v", userID, feedID, before) + + if feedID <= 0 { + return + } + + go func() { + if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil { + logger.Error("[Fever] MarkFeedAsRead failed: %v", err) + } + }() + + json.OK(w, r, newBaseResponse()) +} + +/* + mark=group + as=read + id=? where ? is replaced with the id of the feed or group to modify + before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request +*/ +func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + groupID := request.FormInt64Value(r, "id") + before := time.Unix(request.FormInt64Value(r, "before"), 0) + + logger.Debug("[Fever] mark=group, userID=%d, groupID=%d, before=%v", userID, groupID, before) + + if groupID < 0 { + return + } + + go func() { + var err error + + if groupID == 0 { + err = h.store.MarkAllAsRead(userID) + } else { + err = h.store.MarkCategoryAsRead(userID, groupID, before) + } + + if err != nil { + logger.Error("[Fever] MarkCategoryAsRead failed: %v", err) + } + }() + + json.OK(w, r, newBaseResponse()) +} + +/* +A feeds_group object has the following members: + + group_id (positive integer) + feed_ids (string/comma-separated list of positive integers) + +*/ +func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups { + feedsGroupedByCategory := make(map[int64][]string) + for _, feed := range feeds { + feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10)) + } + + result := make([]feedsGroups, 0) + for categoryID, feedIDs := range feedsGroupedByCategory { + result = append(result, feedsGroups{ + GroupID: categoryID, + FeedIDs: strings.Join(feedIDs, ","), + }) + } + + return result +} |