package logic import ( "context" "encoding/json" "errors" "time" "spark-server/internal/module/favorite/entity" msgentity "spark-server/internal/module/message/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/reqctx" "spark-server/internal/pkg/validate" ) // CreateInput favorites an existing message. Expired chat objects fail; no client re-upload. type CreateInput struct { MsgID int64 `json:"msg_id" validate:"required,gt=0" label:"field.msg_id" errmsg:"required:validate.favorite.msg_required;gt:validate.favorite.msg_invalid"` } // Item is the HTTP/TCP favorite view. Content is the snapshot JSON (perm/fav keys). type Item struct { ID int64 `json:"id"` MsgID int64 `json:"msg_id"` ConversationID int64 `json:"conversation_id"` MessageType msgentity.Type `json:"message_type"` Content json.RawMessage `json:"content"` CreatedAt int64 `json:"created_at"` } func (s *Service) Create(ctx context.Context, userID int64, in *CreateInput) (*Item, error) { if !s.ready() || userID <= 0 { return nil, errs.ErrUnauthorized } if in == nil { return nil, errs.ErrInvalidArgument } if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil { return nil, err } if existing, err := s.repo.GetByUserMsg(ctx, userID, in.MsgID); err == nil { return itemOf(existing), nil } else if !errors.Is(err, errs.ErrNotFound) { return nil, err } msg, payload, err := s.loadFavoriteSource(ctx, userID, in.MsgID) if err != nil { return nil, err } copied, err := s.copyMedia(ctx, userID, payload) if err != nil { return nil, err } fav, err := s.persistFavorite(ctx, userID, msg, payload) if err != nil { s.deleteKeys(ctx, copied) if errors.Is(err, errs.ErrDuplicate) { existing, getErr := s.repo.GetByUserMsg(ctx, userID, in.MsgID) if getErr == nil { return itemOf(existing), nil } } return nil, err } return itemOf(fav), nil } func (s *Service) loadFavoriteSource(ctx context.Context, userID, msgID int64) (*msgentity.Message, *msgentity.Content, error) { msg, err := s.msgs.GetByID(ctx, msgID) if err != nil { return nil, nil, err } if msg.Status == msgentity.StatusHidden { return nil, nil, errs.ErrNotFound } if _, err := s.members.GetMember(ctx, msg.ConversationID, userID); err != nil { if errors.Is(err, errs.ErrNotFound) { return nil, nil, errs.ErrForbidden } return nil, nil, err } payload, err := msg.Payload() if err != nil || payload == nil { return nil, nil, errs.ErrInvalidArgument } return msg, payload, nil } func (s *Service) persistFavorite(ctx context.Context, userID int64, msg *msgentity.Message, payload *msgentity.Content) (*entity.Favorite, error) { raw, err := payload.Marshal() if err != nil { return nil, errs.ErrInvalidArgument } id, err := s.ids.NextID() if err != nil { return nil, err } now := time.Now() fav := &entity.Favorite{ ID: id, UserID: userID, MsgID: msg.ID, ConversationID: msg.ConversationID, MessageType: int16(msg.Type), Content: raw, CreatedAtMS: now.UnixMilli(), } if err := s.repo.Create(ctx, fav); err != nil { return nil, err } return fav, nil } func itemOf(fav *entity.Favorite) *Item { if fav == nil { return nil } return &Item{ ID: fav.ID, MsgID: fav.MsgID, ConversationID: fav.ConversationID, MessageType: msgentity.Type(fav.MessageType), Content: fav.Content, CreatedAt: fav.CreatedAtMS, } }