0.29.1原版
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
var (
|
||||
errMemoCreatorNotFound = stderrors.New("memo creator not found")
|
||||
errReactionCreatorNotFound = stderrors.New("reaction creator not found")
|
||||
)
|
||||
|
||||
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) {
|
||||
creatorMap, err := s.listUsersByID(ctx, []int32{memo.CreatorID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list memo creators")
|
||||
}
|
||||
return s.convertMemoFromStoreWithCreators(ctx, memo, reactions, attachments, relations, creatorMap)
|
||||
}
|
||||
|
||||
func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User) (*v1pb.Memo, error) {
|
||||
name := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
|
||||
creator := creatorMap[memo.CreatorID]
|
||||
if creator == nil {
|
||||
return nil, errMemoCreatorNotFound
|
||||
}
|
||||
memoMessage := &v1pb.Memo{
|
||||
Name: name,
|
||||
State: convertStateFromStore(memo.RowStatus),
|
||||
Creator: BuildUserName(creator.Username),
|
||||
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
|
||||
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
|
||||
Content: memo.Content,
|
||||
Visibility: convertVisibilityFromStore(memo.Visibility),
|
||||
Pinned: memo.Pinned,
|
||||
}
|
||||
if memo.Payload != nil {
|
||||
memoMessage.Tags = memo.Payload.Tags
|
||||
memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
|
||||
memoMessage.Location = convertLocationFromStore(memo.Payload.Location)
|
||||
}
|
||||
|
||||
if memo.ParentUID != nil {
|
||||
parentName := fmt.Sprintf("%s%s", MemoNamePrefix, *memo.ParentUID)
|
||||
memoMessage.Parent = &parentName
|
||||
}
|
||||
|
||||
reactionMessages, err := s.convertReactionsFromStoreWithCreators(ctx, reactions, creatorMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert reactions")
|
||||
}
|
||||
memoMessage.Reactions = reactionMessages
|
||||
|
||||
if relations != nil {
|
||||
memoMessage.Relations = relations
|
||||
} else {
|
||||
memoMessage.Relations = []*v1pb.MemoRelation{}
|
||||
}
|
||||
|
||||
memoMessage.Attachments = []*v1pb.Attachment{}
|
||||
for _, attachment := range attachments {
|
||||
attachmentResponse := convertAttachmentFromStore(attachment)
|
||||
memoMessage.Attachments = append(memoMessage.Attachments, attachmentResponse)
|
||||
}
|
||||
|
||||
snippet, err := s.getMemoContentSnippet(memo.Content)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get memo content snippet")
|
||||
}
|
||||
memoMessage.Snippet = snippet
|
||||
|
||||
return memoMessage, nil
|
||||
}
|
||||
|
||||
func (s *APIV1Service) listUsersByIDWithExisting(ctx context.Context, userIDs []int32, existing map[int32]*store.User) (map[int32]*store.User, error) {
|
||||
usersByID := make(map[int32]*store.User, len(existing)+len(userIDs))
|
||||
for userID, user := range existing {
|
||||
if user != nil {
|
||||
usersByID[userID] = user
|
||||
}
|
||||
}
|
||||
|
||||
missingUserIDs := make([]int32, 0, len(userIDs))
|
||||
seenMissingUserIDs := make(map[int32]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if _, ok := usersByID[userID]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenMissingUserIDs[userID]; ok {
|
||||
continue
|
||||
}
|
||||
seenMissingUserIDs[userID] = struct{}{}
|
||||
missingUserIDs = append(missingUserIDs, userID)
|
||||
}
|
||||
|
||||
if len(missingUserIDs) == 0 {
|
||||
return usersByID, nil
|
||||
}
|
||||
|
||||
missingUsersByID, err := s.listUsersByID(ctx, missingUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for userID, user := range missingUsersByID {
|
||||
if user != nil {
|
||||
usersByID[userID] = user
|
||||
}
|
||||
}
|
||||
return usersByID, nil
|
||||
}
|
||||
|
||||
func (s *APIV1Service) convertReactionsFromStoreWithCreators(ctx context.Context, reactions []*store.Reaction, creatorMap map[int32]*store.User) ([]*v1pb.Reaction, error) {
|
||||
if len(reactions) == 0 {
|
||||
return []*v1pb.Reaction{}, nil
|
||||
}
|
||||
|
||||
creatorIDs := make([]int32, 0, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
creatorIDs = append(creatorIDs, reaction.CreatorID)
|
||||
}
|
||||
creatorsByID, err := s.listUsersByIDWithExisting(ctx, creatorIDs, creatorMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reactionMessages := make([]*v1pb.Reaction, 0, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
reactionMessage, err := convertReactionFromStoreWithCreators(reaction, creatorsByID)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, errReactionCreatorNotFound) {
|
||||
slog.Warn("Skipping reaction with missing creator",
|
||||
slog.Int64("reaction_id", int64(reaction.ID)),
|
||||
slog.Int64("creator_id", int64(reaction.CreatorID)),
|
||||
slog.String("content_id", reaction.ContentID),
|
||||
)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
reactionMessages = append(reactionMessages, reactionMessage)
|
||||
}
|
||||
return reactionMessages, nil
|
||||
}
|
||||
|
||||
func convertReactionFromStoreWithCreators(reaction *store.Reaction, creatorsByID map[int32]*store.User) (*v1pb.Reaction, error) {
|
||||
creator := creatorsByID[reaction.CreatorID]
|
||||
if creator == nil {
|
||||
return nil, errReactionCreatorNotFound
|
||||
}
|
||||
|
||||
reactionUID := fmt.Sprintf("%d", reaction.ID)
|
||||
return &v1pb.Reaction{
|
||||
Name: fmt.Sprintf("%s/%s%s", reaction.ContentID, ReactionNamePrefix, reactionUID),
|
||||
Creator: BuildUserName(creator.Username),
|
||||
ContentId: reaction.ContentID,
|
||||
ReactionType: reaction.ReactionType,
|
||||
CreateTime: timestamppb.New(time.Unix(reaction.CreatedTs, 0)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// batchConvertMemoRelations batch-loads relations for a list of memos and returns
|
||||
// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.
|
||||
func (s *APIV1Service) batchConvertMemoRelations(ctx context.Context, memos []*store.Memo, includeSnippets bool) (map[int32][]*v1pb.MemoRelation, error) {
|
||||
if len(memos) == 0 {
|
||||
return map[int32][]*v1pb.MemoRelation{}, nil
|
||||
}
|
||||
|
||||
currentUser, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get user")
|
||||
}
|
||||
var memoFilter string
|
||||
if currentUser == nil {
|
||||
memoFilter = `visibility == "PUBLIC"`
|
||||
} else {
|
||||
memoFilter = fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID)
|
||||
}
|
||||
|
||||
memoIDs := make([]int32, len(memos))
|
||||
memoIDSet := make(map[int32]bool, len(memos))
|
||||
for i, m := range memos {
|
||||
memoIDs[i] = m.ID
|
||||
memoIDSet[m.ID] = true
|
||||
}
|
||||
|
||||
outgoingRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
|
||||
SourceMemoIDList: memoIDs,
|
||||
MemoFilter: &memoFilter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to batch list outgoing memo relations")
|
||||
}
|
||||
incomingRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
|
||||
RelatedMemoIDList: memoIDs,
|
||||
MemoFilter: &memoFilter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to batch list incoming memo relations")
|
||||
}
|
||||
allRelations := mergeMemoRelations(outgoingRelations, incomingRelations)
|
||||
|
||||
// Collect all memo IDs referenced in relations that we need to resolve.
|
||||
neededIDs := make(map[int32]bool)
|
||||
for _, r := range allRelations {
|
||||
neededIDs[r.MemoID] = true
|
||||
neededIDs[r.RelatedMemoID] = true
|
||||
}
|
||||
|
||||
// Build ID→UID map from the memos we already have.
|
||||
memoIDToUID := make(map[int32]string, len(memos))
|
||||
memoIDToSnippet := make(map[int32]string, len(memos))
|
||||
for _, m := range memos {
|
||||
memoIDToUID[m.ID] = m.UID
|
||||
if includeSnippets {
|
||||
snippet, err := s.getMemoContentSnippet(m.Content)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get memo content snippet")
|
||||
}
|
||||
memoIDToSnippet[m.ID] = snippet
|
||||
}
|
||||
delete(neededIDs, m.ID)
|
||||
}
|
||||
|
||||
// Batch fetch any additional memos referenced by relations that we don't already have.
|
||||
if len(neededIDs) > 0 {
|
||||
extraIDs := make([]int32, 0, len(neededIDs))
|
||||
for id := range neededIDs {
|
||||
extraIDs = append(extraIDs, id)
|
||||
}
|
||||
extraFind := &store.FindMemo{IDList: extraIDs, ExcludeContent: !includeSnippets}
|
||||
extraMemos, err := s.Store.ListMemos(ctx, extraFind)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to batch fetch related memos")
|
||||
}
|
||||
for _, m := range extraMemos {
|
||||
memoIDToUID[m.ID] = m.UID
|
||||
if includeSnippets {
|
||||
snippet, err := s.getMemoContentSnippet(m.Content)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get related memo content snippet")
|
||||
}
|
||||
memoIDToSnippet[m.ID] = snippet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the result map: memo ID → its relations (both directions).
|
||||
result := make(map[int32][]*v1pb.MemoRelation, len(memos))
|
||||
for _, r := range allRelations {
|
||||
memoUID, ok1 := memoIDToUID[r.MemoID]
|
||||
relatedUID, ok2 := memoIDToUID[r.RelatedMemoID]
|
||||
if !ok1 || !ok2 {
|
||||
continue
|
||||
}
|
||||
|
||||
relation := &v1pb.MemoRelation{
|
||||
Memo: &v1pb.MemoRelation_Memo{
|
||||
Name: fmt.Sprintf("%s%s", MemoNamePrefix, memoUID),
|
||||
Snippet: memoIDToSnippet[r.MemoID],
|
||||
},
|
||||
RelatedMemo: &v1pb.MemoRelation_Memo{
|
||||
Name: fmt.Sprintf("%s%s", MemoNamePrefix, relatedUID),
|
||||
Snippet: memoIDToSnippet[r.RelatedMemoID],
|
||||
},
|
||||
Type: convertMemoRelationTypeFromStore(r.Type),
|
||||
}
|
||||
|
||||
// Add to the memo that owns this relation (both directions).
|
||||
if memoIDSet[r.MemoID] {
|
||||
result[r.MemoID] = append(result[r.MemoID], relation)
|
||||
}
|
||||
if memoIDSet[r.RelatedMemoID] {
|
||||
result[r.RelatedMemoID] = append(result[r.RelatedMemoID], relation)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// loadMemoRelations loads relations for a single memo and converts them to API format.
|
||||
func (s *APIV1Service) loadMemoRelations(ctx context.Context, memo *store.Memo) ([]*v1pb.MemoRelation, error) {
|
||||
relationMap, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return relationMap[memo.ID], nil
|
||||
}
|
||||
|
||||
func mergeMemoRelations(groups ...[]*store.MemoRelation) []*store.MemoRelation {
|
||||
seen := make(map[string]struct{})
|
||||
merged := make([]*store.MemoRelation, 0)
|
||||
for _, relations := range groups {
|
||||
for _, relation := range relations {
|
||||
key := fmt.Sprintf("%d:%d:%s", relation.MemoID, relation.RelatedMemoID, relation.Type)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
merged = append(merged, relation)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.Memo_Property {
|
||||
if property == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1pb.Memo_Property{
|
||||
HasLink: property.HasLink,
|
||||
HasTaskList: property.HasTaskList,
|
||||
HasCode: property.HasCode,
|
||||
HasIncompleteTasks: property.HasIncompleteTasks,
|
||||
Title: property.Title,
|
||||
}
|
||||
}
|
||||
|
||||
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_PROTECTED:
|
||||
return store.Protected
|
||||
case v1pb.Visibility_PUBLIC:
|
||||
return store.Public
|
||||
default:
|
||||
return store.Private
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user