172 lines
6.0 KiB
Go
172 lines
6.0 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"haixun-backend/internal/library/clock"
|
|
app "haixun-backend/internal/library/errors"
|
|
"haixun-backend/internal/library/errors/code"
|
|
"haixun-backend/internal/model/publish_guard/domain/entity"
|
|
domrepo "haixun-backend/internal/model/publish_guard/domain/repository"
|
|
domusecase "haixun-backend/internal/model/publish_guard/domain/usecase"
|
|
threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type guardUseCase struct {
|
|
repo domrepo.Repository
|
|
threadsAccount threadsaccountdomain.UseCase
|
|
}
|
|
|
|
func NewUseCase(repo domrepo.Repository, threadsAccount threadsaccountdomain.UseCase) domusecase.UseCase {
|
|
return &guardUseCase{repo: repo, threadsAccount: threadsAccount}
|
|
}
|
|
|
|
func (u *guardUseCase) Get(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) {
|
|
if err := u.require(ctx, tenantID, ownerUID, accountID); err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := u.repo.Get(ctx, tenantID, ownerUID, accountID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
item = defaultPolicy(tenantID, ownerUID, accountID)
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
out := toSummary(item)
|
|
return &out, nil
|
|
}
|
|
|
|
func (u *guardUseCase) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.PolicySummary, error) {
|
|
if err := u.require(ctx, req.TenantID, req.OwnerUID, req.AccountID); err != nil {
|
|
return nil, err
|
|
}
|
|
item := defaultPolicy(req.TenantID, req.OwnerUID, req.AccountID)
|
|
item.Enabled = req.Enabled
|
|
if req.MaxDailyPosts > 0 {
|
|
item.MaxDailyPosts = req.MaxDailyPosts
|
|
}
|
|
if req.MinIntervalMinutes > 0 {
|
|
item.MinIntervalMinutes = req.MinIntervalMinutes
|
|
}
|
|
if req.ConsecutiveFailurePauseLimit > 0 {
|
|
item.ConsecutiveFailurePauseLimit = req.ConsecutiveFailurePauseLimit
|
|
}
|
|
if req.Paused != nil {
|
|
item.Paused = *req.Paused
|
|
}
|
|
item.PausedReason = strings.TrimSpace(req.PausedReason)
|
|
item.UpdateAt = clock.NowUnixNano()
|
|
saved, err := u.repo.Upsert(ctx, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := toSummary(saved)
|
|
return &out, nil
|
|
}
|
|
|
|
func (u *guardUseCase) Resume(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) {
|
|
policy, err := u.Get(ctx, tenantID, ownerUID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paused := false
|
|
return u.Upsert(ctx, domusecase.UpsertRequest{
|
|
TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled,
|
|
MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes,
|
|
ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit,
|
|
Paused: &paused,
|
|
})
|
|
}
|
|
|
|
func (u *guardUseCase) Pause(ctx context.Context, tenantID, ownerUID, accountID, reason string) (*domusecase.PolicySummary, error) {
|
|
policy, err := u.Get(ctx, tenantID, ownerUID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paused := true
|
|
return u.Upsert(ctx, domusecase.UpsertRequest{
|
|
TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled,
|
|
MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes,
|
|
ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit,
|
|
Paused: &paused, PausedReason: reason,
|
|
})
|
|
}
|
|
|
|
func (u *guardUseCase) Check(ctx context.Context, req domusecase.CheckRequest) (*domusecase.CheckResult, error) {
|
|
policy, err := u.Get(ctx, req.TenantID, req.OwnerUID, req.AccountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !policy.Enabled {
|
|
return &domusecase.CheckResult{Allowed: true}, nil
|
|
}
|
|
now := req.Now
|
|
if now <= 0 {
|
|
now = clock.NowUnixNano()
|
|
}
|
|
if policy.Paused {
|
|
return &domusecase.CheckResult{Allowed: false, Reason: firstNonEmpty(policy.PausedReason, "發文已暫停")}, nil
|
|
}
|
|
if policy.MaxDailyPosts > 0 && req.PublishedToday >= int64(policy.MaxDailyPosts) {
|
|
return &domusecase.CheckResult{Allowed: false, DelayUntil: now + int64(time.Hour), Reason: "已達每日發文上限"}, nil
|
|
}
|
|
if policy.MinIntervalMinutes > 0 && req.LastPublishedAt > 0 {
|
|
minNext := req.LastPublishedAt + int64(time.Duration(policy.MinIntervalMinutes)*time.Minute)
|
|
if minNext > now {
|
|
return &domusecase.CheckResult{Allowed: false, DelayUntil: minNext, Reason: "未達最小發文間隔"}, nil
|
|
}
|
|
}
|
|
if policy.ConsecutiveFailurePauseLimit > 0 && req.FailureStreak >= policy.ConsecutiveFailurePauseLimit {
|
|
return &domusecase.CheckResult{Allowed: false, Reason: "連續失敗達上限,已暫停帳號發文", ShouldPause: true}, nil
|
|
}
|
|
return &domusecase.CheckResult{Allowed: true}, nil
|
|
}
|
|
|
|
func (u *guardUseCase) require(ctx context.Context, tenantID, ownerUID, accountID string) error {
|
|
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" {
|
|
return app.For(code.Auth).AuthUnauthorized("missing actor")
|
|
}
|
|
if strings.TrimSpace(accountID) == "" {
|
|
return app.For(code.ThreadsAccount).InputMissingRequired("account_id is required")
|
|
}
|
|
if u.threadsAccount != nil {
|
|
_, err := u.threadsAccount.Get(ctx, tenantID, ownerUID, accountID)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultPolicy(tenantID, ownerUID, accountID string) *entity.Policy {
|
|
now := clock.NowUnixNano()
|
|
return &entity.Policy{
|
|
ID: uuid.NewString(), TenantID: strings.TrimSpace(tenantID), OwnerUID: strings.TrimSpace(ownerUID), AccountID: strings.TrimSpace(accountID),
|
|
Enabled: true, MaxDailyPosts: 6, MinIntervalMinutes: 90, ConsecutiveFailurePauseLimit: 3,
|
|
CreateAt: now, UpdateAt: now,
|
|
}
|
|
}
|
|
|
|
func toSummary(item *entity.Policy) domusecase.PolicySummary {
|
|
if item == nil {
|
|
return domusecase.PolicySummary{}
|
|
}
|
|
return domusecase.PolicySummary{
|
|
AccountID: item.AccountID, Enabled: item.Enabled, MaxDailyPosts: item.MaxDailyPosts,
|
|
MinIntervalMinutes: item.MinIntervalMinutes, ConsecutiveFailurePauseLimit: item.ConsecutiveFailurePauseLimit,
|
|
Paused: item.Paused, PausedReason: item.PausedReason, UpdateAt: item.UpdateAt,
|
|
}
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|