Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
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 int16 `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: fav.MessageType, Content: fav.Content, CreatedAt: fav.CreatedAtMS,
|
|
}
|
|
}
|