package creator import ( "errors" "math" "strings" "time" ) var ( ErrInvalid = errors.New("invalid creator input") ErrConflict = errors.New("creator resource conflicts with existing state") ErrNotFound = errors.New("creator resource not found") ErrUnavailable = errors.New("creator capability is unavailable") ErrUncertain = errors.New("creator operation result is uncertain") ) func ValidatePlatform(platform string) bool { return platform == PlatformDouyin || platform == PlatformXiaohongshu } const maxDurationSeconds = int64(math.MaxInt64 / int64(time.Second)) func ValidateSettings(input SettingsUpdate) error { if input.LookbackDays <= 0 || int64(input.LookbackDays) > maxDurationSeconds/(24*60*60) || input.NewWorkIntervalSeconds <= 0 || input.NewWorkIntervalSeconds > maxDurationSeconds || input.MetricInitialIntervalSeconds <= 0 || input.MetricInitialIntervalSeconds > maxDurationSeconds || math.IsNaN(input.MetricMultiplier) || math.IsInf(input.MetricMultiplier, 0) || input.MetricMultiplier <= 1 || input.MetricMaxIntervalSeconds < input.MetricInitialIntervalSeconds || input.MetricMaxIntervalSeconds > maxDurationSeconds || input.MetricAgeSeconds <= 0 || input.MetricAgeSeconds > maxDurationSeconds { return ErrInvalid } return nil } // NextMetricAt returns the next scheduled point strictly after now. A point // exactly at now is considered covered by the current collection and is not // returned again. func NextMetricAt(publishedAt, now time.Time, initial, max time.Duration, multiplier float64, monitoringAge time.Duration) (time.Time, string) { publishedAt, now = publishedAt.UTC(), now.UTC() if publishedAt.IsZero() || !publishedAt.Before(now) || initial <= 0 || max < initial || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 1 || monitoringAge <= 0 { return time.Time{}, "published_at_pending_verification" } if !now.Before(publishedAt.Add(monitoringAge)) { return time.Time{}, "monitoring_age_reached" } elapsed := now.Sub(publishedAt) interval := initial total := initial for total <= elapsed { candidate := float64(interval) * multiplier var next time.Duration if math.IsNaN(candidate) || math.IsInf(candidate, 0) || candidate < float64(interval) || candidate > float64(max) { next = max } else { next = time.Duration(candidate) if next < interval { next = max } } interval = next if total > time.Duration(math.MaxInt64)-interval { return time.Time{}, "monitoring_age_reached" } total += interval } candidate := publishedAt.Add(total) if !candidate.Before(publishedAt.Add(monitoringAge)) { return time.Time{}, "monitoring_age_reached" } return candidate, "" } func MetricSchedule(publishedAt time.Time, initial, max time.Duration, multiplier float64, count int) []time.Time { if count <= 0 || publishedAt.IsZero() || initial <= 0 || max < initial || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 1 { return []time.Time{} } points := make([]time.Time, 0, count) interval, total := initial, initial for len(points) < count { points = append(points, publishedAt.UTC().Add(total)) candidate := float64(interval) * multiplier var next time.Duration if math.IsNaN(candidate) || math.IsInf(candidate, 0) || candidate < float64(interval) || candidate > float64(max) { next = max } else { next = time.Duration(candidate) if next < interval { next = max } } interval = next if total > time.Duration(math.MaxInt64)-interval { break } total += interval } return points } // TrimKeyword removes only the edge whitespace allowed by the product rule. // It intentionally does not normalize case, Unicode, full-width characters, // or whitespace inside a keyword. func TrimKeyword(value string) string { return strings.Trim(value, " \t\r\n") } func NormalizeKeywords(values []string) ([]string, error) { result := make([]string, len(values)) for index, value := range values { result[index] = TrimKeyword(value) if result[index] == "" { return nil, ErrInvalid } } return result, nil } func ValidateRuleInput(input LeadRuleInput) (LeadRuleInput, error) { input.Name = strings.TrimSpace(input.Name) input.Topic = strings.TrimSpace(input.Topic) input.AIRequirement = strings.TrimSpace(input.AIRequirement) input.SourceType = strings.TrimSpace(input.SourceType) if input.SourceType == "" { input.SourceType = "all" } if input.Name == "" || input.Topic == "" || input.AIRequirement == "" || (input.SourceType != "all" && input.SourceType != SourceOwned && input.SourceType != SourceCompetitor) { return LeadRuleInput{}, ErrInvalid } var err error input.IncludeKeywords, err = NormalizeKeywords(input.IncludeKeywords) if err != nil || len(input.IncludeKeywords) == 0 { return LeadRuleInput{}, ErrInvalid } input.ExcludeKeywords, err = NormalizeKeywords(input.ExcludeKeywords) if err != nil { return LeadRuleInput{}, ErrInvalid } return input, nil } func MatchKeywords(content string, include, exclude []string) (bool, []string) { matched := make([]string, 0, len(include)) for _, keyword := range include { if strings.Contains(content, keyword) { matched = append(matched, keyword) } } if len(matched) == 0 { return false, matched } for _, keyword := range exclude { if strings.Contains(content, keyword) { return false, matched } } return true, matched } func ValidEventType(eventType string) bool { switch eventType { case "comment", "like", "repost", "follow", "dm": return true default: return false } } func ValidMessageType(messageType string) bool { switch messageType { case MessageTypeText, MessageTypeImage, MessageTypeVoice, MessageTypeVideo, MessageTypeSticker, MessageTypeUnknown: return true default: return false } } func ValidAction(action string) bool { switch action { case ActionDM, ActionReplyComment, ActionLikeComment, ActionLikeWork, ActionFollow, ActionRepost: return true default: return false } } func ActionRequiresText(action string) bool { return action == ActionDM || action == ActionReplyComment || action == ActionRepost } func validCooldownSeconds(seconds int64) bool { return seconds > 0 && seconds <= maxDurationSeconds } func ActionTargetValid(action string, interactorUID, commentID, workID, targetType string) bool { if interactorUID == "" { return false } switch action { case ActionDM, ActionFollow: return true case ActionReplyComment, ActionLikeComment: return commentID != "" case ActionLikeWork: return workID != "" case ActionRepost: return workID != "" default: return false } } func CanWrite(profile AccountProfile, automatic bool, action string) error { if !ValidAction(action) { return ErrInvalid } if profile.BusinessStatus != "normal" { if automatic || profile.BusinessStatus == "banned" || profile.BusinessStatus == "deleted" || (action == ActionReplyComment || action == ActionDM || action == ActionRepost) { return ErrConflict } } if profile.LoginStatus != "logged_in" || profile.AuthorizationStatus != "authorized" { return ErrConflict } return nil } func InWindow(publishedAt, start, end time.Time) bool { return !publishedAt.IsZero() && !publishedAt.Before(start.UTC()) && !publishedAt.After(end.UTC()) }