aboutsummaryrefslogtreecommitdiffhomepage
path: root/fever
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2018-11-11 09:52:12 -0800
committerGravatar Frédéric Guillot <fred@miniflux.net>2018-11-11 09:54:32 -0800
commita9f98adb0739f78495af745d30839a8aaf2f6979 (patch)
tree2edf0d16e6052079c832ba92dc3f08d5d4ceff4b /fever
parent25c12053a6253f4a129016f8b5ef2638395f960f (diff)
Move Fever middleware and routes to fever package
Diffstat (limited to 'fever')
-rw-r--r--fever/handler.go (renamed from fever/fever.go)201
-rw-r--r--fever/middleware.go58
-rw-r--r--fever/response.go120
3 files changed, 227 insertions, 152 deletions
diff --git a/fever/fever.go b/fever/handler.go
index 4a89f45..95d0a44 100644
--- a/fever/fever.go
+++ b/fever/handler.go
@@ -1,4 +1,4 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
+// 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.
@@ -17,142 +17,44 @@ import (
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/storage"
-)
-
-type baseResponse struct {
- Version int `json:"api_version"`
- Authenticated int `json:"auth"`
- LastRefresh int64 `json:"last_refreshed_on_time"`
-}
-
-func (b *baseResponse) SetCommonValues() {
- b.Version = 3
- b.Authenticated = 1
- b.LastRefresh = time.Now().Unix()
-}
-
-/*
-The default response is a JSON object containing two members:
-
- api_version contains the version of the API responding (positive integer)
- auth whether the request was successfully authenticated (boolean integer)
-
-The API can also return XML by passing xml as the optional value of the api argument like so:
-
-http://yourdomain.com/fever/?api=xml
-The top level XML element is named response.
-
-The response to each successfully authenticated request will have auth set to 1 and include
-at least one additional member:
-
- last_refreshed_on_time contains the time of the most recently refreshed (not updated)
- feed (Unix timestamp/integer)
-
-*/
-func newBaseResponse() baseResponse {
- r := baseResponse{}
- r.SetCommonValues()
- return r
-}
-
-type groupsResponse struct {
- baseResponse
- Groups []group `json:"groups"`
- FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type feedsResponse struct {
- baseResponse
- Feeds []feed `json:"feeds"`
- FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type faviconsResponse struct {
- baseResponse
- Favicons []favicon `json:"favicons"`
-}
-
-type itemsResponse struct {
- baseResponse
- Items []item `json:"items"`
- Total int `json:"total_items"`
-}
-
-type unreadResponse struct {
- baseResponse
- ItemIDs string `json:"unread_item_ids"`
-}
-
-type savedResponse struct {
- baseResponse
- ItemIDs string `json:"saved_item_ids"`
-}
-
-type group struct {
- ID int64 `json:"id"`
- Title string `json:"title"`
-}
-
-type feedsGroups struct {
- GroupID int64 `json:"group_id"`
- FeedIDs string `json:"feed_ids"`
-}
-
-type feed struct {
- ID int64 `json:"id"`
- FaviconID int64 `json:"favicon_id"`
- Title string `json:"title"`
- URL string `json:"url"`
- SiteURL string `json:"site_url"`
- IsSpark int `json:"is_spark"`
- LastUpdated int64 `json:"last_updated_on_time"`
-}
+ "github.com/gorilla/mux"
+)
-type item struct {
- ID int64 `json:"id"`
- FeedID int64 `json:"feed_id"`
- Title string `json:"title"`
- Author string `json:"author"`
- HTML string `json:"html"`
- URL string `json:"url"`
- IsSaved int `json:"is_saved"`
- IsRead int `json:"is_read"`
- CreatedAt int64 `json:"created_on_time"`
-}
+// Serve handles Fever API calls.
+func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
+ handler := &handler{cfg, store}
-type favicon struct {
- ID int64 `json:"id"`
- Data string `json:"data"`
+ sr := router.PathPrefix("/fever").Subrouter()
+ sr.Use(newMiddleware(store).serve)
+ sr.HandleFunc("/", handler.serve).Name("feverEndpoint")
}
-// Controller implements the Fever API.
-type Controller struct {
+type handler struct {
cfg *config.Config
store *storage.Storage
}
-// Handler handles Fever API calls
-func (c *Controller) Handler(w http.ResponseWriter, r *http.Request) {
+func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
switch {
case request.HasQueryParam(r, "groups"):
- c.handleGroups(w, r)
+ h.handleGroups(w, r)
case request.HasQueryParam(r, "feeds"):
- c.handleFeeds(w, r)
+ h.handleFeeds(w, r)
case request.HasQueryParam(r, "favicons"):
- c.handleFavicons(w, r)
+ h.handleFavicons(w, r)
case request.HasQueryParam(r, "unread_item_ids"):
- c.handleUnreadItems(w, r)
+ h.handleUnreadItems(w, r)
case request.HasQueryParam(r, "saved_item_ids"):
- c.handleSavedItems(w, r)
+ h.handleSavedItems(w, r)
case request.HasQueryParam(r, "items"):
- c.handleItems(w, r)
+ h.handleItems(w, r)
case r.FormValue("mark") == "item":
- c.handleWriteItems(w, r)
+ h.handleWriteItems(w, r)
case r.FormValue("mark") == "feed":
- c.handleWriteFeeds(w, r)
+ h.handleWriteFeeds(w, r)
case r.FormValue("mark") == "group":
- c.handleWriteGroups(w, r)
+ h.handleWriteGroups(w, r)
default:
json.OK(w, r, newBaseResponse())
}
@@ -178,17 +80,17 @@ The “Sparks” super group is not included in this response and is composed of
is_spark equal to 1.
*/
-func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) {
+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 := c.store.Categories(userID)
+ categories, err := h.store.Categories(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
- feeds, err := c.store.Feeds(userID)
+ feeds, err := h.store.Feeds(userID)
if err != nil {
json.ServerError(w, r, err)
return
@@ -199,7 +101,7 @@ func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) {
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
}
- result.FeedsGroups = c.buildFeedGroups(feeds)
+ result.FeedsGroups = h.buildFeedGroups(feeds)
result.SetCommonValues()
json.OK(w, r, result)
}
@@ -228,11 +130,11 @@ 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 (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) {
+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 := c.store.Feeds(userID)
+ feeds, err := h.store.Feeds(userID)
if err != nil {
json.ServerError(w, r, err)
return
@@ -257,7 +159,7 @@ func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) {
result.Feeds = append(result.Feeds, subscripion)
}
- result.FeedsGroups = c.buildFeedGroups(feeds)
+ result.FeedsGroups = h.buildFeedGroups(feeds)
result.SetCommonValues()
json.OK(w, r, result)
}
@@ -281,11 +183,11 @@ A PHP/HTML example:
echo '<img src="data:'.$favicon['data'].'">';
*/
-func (c *Controller) handleFavicons(w http.ResponseWriter, r *http.Request) {
+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 := c.store.Icons(userID)
+ icons, err := h.store.Icons(userID)
if err != nil {
json.ServerError(w, r, err)
return
@@ -334,13 +236,13 @@ Three optional arguments control determine the items included in the response.
(added in API version 2)
*/
-func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) {
+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 := c.store.NewEntryQueryBuilder(userID)
+ builder := h.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithLimit(50)
builder.WithOrder("id")
@@ -375,7 +277,7 @@ func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) {
return
}
- builder = c.store.NewEntryQueryBuilder(userID)
+ builder = h.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
result.Total, err = builder.CountEntries()
if err != nil {
@@ -419,11 +321,11 @@ 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 (c *Controller) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
+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 := c.store.NewEntryQueryBuilder(userID)
+ builder := h.store.NewEntryQueryBuilder(userID)
builder.WithStatus(model.EntryStatusUnread)
entries, err := builder.GetEntries()
if err != nil {
@@ -450,11 +352,11 @@ with the remote Fever installation.
saved_item_ids (string/comma-separated list of positive integers)
*/
-func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) {
+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 := c.store.NewEntryQueryBuilder(userID)
+ builder := h.store.NewEntryQueryBuilder(userID)
builder.WithStarred()
entryIDs, err := builder.GetEntryIDs()
@@ -478,7 +380,7 @@ func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) {
as=? where ? is replaced with read, saved or unsaved
id=? where ? is replaced with the id of the item to modify
*/
-func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
+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)
@@ -487,7 +389,7 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
return
}
- builder := c.store.NewEntryQueryBuilder(userID)
+ builder := h.store.NewEntryQueryBuilder(userID)
builder.WithEntryID(entryID)
builder.WithoutStatus(model.EntryStatusRemoved)
@@ -504,25 +406,25 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
switch r.FormValue("as") {
case "read":
logger.Debug("[Fever] Mark entry #%d as read", entryID)
- c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
+ h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
case "unread":
logger.Debug("[Fever] Mark entry #%d as unread", entryID)
- c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
+ h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
case "saved", "unsaved":
logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID)
- if err := c.store.ToggleBookmark(userID, entryID); err != nil {
+ if err := h.store.ToggleBookmark(userID, entryID); err != nil {
json.ServerError(w, r, err)
return
}
- settings, err := c.store.Integration(userID)
+ settings, err := h.store.Integration(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
go func() {
- integration.SendEntry(c.cfg, entry, settings)
+ integration.SendEntry(h.cfg, entry, settings)
}()
}
@@ -535,7 +437,7 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
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 (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.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)
@@ -547,7 +449,7 @@ func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
}
go func() {
- if err := c.store.MarkFeedAsRead(userID, feedID, before); err != nil {
+ if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
}
}()
@@ -561,7 +463,7 @@ func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
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 (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.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)
@@ -576,9 +478,9 @@ func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
var err error
if groupID == 0 {
- err = c.store.MarkAllAsRead(userID)
+ err = h.store.MarkAllAsRead(userID)
} else {
- err = c.store.MarkCategoryAsRead(userID, groupID, before)
+ err = h.store.MarkCategoryAsRead(userID, groupID, before)
}
if err != nil {
@@ -596,7 +498,7 @@ A feeds_group object has the following members:
feed_ids (string/comma-separated list of positive integers)
*/
-func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
+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))
@@ -612,8 +514,3 @@ func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
return result
}
-
-// NewController returns a new Fever API.
-func NewController(cfg *config.Config, store *storage.Storage) *Controller {
- return &Controller{cfg, store}
-}
diff --git a/fever/middleware.go b/fever/middleware.go
new file mode 100644
index 0000000..28dac7d
--- /dev/null
+++ b/fever/middleware.go
@@ -0,0 +1,58 @@
+// 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 (
+ "context"
+ "net/http"
+
+ "miniflux.app/http/request"
+ "miniflux.app/http/response/json"
+ "miniflux.app/logger"
+ "miniflux.app/storage"
+)
+
+type middleware struct {
+ store *storage.Storage
+}
+
+func newMiddleware(s *storage.Storage) *middleware {
+ return &middleware{s}
+}
+
+func (m *middleware) serve(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ apiKey := r.FormValue("api_key")
+ if apiKey == "" {
+ logger.Info("[Fever] No API key provided")
+ json.OK(w, r, newAuthFailureResponse())
+ return
+ }
+
+ user, err := m.store.UserByFeverToken(apiKey)
+ if err != nil {
+ logger.Error("[Fever] %v", err)
+ json.OK(w, r, newAuthFailureResponse())
+ return
+ }
+
+ if user == nil {
+ logger.Info("[Fever] No user found with this API key")
+ json.OK(w, r, newAuthFailureResponse())
+ return
+ }
+
+ logger.Info("[Fever] User #%d is authenticated", user.ID)
+ m.store.SetLastLogin(user.ID)
+
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
+ ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
+ ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
+ ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
diff --git a/fever/response.go b/fever/response.go
new file mode 100644
index 0000000..444af81
--- /dev/null
+++ b/fever/response.go
@@ -0,0 +1,120 @@
+// 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 (
+ "time"
+)
+
+type baseResponse struct {
+ Version int `json:"api_version"`
+ Authenticated int `json:"auth"`
+ LastRefresh int64 `json:"last_refreshed_on_time"`
+}
+
+func (b *baseResponse) SetCommonValues() {
+ b.Version = 3
+ b.Authenticated = 1
+ b.LastRefresh = time.Now().Unix()
+}
+
+/*
+The default response is a JSON object containing two members:
+
+ api_version contains the version of the API responding (positive integer)
+ auth whether the request was successfully authenticated (boolean integer)
+
+The API can also return XML by passing xml as the optional value of the api argument like so:
+
+http://yourdomain.com/fever/?api=xml
+
+The top level XML element is named response.
+
+The response to each successfully authenticated request will have auth set to 1 and include
+at least one additional member:
+
+ last_refreshed_on_time contains the time of the most recently refreshed (not updated)
+ feed (Unix timestamp/integer)
+
+*/
+func newBaseResponse() baseResponse {
+ r := baseResponse{}
+ r.SetCommonValues()
+ return r
+}
+
+func newAuthFailureResponse() baseResponse {
+ return baseResponse{Version: 3, Authenticated: 0}
+}
+
+type groupsResponse struct {
+ baseResponse
+ Groups []group `json:"groups"`
+ FeedsGroups []feedsGroups `json:"feeds_groups"`
+}
+
+type feedsResponse struct {
+ baseResponse
+ Feeds []feed `json:"feeds"`
+ FeedsGroups []feedsGroups `json:"feeds_groups"`
+}
+
+type faviconsResponse struct {
+ baseResponse
+ Favicons []favicon `json:"favicons"`
+}
+
+type itemsResponse struct {
+ baseResponse
+ Items []item `json:"items"`
+ Total int `json:"total_items"`
+}
+
+type unreadResponse struct {
+ baseResponse
+ ItemIDs string `json:"unread_item_ids"`
+}
+
+type savedResponse struct {
+ baseResponse
+ ItemIDs string `json:"saved_item_ids"`
+}
+
+type group struct {
+ ID int64 `json:"id"`
+ Title string `json:"title"`
+}
+
+type feedsGroups struct {
+ GroupID int64 `json:"group_id"`
+ FeedIDs string `json:"feed_ids"`
+}
+
+type feed struct {
+ ID int64 `json:"id"`
+ FaviconID int64 `json:"favicon_id"`
+ Title string `json:"title"`
+ URL string `json:"url"`
+ SiteURL string `json:"site_url"`
+ IsSpark int `json:"is_spark"`
+ LastUpdated int64 `json:"last_updated_on_time"`
+}
+
+type item struct {
+ ID int64 `json:"id"`
+ FeedID int64 `json:"feed_id"`
+ Title string `json:"title"`
+ Author string `json:"author"`
+ HTML string `json:"html"`
+ URL string `json:"url"`
+ IsSaved int `json:"is_saved"`
+ IsRead int `json:"is_read"`
+ CreatedAt int64 `json:"created_on_time"`
+}
+
+type favicon struct {
+ ID int64 `json:"id"`
+ Data string `json:"data"`
+}