tag_service.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package v2
  2. import (
  3. "context"
  4. "fmt"
  5. "regexp"
  6. "sort"
  7. "github.com/pkg/errors"
  8. "golang.org/x/exp/slices"
  9. "google.golang.org/grpc/codes"
  10. "google.golang.org/grpc/status"
  11. "github.com/usememos/memos/plugin/gomark/ast"
  12. "github.com/usememos/memos/plugin/gomark/parser"
  13. "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
  14. "github.com/usememos/memos/plugin/gomark/restore"
  15. apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
  16. "github.com/usememos/memos/store"
  17. )
  18. func (s *APIV2Service) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTagRequest) (*apiv2pb.UpsertTagResponse, error) {
  19. user, err := getCurrentUser(ctx, s.Store)
  20. if err != nil {
  21. return nil, status.Errorf(codes.Internal, "failed to get user")
  22. }
  23. tag, err := s.Store.UpsertTag(ctx, &store.Tag{
  24. Name: request.Name,
  25. CreatorID: user.ID,
  26. })
  27. if err != nil {
  28. return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
  29. }
  30. t, err := s.convertTagFromStore(ctx, tag)
  31. if err != nil {
  32. return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
  33. }
  34. return &apiv2pb.UpsertTagResponse{
  35. Tag: t,
  36. }, nil
  37. }
  38. func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
  39. username, err := ExtractUsernameFromName(request.User)
  40. if err != nil {
  41. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
  42. }
  43. user, err := s.Store.GetUser(ctx, &store.FindUser{
  44. Username: &username,
  45. })
  46. if err != nil {
  47. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  48. }
  49. if user == nil {
  50. return nil, status.Errorf(codes.NotFound, "user not found")
  51. }
  52. tags, err := s.Store.ListTags(ctx, &store.FindTag{
  53. CreatorID: user.ID,
  54. })
  55. if err != nil {
  56. return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
  57. }
  58. response := &apiv2pb.ListTagsResponse{}
  59. for _, tag := range tags {
  60. t, err := s.convertTagFromStore(ctx, tag)
  61. if err != nil {
  62. return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
  63. }
  64. response.Tags = append(response.Tags, t)
  65. }
  66. return response, nil
  67. }
  68. func (s *APIV2Service) RenameTag(ctx context.Context, request *apiv2pb.RenameTagRequest) (*apiv2pb.RenameTagResponse, error) {
  69. username, err := ExtractUsernameFromName(request.User)
  70. if err != nil {
  71. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
  72. }
  73. user, err := s.Store.GetUser(ctx, &store.FindUser{
  74. Username: &username,
  75. })
  76. if err != nil {
  77. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  78. }
  79. if user == nil {
  80. return nil, status.Errorf(codes.NotFound, "user not found")
  81. }
  82. // Find all related memos.
  83. memos, err := s.Store.ListMemos(ctx, &store.FindMemo{
  84. CreatorID: &user.ID,
  85. ContentSearch: []string{fmt.Sprintf("#%s", request.OldName)},
  86. })
  87. if err != nil {
  88. return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
  89. }
  90. // Replace tag name in memo content.
  91. for _, memo := range memos {
  92. nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
  93. if err != nil {
  94. return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
  95. }
  96. traverseASTNodes(nodes, func(node ast.Node) {
  97. if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldName {
  98. tag.Content = request.NewName
  99. }
  100. })
  101. content := restore.Restore(nodes)
  102. if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
  103. ID: memo.ID,
  104. Content: &content,
  105. }); err != nil {
  106. return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err)
  107. }
  108. }
  109. // Delete old tag and create new tag.
  110. if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
  111. CreatorID: user.ID,
  112. Name: request.OldName,
  113. }); err != nil {
  114. return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
  115. }
  116. tag, err := s.Store.UpsertTag(ctx, &store.Tag{
  117. CreatorID: user.ID,
  118. Name: request.NewName,
  119. })
  120. if err != nil {
  121. return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
  122. }
  123. tagMessage, err := s.convertTagFromStore(ctx, tag)
  124. if err != nil {
  125. return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
  126. }
  127. return &apiv2pb.RenameTagResponse{Tag: tagMessage}, nil
  128. }
  129. func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) {
  130. username, err := ExtractUsernameFromName(request.Tag.Creator)
  131. if err != nil {
  132. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
  133. }
  134. user, err := s.Store.GetUser(ctx, &store.FindUser{
  135. Username: &username,
  136. })
  137. if err != nil {
  138. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  139. }
  140. if user == nil {
  141. return nil, status.Errorf(codes.NotFound, "user not found")
  142. }
  143. if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
  144. Name: request.Tag.Name,
  145. CreatorID: user.ID,
  146. }); err != nil {
  147. return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
  148. }
  149. return &apiv2pb.DeleteTagResponse{}, nil
  150. }
  151. func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.GetTagSuggestionsRequest) (*apiv2pb.GetTagSuggestionsResponse, error) {
  152. username, err := ExtractUsernameFromName(request.User)
  153. if err != nil {
  154. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
  155. }
  156. user, err := s.Store.GetUser(ctx, &store.FindUser{
  157. Username: &username,
  158. })
  159. if err != nil {
  160. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  161. }
  162. if user == nil {
  163. return nil, status.Errorf(codes.NotFound, "user not found")
  164. }
  165. normalRowStatus := store.Normal
  166. memoFind := &store.FindMemo{
  167. CreatorID: &user.ID,
  168. ContentSearch: []string{"#"},
  169. RowStatus: &normalRowStatus,
  170. }
  171. memoList, err := s.Store.ListMemos(ctx, memoFind)
  172. if err != nil {
  173. return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
  174. }
  175. tagList, err := s.Store.ListTags(ctx, &store.FindTag{
  176. CreatorID: user.ID,
  177. })
  178. if err != nil {
  179. return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
  180. }
  181. tagNameList := []string{}
  182. for _, tag := range tagList {
  183. tagNameList = append(tagNameList, tag.Name)
  184. }
  185. tagMapSet := make(map[string]bool)
  186. for _, memo := range memoList {
  187. for _, tag := range findTagListFromMemoContent(memo.Content) {
  188. if !slices.Contains(tagNameList, tag) {
  189. tagMapSet[tag] = true
  190. }
  191. }
  192. }
  193. suggestions := []string{}
  194. for tag := range tagMapSet {
  195. suggestions = append(suggestions, tag)
  196. }
  197. sort.Strings(suggestions)
  198. return &apiv2pb.GetTagSuggestionsResponse{
  199. Tags: suggestions,
  200. }, nil
  201. }
  202. func (s *APIV2Service) convertTagFromStore(ctx context.Context, tag *store.Tag) (*apiv2pb.Tag, error) {
  203. user, err := s.Store.GetUser(ctx, &store.FindUser{
  204. ID: &tag.CreatorID,
  205. })
  206. if err != nil {
  207. return nil, errors.Wrap(err, "failed to get user")
  208. }
  209. return &apiv2pb.Tag{
  210. Name: tag.Name,
  211. Creator: fmt.Sprintf("%s%s", UserNamePrefix, user.Username),
  212. }, nil
  213. }
  214. var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
  215. func findTagListFromMemoContent(memoContent string) []string {
  216. tagMapSet := make(map[string]bool)
  217. matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
  218. for _, v := range matches {
  219. tagName := v[1]
  220. tagMapSet[tagName] = true
  221. }
  222. tagList := []string{}
  223. for tag := range tagMapSet {
  224. tagList = append(tagList, tag)
  225. }
  226. sort.Strings(tagList)
  227. return tagList
  228. }