147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"spark-server/internal/module/square/entity"
|
|
"spark-server/internal/pkg/errs"
|
|
"spark-server/internal/pkg/i18n"
|
|
"spark-server/internal/pkg/reqctx"
|
|
"spark-server/internal/pkg/validate"
|
|
)
|
|
|
|
type PublishInput struct {
|
|
Text string `json:"text" validate:"omitempty,max=1000" label:"field.square_text" errmsg:"max:validate.square.text_invalid"`
|
|
Images []*entity.Image `json:"images" validate:"omitempty,max=9,dive"`
|
|
Video *entity.Video `json:"video"`
|
|
Visibility entity.Visibility `json:"visibility" validate:"omitempty,oneof=0 1 2 3" label:"field.square_visibility" errmsg:"oneof:validate.square.visibility_invalid"`
|
|
AudienceUIDs []int64 `json:"audience_uids" validate:"omitempty,max=200,dive,gt=0" label:"field.square_audience"`
|
|
POI *entity.POI `json:"poi"`
|
|
}
|
|
|
|
func (s *Service) Publish(ctx context.Context, userID int64, in *PublishInput) (*View, 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 !s.allowPublish(ctx, userID) {
|
|
return nil, errs.ErrRateLimited
|
|
}
|
|
content, kind, err := buildContent(ctx, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, err := content.Marshal()
|
|
if err != nil {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil))
|
|
}
|
|
if err := s.checkAudience(ctx, userID, in); err != nil {
|
|
return nil, err
|
|
}
|
|
id, err := s.ids.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
vis := in.Visibility
|
|
if !vis.Valid() {
|
|
vis = entity.VisibilityPublic
|
|
}
|
|
post := &entity.Post{
|
|
ID: id, UserID: userID, Kind: kind, Text: strings.TrimSpace(in.Text), Content: raw,
|
|
Visibility: vis, AuditStatus: entity.AuditApproved, Status: entity.StatusNormal,
|
|
CreatedAtMS: now.UnixMilli(),
|
|
}
|
|
if in.POI != nil {
|
|
post.POI = *in.POI
|
|
}
|
|
if err := s.repo.CreatePost(ctx, post, uniqueIDs(in.AudienceUIDs)); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.viewOf(ctx, userID, post, true)
|
|
}
|
|
|
|
func (s *Service) allowPublish(ctx context.Context, userID int64) bool {
|
|
key := strconv.FormatInt(userID, 10) + ":" + reqctx.RealIP(ctx)
|
|
return s.burst.allow("p1:"+key, time.Minute, 2) && s.burst.allow("pd:"+key, 24*time.Hour, 20)
|
|
}
|
|
|
|
func (s *Service) allowInteract(ctx context.Context, userID int64) bool {
|
|
key := strconv.FormatInt(userID, 10) + ":" + reqctx.RealIP(ctx)
|
|
return s.burst.allow("i:"+key, time.Minute, 60)
|
|
}
|
|
|
|
func buildContent(ctx context.Context, in *PublishInput) (*entity.Content, entity.Kind, error) {
|
|
lang := reqctx.Lang(ctx)
|
|
text := strings.TrimSpace(in.Text)
|
|
if utf8.RuneCountInString(text) > entity.MaxTextRunes {
|
|
return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareTextInvalid, nil))
|
|
}
|
|
if len(in.Images) > 0 && in.Video != nil {
|
|
return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareMediaConflict, nil))
|
|
}
|
|
if text == "" && len(in.Images) == 0 && in.Video == nil {
|
|
return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareEmpty, nil))
|
|
}
|
|
c := &entity.Content{Images: in.Images, Video: in.Video}
|
|
if err := c.Validate(); err != nil {
|
|
return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareContentInvalid, nil))
|
|
}
|
|
return c, c.Kind(), nil
|
|
}
|
|
|
|
func (s *Service) checkAudience(ctx context.Context, userID int64, in *PublishInput) error {
|
|
lang := reqctx.Lang(ctx)
|
|
if !in.Visibility.Valid() {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareVisibilityInvalid, nil))
|
|
}
|
|
uids := uniqueIDs(in.AudienceUIDs)
|
|
if !in.Visibility.NeedsAudience() {
|
|
if len(uids) > 0 {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil))
|
|
}
|
|
return nil
|
|
}
|
|
if len(uids) == 0 || len(uids) > entity.MaxAudience {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil))
|
|
}
|
|
for _, peer := range uids {
|
|
if peer == userID {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil))
|
|
}
|
|
ok, err := s.friends.AreFriends(ctx, userID, peer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func uniqueIDs(ids []int64) []int64 {
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
out := make([]int64, 0, len(ids))
|
|
for _, id := range ids {
|
|
if id <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|