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:
@@ -1,95 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Observers\ArtworkCommentObserver;
|
||||
use App\Services\ContentSanitizer;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* App\Models\ArtworkComment
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $artwork_id
|
||||
* @property int $user_id
|
||||
* @property string|null $content Legacy plain-text column
|
||||
* @property string|null $raw_content User-submitted Markdown
|
||||
* @property string|null $rendered_content Cached sanitized HTML
|
||||
* @property bool $is_approved
|
||||
* @property int $id
|
||||
* @property int $artwork_id
|
||||
* @property int $user_id
|
||||
* @property string|null $content Legacy plain-text column
|
||||
* @property string|null $raw_content User-submitted Markdown
|
||||
* @property string|null $rendered_content Cached sanitized HTML
|
||||
* @property bool $is_approved
|
||||
* @property-read Artwork $artwork
|
||||
* @property-read User $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|CommentReaction[] $reactions
|
||||
* @property-read Collection|CommentReaction[] $reactions
|
||||
*/
|
||||
#[ObservedBy([ArtworkCommentObserver::class])]
|
||||
class ArtworkComment extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'artwork_comments';
|
||||
protected $table = 'artwork_comments';
|
||||
|
||||
protected $fillable = [
|
||||
'legacy_id',
|
||||
'artwork_id',
|
||||
'user_id',
|
||||
'parent_id',
|
||||
'content',
|
||||
'raw_content',
|
||||
'rendered_content',
|
||||
'is_approved',
|
||||
];
|
||||
protected $fillable = [
|
||||
'legacy_id',
|
||||
'artwork_id',
|
||||
'user_id',
|
||||
'parent_id',
|
||||
'content',
|
||||
'raw_content',
|
||||
'rendered_content',
|
||||
'is_approved',
|
||||
'spam_score',
|
||||
'spam_probability',
|
||||
'spam_reason',
|
||||
'moderation_source',
|
||||
'moderated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_approved' => 'boolean',
|
||||
];
|
||||
protected $casts = [
|
||||
'is_approved' => 'boolean',
|
||||
'spam_score' => 'integer',
|
||||
'spam_probability' => 'integer',
|
||||
'moderated_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('created_at');
|
||||
}
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively eager-load approved replies (tree structure).
|
||||
*/
|
||||
public function approvedReplies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')
|
||||
->where('is_approved', true)
|
||||
->orderBy('created_at')
|
||||
->with(['user.profile', 'approvedReplies']);
|
||||
}
|
||||
/**
|
||||
* Recursively eager-load approved replies (tree structure).
|
||||
*/
|
||||
public function approvedReplies(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')
|
||||
->where('is_approved', true)
|
||||
->orderBy('created_at')
|
||||
->with(['user.profile', 'approvedReplies']);
|
||||
}
|
||||
|
||||
public function reactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommentReaction::class, 'comment_id');
|
||||
}
|
||||
public function reactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommentReaction::class, 'comment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the best available rendered content for display.
|
||||
* Falls back to escaping raw legacy content if rendering isn't done yet.
|
||||
*/
|
||||
public function getDisplayHtml(): string
|
||||
{
|
||||
if ($this->rendered_content !== null) {
|
||||
return $this->rendered_content;
|
||||
}
|
||||
/**
|
||||
* Return the best available rendered content for display.
|
||||
* Falls back to escaping raw legacy content if rendering isn't done yet.
|
||||
*/
|
||||
public function getDisplayHtml(): string
|
||||
{
|
||||
if ($this->rendered_content !== null) {
|
||||
return $this->rendered_content;
|
||||
}
|
||||
|
||||
// Lazy render: raw_content takes priority over legacy content
|
||||
$raw = $this->raw_content ?? $this->content ?? '';
|
||||
return \App\Services\ContentSanitizer::render($raw);
|
||||
}
|
||||
// Lazy render: raw_content takes priority over legacy content
|
||||
$raw = $this->raw_content ?? $this->content ?? '';
|
||||
|
||||
return ContentSanitizer::render($raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CommentSpamSignature extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'content_hash', 'pattern_signature', 'source', 'reason',
|
||||
'confidence', 'hit_count', 'reviewed_by', 'metadata', 'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'confidence' => 'integer',
|
||||
'hit_count' => 'integer',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user