tag.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package server
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "regexp"
  7. "sort"
  8. "github.com/pkg/errors"
  9. "github.com/usememos/memos/api"
  10. "github.com/usememos/memos/common"
  11. "github.com/usememos/memos/store"
  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. return c.JSON(http.StatusOK, composeResponse(tag.Name))
  38. })
  39. g.GET("/tag", func(c echo.Context) error {
  40. ctx := c.Request().Context()
  41. userID, ok := c.Get(getUserIDContextKey()).(int)
  42. if !ok {
  43. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
  44. }
  45. tagFind := &api.TagFind{
  46. CreatorID: userID,
  47. }
  48. tagList, err := s.Store.FindTagList(ctx, tagFind)
  49. if err != nil {
  50. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
  51. }
  52. tagNameList := []string{}
  53. for _, tag := range tagList {
  54. tagNameList = append(tagNameList, tag.Name)
  55. }
  56. return c.JSON(http.StatusOK, composeResponse(tagNameList))
  57. })
  58. g.GET("/tag/suggestion", func(c echo.Context) error {
  59. ctx := c.Request().Context()
  60. userID, ok := c.Get(getUserIDContextKey()).(int)
  61. if !ok {
  62. return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
  63. }
  64. normalRowStatus := store.Normal
  65. memoFind := &store.FindMemoMessage{
  66. CreatorID: &userID,
  67. ContentSearch: []string{"#"},
  68. RowStatus: &normalRowStatus,
  69. }
  70. memoMessageList, err := s.Store.ListMemos(ctx, memoFind)
  71. if err != nil {
  72. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  73. }
  74. tagFind := &api.TagFind{
  75. CreatorID: userID,
  76. }
  77. existTagList, err := s.Store.FindTagList(ctx, tagFind)
  78. if err != nil {
  79. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
  80. }
  81. tagNameList := []string{}
  82. for _, tag := range existTagList {
  83. tagNameList = append(tagNameList, tag.Name)
  84. }
  85. tagMapSet := make(map[string]bool)
  86. for _, memo := range memoMessageList {
  87. for _, tag := range findTagListFromMemoContent(memo.Content) {
  88. if !slices.Contains(tagNameList, tag) {
  89. tagMapSet[tag] = true
  90. }
  91. }
  92. }
  93. tagList := []string{}
  94. for tag := range tagMapSet {
  95. tagList = append(tagList, tag)
  96. }
  97. sort.Strings(tagList)
  98. return c.JSON(http.StatusOK, composeResponse(tagList))
  99. })
  100. g.POST("/tag/delete", func(c echo.Context) error {
  101. ctx := c.Request().Context()
  102. userID, ok := c.Get(getUserIDContextKey()).(int)
  103. if !ok {
  104. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  105. }
  106. tagDelete := &api.TagDelete{}
  107. if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
  108. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
  109. }
  110. if tagDelete.Name == "" {
  111. return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
  112. }
  113. tagDelete.CreatorID = userID
  114. if err := s.Store.DeleteTag(ctx, tagDelete); err != nil {
  115. if common.ErrorCode(err) == common.NotFound {
  116. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Tag name not found: %s", tagDelete.Name))
  117. }
  118. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
  119. }
  120. return c.JSON(http.StatusOK, true)
  121. })
  122. }
  123. var tagRegexp = regexp.MustCompile(`#([^\s#]+)`)
  124. func findTagListFromMemoContent(memoContent string) []string {
  125. tagMapSet := make(map[string]bool)
  126. matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
  127. for _, v := range matches {
  128. tagName := v[1]
  129. tagMapSet[tagName] = true
  130. }
  131. tagList := []string{}
  132. for tag := range tagMapSet {
  133. tagList = append(tagList, tag)
  134. }
  135. sort.Strings(tagList)
  136. return tagList
  137. }
  138. func (s *Server) createTagCreateActivity(c echo.Context, tag *api.Tag) error {
  139. ctx := c.Request().Context()
  140. payload := api.ActivityTagCreatePayload{
  141. TagName: tag.Name,
  142. }
  143. payloadBytes, err := json.Marshal(payload)
  144. if err != nil {
  145. return errors.Wrap(err, "failed to marshal activity payload")
  146. }
  147. activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
  148. CreatorID: tag.CreatorID,
  149. Type: api.ActivityTagCreate,
  150. Level: api.ActivityInfo,
  151. Payload: string(payloadBytes),
  152. })
  153. if err != nil || activity == nil {
  154. return errors.Wrap(err, "failed to create activity")
  155. }
  156. return err
  157. }