rss.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. systemStatus := api.SystemStatus{
  98. CustomizedProfile: api.CustomizedProfile{
  99. Name: "memos",
  100. LogoURL: "",
  101. Description: "",
  102. Locale: "en",
  103. Appearance: "system",
  104. ExternalURL: "",
  105. },
  106. }
  107. systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
  108. if err != nil {
  109. return api.CustomizedProfile{}, err
  110. }
  111. for _, systemSetting := range systemSettingList {
  112. if systemSetting.Name == api.SystemSettingServerID || systemSetting.Name == api.SystemSettingSecretSessionName {
  113. continue
  114. }
  115. var value interface{}
  116. err := json.Unmarshal([]byte(systemSetting.Value), &value)
  117. if err != nil {
  118. return api.CustomizedProfile{}, err
  119. }
  120. if systemSetting.Name == api.SystemSettingCustomizedProfileName {
  121. valueMap := value.(map[string]interface{})
  122. systemStatus.CustomizedProfile = api.CustomizedProfile{}
  123. if v := valueMap["name"]; v != nil {
  124. systemStatus.CustomizedProfile.Name = v.(string)
  125. }
  126. if v := valueMap["logoUrl"]; v != nil {
  127. systemStatus.CustomizedProfile.LogoURL = v.(string)
  128. }
  129. if v := valueMap["description"]; v != nil {
  130. systemStatus.CustomizedProfile.Description = v.(string)
  131. }
  132. if v := valueMap["locale"]; v != nil {
  133. systemStatus.CustomizedProfile.Locale = v.(string)
  134. }
  135. if v := valueMap["appearance"]; v != nil {
  136. systemStatus.CustomizedProfile.Appearance = v.(string)
  137. }
  138. if v := valueMap["externalUrl"]; v != nil {
  139. systemStatus.CustomizedProfile.ExternalURL = v.(string)
  140. }
  141. }
  142. }
  143. return systemStatus.CustomizedProfile, nil
  144. }
  145. func min(a, b int) int {
  146. if a < b {
  147. return a
  148. }
  149. return b
  150. }
  151. func getRSSItemTitle(content string) string {
  152. var title string
  153. if isTitleDefined(content) {
  154. title = strings.Split(content, "\n")[0][2:]
  155. } else {
  156. title = strings.Split(content, "\n")[0]
  157. var titleLengthLimit = min(len(title), MaxRSSItemTitleLength)
  158. if titleLengthLimit < len(title) {
  159. title = title[:titleLengthLimit] + "..."
  160. }
  161. }
  162. return title
  163. }
  164. func getRSSItemDescription(content string) string {
  165. var description string
  166. if isTitleDefined(content) {
  167. var firstLineEnd = strings.Index(content, "\n")
  168. description = strings.Trim(content[firstLineEnd+1:], " ")
  169. } else {
  170. description = content
  171. }
  172. return description
  173. }
  174. func isTitleDefined(content string) bool {
  175. return strings.HasPrefix(content, "# ")
  176. }