tag.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package v1
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "regexp"
  7. "sort"
  8. "github.com/labstack/echo/v4"
  9. "github.com/pkg/errors"
  10. "github.com/usememos/memos/store"
  11. "golang.org/x/exp/slices"
  12. )
  13. type Tag struct {
  14. Name string
  15. CreatorID int
  16. }
  17. type UpsertTagRequest struct {
  18. Name string `json:"name"`
  19. }
  20. type DeleteTagRequest struct {
  21. Name string `json:"name"`
  22. }
  23. func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
  24. g.POST("/tag", func(c echo.Context) error {
  25. ctx := c.Request().Context()
  26. userID, ok := c.Get(getUserIDContextKey()).(int)
  27. if !ok {
  28. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  29. }
  30. tagUpsert := &UpsertTagRequest{}
  31. if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
  32. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
  33. }
  34. if tagUpsert.Name == "" {
  35. return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
  36. }
  37. tag, err := s.Store.UpsertTag(ctx, &store.Tag{
  38. Name: tagUpsert.Name,
  39. CreatorID: userID,
  40. })
  41. if err != nil {
  42. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
  43. }
  44. tagMessage := convertTagFromStore(tag)
  45. if err := s.createTagCreateActivity(c, tagMessage); err != nil {
  46. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  47. }
  48. return c.JSON(http.StatusOK, tagMessage.Name)
  49. })
  50. g.GET("/tag", func(c echo.Context) error {
  51. ctx := c.Request().Context()
  52. userID, ok := c.Get(getUserIDContextKey()).(int)
  53. if !ok {
  54. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
  55. }
  56. list, err := s.Store.ListTags(ctx, &store.FindTag{
  57. CreatorID: userID,
  58. })
  59. if err != nil {
  60. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
  61. }
  62. tagNameList := []string{}
  63. for _, tag := range list {
  64. tagNameList = append(tagNameList, tag.Name)
  65. }
  66. return c.JSON(http.StatusOK, tagNameList)
  67. })
  68. g.GET("/tag/suggestion", func(c echo.Context) error {
  69. ctx := c.Request().Context()
  70. userID, ok := c.Get(getUserIDContextKey()).(int)
  71. if !ok {
  72. return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
  73. }
  74. normalRowStatus := store.Normal
  75. memoFind := &store.FindMemo{
  76. CreatorID: &userID,
  77. ContentSearch: []string{"#"},
  78. RowStatus: &normalRowStatus,
  79. }
  80. memoMessageList, err := s.Store.ListMemos(ctx, memoFind)
  81. if err != nil {
  82. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  83. }
  84. list, err := s.Store.ListTags(ctx, &store.FindTag{
  85. CreatorID: userID,
  86. })
  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 list {
  92. tagNameList = append(tagNameList, tag.Name)
  93. }
  94. tagMapSet := make(map[string]bool)
  95. for _, memo := range memoMessageList {
  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. return c.JSON(http.StatusOK, tagList)
  108. })
  109. g.POST("/tag/delete", func(c echo.Context) error {
  110. ctx := c.Request().Context()
  111. userID, ok := c.Get(getUserIDContextKey()).(int)
  112. if !ok {
  113. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  114. }
  115. tagDelete := &DeleteTagRequest{}
  116. if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
  117. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
  118. }
  119. if tagDelete.Name == "" {
  120. return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
  121. }
  122. err := s.Store.DeleteTag(ctx, &store.DeleteTag{
  123. Name: tagDelete.Name,
  124. CreatorID: userID,
  125. })
  126. if err != nil {
  127. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
  128. }
  129. return c.JSON(http.StatusOK, true)
  130. })
  131. }
  132. func (s *APIV1Service) createTagCreateActivity(c echo.Context, tag *Tag) error {
  133. ctx := c.Request().Context()
  134. payload := ActivityTagCreatePayload{
  135. TagName: tag.Name,
  136. }
  137. payloadBytes, err := json.Marshal(payload)
  138. if err != nil {
  139. return errors.Wrap(err, "failed to marshal activity payload")
  140. }
  141. activity, err := s.Store.CreateActivity(ctx, &store.Activity{
  142. CreatorID: tag.CreatorID,
  143. Type: ActivityTagCreate.String(),
  144. Level: ActivityInfo.String(),
  145. Payload: string(payloadBytes),
  146. })
  147. if err != nil || activity == nil {
  148. return errors.Wrap(err, "failed to create activity")
  149. }
  150. return err
  151. }
  152. func convertTagFromStore(tag *store.Tag) *Tag {
  153. return &Tag{
  154. Name: tag.Name,
  155. CreatorID: tag.CreatorID,
  156. }
  157. }
  158. var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
  159. func findTagListFromMemoContent(memoContent string) []string {
  160. tagMapSet := make(map[string]bool)
  161. matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
  162. for _, v := range matches {
  163. tagName := v[1]
  164. tagMapSet[tagName] = true
  165. }
  166. tagList := []string{}
  167. for tag := range tagMapSet {
  168. tagList = append(tagList, tag)
  169. }
  170. sort.Strings(tagList)
  171. return tagList
  172. }