Add comment spam classification and captcha checks.

Score artwork comments with local signatures plus Together AI, and optionally require Turnstile before posting.
This commit is contained in:
2026-09-20 14:48:50 +02:00
parent 37bacc1334
commit 586c44ba74
16 changed files with 858 additions and 135 deletions
@@ -0,0 +1,60 @@
<?php
namespace App\Services\Moderation;
use App\Contracts\Moderation\CommentSpamClassifier;
use App\Data\Moderation\CommentSpamClassification;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use RuntimeException;
final class TogetherCommentSpamClassifier implements CommentSpamClassifier
{
public function classify(string $content): CommentSpamClassification
{
$config = (array) config('comment_spam.ai', []);
$key = (string) ($config['api_key'] ?? '');
$model = (string) ($config['model'] ?? '');
if ($key === '' || $model === '') {
throw new RuntimeException('Together AI is not configured.');
}
if (! in_array($model, (array) ($config['allowed_models'] ?? []), true)) {
throw new RuntimeException('Unsupported Together AI comment spam model.');
}
$started = microtime(true);
$response = Http::timeout(min(5, max(1, (int) ($config['timeout'] ?? 5))))
->withToken($key)
->post(rtrim((string) ($config['base_url'] ?? 'https://api.together.xyz/v1'), '/').'/chat/completions', [
'model' => $model,
'temperature' => 0,
'response_format' => ['type' => 'json_object'],
'messages' => [
['role' => 'system', 'content' => 'Classify the comment as spam or not spam. Return only JSON: {"spam":true|false,"confidence":0.0,"reason":"short explanation"}. Confidence must be a number from 0 to 1. Spam includes advertising, SEO promotion, unsolicited links, scams, and bot-like promotional repetition.'],
['role' => 'user', 'content' => mb_substr($content, 0, (int) ($config['max_input_chars'] ?? 2500))],
],
]);
if ($response->failed()) {
throw new RuntimeException('Together AI request failed with status '.$response->status().'.');
}
$message = Arr::get($response->json(), 'choices.0.message.content');
$decoded = is_string($message) ? json_decode($message, true) : null;
if (! is_array($decoded) || ! is_bool($decoded['spam'] ?? null)
|| ! is_numeric($decoded['confidence'] ?? null)
|| $decoded['confidence'] < 0 || $decoded['confidence'] > 1
|| ! is_string($decoded['reason'] ?? null)) {
throw new RuntimeException('Together AI returned invalid moderation JSON.');
}
return new CommentSpamClassification(
$decoded['spam'],
(float) $decoded['confidence'],
mb_substr(trim($decoded['reason']), 0, 500),
'together',
$model,
(int) round((microtime(true) - $started) * 1000),
);
}
}