diff --git a/internal/googlereader/handler.go b/internal/googlereader/handler.go index 8e988ecf..84758349 100644 --- a/internal/googlereader/handler.go +++ b/internal/googlereader/handler.go @@ -95,6 +95,8 @@ const ( ParamDestination = "dest" // ParamContinuation - name of the parameter for callers to pass to receive the next page of results ParamContinuation = "c" + // ParamStreamType - name of the parameter for unix timestamp + ParamTimestamp = "ts" ) var ( @@ -234,6 +236,7 @@ func Serve(router *mux.Router, store *storage.Storage) { sr.HandleFunc("/subscription/quickadd", handler.quickAddHandler).Methods(http.MethodPost).Name("QuickAdd") sr.HandleFunc("/stream/items/ids", handler.streamItemIDsHandler).Methods(http.MethodGet).Name("StreamItemIDs") sr.HandleFunc("/stream/items/contents", handler.streamItemContentsHandler).Methods(http.MethodPost).Name("StreamItemsContents") + sr.HandleFunc("/mark-all-as-read", handler.markAllAsReadHandler).Methods(http.MethodPost).Name("MarkAllAsRead") sr.PathPrefix("/").HandlerFunc(handler.serveHandler).Methods(http.MethodPost, http.MethodGet).Name("GoogleReaderApiEndpoint") } @@ -324,12 +327,12 @@ func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType switch s.Type { case ReadStream: if _, ok := tags[KeptUnreadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) + return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", KeptUnread, Read) } tags[ReadStream] = true case KeptUnreadStream: if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) + return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", KeptUnread, Read) } tags[ReadStream] = false case StarredStream: @@ -344,12 +347,12 @@ func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType switch s.Type { case ReadStream: if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) + return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", KeptUnread, Read) } tags[ReadStream] = false case KeptUnreadStream: if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) + return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", KeptUnread, Read) } tags[ReadStream] = true case StarredStream: @@ -1551,3 +1554,82 @@ func (h *handler) handleFeedStreamHandler(w http.ResponseWriter, r *http.Request json.OK(w, r, streamIDResponse{itemRefs, continuation}) } + +func (h *handler) markAllAsReadHandler(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + clientIP := request.ClientIP(r) + + slog.Debug("[GoogleReader] Handle /mark-all-as-read", + slog.String("handler", "markAllAsReadHandler"), + slog.String("client_ip", clientIP), + slog.String("user_agent", r.UserAgent()), + ) + + if err := r.ParseForm(); err != nil { + json.BadRequest(w, r, err) + return + } + + stream, err := getStream(r.Form.Get(ParamStreamID), userID) + if err != nil { + json.BadRequest(w, r, err) + return + } + + var before time.Time + if timestampParamValue := r.Form.Get(ParamTimestamp); timestampParamValue != "" { + timestampParsedValue, err := strconv.ParseInt(timestampParamValue, 10, 64) + if err != nil { + json.BadRequest(w, r, err) + return + } + + if timestampParsedValue > 0 { + // It's unclear if the timestamp is in seconds or microseconds, so we try both using a naive approach. + if len(timestampParamValue) >= 16 { + before = time.UnixMicro(timestampParsedValue) + } else { + before = time.Unix(timestampParsedValue, 0) + } + } + } + + if before.IsZero() { + before = time.Now() + } + + switch stream.Type { + case FeedStream: + feedID, err := strconv.ParseInt(stream.ID, 10, 64) + if err != nil { + json.BadRequest(w, r, err) + return + } + err = h.store.MarkFeedAsRead(userID, feedID, before) + if err != nil { + json.ServerError(w, r, err) + return + } + case LabelStream: + category, err := h.store.CategoryByTitle(userID, stream.ID) + if err != nil { + json.ServerError(w, r, err) + return + } + if category == nil { + json.NotFound(w, r) + return + } + if err := h.store.MarkCategoryAsRead(userID, category.ID, before); err != nil { + json.ServerError(w, r, err) + return + } + case ReadingListStream: + if err = h.store.MarkAllAsReadBeforeDate(userID, before); err != nil { + json.ServerError(w, r, err) + return + } + } + + OK(w, r) +} diff --git a/internal/storage/entry.go b/internal/storage/entry.go index 171c0cbe..c6add8e2 100644 --- a/internal/storage/entry.go +++ b/internal/storage/entry.go @@ -476,6 +476,30 @@ func (s *Storage) MarkAllAsRead(userID int64) error { return nil } +// MarkAllAsReadBeforeDate updates all user entries to the read status before the given date. +func (s *Storage) MarkAllAsReadBeforeDate(userID int64, before time.Time) error { + query := ` + UPDATE + entries + SET + status=$1, + changed_at=now() + WHERE + user_id=$2 AND status=$3 AND published_at < $4 + ` + result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, before) + if err != nil { + return fmt.Errorf(`store: unable to mark all entries as read before %s: %v`, before.Format(time.RFC3339), err) + } + count, _ := result.RowsAffected() + slog.Debug("Marked all entries as read before date", + slog.Int64("user_id", userID), + slog.Int64("nb_entries", count), + slog.String("before", before.Format(time.RFC3339)), + ) + return nil +} + // MarkGloballyVisibleFeedsAsRead updates all user entries to the read status. func (s *Storage) MarkGloballyVisibleFeedsAsRead(userID int64) error { query := ` @@ -527,6 +551,7 @@ func (s *Storage) MarkFeedAsRead(userID, feedID int64, before time.Time) error { slog.Int64("user_id", userID), slog.Int64("feed_id", feedID), slog.Int64("nb_entries", count), + slog.String("before", before.Format(time.RFC3339)), ) return nil @@ -563,6 +588,7 @@ func (s *Storage) MarkCategoryAsRead(userID, categoryID int64, before time.Time) slog.Int64("user_id", userID), slog.Int64("category_id", categoryID), slog.Int64("nb_entries", count), + slog.String("before", before.Format(time.RFC3339)), ) return nil