Files
remark42/backend/app/rest/api/rss.go
T
Dmitry VerkhoturovandUmputun 02db7a917d fix lack of error on file site export
Previously, status 200 was set for file export, which is used
for backup, which resulted in an inability to set an error status code
in case of a problem with file generation.

After this change, status code 200 would be written automatically by Go
before we start writing the response's body.
2024-01-28 12:16:24 -06:00

174 lines
5.1 KiB
Go

package api
import (
"fmt"
"net/http"
"time"
cache "github.com/go-pkgz/lcw"
log "github.com/go-pkgz/lgr"
"github.com/gorilla/feeds"
"github.com/umputun/remark42/backend/app/rest"
"github.com/umputun/remark42/backend/app/store"
)
type rss struct {
dataService rssStore
cache LoadingCache
}
type rssStore interface {
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
Last(siteID string, limit int, since time.Time, user store.User) ([]store.Comment, error)
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
UserReplies(siteID, userID string, limit int, duration time.Duration) ([]store.Comment, string, error)
}
const maxRssItems = 20
const maxReplyDuration = 31 * 24 * time.Hour
// ui uses links like <post-url>#remark42__comment-<comment-id>
const uiNav = "#remark42__comment-"
// GET /rss/post?site=siteID&url=post-url
func (s *rss) postCommentsCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
log.Printf("[DEBUG] get rss for post %+v", locator)
key := cache.NewKey(locator.SiteID).ID(URLKey(r)).Scopes(locator.SiteID, locator.URL)
data, err := s.cache.Get(key, func() ([]byte, error) {
comments, e := s.dataService.Find(locator, "-time", rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
feed, e := s.toRssFeed(locator.URL, comments, "post comments for "+r.URL.Query().Get("url"))
if e != nil {
return nil, e
}
return []byte(feed), e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments", rest.ErrPostNotFound)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
// GET /rss/site?site=siteID
func (s *rss) siteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
log.Printf("[DEBUG] get rss for site %s", siteID)
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(siteID, lastCommentsScope)
data, err := s.cache.Get(key, func() ([]byte, error) {
comments, e := s.dataService.Last(siteID, maxRssItems, time.Time{}, rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
feed, e := s.toRssFeed(r.URL.Query().Get("site"), comments, "site comment for "+siteID)
if e != nil {
return nil, e
}
return []byte(feed), e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get last comments", rest.ErrSiteNotFound)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
// GET /rss/reply?user=userID&site=siteID
func (s *rss) repliesCtrl(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user")
siteID := r.URL.Query().Get("site")
log.Printf("[DEBUG] get rss replies to user %s for site %s", userID, siteID)
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(siteID, lastCommentsScope)
data, err := s.cache.Get(key, func() (res []byte, e error) {
replies, userName, e := s.dataService.UserReplies(siteID, userID, maxRssItems, maxReplyDuration)
if e != nil {
return nil, fmt.Errorf("can't get last comments: %w", e)
}
feed, e := s.toRssFeed(siteID, replies, "replies to "+userName)
if e != nil {
return nil, e
}
return []byte(feed), e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get replies", rest.ErrSiteNotFound)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
func (s *rss) toRssFeed(url string, comments []store.Comment, description string) (string, error) {
if description == "" {
description = "comment updates"
}
lastCommentTS := time.Unix(0, 0)
if len(comments) > 0 {
lastCommentTS = comments[0].Timestamp
}
feed := &feeds.Feed{
Title: "Remark42 comments",
Link: &feeds.Link{Href: url},
Description: description,
Created: lastCommentTS,
}
feed.Items = []*feeds.Item{}
for i, c := range comments {
f := feeds.Item{
Title: c.User.Name,
Link: &feeds.Link{Href: c.Locator.URL + uiNav + c.ID},
Description: c.Text,
Created: c.Timestamp,
Author: &feeds.Author{Name: c.User.Name},
Id: c.ID,
}
if c.ParentID != "" {
// add indication to parent comment
parentComment, err := s.dataService.Get(c.Locator, c.ParentID, store.User{})
if err == nil {
f.Title = fmt.Sprintf("%s > %s", c.User.Name, parentComment.User.Name)
f.Description = f.Description + "<blockquote><p>" + parentComment.Snippet(300) + "</p></blockquote>"
} else {
log.Printf("[WARN] failed to get info about parent comment, %s", err)
}
}
if c.PostTitle != "" {
f.Title = f.Title + ", " + c.PostTitle
}
feed.Items = append(feed.Items, &f)
if i > maxRssItems {
break
}
}
return feed.ToRss()
}