mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
refactor: split memo service
This commit is contained in:
@ -10,6 +10,11 @@ import (
|
|||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultPageSize is the default page size for requests.
|
||||||
|
DefaultPageSize = 10
|
||||||
|
)
|
||||||
|
|
||||||
func convertRowStatusFromStore(rowStatus store.RowStatus) v1pb.RowStatus {
|
func convertRowStatusFromStore(rowStatus store.RowStatus) v1pb.RowStatus {
|
||||||
switch rowStatus {
|
switch rowStatus {
|
||||||
case store.Normal:
|
case store.Normal:
|
||||||
@ -57,3 +62,7 @@ func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSuperUser(user *store.User) bool {
|
||||||
|
return user.Role == store.RoleAdmin || user.Role == store.RoleHost
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/google/cel-go/cel"
|
|
||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/lithammer/shortuuid/v4"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/usememos/gomark/ast"
|
"github.com/usememos/gomark/ast"
|
||||||
@ -15,7 +14,6 @@ import (
|
|||||||
"github.com/usememos/gomark/parser/tokenizer"
|
"github.com/usememos/gomark/parser/tokenizer"
|
||||||
"github.com/usememos/gomark/renderer"
|
"github.com/usememos/gomark/renderer"
|
||||||
"github.com/usememos/gomark/restore"
|
"github.com/usememos/gomark/restore"
|
||||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@ -28,11 +26,6 @@ import (
|
|||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultPageSize is the default page size for listing memos.
|
|
||||||
DefaultPageSize = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
|
func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
|
||||||
user, err := s.GetCurrentUser(ctx)
|
user, err := s.GetCurrentUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -619,253 +612,6 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe
|
|||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, view v1pb.MemoView) (*v1pb.Memo, error) {
|
|
||||||
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 {
|
|
||||||
displayTs = memo.UpdatedTs
|
|
||||||
}
|
|
||||||
|
|
||||||
name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
|
|
||||||
memoMessage := &v1pb.Memo{
|
|
||||||
Name: name,
|
|
||||||
Uid: memo.UID,
|
|
||||||
RowStatus: convertRowStatusFromStore(memo.RowStatus),
|
|
||||||
Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID),
|
|
||||||
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,
|
|
||||||
Visibility: convertVisibilityFromStore(memo.Visibility),
|
|
||||||
Pinned: memo.Pinned,
|
|
||||||
Tags: memo.Payload.Tags,
|
|
||||||
}
|
|
||||||
if memo.Payload != nil {
|
|
||||||
memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
|
|
||||||
memoMessage.Location = convertLocationFromStore(memo.Payload.Location)
|
|
||||||
}
|
|
||||||
if memo.ParentID != nil {
|
|
||||||
parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID)
|
|
||||||
memoMessage.Parent = &parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill content when view is MEMO_VIEW_FULL.
|
|
||||||
if view == v1pb.MemoView_MEMO_VIEW_FULL {
|
|
||||||
listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to list memo relations")
|
|
||||||
}
|
|
||||||
memoMessage.Relations = listMemoRelationsResponse.Relations
|
|
||||||
|
|
||||||
listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to list memo resources")
|
|
||||||
}
|
|
||||||
memoMessage.Resources = listMemoResourcesResponse.Resources
|
|
||||||
|
|
||||||
listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to list memo reactions")
|
|
||||||
}
|
|
||||||
memoMessage.Reactions = listMemoReactionsResponse.Reactions
|
|
||||||
|
|
||||||
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to parse content")
|
|
||||||
}
|
|
||||||
memoMessage.Nodes = convertFromASTNodes(nodes)
|
|
||||||
|
|
||||||
snippet, err := getMemoContentSnippet(memo.Content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to get memo content snippet")
|
|
||||||
}
|
|
||||||
memoMessage.Snippet = snippet
|
|
||||||
}
|
|
||||||
|
|
||||||
return memoMessage, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty {
|
|
||||||
if property == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &v1pb.MemoProperty{
|
|
||||||
HasLink: property.HasLink,
|
|
||||||
HasTaskList: property.HasTaskList,
|
|
||||||
HasCode: property.HasCode,
|
|
||||||
HasIncompleteTasks: property.HasIncompleteTasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location {
|
|
||||||
if location == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &v1pb.Location{
|
|
||||||
Placeholder: location.Placeholder,
|
|
||||||
Latitude: location.Latitude,
|
|
||||||
Longitude: location.Longitude,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location {
|
|
||||||
if location == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &storepb.MemoPayload_Location{
|
|
||||||
Placeholder: location.Placeholder,
|
|
||||||
Latitude: location.Latitude,
|
|
||||||
Longitude: location.Longitude,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {
|
|
||||||
switch visibility {
|
|
||||||
case store.Private:
|
|
||||||
return v1pb.Visibility_PRIVATE
|
|
||||||
case store.Protected:
|
|
||||||
return v1pb.Visibility_PROTECTED
|
|
||||||
case store.Public:
|
|
||||||
return v1pb.Visibility_PUBLIC
|
|
||||||
default:
|
|
||||||
return v1pb.Visibility_VISIBILITY_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {
|
|
||||||
switch visibility {
|
|
||||||
case v1pb.Visibility_PRIVATE:
|
|
||||||
return store.Private
|
|
||||||
case v1pb.Visibility_PROTECTED:
|
|
||||||
return store.Protected
|
|
||||||
case v1pb.Visibility_PUBLIC:
|
|
||||||
return store.Public
|
|
||||||
default:
|
|
||||||
return store.Private
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error {
|
|
||||||
if find.PayloadFind == nil {
|
|
||||||
find.PayloadFind = &store.FindMemoPayload{}
|
|
||||||
}
|
|
||||||
if filter != "" {
|
|
||||||
filter, err := parseMemoFilter(filter)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if filter.TagSearch != nil {
|
|
||||||
if find.PayloadFind == nil {
|
|
||||||
find.PayloadFind = &store.FindMemoPayload{}
|
|
||||||
}
|
|
||||||
find.PayloadFind.TagSearch = filter.TagSearch
|
|
||||||
}
|
|
||||||
if filter.OrderByPinned {
|
|
||||||
find.OrderByPinned = filter.OrderByPinned
|
|
||||||
}
|
|
||||||
if filter.OrderByTimeAsc {
|
|
||||||
find.OrderByTimeAsc = filter.OrderByTimeAsc
|
|
||||||
}
|
|
||||||
if filter.DisplayTimeAfter != nil {
|
|
||||||
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
|
||||||
}
|
|
||||||
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
|
||||||
find.UpdatedTsAfter = filter.DisplayTimeAfter
|
|
||||||
} else {
|
|
||||||
find.CreatedTsAfter = filter.DisplayTimeAfter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if filter.DisplayTimeBefore != nil {
|
|
||||||
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
|
||||||
}
|
|
||||||
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
|
||||||
find.UpdatedTsBefore = filter.DisplayTimeBefore
|
|
||||||
} else {
|
|
||||||
find.CreatedTsBefore = filter.DisplayTimeBefore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if filter.Creator != nil {
|
|
||||||
userID, err := ExtractUserIDFromName(*filter.Creator)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid user name")
|
|
||||||
}
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if filter.RowStatus != nil {
|
|
||||||
find.RowStatus = filter.RowStatus
|
|
||||||
}
|
|
||||||
if filter.Random {
|
|
||||||
find.Random = filter.Random
|
|
||||||
}
|
|
||||||
if filter.Limit != nil {
|
|
||||||
find.Limit = filter.Limit
|
|
||||||
}
|
|
||||||
if filter.IncludeComments {
|
|
||||||
find.ExcludeComments = false
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.GetCurrentUser(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return status.Errorf(codes.Internal, "failed to get current user")
|
|
||||||
}
|
|
||||||
// If the user is not authenticated, only public memos are visible.
|
|
||||||
if user == nil {
|
|
||||||
if filter == "" {
|
|
||||||
// If no filter is provided, return an error.
|
|
||||||
return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user")
|
|
||||||
}
|
|
||||||
|
|
||||||
find.VisibilityList = []store.Visibility{store.Public}
|
|
||||||
} else if find.CreatorID == nil || *find.CreatorID != user.ID {
|
|
||||||
// If creator is not specified or the creator is not the current user, only public and protected memos are visible.
|
|
||||||
find.VisibilityList = []store.Visibility{store.Public, store.Protected}
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
|
||||||
}
|
|
||||||
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
|
||||||
find.OrderByUpdatedTs = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
|
func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
|
||||||
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -874,141 +620,6 @@ func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
|
|||||||
return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil
|
return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoFilterCELAttributes are the CEL attributes.
|
|
||||||
var MemoFilterCELAttributes = []cel.EnvOption{
|
|
||||||
cel.Variable("content_search", cel.ListType(cel.StringType)),
|
|
||||||
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
|
||||||
cel.Variable("tag_search", cel.ListType(cel.StringType)),
|
|
||||||
cel.Variable("order_by_pinned", cel.BoolType),
|
|
||||||
cel.Variable("order_by_time_asc", cel.BoolType),
|
|
||||||
cel.Variable("display_time_before", cel.IntType),
|
|
||||||
cel.Variable("display_time_after", cel.IntType),
|
|
||||||
cel.Variable("creator", cel.StringType),
|
|
||||||
cel.Variable("uid", cel.StringType),
|
|
||||||
cel.Variable("row_status", cel.StringType),
|
|
||||||
cel.Variable("random", cel.BoolType),
|
|
||||||
cel.Variable("limit", cel.IntType),
|
|
||||||
cel.Variable("include_comments", cel.BoolType),
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemoFilter struct {
|
|
||||||
ContentSearch []string
|
|
||||||
Visibilities []store.Visibility
|
|
||||||
TagSearch []string
|
|
||||||
OrderByPinned bool
|
|
||||||
OrderByTimeAsc bool
|
|
||||||
DisplayTimeBefore *int64
|
|
||||||
DisplayTimeAfter *int64
|
|
||||||
Creator *string
|
|
||||||
RowStatus *store.RowStatus
|
|
||||||
Random bool
|
|
||||||
Limit *int
|
|
||||||
IncludeComments bool
|
|
||||||
HasLink bool
|
|
||||||
HasTaskList bool
|
|
||||||
HasCode bool
|
|
||||||
HasIncompleteTasks bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMemoFilter(expression string) (*MemoFilter, error) {
|
|
||||||
e, err := cel.NewEnv(MemoFilterCELAttributes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ast, issues := e.Compile(expression)
|
|
||||||
if issues != nil {
|
|
||||||
return nil, errors.Errorf("found issue %v", issues)
|
|
||||||
}
|
|
||||||
filter := &MemoFilter{}
|
|
||||||
expr, err := cel.AstToParsedExpr(ast)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
callExpr := expr.GetExpr().GetCallExpr()
|
|
||||||
findMemoField(callExpr, filter)
|
|
||||||
return filter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findMemoField(callExpr *expr.Expr_Call, filter *MemoFilter) {
|
|
||||||
if len(callExpr.Args) == 2 {
|
|
||||||
idExpr := callExpr.Args[0].GetIdentExpr()
|
|
||||||
if idExpr != nil {
|
|
||||||
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" {
|
|
||||||
visibilities := []store.Visibility{}
|
|
||||||
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
|
||||||
value := expr.GetConstExpr().GetStringValue()
|
|
||||||
visibilities = append(visibilities, store.Visibility(value))
|
|
||||||
}
|
|
||||||
filter.Visibilities = visibilities
|
|
||||||
} else if idExpr.Name == "tag_search" {
|
|
||||||
tagSearch := []string{}
|
|
||||||
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
|
||||||
value := expr.GetConstExpr().GetStringValue()
|
|
||||||
tagSearch = append(tagSearch, value)
|
|
||||||
}
|
|
||||||
filter.TagSearch = tagSearch
|
|
||||||
} else if idExpr.Name == "order_by_pinned" {
|
|
||||||
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
|
||||||
filter.OrderByPinned = value
|
|
||||||
} else if idExpr.Name == "order_by_time_asc" {
|
|
||||||
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
|
||||||
filter.OrderByTimeAsc = value
|
|
||||||
} 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
|
|
||||||
} else if idExpr.Name == "creator" {
|
|
||||||
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
|
||||||
filter.Creator = &creator
|
|
||||||
} else if idExpr.Name == "row_status" {
|
|
||||||
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
|
||||||
filter.RowStatus = &rowStatus
|
|
||||||
} 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
|
|
||||||
} else if idExpr.Name == "include_comments" {
|
|
||||||
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
|
||||||
filter.IncludeComments = value
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, arg := range callExpr.Args {
|
|
||||||
callExpr := arg.GetCallExpr()
|
|
||||||
if callExpr != nil {
|
|
||||||
findMemoField(callExpr, filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
|
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
|
||||||
func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
|
func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error {
|
||||||
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
|
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
|
||||||
@ -1092,7 +703,3 @@ func substring(s string, length int) string {
|
|||||||
|
|
||||||
return s[:byteIndex]
|
return s[:byteIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSuperUser(user *store.User) bool {
|
|
||||||
return user.Role == store.RoleAdmin || user.Role == store.RoleHost
|
|
||||||
}
|
|
||||||
|
145
server/router/api/v1/memo_service_converter.go
Normal file
145
server/router/api/v1/memo_service_converter.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/usememos/gomark/parser"
|
||||||
|
"github.com/usememos/gomark/parser/tokenizer"
|
||||||
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
||||||
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, view v1pb.MemoView) (*v1pb.Memo, error) {
|
||||||
|
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 {
|
||||||
|
displayTs = memo.UpdatedTs
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
|
||||||
|
memoMessage := &v1pb.Memo{
|
||||||
|
Name: name,
|
||||||
|
Uid: memo.UID,
|
||||||
|
RowStatus: convertRowStatusFromStore(memo.RowStatus),
|
||||||
|
Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID),
|
||||||
|
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,
|
||||||
|
Visibility: convertVisibilityFromStore(memo.Visibility),
|
||||||
|
Pinned: memo.Pinned,
|
||||||
|
Tags: memo.Payload.Tags,
|
||||||
|
}
|
||||||
|
if memo.Payload != nil {
|
||||||
|
memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
|
||||||
|
memoMessage.Location = convertLocationFromStore(memo.Payload.Location)
|
||||||
|
}
|
||||||
|
if memo.ParentID != nil {
|
||||||
|
parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID)
|
||||||
|
memoMessage.Parent = &parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill content when view is MEMO_VIEW_FULL.
|
||||||
|
if view == v1pb.MemoView_MEMO_VIEW_FULL {
|
||||||
|
listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to list memo relations")
|
||||||
|
}
|
||||||
|
memoMessage.Relations = listMemoRelationsResponse.Relations
|
||||||
|
|
||||||
|
listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to list memo resources")
|
||||||
|
}
|
||||||
|
memoMessage.Resources = listMemoResourcesResponse.Resources
|
||||||
|
|
||||||
|
listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to list memo reactions")
|
||||||
|
}
|
||||||
|
memoMessage.Reactions = listMemoReactionsResponse.Reactions
|
||||||
|
|
||||||
|
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to parse content")
|
||||||
|
}
|
||||||
|
memoMessage.Nodes = convertFromASTNodes(nodes)
|
||||||
|
|
||||||
|
snippet, err := getMemoContentSnippet(memo.Content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get memo content snippet")
|
||||||
|
}
|
||||||
|
memoMessage.Snippet = snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
return memoMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty {
|
||||||
|
if property == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v1pb.MemoProperty{
|
||||||
|
HasLink: property.HasLink,
|
||||||
|
HasTaskList: property.HasTaskList,
|
||||||
|
HasCode: property.HasCode,
|
||||||
|
HasIncompleteTasks: property.HasIncompleteTasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location {
|
||||||
|
if location == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v1pb.Location{
|
||||||
|
Placeholder: location.Placeholder,
|
||||||
|
Latitude: location.Latitude,
|
||||||
|
Longitude: location.Longitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location {
|
||||||
|
if location == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &storepb.MemoPayload_Location{
|
||||||
|
Placeholder: location.Placeholder,
|
||||||
|
Latitude: location.Latitude,
|
||||||
|
Longitude: location.Longitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {
|
||||||
|
switch visibility {
|
||||||
|
case store.Private:
|
||||||
|
return v1pb.Visibility_PRIVATE
|
||||||
|
case store.Protected:
|
||||||
|
return v1pb.Visibility_PROTECTED
|
||||||
|
case store.Public:
|
||||||
|
return v1pb.Visibility_PUBLIC
|
||||||
|
default:
|
||||||
|
return v1pb.Visibility_VISIBILITY_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {
|
||||||
|
switch visibility {
|
||||||
|
case v1pb.Visibility_PRIVATE:
|
||||||
|
return store.Private
|
||||||
|
case v1pb.Visibility_PROTECTED:
|
||||||
|
return store.Protected
|
||||||
|
case v1pb.Visibility_PUBLIC:
|
||||||
|
return store.Public
|
||||||
|
default:
|
||||||
|
return store.Private
|
||||||
|
}
|
||||||
|
}
|
266
server/router/api/v1/memo_service_filter.go
Normal file
266
server/router/api/v1/memo_service_filter.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error {
|
||||||
|
if find.PayloadFind == nil {
|
||||||
|
find.PayloadFind = &store.FindMemoPayload{}
|
||||||
|
}
|
||||||
|
if filter != "" {
|
||||||
|
filter, err := parseMemoFilter(filter)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if filter.TagSearch != nil {
|
||||||
|
if find.PayloadFind == nil {
|
||||||
|
find.PayloadFind = &store.FindMemoPayload{}
|
||||||
|
}
|
||||||
|
find.PayloadFind.TagSearch = filter.TagSearch
|
||||||
|
}
|
||||||
|
if filter.OrderByPinned {
|
||||||
|
find.OrderByPinned = filter.OrderByPinned
|
||||||
|
}
|
||||||
|
if filter.OrderByTimeAsc {
|
||||||
|
find.OrderByTimeAsc = filter.OrderByTimeAsc
|
||||||
|
}
|
||||||
|
if filter.DisplayTimeAfter != nil {
|
||||||
|
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
||||||
|
}
|
||||||
|
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
||||||
|
find.UpdatedTsAfter = filter.DisplayTimeAfter
|
||||||
|
} else {
|
||||||
|
find.CreatedTsAfter = filter.DisplayTimeAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter.DisplayTimeBefore != nil {
|
||||||
|
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
||||||
|
}
|
||||||
|
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
||||||
|
find.UpdatedTsBefore = filter.DisplayTimeBefore
|
||||||
|
} else {
|
||||||
|
find.CreatedTsBefore = filter.DisplayTimeBefore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter.Creator != nil {
|
||||||
|
userID, err := ExtractUserIDFromName(*filter.Creator)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid user name")
|
||||||
|
}
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if filter.RowStatus != nil {
|
||||||
|
find.RowStatus = filter.RowStatus
|
||||||
|
}
|
||||||
|
if filter.Random {
|
||||||
|
find.Random = filter.Random
|
||||||
|
}
|
||||||
|
if filter.Limit != nil {
|
||||||
|
find.Limit = filter.Limit
|
||||||
|
}
|
||||||
|
if filter.IncludeComments {
|
||||||
|
find.ExcludeComments = false
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.GetCurrentUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "failed to get current user")
|
||||||
|
}
|
||||||
|
// If the user is not authenticated, only public memos are visible.
|
||||||
|
if user == nil {
|
||||||
|
if filter == "" {
|
||||||
|
// If no filter is provided, return an error.
|
||||||
|
return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user")
|
||||||
|
}
|
||||||
|
|
||||||
|
find.VisibilityList = []store.Visibility{store.Public}
|
||||||
|
} else if find.CreatorID == nil || *find.CreatorID != user.ID {
|
||||||
|
// If creator is not specified or the creator is not the current user, only public and protected memos are visible.
|
||||||
|
find.VisibilityList = []store.Visibility{store.Public, store.Protected}
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
|
||||||
|
}
|
||||||
|
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
|
||||||
|
find.OrderByUpdatedTs = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoFilterCELAttributes are the CEL attributes.
|
||||||
|
var MemoFilterCELAttributes = []cel.EnvOption{
|
||||||
|
cel.Variable("content_search", cel.ListType(cel.StringType)),
|
||||||
|
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
||||||
|
cel.Variable("tag_search", cel.ListType(cel.StringType)),
|
||||||
|
cel.Variable("order_by_pinned", cel.BoolType),
|
||||||
|
cel.Variable("order_by_time_asc", cel.BoolType),
|
||||||
|
cel.Variable("display_time_before", cel.IntType),
|
||||||
|
cel.Variable("display_time_after", cel.IntType),
|
||||||
|
cel.Variable("creator", cel.StringType),
|
||||||
|
cel.Variable("uid", cel.StringType),
|
||||||
|
cel.Variable("row_status", cel.StringType),
|
||||||
|
cel.Variable("random", cel.BoolType),
|
||||||
|
cel.Variable("limit", cel.IntType),
|
||||||
|
cel.Variable("include_comments", cel.BoolType),
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoFilter struct {
|
||||||
|
ContentSearch []string
|
||||||
|
Visibilities []store.Visibility
|
||||||
|
TagSearch []string
|
||||||
|
OrderByPinned bool
|
||||||
|
OrderByTimeAsc bool
|
||||||
|
DisplayTimeBefore *int64
|
||||||
|
DisplayTimeAfter *int64
|
||||||
|
Creator *string
|
||||||
|
RowStatus *store.RowStatus
|
||||||
|
Random bool
|
||||||
|
Limit *int
|
||||||
|
IncludeComments bool
|
||||||
|
HasLink bool
|
||||||
|
HasTaskList bool
|
||||||
|
HasCode bool
|
||||||
|
HasIncompleteTasks bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMemoFilter(expression string) (*MemoFilter, error) {
|
||||||
|
e, err := cel.NewEnv(MemoFilterCELAttributes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ast, issues := e.Compile(expression)
|
||||||
|
if issues != nil {
|
||||||
|
return nil, errors.Errorf("found issue %v", issues)
|
||||||
|
}
|
||||||
|
filter := &MemoFilter{}
|
||||||
|
expr, err := cel.AstToParsedExpr(ast)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
callExpr := expr.GetExpr().GetCallExpr()
|
||||||
|
findMemoField(callExpr, filter)
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMemoField(callExpr *expr.Expr_Call, filter *MemoFilter) {
|
||||||
|
if len(callExpr.Args) == 2 {
|
||||||
|
idExpr := callExpr.Args[0].GetIdentExpr()
|
||||||
|
if idExpr != nil {
|
||||||
|
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" {
|
||||||
|
visibilities := []store.Visibility{}
|
||||||
|
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
||||||
|
value := expr.GetConstExpr().GetStringValue()
|
||||||
|
visibilities = append(visibilities, store.Visibility(value))
|
||||||
|
}
|
||||||
|
filter.Visibilities = visibilities
|
||||||
|
} else if idExpr.Name == "tag_search" {
|
||||||
|
tagSearch := []string{}
|
||||||
|
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
||||||
|
value := expr.GetConstExpr().GetStringValue()
|
||||||
|
tagSearch = append(tagSearch, value)
|
||||||
|
}
|
||||||
|
filter.TagSearch = tagSearch
|
||||||
|
} else if idExpr.Name == "order_by_pinned" {
|
||||||
|
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
||||||
|
filter.OrderByPinned = value
|
||||||
|
} else if idExpr.Name == "order_by_time_asc" {
|
||||||
|
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
||||||
|
filter.OrderByTimeAsc = value
|
||||||
|
} 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
|
||||||
|
} else if idExpr.Name == "creator" {
|
||||||
|
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
||||||
|
filter.Creator = &creator
|
||||||
|
} else if idExpr.Name == "row_status" {
|
||||||
|
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
||||||
|
filter.RowStatus = &rowStatus
|
||||||
|
} 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
|
||||||
|
} else if idExpr.Name == "include_comments" {
|
||||||
|
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
||||||
|
filter.IncludeComments = value
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, arg := range callExpr.Args {
|
||||||
|
callExpr := arg.GetCallExpr()
|
||||||
|
if callExpr != nil {
|
||||||
|
findMemoField(callExpr, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user