rss.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package server
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/gorilla/feeds"
  11. "github.com/labstack/echo/v4"
  12. "github.com/usememos/memos/api"
  13. "github.com/usememos/memos/common"
  14. "github.com/usememos/memos/store"
  15. "github.com/yuin/goldmark"
  16. )
  17. func (s *Server) registerRSSRoutes(g *echo.Group) {
  18. g.GET("/explore/rss.xml", func(c echo.Context) error {
  19. ctx := c.Request().Context()
  20. systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
  21. if err != nil {
  22. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
  23. }
  24. normalStatus := store.Normal
  25. memoFind := store.FindMemoMessage{
  26. RowStatus: &normalStatus,
  27. VisibilityList: []store.Visibility{store.Public},
  28. }
  29. memoList, err := s.Store.ListMemos(ctx, &memoFind)
  30. if err != nil {
  31. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  32. }
  33. baseURL := c.Scheme() + "://" + c.Request().Host
  34. rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
  35. if err != nil {
  36. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
  37. }
  38. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
  39. return c.String(http.StatusOK, rss)
  40. })
  41. g.GET("/u/:id/rss.xml", func(c echo.Context) error {
  42. ctx := c.Request().Context()
  43. id, err := strconv.Atoi(c.Param("id"))
  44. if err != nil {
  45. return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
  46. }
  47. systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
  48. if err != nil {
  49. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
  50. }
  51. normalStatus := store.Normal
  52. memoFind := store.FindMemoMessage{
  53. CreatorID: &id,
  54. RowStatus: &normalStatus,
  55. VisibilityList: []store.Visibility{store.Public},
  56. }
  57. memoList, err := s.Store.ListMemos(ctx, &memoFind)
  58. if err != nil {
  59. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  60. }
  61. baseURL := c.Scheme() + "://" + c.Request().Host
  62. rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, systemCustomizedProfile)
  63. if err != nil {
  64. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
  65. }
  66. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
  67. return c.String(http.StatusOK, rss)
  68. })
  69. }
  70. const MaxRSSItemCount = 100
  71. const MaxRSSItemTitleLength = 100
  72. func (s *Server) generateRSSFromMemoList(ctx context.Context, memoList []*store.MemoMessage, baseURL string, profile *api.CustomizedProfile) (string, error) {
  73. feed := &feeds.Feed{
  74. Title: profile.Name,
  75. Link: &feeds.Link{Href: baseURL},
  76. Description: profile.Description,
  77. Created: time.Now(),
  78. }
  79. var itemCountLimit = common.Min(len(memoList), MaxRSSItemCount)
  80. feed.Items = make([]*feeds.Item, itemCountLimit)
  81. for i := 0; i < itemCountLimit; i++ {
  82. memo := memoList[i]
  83. feed.Items[i] = &feeds.Item{
  84. Title: getRSSItemTitle(memo.Content),
  85. Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
  86. Description: getRSSItemDescription(memo.Content),
  87. Created: time.Unix(memo.CreatedTs, 0),
  88. Enclosure: &feeds.Enclosure{Url: baseURL + "/m/" + strconv.Itoa(memo.ID) + "/image"},
  89. }
  90. if len(memo.ResourceIDList) > 0 {
  91. resourceID := memo.ResourceIDList[0]
  92. resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
  93. ID: &resourceID,
  94. })
  95. if err != nil {
  96. return "", err
  97. }
  98. enclosure := feeds.Enclosure{}
  99. if resource.ExternalLink != "" {
  100. enclosure.Url = resource.ExternalLink
  101. } else {
  102. enclosure.Url = baseURL + "/o/r/" + strconv.Itoa(resource.ID) + "/" + resource.PublicID + "/" + resource.Filename
  103. }
  104. enclosure.Length = strconv.Itoa(int(resource.Size))
  105. enclosure.Type = resource.Type
  106. feed.Items[i].Enclosure = &enclosure
  107. }
  108. }
  109. rss, err := feed.ToRss()
  110. if err != nil {
  111. return "", err
  112. }
  113. return rss, nil
  114. }
  115. func (s *Server) getSystemCustomizedProfile(ctx context.Context) (*api.CustomizedProfile, error) {
  116. systemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
  117. Name: api.SystemSettingCustomizedProfileName,
  118. })
  119. if err != nil && common.ErrorCode(err) != common.NotFound {
  120. return nil, err
  121. }
  122. customizedProfile := &api.CustomizedProfile{
  123. Name: "memos",
  124. LogoURL: "",
  125. Description: "",
  126. Locale: "en",
  127. Appearance: "system",
  128. ExternalURL: "",
  129. }
  130. if systemSetting != nil {
  131. if err := json.Unmarshal([]byte(systemSetting.Value), customizedProfile); err != nil {
  132. return nil, err
  133. }
  134. }
  135. return customizedProfile, nil
  136. }
  137. func getRSSItemTitle(content string) string {
  138. var title string
  139. if isTitleDefined(content) {
  140. title = strings.Split(content, "\n")[0][2:]
  141. } else {
  142. title = strings.Split(content, "\n")[0]
  143. var titleLengthLimit = common.Min(len(title), MaxRSSItemTitleLength)
  144. if titleLengthLimit < len(title) {
  145. title = title[:titleLengthLimit] + "..."
  146. }
  147. }
  148. return title
  149. }
  150. func getRSSItemDescription(content string) string {
  151. var description string
  152. if isTitleDefined(content) {
  153. var firstLineEnd = strings.Index(content, "\n")
  154. description = strings.Trim(content[firstLineEnd+1:], " ")
  155. } else {
  156. description = content
  157. }
  158. // TODO: use our `./plugin/gomark` parser to handle markdown-like content.
  159. var buf bytes.Buffer
  160. if err := goldmark.Convert([]byte(description), &buf); err != nil {
  161. panic(err)
  162. }
  163. return buf.String()
  164. }
  165. func isTitleDefined(content string) bool {
  166. return strings.HasPrefix(content, "# ")
  167. }