rss.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package server
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/gorilla/feeds"
  10. "github.com/labstack/echo/v4"
  11. "github.com/usememos/memos/api"
  12. )
  13. func (s *Server) registerRSSRoutes(g *echo.Group) {
  14. g.GET("/explore/rss.xml", func(c echo.Context) error {
  15. ctx := c.Request().Context()
  16. systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s)
  17. if err != nil {
  18. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
  19. }
  20. normalStatus := api.Normal
  21. memoFind := api.MemoFind{
  22. RowStatus: &normalStatus,
  23. VisibilityList: []api.Visibility{
  24. api.Public,
  25. },
  26. }
  27. memoList, err := s.Store.FindMemoList(ctx, &memoFind)
  28. if err != nil {
  29. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  30. }
  31. baseURL := c.Scheme() + "://" + c.Request().Host
  32. rss, err := generateRSSFromMemoList(memoList, baseURL, systemCustomizedProfile)
  33. if err != nil {
  34. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
  35. }
  36. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
  37. return c.String(http.StatusOK, rss)
  38. })
  39. g.GET("/u/:id/rss.xml", func(c echo.Context) error {
  40. ctx := c.Request().Context()
  41. systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s)
  42. if err != nil {
  43. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
  44. }
  45. id, err := strconv.Atoi(c.Param("id"))
  46. if err != nil {
  47. return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
  48. }
  49. normalStatus := api.Normal
  50. memoFind := api.MemoFind{
  51. CreatorID: &id,
  52. RowStatus: &normalStatus,
  53. VisibilityList: []api.Visibility{
  54. api.Public,
  55. },
  56. }
  57. memoList, err := s.Store.FindMemoList(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 := generateRSSFromMemoList(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 generateRSSFromMemoList(memoList []*api.Memo, 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 = 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. }
  89. }
  90. rss, err := feed.ToRss()
  91. if err != nil {
  92. return "", err
  93. }
  94. return rss, nil
  95. }
  96. func getSystemCustomizedProfile(ctx context.Context, s *Server) (*api.CustomizedProfile, error) {
  97. customizedProfile := &api.CustomizedProfile{
  98. Name: "memos",
  99. LogoURL: "",
  100. Description: "",
  101. Locale: "en",
  102. Appearance: "system",
  103. ExternalURL: "",
  104. }
  105. systemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
  106. Name: api.SystemSettingCustomizedProfileName,
  107. })
  108. if err != nil {
  109. return customizedProfile, err
  110. }
  111. err = json.Unmarshal([]byte(systemSetting.Value), customizedProfile)
  112. if err != nil {
  113. return customizedProfile, err
  114. }
  115. return customizedProfile, nil
  116. }
  117. func min(a, b int) int {
  118. if a < b {
  119. return a
  120. }
  121. return b
  122. }
  123. func getRSSItemTitle(content string) string {
  124. var title string
  125. if isTitleDefined(content) {
  126. title = strings.Split(content, "\n")[0][2:]
  127. } else {
  128. title = strings.Split(content, "\n")[0]
  129. var titleLengthLimit = min(len(title), MaxRSSItemTitleLength)
  130. if titleLengthLimit < len(title) {
  131. title = title[:titleLengthLimit] + "..."
  132. }
  133. }
  134. return title
  135. }
  136. func getRSSItemDescription(content string) string {
  137. var description string
  138. if isTitleDefined(content) {
  139. var firstLineEnd = strings.Index(content, "\n")
  140. description = strings.Trim(content[firstLineEnd+1:], " ")
  141. } else {
  142. description = content
  143. }
  144. return description
  145. }
  146. func isTitleDefined(content string) bool {
  147. return strings.HasPrefix(content, "# ")
  148. }