memos/server/router/api/v1/memo_service.go

1265 lines
40 KiB
Go
Raw Normal View History

2024-04-28 00:44:29 +08:00
package v1
import (
"archive/zip"
"bytes"
"context"
2023-12-21 22:42:06 +08:00
"fmt"
2024-02-29 23:54:43 +08:00
"log/slog"
2024-05-08 20:03:01 +08:00
"slices"
2023-12-19 23:49:24 +08:00
"time"
"github.com/google/cel-go/cel"
2024-01-20 23:48:35 +08:00
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
2024-06-06 23:09:13 +08:00
"github.com/usememos/gomark/ast"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
"github.com/usememos/gomark/restore"
2023-12-17 09:53:22 +08:00
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
2024-04-27 22:02:15 +08:00
"google.golang.org/protobuf/types/known/emptypb"
2023-12-19 23:49:24 +08:00
"google.golang.org/protobuf/types/known/timestamppb"
2023-09-17 22:55:13 +08:00
2024-01-20 23:48:35 +08:00
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/webhook"
2024-04-28 00:44:29 +08:00
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
2023-09-17 22:55:13 +08:00
"github.com/usememos/memos/store"
)
2024-01-08 21:17:21 +08:00
const (
DefaultPageSize = 10
2024-01-08 21:17:21 +08:00
)
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2023-10-01 14:44:10 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
create := &store.Memo{
2024-03-20 20:39:16 +08:00
UID: shortuuid.New(),
CreatorID: user.ID,
Content: request.Content,
Visibility: convertVisibilityToStore(request.Visibility),
2023-10-01 14:44:10 +08:00
}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisallowPublicVisible && create.Visibility == store.Public {
return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
}
contentLengthLimit, err := s.getContentLengthLimit(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get content length limit")
}
if len(create.Content) > contentLengthLimit {
return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
}
2024-05-13 22:04:37 +08:00
property, err := getMemoPropertyFromContent(create.Content)
2024-05-08 20:03:01 +08:00
if err != nil {
2024-05-13 22:04:37 +08:00
return nil, status.Errorf(codes.Internal, "failed to get memo property: %v", err)
}
create.Payload = &storepb.MemoPayload{
Property: property,
2024-05-08 20:03:01 +08:00
}
2023-10-01 14:44:10 +08:00
memo, err := s.Store.CreateMemo(ctx, create)
if err != nil {
return nil, err
}
2023-12-19 23:49:24 +08:00
memoMessage, err := s.convertMemoFromStore(ctx, memo)
2023-12-17 09:53:22 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
// Try to dispatch webhook when memo is created.
if err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil {
2024-02-29 23:54:43 +08:00
slog.Warn("Failed to dispatch memo created webhook", err)
}
2024-04-27 22:02:15 +08:00
return memoMessage, nil
2023-10-01 14:44:10 +08:00
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosRequest) (*v1pb.ListMemosResponse, error) {
2024-02-04 20:54:17 +08:00
memoFind := &store.FindMemo{
// Exclude comments by default.
ExcludeComments: true,
}
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
2024-05-17 08:50:02 +08:00
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
}
2024-01-27 11:14:17 +08:00
var limit, offset int
if request.PageToken != "" {
2024-04-28 00:44:29 +08:00
var pageToken v1pb.PageToken
2024-01-27 11:14:17 +08:00
if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
}
limit = int(pageToken.Limit)
offset = int(pageToken.Offset)
} else {
limit = int(request.PageSize)
}
if limit <= 0 {
limit = DefaultPageSize
}
2024-01-27 11:14:17 +08:00
limitPlusOne := limit + 1
memoFind.Limit = &limitPlusOne
2024-02-05 06:40:55 +08:00
memoFind.Offset = &offset
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
2024-04-07 22:35:02 +08:00
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
2024-04-28 00:44:29 +08:00
memoMessages := []*v1pb.Memo{}
2024-01-27 11:14:17 +08:00
nextPageToken := ""
if len(memos) == limitPlusOne {
memos = memos[:limit]
nextPageToken, err = getPageToken(limit, offset+limit)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err)
}
}
for _, memo := range memos {
2023-12-19 23:49:24 +08:00
memoMessage, err := s.convertMemoFromStore(ctx, memo)
2023-12-17 09:53:22 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
2024-01-27 11:14:17 +08:00
memoMessages = append(memoMessages, memoMessage)
}
2024-04-28 00:44:29 +08:00
response := &v1pb.ListMemosResponse{
2024-01-27 11:14:17 +08:00
Memos: memoMessages,
NextPageToken: nextPageToken,
}
return response, nil
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) SearchMemos(ctx context.Context, request *v1pb.SearchMemosRequest) (*v1pb.SearchMemosResponse, error) {
2024-03-30 13:50:18 +08:00
defaultSearchLimit := 10
2024-03-18 23:23:53 +08:00
memoFind := &store.FindMemo{
// Exclude comments by default.
ExcludeComments: true,
2024-03-30 13:50:18 +08:00
Limit: &defaultSearchLimit,
2023-08-05 19:51:32 +08:00
}
2024-03-30 13:50:18 +08:00
err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter)
if err != nil {
2024-05-17 08:50:02 +08:00
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
2023-08-05 19:51:32 +08:00
}
2024-03-18 23:23:53 +08:00
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
2024-05-17 08:50:02 +08:00
return nil, status.Errorf(codes.Internal, "failed to search memos: %v", err)
2024-03-18 23:23:53 +08:00
}
2024-04-28 00:44:29 +08:00
memoMessages := []*v1pb.Memo{}
2024-03-18 23:23:53 +08:00
for _, memo := range memos {
memoMessage, err := s.convertMemoFromStore(ctx, memo)
2023-09-14 20:16:17 +08:00
if err != nil {
2024-03-18 23:23:53 +08:00
return nil, errors.Wrap(err, "failed to convert memo")
2023-08-05 19:51:32 +08:00
}
2024-03-18 23:23:53 +08:00
memoMessages = append(memoMessages, memoMessage)
2023-08-05 19:51:32 +08:00
}
2024-04-28 00:44:29 +08:00
response := &v1pb.SearchMemosResponse{
2024-03-18 23:23:53 +08:00
Memos: memoMessages,
2023-08-05 19:51:32 +08:00
}
return response, nil
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest) (*v1pb.Memo, error) {
2024-03-18 23:23:53 +08:00
id, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
2024-01-20 23:48:35 +08:00
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
2024-03-18 23:23:53 +08:00
ID: &id,
2024-01-20 23:48:35 +08:00
})
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
2024-01-21 10:57:53 +08:00
if memo.Visibility != store.Public {
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2024-01-21 10:57:53 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if memo.Visibility == store.Private && memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
2024-01-20 23:48:35 +08:00
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
2024-04-27 22:02:15 +08:00
return memoMessage, nil
2024-01-20 23:48:35 +08:00
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoRequest) (*v1pb.Memo, error) {
2024-03-18 23:23:53 +08:00
id, err := ExtractMemoIDFromName(request.Memo.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
2023-12-20 23:14:15 +08:00
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
2024-03-18 23:23:53 +08:00
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &id})
2023-12-20 23:14:15 +08:00
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2024-05-08 20:03:01 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
2023-12-20 23:14:15 +08:00
if memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
2023-12-22 09:01:30 +08:00
currentTs := time.Now().Unix()
2023-12-20 23:14:15 +08:00
update := &store.UpdateMemo{
2024-03-18 23:23:53 +08:00
ID: id,
2023-12-22 09:01:30 +08:00
UpdatedTs: &currentTs,
2023-12-20 23:14:15 +08:00
}
for _, path := range request.UpdateMask.Paths {
if path == "content" {
2024-05-08 20:03:01 +08:00
contentLengthLimit, err := s.getContentLengthLimit(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get content length limit")
}
if len(request.Memo.Content) > contentLengthLimit {
return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
}
2023-12-20 23:14:15 +08:00
update.Content = &request.Memo.Content
2024-05-13 22:04:37 +08:00
property, err := getMemoPropertyFromContent(*update.Content)
2024-05-08 20:03:01 +08:00
if err != nil {
2024-05-13 22:04:37 +08:00
return nil, status.Errorf(codes.Internal, "failed to get memo property: %v", err)
2024-05-08 20:03:01 +08:00
}
2024-05-13 22:04:37 +08:00
payload := memo.Payload
payload.Property = property
update.Payload = payload
2024-03-20 20:39:16 +08:00
} else if path == "uid" {
update.UID = &request.Memo.Name
if !util.UIDMatcher.MatchString(*update.UID) {
2024-01-20 23:48:35 +08:00
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name")
}
2023-12-20 23:14:15 +08:00
} else if path == "visibility" {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
visibility := convertVisibilityToStore(request.Memo.Visibility)
if workspaceMemoRelatedSetting.DisallowPublicVisible && visibility == store.Public {
return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
}
2023-12-20 23:14:15 +08:00
update.Visibility = &visibility
} else if path == "row_status" {
rowStatus := convertRowStatusToStore(request.Memo.RowStatus)
update.RowStatus = &rowStatus
2023-12-21 23:40:43 +08:00
} else if path == "created_ts" {
createdTs := request.Memo.CreateTime.AsTime().Unix()
update.CreatedTs = &createdTs
2024-05-18 07:52:35 +08:00
} else if path == "display_ts" {
displayTs := request.Memo.DisplayTime.AsTime().Unix()
memoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
if memoRelatedSetting.DisplayWithUpdateTime {
update.UpdatedTs = &displayTs
} else {
update.CreatedTs = &displayTs
}
2023-12-22 00:31:29 +08:00
} else if path == "pinned" {
if _, err := s.Store.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{
2024-03-18 23:23:53 +08:00
MemoID: id,
2023-12-22 00:31:29 +08:00
UserID: user.ID,
Pinned: request.Memo.Pinned,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert memo organizer")
}
2023-12-20 23:14:15 +08:00
}
}
if err = s.Store.UpdateMemo(ctx, update); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo")
}
memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
2024-03-18 23:23:53 +08:00
ID: &id,
2023-12-20 23:14:15 +08:00
})
if err != nil {
return nil, errors.Wrap(err, "failed to get memo")
}
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
// Try to dispatch webhook when memo is updated.
if err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil {
2024-02-29 23:54:43 +08:00
slog.Warn("Failed to dispatch memo updated webhook", err)
}
2024-04-27 22:02:15 +08:00
return memoMessage, nil
2023-12-20 23:14:15 +08:00
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoRequest) (*emptypb.Empty, error) {
2024-03-18 23:23:53 +08:00
id, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
2023-12-20 23:14:15 +08:00
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
2024-03-18 23:23:53 +08:00
ID: &id,
2023-12-20 23:14:15 +08:00
})
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
2023-12-20 23:14:15 +08:00
if memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
// Try to dispatch webhook when memo is deleted.
if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
2024-02-29 23:54:43 +08:00
slog.Warn("Failed to dispatch memo deleted webhook", err)
}
}
2024-03-18 23:23:53 +08:00
if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: id}); err != nil {
2023-12-20 23:14:15 +08:00
return nil, status.Errorf(codes.Internal, "failed to delete memo")
}
// Delete memo relation
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{MemoID: &id}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo relations")
}
2024-05-02 21:44:17 +08:00
// Delete related resources.
resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &id})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources")
}
for _, resource := range resources {
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ID: resource.ID}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource")
}
}
// Delete memo comments
commentType := store.MemoRelationComment
relations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{RelatedMemoID: &id, Type: &commentType})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo comments")
}
for _, relation := range relations {
if _, err := s.DeleteMemo(ctx, &v1pb.DeleteMemoRequest{Name: fmt.Sprintf("%s%d", MemoNamePrefix, relation.MemoID)}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo comment")
}
}
// Delete memo references
referenceType := store.MemoRelationReference
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{RelatedMemoID: &id, Type: &referenceType}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo references")
}
2024-04-27 22:02:15 +08:00
return &emptypb.Empty{}, nil
2023-12-20 23:14:15 +08:00
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.CreateMemoCommentRequest) (*v1pb.Memo, error) {
2024-03-18 23:23:53 +08:00
id, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &id})
2023-12-21 21:24:08 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
2023-12-21 21:24:08 +08:00
}
2023-10-01 14:44:10 +08:00
// Create the comment memo first.
2024-04-27 22:02:15 +08:00
memo, err := s.CreateMemo(ctx, request.Comment)
2023-10-01 14:44:10 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo")
}
// Build the relation between the comment memo and the original memo.
2024-03-18 23:41:57 +08:00
memoID, err := ExtractMemoIDFromName(memo.Name)
2024-03-18 23:23:53 +08:00
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
2023-10-01 14:44:10 +08:00
_, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
2024-03-18 23:41:57 +08:00
MemoID: memoID,
2024-03-18 23:23:53 +08:00
RelatedMemoID: relatedMemo.ID,
2023-10-01 14:44:10 +08:00
Type: store.MemoRelationComment,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo relation")
}
2024-03-18 12:56:52 +08:00
creatorID, err := ExtractUserIDFromName(memo.Creator)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo creator")
}
2024-04-28 00:44:29 +08:00
if memo.Visibility != v1pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID {
activity, err := s.Store.CreateActivity(ctx, &store.Activity{
2024-03-18 12:56:52 +08:00
CreatorID: creatorID,
Type: store.ActivityTypeMemoComment,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{
MemoComment: &storepb.ActivityMemoCommentPayload{
2024-03-18 23:41:57 +08:00
MemoId: memoID,
2024-03-18 23:23:53 +08:00
RelatedMemoId: relatedMemo.ID,
},
},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create activity")
}
if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
2024-03-18 12:56:52 +08:00
SenderID: creatorID,
ReceiverID: relatedMemo.CreatorID,
Status: store.UNREAD,
Message: &storepb.InboxMessage{
2024-05-13 20:03:04 +08:00
Type: storepb.InboxMessage_MEMO_COMMENT,
ActivityId: &activity.ID,
},
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create inbox")
}
}
2023-10-01 14:44:10 +08:00
2024-04-27 22:02:15 +08:00
return memo, nil
2023-10-01 14:44:10 +08:00
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListMemoCommentsRequest) (*v1pb.ListMemoCommentsResponse, error) {
2024-03-18 23:23:53 +08:00
id, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
2023-10-01 14:44:10 +08:00
memoRelationComment := store.MemoRelationComment
memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
2024-03-18 23:23:53 +08:00
RelatedMemoID: &id,
2023-10-01 14:44:10 +08:00
Type: &memoRelationComment,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo relations")
}
2024-04-28 00:44:29 +08:00
var memos []*v1pb.Memo
2023-10-01 14:44:10 +08:00
for _, memoRelation := range memoRelations {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoRelation.MemoID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
if memo != nil {
2023-12-19 23:49:24 +08:00
memoMessage, err := s.convertMemoFromStore(ctx, memo)
2023-12-17 09:53:22 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
memos = append(memos, memoMessage)
2023-10-01 14:44:10 +08:00
}
}
2024-04-28 00:44:29 +08:00
response := &v1pb.ListMemoCommentsResponse{
2023-10-01 14:44:10 +08:00
Memos: memos,
}
return response, nil
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) GetUserMemosStats(ctx context.Context, request *v1pb.GetUserMemosStatsRequest) (*v1pb.GetUserMemosStatsResponse, error) {
2024-03-18 12:56:52 +08:00
userID, err := ExtractUserIDFromName(request.Name)
if err != nil {
2024-03-18 12:56:52 +08:00
return nil, errors.Wrap(err, "invalid user name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
2024-03-18 12:56:52 +08:00
ID: &userID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
normalRowStatus := store.Normal
2024-01-17 09:17:33 +08:00
memoFind := &store.FindMemo{
CreatorID: &user.ID,
RowStatus: &normalRowStatus,
ExcludeComments: true,
ExcludeContent: true,
2024-01-17 09:17:33 +08:00
}
2024-02-04 20:54:17 +08:00
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter")
2024-01-18 08:06:59 +08:00
}
2024-01-17 09:17:33 +08:00
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
2024-04-07 22:35:02 +08:00
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
location, err := time.LoadLocation(request.Timezone)
if err != nil {
return nil, status.Errorf(codes.Internal, "invalid timezone location")
}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
2024-02-04 20:54:17 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
2024-02-04 20:54:17 +08:00
}
2024-01-17 09:17:33 +08:00
stats := make(map[string]int32)
for _, memo := range memos {
2024-01-17 09:17:33 +08:00
displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
2024-01-17 09:17:33 +08:00
displayTs = memo.UpdatedTs
}
stats[time.Unix(displayTs, 0).In(location).Format("2006-01-02")]++
}
2024-04-28 00:44:29 +08:00
response := &v1pb.GetUserMemosStatsResponse{
2024-01-17 09:17:33 +08:00
Stats: stats,
}
return response, nil
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) ExportMemos(ctx context.Context, request *v1pb.ExportMemosRequest) (*v1pb.ExportMemosResponse, error) {
2024-02-04 20:54:17 +08:00
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
RowStatus: &normalRowStatus,
// Exclude comments by default.
ExcludeComments: true,
}
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
2024-05-17 08:50:02 +08:00
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
2024-04-07 22:35:02 +08:00
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
buf := new(bytes.Buffer)
writer := zip.NewWriter(buf)
for _, memo := range memos {
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
2024-02-04 20:20:14 +08:00
return nil, errors.Wrap(err, "failed to convert memo")
}
file, err := writer.Create(time.Unix(memo.CreatedTs, 0).Format(time.RFC3339) + "-" + string(memo.Visibility) + ".md")
if err != nil {
2024-02-04 20:20:14 +08:00
return nil, status.Errorf(codes.Internal, "Failed to create memo file")
}
_, err = file.Write([]byte(memoMessage.Content))
if err != nil {
2024-02-04 20:20:14 +08:00
return nil, status.Errorf(codes.Internal, "Failed to write to memo file")
}
}
2024-02-04 20:54:17 +08:00
if err := writer.Close(); err != nil {
2024-02-04 20:20:14 +08:00
return nil, status.Errorf(codes.Internal, "Failed to close zip file writer")
}
2024-04-28 00:44:29 +08:00
return &v1pb.ExportMemosResponse{
2024-02-04 20:20:14 +08:00
Content: buf.Bytes(),
}, nil
}
2024-05-27 20:04:07 +08:00
func (s *APIV1Service) ListMemoProperties(ctx context.Context, request *v1pb.ListMemoPropertiesRequest) (*v1pb.ListMemoPropertiesResponse, error) {
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &user.ID,
RowStatus: &normalRowStatus,
ExcludeComments: true,
// Default exclude content for performance.
ExcludeContent: true,
}
if request.Name != "memos/-" {
memoID, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memoFind.ID = &memoID
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
properties := []*v1pb.MemoProperty{}
for _, memo := range memos {
if memo.Payload.Property != nil {
properties = append(properties, convertMemoPropertyFromStore(memo.Payload.Property))
}
}
return &v1pb.ListMemoPropertiesResponse{
Properties: properties,
}, nil
}
2024-05-13 22:04:37 +08:00
func (s *APIV1Service) RebuildMemoProperty(ctx context.Context, request *v1pb.RebuildMemoPropertyRequest) (*emptypb.Empty, error) {
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2024-05-13 22:04:37 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &user.ID,
RowStatus: &normalRowStatus,
ExcludeComments: true,
}
if (request.Name) != "memos/-" {
memoID, err := ExtractMemoIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memoFind.ID = &memoID
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
for _, memo := range memos {
property, err := getMemoPropertyFromContent(memo.Content)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo property: %v", err)
}
memo.Payload.Property = property
if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Payload: memo.Payload,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo")
}
}
return &emptypb.Empty{}, nil
}
2024-05-08 20:03:01 +08:00
func (s *APIV1Service) ListMemoTags(ctx context.Context, request *v1pb.ListMemoTagsRequest) (*v1pb.ListMemoTagsResponse, error) {
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
RowStatus: &normalRowStatus,
ExcludeComments: true,
// Default exclude content for performance.
ExcludeContent: true,
}
if (request.Parent) != "memos/-" {
memoID, err := ExtractMemoIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memoFind.ID = &memoID
}
2024-05-14 08:12:25 +08:00
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
2024-05-17 08:50:02 +08:00
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
2024-05-14 08:12:25 +08:00
}
2024-05-08 20:03:01 +08:00
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
tagAmounts := map[string]int32{}
for _, memo := range memos {
2024-05-13 22:04:37 +08:00
if memo.Payload.Property != nil {
for _, tag := range memo.Payload.Property.Tags {
tagAmounts[tag]++
}
2024-05-08 20:03:01 +08:00
}
}
return &v1pb.ListMemoTagsResponse{
TagAmounts: tagAmounts,
}, nil
}
func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMemoTagRequest) (*emptypb.Empty, error) {
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2024-05-08 20:03:01 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
memoFind := &store.FindMemo{
CreatorID: &user.ID,
2024-05-13 22:04:37 +08:00
PayloadFind: &store.FindMemoPayload{Tag: &request.OldTag},
2024-05-08 20:03:01 +08:00
ExcludeComments: true,
}
if (request.Parent) != "memos/-" {
memoID, err := ExtractMemoIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memoFind.ID = &memoID
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
}
TraverseASTNodes(nodes, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldTag {
tag.Content = request.NewTag
}
})
content := restore.Restore(nodes)
2024-05-13 22:04:37 +08:00
property, err := getMemoPropertyFromContent(content)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo property: %v", err)
}
payload := memo.Payload
payload.Property = property
2024-05-08 20:03:01 +08:00
if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Content: &content,
2024-05-13 22:04:37 +08:00
Payload: payload,
2024-05-08 20:03:01 +08:00
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err)
}
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMemoTagRequest) (*emptypb.Empty, error) {
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
2024-05-08 20:03:01 +08:00
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
memoFind := &store.FindMemo{
CreatorID: &user.ID,
2024-05-13 22:04:37 +08:00
PayloadFind: &store.FindMemoPayload{Tag: &request.Tag},
2024-05-08 20:03:01 +08:00
ExcludeContent: true,
ExcludeComments: true,
}
if (request.Parent) != "memos/-" {
memoID, err := ExtractMemoIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memoFind.ID = &memoID
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
for _, memo := range memos {
if request.DeleteRelatedMemos {
err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo")
}
} else {
archived := store.Archived
err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
RowStatus: &archived,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo")
}
}
}
return &emptypb.Empty{}, nil
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*v1pb.Memo, error) {
2023-12-19 23:49:24 +08:00
displayTs := memo.CreatedTs
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
2023-12-19 23:49:24 +08:00
displayTs = memo.UpdatedTs
}
2023-12-21 22:42:06 +08:00
creator, err := s.Store.GetUser(ctx, &store.FindUser{ID: &memo.CreatorID})
if err != nil {
return nil, errors.Wrap(err, "failed to get creator")
}
2024-03-18 23:23:53 +08:00
name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
2024-04-28 00:44:29 +08:00
listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name})
2023-12-22 20:18:31 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to list memo relations")
}
2024-04-28 00:44:29 +08:00
listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name})
2023-12-22 20:18:31 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to list memo resources")
}
2024-04-28 00:44:29 +08:00
listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name})
2024-02-08 11:54:59 +08:00
if err != nil {
return nil, errors.Wrap(err, "failed to list memo reactions")
}
2024-04-29 08:00:37 +08:00
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse content")
}
memoMessage := &v1pb.Memo{
2024-03-18 23:23:53 +08:00
Name: name,
2024-03-20 20:39:16 +08:00
Uid: memo.UID,
2023-12-19 23:49:24 +08:00
RowStatus: convertRowStatusFromStore(memo.RowStatus),
2024-03-18 12:56:52 +08:00
Creator: fmt.Sprintf("%s%d", UserNamePrefix, creator.ID),
2023-12-19 23:49:24 +08:00
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
Content: memo.Content,
2024-04-29 08:00:37 +08:00
Nodes: convertFromASTNodes(nodes),
2023-12-19 23:49:24 +08:00
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
2023-12-22 20:18:31 +08:00
Relations: listMemoRelationsResponse.Relations,
Resources: listMemoResourcesResponse.Resources,
2024-02-08 11:54:59 +08:00
Reactions: listMemoReactionsResponse.Reactions,
}
if memo.Payload != nil {
memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
}
if memo.ParentID != nil {
parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID)
memoMessage.Parent = &parent
}
return memoMessage, nil
}
func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty {
if property == nil {
return nil
}
return &v1pb.MemoProperty{
Tags: property.Tags,
HasLink: property.HasLink,
HasTaskList: property.HasTaskList,
HasCode: property.HasCode,
HasIncompleteTasks: property.HasIncompleteTasks,
}
2023-12-19 23:49:24 +08:00
}
2024-04-28 00:44:29 +08:00
func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {
2023-12-19 23:49:24 +08:00
switch visibility {
case store.Private:
2024-04-28 00:44:29 +08:00
return v1pb.Visibility_PRIVATE
2023-12-19 23:49:24 +08:00
case store.Protected:
2024-04-28 00:44:29 +08:00
return v1pb.Visibility_PROTECTED
2023-12-19 23:49:24 +08:00
case store.Public:
2024-04-28 00:44:29 +08:00
return v1pb.Visibility_PUBLIC
2023-12-19 23:49:24 +08:00
default:
2024-04-28 00:44:29 +08:00
return v1pb.Visibility_VISIBILITY_UNSPECIFIED
2023-12-19 23:49:24 +08:00
}
}
2024-04-28 00:44:29 +08:00
func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {
2023-12-20 23:14:15 +08:00
switch visibility {
2024-04-28 00:44:29 +08:00
case v1pb.Visibility_PRIVATE:
2023-12-20 23:14:15 +08:00
return store.Private
2024-04-28 00:44:29 +08:00
case v1pb.Visibility_PROTECTED:
2023-12-20 23:14:15 +08:00
return store.Protected
2024-04-28 00:44:29 +08:00
case v1pb.Visibility_PUBLIC:
2023-12-20 23:14:15 +08:00
return store.Public
default:
return store.Private
}
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error {
2024-02-05 06:40:55 +08:00
if find == nil {
find = &store.FindMemo{}
}
2024-05-27 23:25:25 +08:00
if find.PayloadFind == nil {
find.PayloadFind = &store.FindMemoPayload{}
}
2024-02-05 06:40:55 +08:00
if filter != "" {
2024-03-18 23:23:53 +08:00
filter, err := parseSearchMemosFilter(filter)
2024-02-05 06:40:55 +08:00
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if len(filter.ContentSearch) > 0 {
find.ContentSearch = filter.ContentSearch
}
if len(filter.Visibilities) > 0 {
find.VisibilityList = filter.Visibilities
}
2024-05-08 20:03:01 +08:00
if filter.Tag != nil {
2024-05-13 22:04:37 +08:00
if find.PayloadFind == nil {
find.PayloadFind = &store.FindMemoPayload{}
}
find.PayloadFind.Tag = filter.Tag
2024-05-08 20:03:01 +08:00
}
2024-02-05 06:40:55 +08:00
if filter.OrderByPinned {
find.OrderByPinned = filter.OrderByPinned
}
if filter.DisplayTimeAfter != nil {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
2024-02-05 06:40:55 +08:00
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
2024-02-05 06:40:55 +08:00
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
2024-02-05 06:40:55 +08:00
find.UpdatedTsAfter = filter.DisplayTimeAfter
} else {
find.CreatedTsAfter = filter.DisplayTimeAfter
}
}
if filter.DisplayTimeBefore != nil {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
2024-02-05 06:40:55 +08:00
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
2024-02-05 06:40:55 +08:00
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
2024-02-05 06:40:55 +08:00
find.UpdatedTsBefore = filter.DisplayTimeBefore
} else {
find.CreatedTsBefore = filter.DisplayTimeBefore
}
}
if filter.Creator != nil {
2024-03-18 12:56:52 +08:00
userID, err := ExtractUserIDFromName(*filter.Creator)
2024-02-05 06:40:55 +08:00
if err != nil {
2024-03-18 12:56:52 +08:00
return errors.Wrap(err, "invalid user name")
2024-02-05 06:40:55 +08:00
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
2024-03-18 12:56:52 +08:00
ID: &userID,
2024-02-05 06:40:55 +08:00
})
if err != nil {
return status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return status.Errorf(codes.NotFound, "user not found")
}
find.CreatorID = &user.ID
}
2024-03-20 20:39:16 +08:00
if filter.UID != nil {
find.UID = filter.UID
2024-03-18 23:23:53 +08:00
}
2024-02-05 06:40:55 +08:00
if filter.RowStatus != nil {
find.RowStatus = filter.RowStatus
}
2024-03-30 13:50:18 +08:00
if filter.Random {
find.Random = filter.Random
}
if filter.Limit != nil {
find.Limit = filter.Limit
}
2024-04-18 21:04:10 +08:00
if filter.IncludeComments {
find.ExcludeComments = false
}
2024-05-27 23:25:25 +08:00
if filter.HasLink {
find.PayloadFind.HasLink = true
}
if filter.HasTaskList {
find.PayloadFind.HasTaskList = true
}
if filter.HasCode {
find.PayloadFind.HasCode = true
}
if filter.HasIncompleteTasks {
find.PayloadFind.HasIncompleteTasks = true
}
2024-02-05 06:40:55 +08:00
}
2024-05-26 11:02:23 +08:00
user, err := s.GetCurrentUser(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get current user")
}
2024-02-05 06:40:55 +08:00
// If the user is not authenticated, only public memos are visible.
if user == nil {
if filter == "" {
// If no filter is provided, return an error.
2024-05-25 11:05:35 +08:00
return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user")
2024-02-05 06:40:55 +08:00
}
find.VisibilityList = []store.Visibility{store.Public}
} else if find.CreatorID != nil && *find.CreatorID != user.ID {
find.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
2024-02-05 06:40:55 +08:00
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
2024-02-05 06:40:55 +08:00
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
2024-02-05 06:40:55 +08:00
find.OrderByUpdatedTs = true
}
return nil
}
func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return 0, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil
}
2024-03-18 23:23:53 +08:00
// SearchMemosFilterCELAttributes are the CEL attributes.
var SearchMemosFilterCELAttributes = []cel.EnvOption{
2023-12-22 09:09:03 +08:00
cel.Variable("content_search", cel.ListType(cel.StringType)),
2023-12-21 23:40:43 +08:00
cel.Variable("visibilities", cel.ListType(cel.StringType)),
2024-05-08 20:03:01 +08:00
cel.Variable("tag", cel.StringType),
2023-12-22 09:09:03 +08:00
cel.Variable("order_by_pinned", cel.BoolType),
2024-01-18 14:30:20 +08:00
cel.Variable("display_time_before", cel.IntType),
cel.Variable("display_time_after", cel.IntType),
2023-12-19 23:49:24 +08:00
cel.Variable("creator", cel.StringType),
2024-03-20 20:39:16 +08:00
cel.Variable("uid", cel.StringType),
2023-12-19 23:49:24 +08:00
cel.Variable("row_status", cel.StringType),
2024-03-30 13:50:18 +08:00
cel.Variable("random", cel.BoolType),
cel.Variable("limit", cel.IntType),
2024-04-18 21:04:10 +08:00
cel.Variable("include_comments", cel.BoolType),
2024-05-27 23:25:25 +08:00
cel.Variable("has_link", cel.BoolType),
cel.Variable("has_task_list", cel.BoolType),
cel.Variable("has_code", cel.BoolType),
cel.Variable("has_incomplete_tasks", cel.BoolType),
2023-09-13 20:42:44 +08:00
}
2024-03-18 23:23:53 +08:00
type SearchMemosFilter struct {
ContentSearch []string
Visibilities []store.Visibility
Tag *string
OrderByPinned bool
DisplayTimeBefore *int64
DisplayTimeAfter *int64
Creator *string
UID *string
RowStatus *store.RowStatus
Random bool
Limit *int
IncludeComments bool
HasLink bool
HasTaskList bool
HasCode bool
HasIncompleteTasks bool
2023-09-13 20:42:44 +08:00
}
2024-03-18 23:23:53 +08:00
func parseSearchMemosFilter(expression string) (*SearchMemosFilter, error) {
e, err := cel.NewEnv(SearchMemosFilterCELAttributes...)
if err != nil {
2023-09-13 20:42:44 +08:00
return nil, err
}
2023-09-13 20:42:44 +08:00
ast, issues := e.Compile(expression)
if issues != nil {
2023-09-13 20:42:44 +08:00
return nil, errors.Errorf("found issue %v", issues)
}
2024-03-18 23:23:53 +08:00
filter := &SearchMemosFilter{}
expr, err := cel.AstToParsedExpr(ast)
if err != nil {
return nil, err
}
callExpr := expr.GetExpr().GetCallExpr()
2024-03-18 23:23:53 +08:00
findSearchMemosField(callExpr, filter)
2023-09-13 20:42:44 +08:00
return filter, nil
}
2024-03-18 23:23:53 +08:00
func findSearchMemosField(callExpr *expr.Expr_Call, filter *SearchMemosFilter) {
2023-09-13 20:42:44 +08:00
if len(callExpr.Args) == 2 {
idExpr := callExpr.Args[0].GetIdentExpr()
if idExpr != nil {
2023-12-22 00:31:29 +08:00
if idExpr.Name == "content_search" {
contentSearch := []string{}
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
value := expr.GetConstExpr().GetStringValue()
contentSearch = append(contentSearch, value)
}
filter.ContentSearch = contentSearch
} else if idExpr.Name == "visibilities" {
2023-12-21 23:40:43 +08:00
visibilities := []store.Visibility{}
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
value := expr.GetConstExpr().GetStringValue()
visibilities = append(visibilities, store.Visibility(value))
}
filter.Visibilities = visibilities
2024-05-08 20:03:01 +08:00
} else if idExpr.Name == "tag" {
tag := callExpr.Args[1].GetConstExpr().GetStringValue()
filter.Tag = &tag
2023-12-22 09:09:03 +08:00
} else if idExpr.Name == "order_by_pinned" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.OrderByPinned = value
2024-01-18 14:30:20 +08:00
} else if idExpr.Name == "display_time_before" {
displayTimeBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
filter.DisplayTimeBefore = &displayTimeBefore
} else if idExpr.Name == "display_time_after" {
displayTimeAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
filter.DisplayTimeAfter = &displayTimeAfter
2023-12-22 00:31:29 +08:00
} else if idExpr.Name == "creator" {
2023-12-19 23:49:24 +08:00
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
filter.Creator = &creator
2024-03-20 20:39:16 +08:00
} else if idExpr.Name == "uid" {
uid := callExpr.Args[1].GetConstExpr().GetStringValue()
filter.UID = &uid
2023-12-22 00:31:29 +08:00
} else if idExpr.Name == "row_status" {
2023-12-19 23:49:24 +08:00
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
filter.RowStatus = &rowStatus
2024-03-30 13:50:18 +08:00
} else if idExpr.Name == "random" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.Random = value
} else if idExpr.Name == "limit" {
limit := int(callExpr.Args[1].GetConstExpr().GetInt64Value())
filter.Limit = &limit
2024-04-18 21:04:10 +08:00
} else if idExpr.Name == "include_comments" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.IncludeComments = value
2024-05-27 23:25:25 +08:00
} else if idExpr.Name == "has_link" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.HasLink = value
} else if idExpr.Name == "has_task_list" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.HasTaskList = value
} else if idExpr.Name == "has_code" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.HasCode = value
} else if idExpr.Name == "has_incomplete_tasks" {
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
filter.HasIncompleteTasks = value
2023-12-19 23:49:24 +08:00
}
2023-09-13 20:42:44 +08:00
return
}
}
2023-09-13 20:42:44 +08:00
for _, arg := range callExpr.Args {
callExpr := arg.GetCallExpr()
if callExpr != nil {
2024-03-18 23:23:53 +08:00
findSearchMemosField(callExpr, filter)
2023-09-13 20:42:44 +08:00
}
}
}
2024-05-13 22:04:37 +08:00
func getMemoPropertyFromContent(content string) (*storepb.MemoPayload_Property, error) {
nodes, err := parser.Parse(tokenizer.Tokenize(content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse content")
}
property := &storepb.MemoPayload_Property{}
TraverseASTNodes(nodes, func(node ast.Node) {
switch n := node.(type) {
case *ast.Tag:
tag := n.Content
if !slices.Contains(property.Tags, tag) {
property.Tags = append(property.Tags, tag)
}
case *ast.Link, *ast.AutoLink:
property.HasLink = true
case *ast.TaskList:
property.HasTaskList = true
if !n.Complete {
property.HasIncompleteTasks = true
}
2024-05-27 19:43:57 +08:00
case *ast.Code, *ast.CodeBlock:
property.HasCode = true
2024-05-13 22:04:37 +08:00
}
})
return property, nil
}
2024-05-08 20:03:01 +08:00
func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
TraverseASTNodes(n.Children, fn)
case *ast.Heading:
TraverseASTNodes(n.Children, fn)
case *ast.Blockquote:
TraverseASTNodes(n.Children, fn)
case *ast.OrderedList:
TraverseASTNodes(n.Children, fn)
case *ast.UnorderedList:
TraverseASTNodes(n.Children, fn)
case *ast.TaskList:
TraverseASTNodes(n.Children, fn)
case *ast.Bold:
TraverseASTNodes(n.Children, fn)
}
}
}
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
}
// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}
// DispatchMemoDeletedWebhook dispatches webhook when memo is deleted.
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *v1pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
}
2024-04-28 00:44:29 +08:00
func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1pb.Memo, activityType string) error {
2024-03-18 12:56:52 +08:00
creatorID, err := ExtractUserIDFromName(memo.Creator)
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid memo creator")
}
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
2024-03-18 12:56:52 +08:00
CreatorID: &creatorID,
})
if err != nil {
return err
}
for _, hook := range webhooks {
2024-03-18 12:56:52 +08:00
payload, err := convertMemoToWebhookPayload(memo)
if err != nil {
return errors.Wrap(err, "failed to convert memo to webhook payload")
}
payload.ActivityType = activityType
2024-06-05 20:53:20 +08:00
payload.Url = hook.URL
if err := webhook.Post(payload); err != nil {
return errors.Wrap(err, "failed to post webhook")
}
}
return nil
}
2024-06-05 20:53:20 +08:00
func convertMemoToWebhookPayload(memo *v1pb.Memo) (*v1pb.WebhookRequestPayload, error) {
2024-03-18 12:56:52 +08:00
creatorID, err := ExtractUserIDFromName(memo.Creator)
if err != nil {
return nil, errors.Wrap(err, "invalid memo creator")
}
2024-06-05 20:53:20 +08:00
return &v1pb.WebhookRequestPayload{
CreatorId: creatorID,
CreateTime: timestamppb.New(time.Now()),
Memo: memo,
2024-03-18 12:56:52 +08:00
}, nil
}