memo_service.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. package v1
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "time"
  7. "unicode/utf8"
  8. "github.com/lithammer/shortuuid/v4"
  9. "github.com/pkg/errors"
  10. "github.com/usememos/gomark/ast"
  11. "github.com/usememos/gomark/parser"
  12. "github.com/usememos/gomark/parser/tokenizer"
  13. "github.com/usememos/gomark/renderer"
  14. "github.com/usememos/gomark/restore"
  15. "google.golang.org/grpc/codes"
  16. "google.golang.org/grpc/status"
  17. "google.golang.org/protobuf/types/known/emptypb"
  18. "google.golang.org/protobuf/types/known/timestamppb"
  19. "github.com/usememos/memos/plugin/webhook"
  20. v1pb "github.com/usememos/memos/proto/gen/api/v1"
  21. storepb "github.com/usememos/memos/proto/gen/store"
  22. "github.com/usememos/memos/server/runner/memopayload"
  23. "github.com/usememos/memos/store"
  24. )
  25. func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
  26. user, err := s.GetCurrentUser(ctx)
  27. if err != nil {
  28. return nil, status.Errorf(codes.Internal, "failed to get user")
  29. }
  30. create := &store.Memo{
  31. UID: shortuuid.New(),
  32. CreatorID: user.ID,
  33. Content: request.Memo.Content,
  34. Visibility: convertVisibilityToStore(request.Memo.Visibility),
  35. }
  36. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  37. if err != nil {
  38. return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
  39. }
  40. if workspaceMemoRelatedSetting.DisallowPublicVisibility && create.Visibility == store.Public {
  41. return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
  42. }
  43. contentLengthLimit, err := s.getContentLengthLimit(ctx)
  44. if err != nil {
  45. return nil, status.Errorf(codes.Internal, "failed to get content length limit")
  46. }
  47. if len(create.Content) > contentLengthLimit {
  48. return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
  49. }
  50. if err := memopayload.RebuildMemoPayload(create); err != nil {
  51. return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
  52. }
  53. if request.Memo.Location != nil {
  54. create.Payload.Location = convertLocationToStore(request.Memo.Location)
  55. }
  56. memo, err := s.Store.CreateMemo(ctx, create)
  57. if err != nil {
  58. return nil, err
  59. }
  60. if len(request.Memo.Resources) > 0 {
  61. _, err := s.SetMemoResources(ctx, &v1pb.SetMemoResourcesRequest{
  62. Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
  63. Resources: request.Memo.Resources,
  64. })
  65. if err != nil {
  66. return nil, errors.Wrap(err, "failed to set memo resources")
  67. }
  68. }
  69. if len(request.Memo.Relations) > 0 {
  70. _, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{
  71. Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
  72. Relations: request.Memo.Relations,
  73. })
  74. if err != nil {
  75. return nil, errors.Wrap(err, "failed to set memo relations")
  76. }
  77. }
  78. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  79. if err != nil {
  80. return nil, errors.Wrap(err, "failed to convert memo")
  81. }
  82. // Try to dispatch webhook when memo is created.
  83. if err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil {
  84. slog.Warn("Failed to dispatch memo created webhook", slog.Any("err", err))
  85. }
  86. return memoMessage, nil
  87. }
  88. func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosRequest) (*v1pb.ListMemosResponse, error) {
  89. memoFind := &store.FindMemo{
  90. // Exclude comments by default.
  91. ExcludeComments: true,
  92. }
  93. if err := s.buildMemoFindWithFilter(ctx, memoFind, request.OldFilter); err != nil {
  94. return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
  95. }
  96. if request.Parent != "" && request.Parent != "users/-" {
  97. userID, err := ExtractUserIDFromName(request.Parent)
  98. if err != nil {
  99. return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
  100. }
  101. memoFind.CreatorID = &userID
  102. memoFind.OrderByPinned = true
  103. }
  104. if request.State == v1pb.State_ARCHIVED {
  105. state := store.Archived
  106. memoFind.RowStatus = &state
  107. } else {
  108. state := store.Normal
  109. memoFind.RowStatus = &state
  110. }
  111. if request.Direction == v1pb.Direction_ASC {
  112. memoFind.OrderByTimeAsc = true
  113. }
  114. if request.Filter != "" {
  115. memoFind.Filter = &request.Filter
  116. }
  117. currentUser, err := s.GetCurrentUser(ctx)
  118. if err != nil {
  119. return nil, status.Errorf(codes.Internal, "failed to get user")
  120. }
  121. if currentUser == nil {
  122. memoFind.VisibilityList = []store.Visibility{store.Public}
  123. } else if memoFind.CreatorID == nil || *memoFind.CreatorID != currentUser.ID {
  124. memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
  125. }
  126. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  127. if err != nil {
  128. return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
  129. }
  130. if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
  131. memoFind.OrderByUpdatedTs = true
  132. }
  133. var limit, offset int
  134. if request.PageToken != "" {
  135. var pageToken v1pb.PageToken
  136. if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {
  137. return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
  138. }
  139. limit = int(pageToken.Limit)
  140. offset = int(pageToken.Offset)
  141. } else {
  142. limit = int(request.PageSize)
  143. }
  144. if limit <= 0 {
  145. limit = DefaultPageSize
  146. }
  147. limitPlusOne := limit + 1
  148. memoFind.Limit = &limitPlusOne
  149. memoFind.Offset = &offset
  150. memos, err := s.Store.ListMemos(ctx, memoFind)
  151. if err != nil {
  152. return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
  153. }
  154. memoMessages := []*v1pb.Memo{}
  155. nextPageToken := ""
  156. if len(memos) == limitPlusOne {
  157. memos = memos[:limit]
  158. nextPageToken, err = getPageToken(limit, offset+limit)
  159. if err != nil {
  160. return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err)
  161. }
  162. }
  163. for _, memo := range memos {
  164. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  165. if err != nil {
  166. return nil, errors.Wrap(err, "failed to convert memo")
  167. }
  168. memoMessages = append(memoMessages, memoMessage)
  169. }
  170. response := &v1pb.ListMemosResponse{
  171. Memos: memoMessages,
  172. NextPageToken: nextPageToken,
  173. }
  174. return response, nil
  175. }
  176. func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest) (*v1pb.Memo, error) {
  177. memoUID, err := ExtractMemoUIDFromName(request.Name)
  178. if err != nil {
  179. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  180. }
  181. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  182. UID: &memoUID,
  183. })
  184. if err != nil {
  185. return nil, err
  186. }
  187. if memo == nil {
  188. return nil, status.Errorf(codes.NotFound, "memo not found")
  189. }
  190. if memo.Visibility != store.Public {
  191. user, err := s.GetCurrentUser(ctx)
  192. if err != nil {
  193. return nil, status.Errorf(codes.Internal, "failed to get user")
  194. }
  195. if user == nil {
  196. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  197. }
  198. if memo.Visibility == store.Private && memo.CreatorID != user.ID {
  199. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  200. }
  201. }
  202. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  203. if err != nil {
  204. return nil, errors.Wrap(err, "failed to convert memo")
  205. }
  206. return memoMessage, nil
  207. }
  208. func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoRequest) (*v1pb.Memo, error) {
  209. memoUID, err := ExtractMemoUIDFromName(request.Memo.Name)
  210. if err != nil {
  211. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  212. }
  213. if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
  214. return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
  215. }
  216. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
  217. if err != nil {
  218. return nil, err
  219. }
  220. if memo == nil {
  221. return nil, status.Errorf(codes.NotFound, "memo not found")
  222. }
  223. user, err := s.GetCurrentUser(ctx)
  224. if err != nil {
  225. return nil, status.Errorf(codes.Internal, "failed to get current user")
  226. }
  227. // Only the creator or admin can update the memo.
  228. if memo.CreatorID != user.ID && !isSuperUser(user) {
  229. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  230. }
  231. update := &store.UpdateMemo{
  232. ID: memo.ID,
  233. }
  234. for _, path := range request.UpdateMask.Paths {
  235. if path == "content" {
  236. contentLengthLimit, err := s.getContentLengthLimit(ctx)
  237. if err != nil {
  238. return nil, status.Errorf(codes.Internal, "failed to get content length limit")
  239. }
  240. if len(request.Memo.Content) > contentLengthLimit {
  241. return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
  242. }
  243. memo.Content = request.Memo.Content
  244. if err := memopayload.RebuildMemoPayload(memo); err != nil {
  245. return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
  246. }
  247. update.Content = &memo.Content
  248. update.Payload = memo.Payload
  249. } else if path == "visibility" {
  250. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  251. if err != nil {
  252. return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
  253. }
  254. visibility := convertVisibilityToStore(request.Memo.Visibility)
  255. if workspaceMemoRelatedSetting.DisallowPublicVisibility && visibility == store.Public {
  256. return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
  257. }
  258. update.Visibility = &visibility
  259. } else if path == "pinned" {
  260. update.Pinned = &request.Memo.Pinned
  261. } else if path == "state" {
  262. rowStatus := convertStateToStore(request.Memo.State)
  263. update.RowStatus = &rowStatus
  264. } else if path == "create_time" {
  265. createdTs := request.Memo.CreateTime.AsTime().Unix()
  266. update.CreatedTs = &createdTs
  267. } else if path == "update_time" {
  268. updatedTs := time.Now().Unix()
  269. if request.Memo.UpdateTime != nil {
  270. updatedTs = request.Memo.UpdateTime.AsTime().Unix()
  271. }
  272. update.UpdatedTs = &updatedTs
  273. } else if path == "display_time" {
  274. displayTs := request.Memo.DisplayTime.AsTime().Unix()
  275. memoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  276. if err != nil {
  277. return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
  278. }
  279. if memoRelatedSetting.DisplayWithUpdateTime {
  280. update.UpdatedTs = &displayTs
  281. } else {
  282. update.CreatedTs = &displayTs
  283. }
  284. } else if path == "location" {
  285. payload := memo.Payload
  286. payload.Location = convertLocationToStore(request.Memo.Location)
  287. update.Payload = payload
  288. } else if path == "resources" {
  289. _, err := s.SetMemoResources(ctx, &v1pb.SetMemoResourcesRequest{
  290. Name: request.Memo.Name,
  291. Resources: request.Memo.Resources,
  292. })
  293. if err != nil {
  294. return nil, errors.Wrap(err, "failed to set memo resources")
  295. }
  296. } else if path == "relations" {
  297. _, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{
  298. Name: request.Memo.Name,
  299. Relations: request.Memo.Relations,
  300. })
  301. if err != nil {
  302. return nil, errors.Wrap(err, "failed to set memo relations")
  303. }
  304. }
  305. }
  306. if err = s.Store.UpdateMemo(ctx, update); err != nil {
  307. return nil, status.Errorf(codes.Internal, "failed to update memo")
  308. }
  309. memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
  310. ID: &memo.ID,
  311. })
  312. if err != nil {
  313. return nil, errors.Wrap(err, "failed to get memo")
  314. }
  315. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  316. if err != nil {
  317. return nil, errors.Wrap(err, "failed to convert memo")
  318. }
  319. // Try to dispatch webhook when memo is updated.
  320. if err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil {
  321. slog.Warn("Failed to dispatch memo updated webhook", slog.Any("err", err))
  322. }
  323. return memoMessage, nil
  324. }
  325. func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoRequest) (*emptypb.Empty, error) {
  326. memoUID, err := ExtractMemoUIDFromName(request.Name)
  327. if err != nil {
  328. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  329. }
  330. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  331. UID: &memoUID,
  332. })
  333. if err != nil {
  334. return nil, err
  335. }
  336. if memo == nil {
  337. return nil, status.Errorf(codes.NotFound, "memo not found")
  338. }
  339. user, err := s.GetCurrentUser(ctx)
  340. if err != nil {
  341. return nil, status.Errorf(codes.Internal, "failed to get current user")
  342. }
  343. // Only the creator or admin can update the memo.
  344. if memo.CreatorID != user.ID && !isSuperUser(user) {
  345. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  346. }
  347. if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
  348. // Try to dispatch webhook when memo is deleted.
  349. if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
  350. slog.Warn("Failed to dispatch memo deleted webhook", slog.Any("err", err))
  351. }
  352. }
  353. if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil {
  354. return nil, status.Errorf(codes.Internal, "failed to delete memo")
  355. }
  356. // Delete memo relation
  357. if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{MemoID: &memo.ID}); err != nil {
  358. return nil, status.Errorf(codes.Internal, "failed to delete memo relations")
  359. }
  360. // Delete related resources.
  361. resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &memo.ID})
  362. if err != nil {
  363. return nil, status.Errorf(codes.Internal, "failed to list resources")
  364. }
  365. for _, resource := range resources {
  366. if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ID: resource.ID}); err != nil {
  367. return nil, status.Errorf(codes.Internal, "failed to delete resource")
  368. }
  369. }
  370. // Delete memo comments
  371. commentType := store.MemoRelationComment
  372. relations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{RelatedMemoID: &memo.ID, Type: &commentType})
  373. if err != nil {
  374. return nil, status.Errorf(codes.Internal, "failed to list memo comments")
  375. }
  376. for _, relation := range relations {
  377. if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: relation.MemoID}); err != nil {
  378. return nil, status.Errorf(codes.Internal, "failed to delete memo comment")
  379. }
  380. }
  381. // Delete memo references
  382. referenceType := store.MemoRelationReference
  383. if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{RelatedMemoID: &memo.ID, Type: &referenceType}); err != nil {
  384. return nil, status.Errorf(codes.Internal, "failed to delete memo references")
  385. }
  386. return &emptypb.Empty{}, nil
  387. }
  388. func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.CreateMemoCommentRequest) (*v1pb.Memo, error) {
  389. memoUID, err := ExtractMemoUIDFromName(request.Name)
  390. if err != nil {
  391. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  392. }
  393. relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
  394. if err != nil {
  395. return nil, status.Errorf(codes.Internal, "failed to get memo")
  396. }
  397. // Create the memo comment first.
  398. memoComment, err := s.CreateMemo(ctx, &v1pb.CreateMemoRequest{Memo: request.Comment})
  399. if err != nil {
  400. return nil, status.Errorf(codes.Internal, "failed to create memo")
  401. }
  402. memoUID, err = ExtractMemoUIDFromName(memoComment.Name)
  403. if err != nil {
  404. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  405. }
  406. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
  407. if err != nil {
  408. return nil, status.Errorf(codes.Internal, "failed to get memo")
  409. }
  410. // Build the relation between the comment memo and the original memo.
  411. _, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
  412. MemoID: memo.ID,
  413. RelatedMemoID: relatedMemo.ID,
  414. Type: store.MemoRelationComment,
  415. })
  416. if err != nil {
  417. return nil, status.Errorf(codes.Internal, "failed to create memo relation")
  418. }
  419. creatorID, err := ExtractUserIDFromName(memoComment.Creator)
  420. if err != nil {
  421. return nil, status.Errorf(codes.InvalidArgument, "invalid memo creator")
  422. }
  423. if memoComment.Visibility != v1pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID {
  424. activity, err := s.Store.CreateActivity(ctx, &store.Activity{
  425. CreatorID: creatorID,
  426. Type: store.ActivityTypeMemoComment,
  427. Level: store.ActivityLevelInfo,
  428. Payload: &storepb.ActivityPayload{
  429. MemoComment: &storepb.ActivityMemoCommentPayload{
  430. MemoId: memo.ID,
  431. RelatedMemoId: relatedMemo.ID,
  432. },
  433. },
  434. })
  435. if err != nil {
  436. return nil, status.Errorf(codes.Internal, "failed to create activity")
  437. }
  438. if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
  439. SenderID: creatorID,
  440. ReceiverID: relatedMemo.CreatorID,
  441. Status: store.UNREAD,
  442. Message: &storepb.InboxMessage{
  443. Type: storepb.InboxMessage_MEMO_COMMENT,
  444. ActivityId: &activity.ID,
  445. },
  446. }); err != nil {
  447. return nil, status.Errorf(codes.Internal, "failed to create inbox")
  448. }
  449. }
  450. return memoComment, nil
  451. }
  452. func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListMemoCommentsRequest) (*v1pb.ListMemoCommentsResponse, error) {
  453. memoUID, err := ExtractMemoUIDFromName(request.Name)
  454. if err != nil {
  455. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  456. }
  457. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
  458. if err != nil {
  459. return nil, status.Errorf(codes.Internal, "failed to get memo")
  460. }
  461. memoRelationComment := store.MemoRelationComment
  462. memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
  463. RelatedMemoID: &memo.ID,
  464. Type: &memoRelationComment,
  465. })
  466. if err != nil {
  467. return nil, status.Errorf(codes.Internal, "failed to list memo relations")
  468. }
  469. var memos []*v1pb.Memo
  470. for _, memoRelation := range memoRelations {
  471. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  472. ID: &memoRelation.MemoID,
  473. })
  474. if err != nil {
  475. return nil, status.Errorf(codes.Internal, "failed to get memo")
  476. }
  477. if memo != nil {
  478. memoMessage, err := s.convertMemoFromStore(ctx, memo)
  479. if err != nil {
  480. return nil, errors.Wrap(err, "failed to convert memo")
  481. }
  482. memos = append(memos, memoMessage)
  483. }
  484. }
  485. response := &v1pb.ListMemoCommentsResponse{
  486. Memos: memos,
  487. }
  488. return response, nil
  489. }
  490. func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMemoTagRequest) (*emptypb.Empty, error) {
  491. user, err := s.GetCurrentUser(ctx)
  492. if err != nil {
  493. return nil, status.Errorf(codes.Internal, "failed to get current user")
  494. }
  495. memoFind := &store.FindMemo{
  496. CreatorID: &user.ID,
  497. PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.OldTag}},
  498. ExcludeComments: true,
  499. }
  500. if (request.Parent) != "memos/-" {
  501. memoUID, err := ExtractMemoUIDFromName(request.Parent)
  502. if err != nil {
  503. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  504. }
  505. memoFind.UID = &memoUID
  506. }
  507. memos, err := s.Store.ListMemos(ctx, memoFind)
  508. if err != nil {
  509. return nil, status.Errorf(codes.Internal, "failed to list memos")
  510. }
  511. for _, memo := range memos {
  512. nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
  513. if err != nil {
  514. return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
  515. }
  516. memopayload.TraverseASTNodes(nodes, func(node ast.Node) {
  517. if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldTag {
  518. tag.Content = request.NewTag
  519. }
  520. })
  521. memo.Content = restore.Restore(nodes)
  522. if err := memopayload.RebuildMemoPayload(memo); err != nil {
  523. return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
  524. }
  525. if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
  526. ID: memo.ID,
  527. Content: &memo.Content,
  528. Payload: memo.Payload,
  529. }); err != nil {
  530. return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err)
  531. }
  532. }
  533. return &emptypb.Empty{}, nil
  534. }
  535. func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMemoTagRequest) (*emptypb.Empty, error) {
  536. user, err := s.GetCurrentUser(ctx)
  537. if err != nil {
  538. return nil, status.Errorf(codes.Internal, "failed to get current user")
  539. }
  540. memoFind := &store.FindMemo{
  541. CreatorID: &user.ID,
  542. PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.Tag}},
  543. ExcludeContent: true,
  544. ExcludeComments: true,
  545. }
  546. if request.Parent != "memos/-" {
  547. memoUID, err := ExtractMemoUIDFromName(request.Parent)
  548. if err != nil {
  549. return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
  550. }
  551. memoFind.UID = &memoUID
  552. }
  553. memos, err := s.Store.ListMemos(ctx, memoFind)
  554. if err != nil {
  555. return nil, status.Errorf(codes.Internal, "failed to list memos")
  556. }
  557. for _, memo := range memos {
  558. if request.DeleteRelatedMemos {
  559. err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID})
  560. if err != nil {
  561. return nil, status.Errorf(codes.Internal, "failed to delete memo")
  562. }
  563. } else {
  564. archived := store.Archived
  565. err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
  566. ID: memo.ID,
  567. RowStatus: &archived,
  568. })
  569. if err != nil {
  570. return nil, status.Errorf(codes.Internal, "failed to update memo")
  571. }
  572. }
  573. }
  574. return &emptypb.Empty{}, nil
  575. }
  576. func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
  577. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  578. if err != nil {
  579. return 0, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
  580. }
  581. return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil
  582. }
  583. // DispatchMemoCreatedWebhook dispatches webhook when memo is created.
  584. func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
  585. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
  586. }
  587. // DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
  588. func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
  589. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
  590. }
  591. // DispatchMemoDeletedWebhook dispatches webhook when memo is deleted.
  592. func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *v1pb.Memo) error {
  593. return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
  594. }
  595. func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1pb.Memo, activityType string) error {
  596. creatorID, err := ExtractUserIDFromName(memo.Creator)
  597. if err != nil {
  598. return status.Errorf(codes.InvalidArgument, "invalid memo creator")
  599. }
  600. webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
  601. CreatorID: &creatorID,
  602. })
  603. if err != nil {
  604. return err
  605. }
  606. for _, hook := range webhooks {
  607. payload, err := convertMemoToWebhookPayload(memo)
  608. if err != nil {
  609. return errors.Wrap(err, "failed to convert memo to webhook payload")
  610. }
  611. payload.ActivityType = activityType
  612. payload.Url = hook.URL
  613. if err := webhook.Post(payload); err != nil {
  614. return errors.Wrap(err, "failed to post webhook")
  615. }
  616. }
  617. return nil
  618. }
  619. func convertMemoToWebhookPayload(memo *v1pb.Memo) (*v1pb.WebhookRequestPayload, error) {
  620. creatorID, err := ExtractUserIDFromName(memo.Creator)
  621. if err != nil {
  622. return nil, errors.Wrap(err, "invalid memo creator")
  623. }
  624. return &v1pb.WebhookRequestPayload{
  625. Creator: fmt.Sprintf("%s%d", UserNamePrefix, creatorID),
  626. CreateTime: timestamppb.New(time.Now()),
  627. Memo: memo,
  628. }, nil
  629. }
  630. func getMemoContentSnippet(content string) (string, error) {
  631. nodes, err := parser.Parse(tokenizer.Tokenize(content))
  632. if err != nil {
  633. return "", errors.Wrap(err, "failed to parse content")
  634. }
  635. plainText := renderer.NewStringRenderer().Render(nodes)
  636. if len(plainText) > 64 {
  637. return substring(plainText, 64) + "...", nil
  638. }
  639. return plainText, nil
  640. }
  641. func substring(s string, length int) string {
  642. if length <= 0 {
  643. return ""
  644. }
  645. runeCount := 0
  646. byteIndex := 0
  647. for byteIndex < len(s) {
  648. _, size := utf8.DecodeRuneInString(s[byteIndex:])
  649. byteIndex += size
  650. runeCount++
  651. if runeCount == length {
  652. break
  653. }
  654. }
  655. return s[:byteIndex]
  656. }