tag.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package server
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "regexp"
  8. "sort"
  9. "github.com/pkg/errors"
  10. "github.com/usememos/memos/api"
  11. "github.com/usememos/memos/common"
  12. "golang.org/x/exp/slices"
  13. "github.com/labstack/echo/v4"
  14. )
  15. func (s *Server) registerTagRoutes(g *echo.Group) {
  16. g.POST("/tag", func(c echo.Context) error {
  17. ctx := c.Request().Context()
  18. userID, ok := c.Get(getUserIDContextKey()).(int)
  19. if !ok {
  20. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  21. }
  22. tagUpsert := &api.TagUpsert{}
  23. if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
  24. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
  25. }
  26. if tagUpsert.Name == "" {
  27. return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
  28. }
  29. tagUpsert.CreatorID = userID
  30. tag, err := s.Store.UpsertTag(ctx, tagUpsert)
  31. if err != nil {
  32. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
  33. }
  34. if err := s.createTagCreateActivity(c, tag); err != nil {
  35. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  36. }
  37. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  38. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tag.Name)); err != nil {
  39. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tag response").SetInternal(err)
  40. }
  41. return nil
  42. })
  43. g.GET("/tag", func(c echo.Context) error {
  44. ctx := c.Request().Context()
  45. userID, ok := c.Get(getUserIDContextKey()).(int)
  46. if !ok {
  47. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
  48. }
  49. tagFind := &api.TagFind{
  50. CreatorID: userID,
  51. }
  52. tagList, err := s.Store.FindTagList(ctx, tagFind)
  53. if err != nil {
  54. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
  55. }
  56. tagNameList := []string{}
  57. for _, tag := range tagList {
  58. tagNameList = append(tagNameList, tag.Name)
  59. }
  60. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  61. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagNameList)); err != nil {
  62. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
  63. }
  64. return nil
  65. })
  66. g.GET("/tag/suggestion", func(c echo.Context) error {
  67. ctx := c.Request().Context()
  68. userID, ok := c.Get(getUserIDContextKey()).(int)
  69. if !ok {
  70. return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
  71. }
  72. contentSearch := "#"
  73. normalRowStatus := api.Normal
  74. memoFind := api.MemoFind{
  75. CreatorID: &userID,
  76. ContentSearch: &contentSearch,
  77. RowStatus: &normalRowStatus,
  78. }
  79. memoList, err := s.Store.FindMemoList(ctx, &memoFind)
  80. if err != nil {
  81. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  82. }
  83. tagFind := &api.TagFind{
  84. CreatorID: userID,
  85. }
  86. existTagList, err := s.Store.FindTagList(ctx, tagFind)
  87. if err != nil {
  88. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
  89. }
  90. tagNameList := []string{}
  91. for _, tag := range existTagList {
  92. tagNameList = append(tagNameList, tag.Name)
  93. }
  94. tagMapSet := make(map[string]bool)
  95. for _, memo := range memoList {
  96. for _, tag := range findTagListFromMemoContent(memo.Content) {
  97. if !slices.Contains(tagNameList, tag) {
  98. tagMapSet[tag] = true
  99. }
  100. }
  101. }
  102. tagList := []string{}
  103. for tag := range tagMapSet {
  104. tagList = append(tagList, tag)
  105. }
  106. sort.Strings(tagList)
  107. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  108. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagList)); err != nil {
  109. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
  110. }
  111. return nil
  112. })
  113. g.DELETE("/tag/:tagName", func(c echo.Context) error {
  114. ctx := c.Request().Context()
  115. userID, ok := c.Get(getUserIDContextKey()).(int)
  116. if !ok {
  117. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  118. }
  119. tagName, err := url.QueryUnescape(c.Param("tagName"))
  120. if err != nil {
  121. return echo.NewHTTPError(http.StatusBadRequest, "Invalid tag name").SetInternal(err)
  122. } else if tagName == "" {
  123. return echo.NewHTTPError(http.StatusBadRequest, "Tag name cannot be empty")
  124. }
  125. tagDelete := &api.TagDelete{
  126. Name: tagName,
  127. CreatorID: userID,
  128. }
  129. if err := s.Store.DeleteTag(ctx, tagDelete); err != nil {
  130. if common.ErrorCode(err) == common.NotFound {
  131. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Tag name not found: %s", tagName))
  132. }
  133. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagName)).SetInternal(err)
  134. }
  135. return c.JSON(http.StatusOK, true)
  136. })
  137. }
  138. var tagRegexp = regexp.MustCompile(`#([^\s#]+)`)
  139. func findTagListFromMemoContent(memoContent string) []string {
  140. tagMapSet := make(map[string]bool)
  141. matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
  142. for _, v := range matches {
  143. tagName := v[1]
  144. tagMapSet[tagName] = true
  145. }
  146. tagList := []string{}
  147. for tag := range tagMapSet {
  148. tagList = append(tagList, tag)
  149. }
  150. sort.Strings(tagList)
  151. return tagList
  152. }
  153. func (s *Server) createTagCreateActivity(c echo.Context, tag *api.Tag) error {
  154. ctx := c.Request().Context()
  155. payload := api.ActivityTagCreatePayload{
  156. TagName: tag.Name,
  157. }
  158. payloadStr, err := json.Marshal(payload)
  159. if err != nil {
  160. return errors.Wrap(err, "failed to marshal activity payload")
  161. }
  162. activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
  163. CreatorID: tag.CreatorID,
  164. Type: api.ActivityTagCreate,
  165. Level: api.ActivityInfo,
  166. Payload: string(payloadStr),
  167. })
  168. if err != nil || activity == nil {
  169. return errors.Wrap(err, "failed to create activity")
  170. }
  171. return err
  172. }