173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
storepb "github.com/usememos/memos/proto/gen/store"
|
|
"github.com/usememos/memos/server/access"
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
func (s *APIV1Service) resolveMentionTargets(ctx context.Context, content string) (map[int32]*store.User, error) {
|
|
targets := make(map[int32]*store.User)
|
|
if content == "" {
|
|
return targets, nil
|
|
}
|
|
|
|
data, err := s.MarkdownService.ExtractAll([]byte(content))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to extract mentions")
|
|
}
|
|
if len(data.Mentions) == 0 {
|
|
return targets, nil
|
|
}
|
|
|
|
normal := store.Normal
|
|
users, err := s.Store.ListUsers(ctx, &store.FindUser{
|
|
UsernameList: data.Mentions,
|
|
RowStatus: &normal,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to resolve mention users")
|
|
}
|
|
|
|
for _, user := range users {
|
|
targets[user.ID] = user
|
|
}
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
// mentionContextFacts holds the viewer-independent authorization facts for a
|
|
// mention's memo and its optional context memo. They are resolved once per
|
|
// dispatch and reused for every mentioned user.
|
|
type mentionContextFacts struct {
|
|
memo access.MemoReadFacts
|
|
related *access.MemoReadFacts
|
|
}
|
|
|
|
func (s *APIV1Service) resolveMentionContextFacts(ctx context.Context, memo *store.Memo, relatedMemo *store.Memo) (*mentionContextFacts, error) {
|
|
memoFacts, err := access.ResolveMemoReadFacts(ctx, s.Store, memo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
facts := &mentionContextFacts{memo: memoFacts}
|
|
if relatedMemo != nil {
|
|
relatedFacts, err := access.ResolveMemoReadFacts(ctx, s.Store, relatedMemo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
facts.related = &relatedFacts
|
|
}
|
|
return facts, nil
|
|
}
|
|
|
|
func (s *APIV1Service) canUserAccessMentionContext(ctx context.Context, facts *mentionContextFacts, target *store.User) bool {
|
|
if target == nil || facts == nil || facts.memo.Memo == nil {
|
|
return false
|
|
}
|
|
readContext, err := facts.memo.WithViewer(ctx, s.Store, target, false, nil)
|
|
if err != nil || !access.CheckMemoReadContext(readContext).Allowed() {
|
|
return false
|
|
}
|
|
if facts.related == nil {
|
|
return true
|
|
}
|
|
relatedContext, err := facts.related.WithViewer(ctx, s.Store, target, false, nil)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return access.CheckMemoReadContext(relatedContext).Allowed()
|
|
}
|
|
|
|
// canUserAccessMentionMemos is the one-shot form for a single target, where
|
|
// there is no fan-out to amortize the fact resolution over.
|
|
func (s *APIV1Service) canUserAccessMentionMemos(ctx context.Context, target *store.User, memo *store.Memo, relatedMemo *store.Memo) bool {
|
|
facts, err := s.resolveMentionContextFacts(ctx, memo, relatedMemo)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return s.canUserAccessMentionContext(ctx, facts, target)
|
|
}
|
|
|
|
func (s *APIV1Service) shouldSkipMentionInbox(ctx context.Context, facts *mentionContextFacts, target *store.User, memo *store.Memo, relatedMemo *store.Memo) bool {
|
|
if target == nil || memo == nil {
|
|
return true
|
|
}
|
|
|
|
if target.ID == memo.CreatorID {
|
|
return true
|
|
}
|
|
|
|
// Comment creation already generates a memo-comment inbox item for the parent creator.
|
|
if relatedMemo != nil && target.ID == relatedMemo.CreatorID && memo.CreatorID != relatedMemo.CreatorID {
|
|
return true
|
|
}
|
|
|
|
return !s.canUserAccessMentionContext(ctx, facts, target)
|
|
}
|
|
|
|
func (s *APIV1Service) dispatchMemoMentionNotifications(ctx context.Context, memo *store.Memo, relatedMemo *store.Memo, previousContent string) error {
|
|
if memo == nil {
|
|
return nil
|
|
}
|
|
|
|
currentTargets, err := s.resolveMentionTargets(ctx, memo.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(currentTargets) == 0 {
|
|
return nil
|
|
}
|
|
|
|
previousTargets, err := s.resolveMentionTargets(ctx, previousContent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
facts, err := s.resolveMentionContextFacts(ctx, memo, relatedMemo)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to resolve mention context")
|
|
}
|
|
|
|
for userID, target := range currentTargets {
|
|
if _, exists := previousTargets[userID]; exists {
|
|
continue
|
|
}
|
|
if s.shouldSkipMentionInbox(ctx, facts, target, memo, relatedMemo) {
|
|
continue
|
|
}
|
|
|
|
payload := &storepb.InboxMessage_MemoMentionPayload{
|
|
MemoId: memo.ID,
|
|
}
|
|
if relatedMemo != nil {
|
|
payload.RelatedMemoId = relatedMemo.ID
|
|
}
|
|
|
|
if _, err := s.createInboxWithEmailNotification(ctx, &store.Inbox{
|
|
SenderID: memo.CreatorID,
|
|
ReceiverID: target.ID,
|
|
Status: store.UNREAD,
|
|
Message: &storepb.InboxMessage{
|
|
Type: storepb.InboxMessage_MEMO_MENTION,
|
|
Payload: &storepb.InboxMessage_MemoMention{
|
|
MemoMention: payload,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
return errors.Wrap(err, "failed to create mention inbox")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *APIV1Service) dispatchMemoMentionNotificationsBestEffort(ctx context.Context, memo *store.Memo, relatedMemo *store.Memo, previousContent string) {
|
|
if err := s.dispatchMemoMentionNotifications(ctx, memo, relatedMemo, previousContent); err != nil {
|
|
slog.Warn("Failed to dispatch memo mention notifications", slog.Any("err", err), slog.Int64("memo_id", int64(memo.ID)))
|
|
}
|
|
}
|