Files
SkinbaseNova/app/Services/Moderation/ArtworkUploadPolicy.php
T
klevze 0f52803b05 Send new artwork uploads through a trust-based review policy.
Require review for untrusted accounts, add admin artwork review APIs, and keep queued or auto-trusted publishes from counting as established history.
2026-09-20 14:49:06 +02:00

98 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Moderation;
use App\Models\Artwork;
use App\Models\User;
final class ArtworkUploadPolicy
{
/** @return array{requires_review: bool, reasons: array<int, string>, approved_uploads: int} */
public function assess(User $user, ?Artwork $artwork = null): array
{
if ($user->isAdmin()) {
return ['requires_review' => false, 'reasons' => [], 'approved_uploads' => PHP_INT_MAX];
}
$approvedUploads = $this->approvedArtworkCount($user, $artwork);
$minimumApproved = max(1, (int) config('uploads.moderation.minimum_approved_uploads', 5));
$reasons = [];
if ($approvedUploads < $minimumApproved) {
$reasons[] = 'insufficient_approved_uploads';
if ($user->email_verified_at === null) {
$reasons[] = 'email_not_verified';
}
$minimumAgeDays = max(0, (int) config('uploads.moderation.minimum_account_age_days', 7));
if ($user->created_at?->gt(now()->subDays($minimumAgeDays))) {
$reasons[] = 'new_account';
}
if ((int) ($user->level ?? 1) <= (int) config('uploads.moderation.maximum_untrusted_level', 1)) {
$reasons[] = 'low_level';
}
if ((int) ($user->bot_risk_score ?? 0) >= (int) config('uploads.moderation.bot_risk_review_threshold', 40)) {
$reasons[] = 'bot_risk';
}
if ((int) ($user->spam_reports ?? 0) > 0) {
$reasons[] = 'spam_reports';
}
}
if ($artwork && $this->looksPromotional($artwork)) {
$reasons[] = 'promotional_content';
}
return [
'requires_review' => $reasons !== [],
'reasons' => array_values(array_unique($reasons)),
'approved_uploads' => $approvedUploads,
];
}
private function approvedArtworkCount(User $user, ?Artwork $artwork): int
{
$query = $user->artworks()
->where('is_approved', true)
->whereNotNull('published_at')
->where(function ($status): void {
$status->whereNull('artwork_status')
->orWhereNotIn('artwork_status', ['review', 'rejected', 'scheduled', 'draft']);
})
->where(function ($source): void {
$source->whereNull('approval_source')
->orWhere('approval_source', 'moderator');
});
if ($artwork?->exists) {
$query->whereKeyNot($artwork->getKey());
}
return (int) $query->count();
}
private function looksPromotional(Artwork $artwork): bool
{
$text = strtolower(implode(' ', [
(string) $artwork->title,
(string) $artwork->description,
(string) $artwork->file_name,
]));
foreach ((array) config('uploads.moderation.promotional_patterns', []) as $pattern) {
if ($pattern !== '' && str_contains($text, strtolower((string) $pattern))) {
return true;
}
}
return (bool) preg_match('/(?:https?:\/\/|www\.|\b[a-z0-9-]+\.(?:com|net|org|io|co)\b)/i', $text);
}
}