Compare commits
No commits in common. "6cfd6309d8b830a9b98edb787c4abf46043dc775" and "f708a86d5c98b476c19fe843f33c0369fb617c4e" have entirely different histories.
6cfd6309d8
...
f708a86d5c
@ -25,7 +25,6 @@ type Config struct {
|
|||||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||||
Message MessageConfig `yaml:"message"`
|
Message MessageConfig `yaml:"message"`
|
||||||
ObjectStorage ObjectStorageConfig `yaml:"object_storage"`
|
ObjectStorage ObjectStorageConfig `yaml:"object_storage"`
|
||||||
Media MediaConfig `yaml:"media"`
|
|
||||||
SMS SMSConfig `yaml:"sms"`
|
SMS SMSConfig `yaml:"sms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,23 +135,6 @@ type ObjectStorageConfig struct {
|
|||||||
ChatExpireDays int `yaml:"chat_expire_days"`
|
ChatExpireDays int `yaml:"chat_expire_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaConfig is server-side media detect (users) + compress (bots).
|
|
||||||
// Client App still compresses user uploads; see docs/media.md.
|
|
||||||
type MediaConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
UserReject bool `yaml:"user_reject"` // false=log only; true=return 40005
|
|
||||||
BotCompress bool `yaml:"bot_compress"`
|
|
||||||
ImageMaxEdge int `yaml:"image_max_edge"`
|
|
||||||
VideoMaxEdge int `yaml:"video_max_edge"`
|
|
||||||
AvatarMaxEdge int `yaml:"avatar_max_edge"`
|
|
||||||
ImageMaxBytes int64 `yaml:"image_max_bytes"`
|
|
||||||
VideoMaxBytes int64 `yaml:"video_max_bytes"`
|
|
||||||
AvatarMaxBytes int64 `yaml:"avatar_max_bytes"`
|
|
||||||
JPEGQuality int `yaml:"jpeg_quality"`
|
|
||||||
VideoCRF int `yaml:"video_crf"`
|
|
||||||
FFmpegPath string `yaml:"ffmpeg_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageConfig struct {
|
type MessageConfig struct {
|
||||||
MaxContentSize int `yaml:"max_content_size"`
|
MaxContentSize int `yaml:"max_content_size"`
|
||||||
SyncLimit int `yaml:"sync_limit"`
|
SyncLimit int `yaml:"sync_limit"`
|
||||||
@ -332,42 +314,6 @@ func applyDefaults(cfg *Config) {
|
|||||||
if cfg.RateLimit.UploadPerSecond < 1 {
|
if cfg.RateLimit.UploadPerSecond < 1 {
|
||||||
cfg.RateLimit.UploadPerSecond = 10
|
cfg.RateLimit.UploadPerSecond = 10
|
||||||
}
|
}
|
||||||
applyMediaDefaults(&cfg.Media)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyMediaDefaults(m *MediaConfig) {
|
|
||||||
if m == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Numeric/path defaults only. Toggle via YAML media.enabled / media.bot_compress
|
|
||||||
// (configs ship with enabled: true). Explicit enabled: false stays off.
|
|
||||||
if m.ImageMaxEdge <= 0 {
|
|
||||||
m.ImageMaxEdge = 1080
|
|
||||||
}
|
|
||||||
if m.VideoMaxEdge <= 0 {
|
|
||||||
m.VideoMaxEdge = 720
|
|
||||||
}
|
|
||||||
if m.AvatarMaxEdge <= 0 {
|
|
||||||
m.AvatarMaxEdge = 200
|
|
||||||
}
|
|
||||||
if m.ImageMaxBytes <= 0 {
|
|
||||||
m.ImageMaxBytes = 10 << 20
|
|
||||||
}
|
|
||||||
if m.VideoMaxBytes <= 0 {
|
|
||||||
m.VideoMaxBytes = 100 << 20
|
|
||||||
}
|
|
||||||
if m.AvatarMaxBytes <= 0 {
|
|
||||||
m.AvatarMaxBytes = 2 << 20
|
|
||||||
}
|
|
||||||
if m.JPEGQuality <= 0 {
|
|
||||||
m.JPEGQuality = 85
|
|
||||||
}
|
|
||||||
if m.VideoCRF <= 0 {
|
|
||||||
m.VideoCRF = 23
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(m.FFmpegPath) == "" {
|
|
||||||
m.FFmpegPath = "ffmpeg"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEnv(raw string) string {
|
func parseEnv(raw string) string {
|
||||||
|
|||||||
@ -1,135 +0,0 @@
|
|||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"spark-server/internal/infrastructure/id"
|
|
||||||
friendlogic "spark-server/internal/module/friend/logic"
|
|
||||||
friendmodel "spark-server/internal/module/friend/model"
|
|
||||||
squarelogic "spark-server/internal/module/square/logic"
|
|
||||||
squaremodel "spark-server/internal/module/square/model"
|
|
||||||
userentity "spark-server/internal/module/user/entity"
|
|
||||||
"spark-server/internal/module/user/logictest"
|
|
||||||
"spark-server/internal/pkg/errs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseRSSAndFormat(t *testing.T) {
|
|
||||||
body := []byte(`<?xml version="1.0"?>
|
|
||||||
<rss version="2.0"><channel>
|
|
||||||
<title>Demo News</title>
|
|
||||||
<item>
|
|
||||||
<title>Hello & World</title>
|
|
||||||
<link>https://example.com/a</link>
|
|
||||||
<description><p>Short summary here.</p></description>
|
|
||||||
</item>
|
|
||||||
</channel></rss>`)
|
|
||||||
items, err := parseFeed(body, "https://example.com/rss")
|
|
||||||
if err != nil || len(items) != 1 {
|
|
||||||
t.Fatalf("%+v %v", items, err)
|
|
||||||
}
|
|
||||||
if items[0].Title != "Hello & World" || items[0].Link != "https://example.com/a" {
|
|
||||||
t.Fatalf("%+v", items[0])
|
|
||||||
}
|
|
||||||
text := FormatPostText(items[0])
|
|
||||||
if !strings.Contains(text, "Demo News") || !strings.Contains(text, "https://example.com/a") {
|
|
||||||
t.Fatalf("text=%q", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublishBotMediaTTLAndDedupe(t *testing.T) {
|
|
||||||
ids, err := id.New(2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
users := logictest.Users(ids, nil)
|
|
||||||
friends := friendlogic.NewService(friendmodel.NewMemoryRepo(), users, ids, nil)
|
|
||||||
square := squarelogic.NewService(squaremodel.NewMemoryRepo(), friends, users, nil, ids)
|
|
||||||
ctx := t.Context()
|
|
||||||
u, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{
|
|
||||||
DeviceID: 9, DeviceType: userentity.DeviceIOS,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
in := &squarelogic.BotPublishInput{
|
|
||||||
Text: "title\n\nsum\n\nhttps://example.com/x", SourceURL: "https://example.com/x", TTL: time.Hour,
|
|
||||||
}
|
|
||||||
v, err := square.PublishBotMedia(ctx, u.ID, in)
|
|
||||||
if err != nil || v == nil {
|
|
||||||
t.Fatalf("%v %v", v, err)
|
|
||||||
}
|
|
||||||
_, err = square.PublishBotMedia(ctx, u.ID, in)
|
|
||||||
if !errors.Is(err, errs.ErrDuplicate) {
|
|
||||||
t.Fatalf("want duplicate got %v", err)
|
|
||||||
}
|
|
||||||
feed, err := square.Feed(ctx, u.ID, 0, 20)
|
|
||||||
if err != nil || len(feed.Items) < 1 {
|
|
||||||
t.Fatalf("feed %+v %v", feed, err)
|
|
||||||
}
|
|
||||||
n, err := square.ExpireBotPosts(ctx, time.Now().Add(2*time.Hour), 10)
|
|
||||||
if err != nil || n < 1 {
|
|
||||||
t.Fatalf("expire %d %v", n, err)
|
|
||||||
}
|
|
||||||
feed2, err := square.Feed(ctx, u.ID, 0, 20)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, it := range feed2.Items {
|
|
||||||
if it.ID == v.ID {
|
|
||||||
t.Fatal("expired post still visible")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceDisabledWithoutDeps(t *testing.T) {
|
|
||||||
svc := NewService(nil, nil, nil, nil, Options{Enabled: true})
|
|
||||||
if svc.Enabled() {
|
|
||||||
t.Fatal("enabled without deps")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type stubUsers struct {
|
|
||||||
u *userentity.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s stubUsers) GetByEmail(context.Context, string) (*userentity.User, error) {
|
|
||||||
if s.u == nil {
|
|
||||||
return nil, errs.ErrNotFound
|
|
||||||
}
|
|
||||||
return s.u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type stubSquare struct {
|
|
||||||
published int
|
|
||||||
expired int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubSquare) PublishBotMedia(context.Context, int64, *squarelogic.BotPublishInput) (*squarelogic.View, error) {
|
|
||||||
s.published++
|
|
||||||
return &squarelogic.View{ID: 1}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubSquare) ExpireBotPosts(context.Context, time.Time, int) (int, error) {
|
|
||||||
s.expired++
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTickExpiresEvenIfFeedFails(t *testing.T) {
|
|
||||||
sq := &stubSquare{}
|
|
||||||
users := stubUsers{u: &userentity.User{ID: 7, Nickname: "Bot"}}
|
|
||||||
svc := NewService(nil, sq, users, nil, Options{
|
|
||||||
Enabled: true, Interval: time.Minute, TTL: time.Hour, BotIndexes: []int{1},
|
|
||||||
})
|
|
||||||
svc.opts.Feeds = []string{"http://127.0.0.1:1/nope"}
|
|
||||||
err := svc.Tick(t.Context())
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected feed error")
|
|
||||||
}
|
|
||||||
if sq.expired != 1 {
|
|
||||||
t.Fatalf("expire called %d", sq.expired)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package logic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Worker runs Tick on an interval until Stop.
|
|
||||||
type Worker struct {
|
|
||||||
log *zap.Logger
|
|
||||||
svc *Service
|
|
||||||
stop chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorker(log *zap.Logger, svc *Service) *Worker {
|
|
||||||
if log == nil {
|
|
||||||
log = zap.NewNop()
|
|
||||||
}
|
|
||||||
return &Worker{log: log, svc: svc, stop: make(chan struct{})}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Worker) Start() {
|
|
||||||
if w == nil || w.svc == nil || !w.svc.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.wg.Add(1)
|
|
||||||
go w.loop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Worker) Stop() {
|
|
||||||
if w == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-w.stop:
|
|
||||||
default:
|
|
||||||
close(w.stop)
|
|
||||||
}
|
|
||||||
w.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Worker) loop() {
|
|
||||||
defer w.wg.Done()
|
|
||||||
interval := w.svc.Interval()
|
|
||||||
// Run once shortly after boot so ops can verify without waiting a full interval.
|
|
||||||
timer := time.NewTimer(15 * time.Second)
|
|
||||||
defer timer.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-w.stop:
|
|
||||||
return
|
|
||||||
case <-timer.C:
|
|
||||||
w.runOnce()
|
|
||||||
timer.Reset(interval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Worker) runOnce() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := w.svc.Tick(ctx); err != nil {
|
|
||||||
w.log.Warn("squarebot tick", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,11 +7,8 @@ import (
|
|||||||
"spark-server/internal/module/upload/entity"
|
"spark-server/internal/module/upload/entity"
|
||||||
"spark-server/internal/pkg/errs"
|
"spark-server/internal/pkg/errs"
|
||||||
"spark-server/internal/pkg/i18n"
|
"spark-server/internal/pkg/i18n"
|
||||||
"spark-server/internal/pkg/media"
|
|
||||||
"spark-server/internal/pkg/reqctx"
|
"spark-server/internal/pkg/reqctx"
|
||||||
"spark-server/internal/pkg/validate"
|
"spark-server/internal/pkg/validate"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateInput requests a single-object Presigned PUT. The client does not choose the key.
|
// CreateInput requests a single-object Presigned PUT. The client does not choose the key.
|
||||||
@ -46,9 +43,6 @@ func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.checkDeclaredSize(ctx, in.Purpose, in.Size); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name := reqctx.NewObjectName()
|
name := reqctx.NewObjectName()
|
||||||
ownerID := userID
|
ownerID := userID
|
||||||
if in.Purpose == entity.PurposeGroupAvatar {
|
if in.Purpose == entity.PurposeGroupAvatar {
|
||||||
@ -75,41 +69,6 @@ func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDeclaredSize applies soft media policy on credential size (no dimensions at this stage).
|
|
||||||
func (s *Service) checkDeclaredSize(ctx context.Context, purpose string, size int64) error {
|
|
||||||
if s == nil || !s.media.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var v *media.Violation
|
|
||||||
switch purpose {
|
|
||||||
case entity.PurposeAvatar, entity.PurposeGroupAvatar, entity.PurposeMomentCover:
|
|
||||||
v = s.media.CheckAvatarMeta(0, 0, size)
|
|
||||||
case entity.PurposeChatVideo, entity.PurposeMomentVideo, entity.PurposeSquareVideo:
|
|
||||||
v = s.media.CheckVideoMeta(0, 0, size, 0)
|
|
||||||
case entity.PurposeChatImage, entity.PurposeMomentImage, entity.PurposeSquareImage, entity.PurposeChatGIF:
|
|
||||||
v = s.media.CheckImageMeta(0, 0, size)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log := s.log
|
|
||||||
if log == nil {
|
|
||||||
log = zap.NewNop()
|
|
||||||
}
|
|
||||||
log.Warn("upload media size soft-check",
|
|
||||||
zap.String("purpose", purpose),
|
|
||||||
zap.String("reason", v.Reason),
|
|
||||||
zap.Int64("size", v.Size),
|
|
||||||
zap.Int64("max_size", v.MaxSize),
|
|
||||||
)
|
|
||||||
if !s.media.UserReject {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errs.ErrMediaOversized.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyMediaOversized, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchSpec(ctx context.Context, in *CreateInput) (entity.Spec, string, error) {
|
func matchSpec(ctx context.Context, in *CreateInput) (entity.Spec, string, error) {
|
||||||
lang := reqctx.Lang(ctx)
|
lang := reqctx.Lang(ctx)
|
||||||
spec, ok := entity.LookupSpec(in.Purpose)
|
spec, ok := entity.LookupSpec(in.Purpose)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user