2023-06-19 23:42:47 +02:00
|
|
|
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2023-08-11 04:46:45 +02:00
|
|
|
|
package fever // import "miniflux.app/v2/internal/fever"
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
import (
|
2023-09-25 01:32:09 +02:00
|
|
|
|
"log/slog"
|
2018-04-30 01:35:04 +02:00
|
|
|
|
"net/http"
|
2017-12-04 02:44:27 +01:00
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2023-08-11 04:46:45 +02:00
|
|
|
|
"miniflux.app/v2/internal/http/request"
|
|
|
|
|
"miniflux.app/v2/internal/http/response/json"
|
|
|
|
|
"miniflux.app/v2/internal/integration"
|
|
|
|
|
"miniflux.app/v2/internal/model"
|
|
|
|
|
"miniflux.app/v2/internal/proxy"
|
|
|
|
|
"miniflux.app/v2/internal/storage"
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
// Serve handles Fever API calls.
|
2019-06-02 03:18:09 +02:00
|
|
|
|
func Serve(router *mux.Router, store *storage.Storage) {
|
2022-10-15 08:17:17 +02:00
|
|
|
|
handler := &handler{store, router}
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
sr := router.PathPrefix("/fever").Subrouter()
|
|
|
|
|
sr.Use(newMiddleware(store).serve)
|
|
|
|
|
sr.HandleFunc("/", handler.serve).Name("feverEndpoint")
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
type handler struct {
|
2022-10-15 08:17:17 +02:00
|
|
|
|
store *storage.Storage
|
|
|
|
|
router *mux.Router
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
switch {
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "groups"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleGroups(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "feeds"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleFeeds(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "favicons"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleFavicons(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "unread_item_ids"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleUnreadItems(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "saved_item_ids"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleSavedItems(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case request.HasQueryParam(r, "items"):
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleItems(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case r.FormValue("mark") == "item":
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleWriteItems(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case r.FormValue("mark") == "feed":
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleWriteFeeds(w, r)
|
2018-04-30 01:35:04 +02:00
|
|
|
|
case r.FormValue("mark") == "group":
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.handleWriteGroups(w, r)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
default:
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, newBaseResponse())
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
A request with the groups argument will return two additional members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
groups contains an array of group objects
|
|
|
|
|
feeds_groups contains an array of feeds_group objects
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
A group object has the following members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
id (positive integer)
|
|
|
|
|
title (utf-8 string)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching groups",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
categories, err := h.store.Categories(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
feeds, err := h.store.Feeds(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result groupsResponse
|
|
|
|
|
for _, category := range categories {
|
|
|
|
|
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
result.FeedsGroups = h.buildFeedGroups(feeds)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
A request with the feeds argument will return two additional members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
feeds contains an array of group objects
|
|
|
|
|
feeds_groups contains an array of feeds_group objects
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
A feed object has the following members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
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)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching feeds",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
feeds, err := h.store.Feeds(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result feedsResponse
|
2017-12-22 20:33:01 +01:00
|
|
|
|
result.Feeds = make([]feed, 0)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
for _, f := range feeds {
|
2017-12-30 00:32:04 +01:00
|
|
|
|
subscripion := feed{
|
2017-12-04 02:44:27 +01:00
|
|
|
|
ID: f.ID,
|
|
|
|
|
Title: f.Title,
|
|
|
|
|
URL: f.FeedURL,
|
|
|
|
|
SiteURL: f.SiteURL,
|
|
|
|
|
IsSpark: 0,
|
|
|
|
|
LastUpdated: f.CheckedAt.Unix(),
|
2017-12-30 00:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if f.Icon != nil {
|
|
|
|
|
subscripion.FaviconID = f.Icon.IconID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.Feeds = append(result.Feeds, subscripion)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
result.FeedsGroups = h.buildFeedGroups(feeds)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
A request with the favicons argument will return one additional member:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
favicons contains an array of favicon objects
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
A favicon object has the following members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
id (positive integer)
|
|
|
|
|
data (base64 encoded image data; prefixed by image type)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
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'].'">';
|
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching favicons",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
icons, err := h.store.Icons(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result faviconsResponse
|
|
|
|
|
for _, i := range icons {
|
|
|
|
|
result.Favicons = append(result.Favicons, favicon{
|
|
|
|
|
ID: i.ID,
|
|
|
|
|
Data: i.DataURL(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
A request with the items argument will return two additional members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
items contains an array of item objects
|
|
|
|
|
total_items contains the total number of items stored in the database (added in API version 2)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
An item object has the following members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
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)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
var result itemsResponse
|
|
|
|
|
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
builder := h.store.NewEntryQueryBuilder(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
|
|
|
builder.WithLimit(50)
|
2023-06-19 23:00:10 +02:00
|
|
|
|
builder.WithSorting("id", model.DefaultSortingDirection)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2020-08-09 06:03:02 +02:00
|
|
|
|
switch {
|
|
|
|
|
case request.HasQueryParam(r, "since_id"):
|
|
|
|
|
sinceID := request.QueryInt64Param(r, "since_id", 0)
|
|
|
|
|
if sinceID > 0 {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching items since a given date",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("since_id", sinceID),
|
|
|
|
|
)
|
2020-08-09 06:03:02 +02:00
|
|
|
|
builder.AfterEntryID(sinceID)
|
|
|
|
|
}
|
|
|
|
|
case request.HasQueryParam(r, "max_id"):
|
|
|
|
|
maxID := request.QueryInt64Param(r, "max_id", 0)
|
|
|
|
|
if maxID == 0 {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching most recent items",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2023-06-19 23:00:10 +02:00
|
|
|
|
builder.WithSorting("id", "DESC")
|
2020-08-09 06:03:02 +02:00
|
|
|
|
} else if maxID > 0 {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching items before a given item ID",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("max_id", maxID),
|
|
|
|
|
)
|
2020-08-09 06:03:02 +02:00
|
|
|
|
builder.BeforeEntryID(maxID)
|
2023-06-19 23:00:10 +02:00
|
|
|
|
builder.WithSorting("id", "DESC")
|
2020-08-09 06:03:02 +02:00
|
|
|
|
}
|
|
|
|
|
case request.HasQueryParam(r, "with_ids"):
|
|
|
|
|
csvItemIDs := request.QueryStringParam(r, "with_ids", "")
|
|
|
|
|
if csvItemIDs != "" {
|
|
|
|
|
var itemIDs []int64
|
|
|
|
|
|
|
|
|
|
for _, strItemID := range strings.Split(csvItemIDs, ",") {
|
|
|
|
|
strItemID = strings.TrimSpace(strItemID)
|
|
|
|
|
itemID, _ := strconv.ParseInt(strItemID, 10, 64)
|
|
|
|
|
itemIDs = append(itemIDs, itemID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder.WithEntryIDs(itemIDs)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
2020-08-09 06:19:47 +02:00
|
|
|
|
default:
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching oldest items",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries, err := builder.GetEntries()
|
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
builder = h.store.NewEntryQueryBuilder(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
|
|
|
result.Total, err = builder.CountEntries()
|
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-09 07:08:01 +02:00
|
|
|
|
result.Items = make([]item, 0)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
isRead := 0
|
|
|
|
|
if entry.Status == model.EntryStatusRead {
|
|
|
|
|
isRead = 1
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-22 20:33:01 +01:00
|
|
|
|
isSaved := 0
|
|
|
|
|
if entry.Starred {
|
|
|
|
|
isSaved = 1
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-04 02:44:27 +01:00
|
|
|
|
result.Items = append(result.Items, item{
|
|
|
|
|
ID: entry.ID,
|
|
|
|
|
FeedID: entry.FeedID,
|
|
|
|
|
Title: entry.Title,
|
|
|
|
|
Author: entry.Author,
|
2023-02-25 09:36:19 +01:00
|
|
|
|
HTML: proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content),
|
2017-12-04 02:44:27 +01:00
|
|
|
|
URL: entry.URL,
|
2017-12-22 20:33:01 +01:00
|
|
|
|
IsSaved: isSaved,
|
2017-12-04 02:44:27 +01:00
|
|
|
|
IsRead: isRead,
|
|
|
|
|
CreatedAt: entry.Date.Unix(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
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:
|
2022-08-09 06:33:38 +02:00
|
|
|
|
|
|
|
|
|
unread_item_ids (string/comma-separated list of positive integers)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching unread items",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
builder := h.store.NewEntryQueryBuilder(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
builder.WithStatus(model.EntryStatusUnread)
|
2020-06-24 06:48:25 +02:00
|
|
|
|
rawEntryIDs, err := builder.GetEntryIDs()
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var itemIDs []string
|
2020-06-24 06:48:25 +02:00
|
|
|
|
for _, entryID := range rawEntryIDs {
|
|
|
|
|
itemIDs = append(itemIDs, strconv.FormatInt(entryID, 10))
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result unreadResponse
|
|
|
|
|
result.ItemIDs = strings.Join(itemIDs, ",")
|
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
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)
|
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Fetching saved items",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
builder := h.store.NewEntryQueryBuilder(userID)
|
2022-04-14 06:53:06 +02:00
|
|
|
|
builder.WithStarred(true)
|
2017-12-22 20:33:01 +01:00
|
|
|
|
|
|
|
|
|
entryIDs, err := builder.GetEntryIDs()
|
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-22 20:33:01 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var itemsIDs []string
|
|
|
|
|
for _, entryID := range entryIDs {
|
|
|
|
|
itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
2017-12-04 02:44:27 +01:00
|
|
|
|
result.SetCommonValues()
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, result)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2022-08-09 06:33:38 +02:00
|
|
|
|
mark=item
|
|
|
|
|
as=? where ? is replaced with read, saved or unsaved
|
|
|
|
|
id=? where ? is replaced with the id of the item to modify
|
2017-12-04 02:44:27 +01:00
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Receiving mark=item call",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-06-10 04:13:41 +02:00
|
|
|
|
entryID := request.FormInt64Value(r, "id")
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if entryID <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
builder := h.store.NewEntryQueryBuilder(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
builder.WithEntryID(entryID)
|
|
|
|
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
|
|
|
|
|
|
|
|
|
entry, err := builder.GetEntry()
|
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if entry == nil {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Entry not found",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("entry_id", entryID),
|
|
|
|
|
)
|
2019-11-01 02:59:04 +01:00
|
|
|
|
json.OK(w, r, newBaseResponse())
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-30 01:35:04 +02:00
|
|
|
|
switch r.FormValue("as") {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
case "read":
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark entry as read",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("entry_id", entryID),
|
|
|
|
|
)
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
case "unread":
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark entry as unread",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("entry_id", entryID),
|
|
|
|
|
)
|
2018-11-11 18:52:12 +01:00
|
|
|
|
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
2020-08-09 06:34:04 +02:00
|
|
|
|
case "saved":
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark entry as saved",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("entry_id", entryID),
|
|
|
|
|
)
|
2018-11-11 18:52:12 +01:00
|
|
|
|
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-22 20:33:01 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:52:12 +01:00
|
|
|
|
settings, err := h.store.Integration(userID)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
if err != nil {
|
2018-10-08 03:42:43 +02:00
|
|
|
|
json.ServerError(w, r, err)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go func() {
|
2019-06-02 03:18:09 +02:00
|
|
|
|
integration.SendEntry(entry, settings)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}()
|
2020-08-09 06:34:04 +02:00
|
|
|
|
case "unsaved":
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark entry as unsaved",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("entry_id", entryID),
|
|
|
|
|
)
|
2020-08-09 06:34:04 +02:00
|
|
|
|
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
|
|
|
|
json.ServerError(w, r, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, newBaseResponse())
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2022-08-09 06:33:38 +02:00
|
|
|
|
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
|
2017-12-04 02:44:27 +01:00
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2018-06-10 04:13:41 +02:00
|
|
|
|
feedID := request.FormInt64Value(r, "id")
|
2018-10-07 21:50:59 +02:00
|
|
|
|
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark feed as read before a given date",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("feed_id", feedID),
|
|
|
|
|
slog.Time("before_ts", before),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-10-07 21:50:59 +02:00
|
|
|
|
if feedID <= 0 {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-07 21:50:59 +02:00
|
|
|
|
go func() {
|
2018-11-11 18:52:12 +01:00
|
|
|
|
if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Error("[Fever] Unable to mark feed as read",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("feed_id", feedID),
|
|
|
|
|
slog.Time("before_ts", before),
|
|
|
|
|
slog.Any("error", err),
|
|
|
|
|
)
|
2018-10-07 21:50:59 +02:00
|
|
|
|
}
|
|
|
|
|
}()
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, newBaseResponse())
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2022-08-09 06:33:38 +02:00
|
|
|
|
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
|
2017-12-04 02:44:27 +01:00
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
2018-09-03 23:26:40 +02:00
|
|
|
|
userID := request.UserID(r)
|
2018-06-10 04:13:41 +02:00
|
|
|
|
groupID := request.FormInt64Value(r, "id")
|
2018-10-07 21:50:59 +02:00
|
|
|
|
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Debug("[Fever] Mark group as read before a given date",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("group_id", groupID),
|
|
|
|
|
slog.Time("before_ts", before),
|
|
|
|
|
)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-10-07 21:50:59 +02:00
|
|
|
|
if groupID < 0 {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-07 21:50:59 +02:00
|
|
|
|
go func() {
|
2018-10-27 04:49:49 +02:00
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
if groupID == 0 {
|
2018-11-11 18:52:12 +01:00
|
|
|
|
err = h.store.MarkAllAsRead(userID)
|
2018-10-27 04:49:49 +02:00
|
|
|
|
} else {
|
2018-11-11 18:52:12 +01:00
|
|
|
|
err = h.store.MarkCategoryAsRead(userID, groupID, before)
|
2018-10-27 04:49:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2023-09-25 01:32:09 +02:00
|
|
|
|
slog.Error("[Fever] Unable to mark group as read",
|
|
|
|
|
slog.Int64("user_id", userID),
|
|
|
|
|
slog.Int64("group_id", groupID),
|
|
|
|
|
slog.Time("before_ts", before),
|
|
|
|
|
slog.Any("error", err),
|
|
|
|
|
)
|
2018-10-07 21:50:59 +02:00
|
|
|
|
}
|
|
|
|
|
}()
|
2017-12-04 02:44:27 +01:00
|
|
|
|
|
2018-07-20 04:27:05 +02:00
|
|
|
|
json.OK(w, r, newBaseResponse())
|
2017-12-04 02:44:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
A feeds_group object has the following members:
|
|
|
|
|
|
2022-08-09 06:33:38 +02:00
|
|
|
|
group_id (positive integer)
|
|
|
|
|
feed_ids (string/comma-separated list of positive integers)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
*/
|
2018-11-11 18:52:12 +01:00
|
|
|
|
func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups {
|
2017-12-04 02:44:27 +01:00
|
|
|
|
feedsGroupedByCategory := make(map[int64][]string)
|
|
|
|
|
for _, feed := range feeds {
|
|
|
|
|
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-22 20:33:01 +01:00
|
|
|
|
result := make([]feedsGroups, 0)
|
2017-12-04 02:44:27 +01:00
|
|
|
|
for categoryID, feedIDs := range feedsGroupedByCategory {
|
|
|
|
|
result = append(result, feedsGroups{
|
|
|
|
|
GroupID: categoryID,
|
|
|
|
|
FeedIDs: strings.Join(feedIDs, ","),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|