Implement creator studio and upload updates
This commit is contained in:
182
app/Services/Moderation/ContentModerationPersistenceService.php
Normal file
182
app/Services/Moderation/ContentModerationPersistenceService.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Moderation;
|
||||
|
||||
use App\Data\Moderation\ModerationResultData;
|
||||
use App\Enums\ModerationEscalationStatus;
|
||||
use App\Enums\ModerationContentType;
|
||||
use App\Enums\ModerationStatus;
|
||||
use App\Models\ContentModerationAiSuggestion;
|
||||
use App\Models\ContentModerationFinding;
|
||||
|
||||
class ContentModerationPersistenceService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContentModerationReviewService $review,
|
||||
private readonly ContentModerationActionLogService $actionLogs,
|
||||
) {
|
||||
}
|
||||
|
||||
public function shouldQueue(ModerationResultData $result): bool
|
||||
{
|
||||
return $result->score >= (int) \app('config')->get('content_moderation.queue_threshold', 30);
|
||||
}
|
||||
|
||||
public function hasCurrentFinding(string $contentType, int $contentId, string $contentHash, string $scannerVersion): bool
|
||||
{
|
||||
return ContentModerationFinding::query()
|
||||
->where('content_type', $contentType)
|
||||
->where('content_id', $contentId)
|
||||
->where('content_hash', $contentHash)
|
||||
->where('scanner_version', $scannerVersion)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
* @return array{finding:?ContentModerationFinding, created:bool, updated:bool}
|
||||
*/
|
||||
public function persist(ModerationResultData $result, array $context): array
|
||||
{
|
||||
$contentType = (string) ($context['content_type'] ?? '');
|
||||
$contentId = (int) ($context['content_id'] ?? 0);
|
||||
|
||||
if ($contentType === '' || $contentId <= 0) {
|
||||
return ['finding' => null, 'created' => false, 'updated' => false];
|
||||
}
|
||||
|
||||
$existing = ContentModerationFinding::query()
|
||||
->where('content_type', $contentType)
|
||||
->where('content_id', $contentId)
|
||||
->where('content_hash', $result->contentHash)
|
||||
->where('scanner_version', $result->scannerVersion)
|
||||
->first();
|
||||
|
||||
if (! $this->shouldQueue($result) && $existing === null) {
|
||||
return ['finding' => null, 'created' => false, 'updated' => false];
|
||||
}
|
||||
|
||||
$finding = $existing ?? new ContentModerationFinding();
|
||||
$isNew = ! $finding->exists;
|
||||
|
||||
$finding->fill([
|
||||
'content_type' => $contentType,
|
||||
'content_id' => $contentId,
|
||||
'content_target_type' => $result->contentTargetType,
|
||||
'content_target_id' => $result->contentTargetId,
|
||||
'artwork_id' => $context['artwork_id'] ?? null,
|
||||
'user_id' => $context['user_id'] ?? null,
|
||||
'severity' => $result->severity->value,
|
||||
'score' => $result->score,
|
||||
'content_hash' => $result->contentHash,
|
||||
'scanner_version' => $result->scannerVersion,
|
||||
'reasons_json' => $result->reasons,
|
||||
'matched_links_json' => $result->matchedLinks,
|
||||
'matched_domains_json' => $result->matchedDomains,
|
||||
'matched_keywords_json' => $result->matchedKeywords,
|
||||
'rule_hits_json' => $result->ruleHits,
|
||||
'score_breakdown_json' => $result->scoreBreakdown,
|
||||
'content_hash_normalized' => $result->contentHashNormalized,
|
||||
'group_key' => $result->groupKey,
|
||||
'campaign_key' => $result->campaignKey,
|
||||
'cluster_score' => $result->clusterScore,
|
||||
'cluster_reason' => $result->clusterReason,
|
||||
'priority_score' => $result->priorityScore,
|
||||
'policy_name' => $result->policyName,
|
||||
'review_bucket' => $result->reviewBucket,
|
||||
'escalation_status' => $result->escalationStatus ?? ModerationEscalationStatus::None->value,
|
||||
'ai_provider' => $result->aiProvider,
|
||||
'ai_label' => $result->aiLabel,
|
||||
'ai_suggested_action' => $result->aiSuggestedAction,
|
||||
'ai_confidence' => $result->aiConfidence,
|
||||
'ai_explanation' => $result->aiExplanation,
|
||||
'user_risk_score' => $result->userRiskScore,
|
||||
'content_snapshot' => (string) ($context['content_snapshot'] ?? ''),
|
||||
'auto_action_taken' => $result->autoHideRecommended ? 'recommended' : null,
|
||||
]);
|
||||
|
||||
if ($isNew) {
|
||||
$finding->status = $result->status;
|
||||
} elseif (! $this->shouldQueue($result) && $finding->isPending()) {
|
||||
$finding->status = ModerationStatus::Resolved;
|
||||
$finding->action_taken = 'rescanned_clean';
|
||||
$finding->resolved_at = \now();
|
||||
$finding->resolved_by = null;
|
||||
}
|
||||
|
||||
$finding->save();
|
||||
|
||||
if ($result->aiProvider !== null && ($result->aiLabel !== null || $result->aiExplanation !== null || $result->aiConfidence !== null)) {
|
||||
ContentModerationAiSuggestion::query()->create([
|
||||
'finding_id' => $finding->id,
|
||||
'provider' => $result->aiProvider,
|
||||
'suggested_label' => $result->aiLabel,
|
||||
'suggested_action' => $result->aiSuggestedAction,
|
||||
'confidence' => $result->aiConfidence,
|
||||
'explanation' => $result->aiExplanation,
|
||||
'campaign_tags_json' => $result->campaignKey ? [$result->campaignKey] : [],
|
||||
'raw_response_json' => $result->aiRawResponse,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $isNew && ! $this->shouldQueue($result) && $finding->action_taken === 'rescanned_clean') {
|
||||
$this->actionLogs->log(
|
||||
$finding,
|
||||
'finding',
|
||||
$finding->id,
|
||||
'rescan',
|
||||
null,
|
||||
ModerationStatus::Pending->value,
|
||||
ModerationStatus::Resolved->value,
|
||||
null,
|
||||
null,
|
||||
'Finding resolved automatically after a clean rescan.',
|
||||
['scanner_version' => $result->scannerVersion],
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'finding' => $finding,
|
||||
'created' => $isNew,
|
||||
'updated' => ! $isNew,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function applyAutomatedActionIfNeeded(ContentModerationFinding $finding, ModerationResultData $result, array $context): bool
|
||||
{
|
||||
if (! $result->autoHideRecommended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($finding->is_auto_hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$supportedTypes = (array) \app('config')->get('content_moderation.auto_hide.supported_types', []);
|
||||
if (! in_array($finding->content_type->value, $supportedTypes, true)) {
|
||||
$finding->forceFill([
|
||||
'auto_action_taken' => 'recommended_review',
|
||||
])->save();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! in_array($finding->content_type, [ModerationContentType::ArtworkComment, ModerationContentType::ArtworkDescription], true)) {
|
||||
if ($finding->content_type !== ModerationContentType::ArtworkTitle) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! in_array($finding->content_type, [ModerationContentType::ArtworkComment, ModerationContentType::ArtworkDescription, ModerationContentType::ArtworkTitle], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->review->autoHideContent($finding, 'Triggered by automated moderation threshold.');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user