Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -379,6 +379,18 @@ class Artwork extends Model
protected static function booted(): void
{
static::saving(function (Artwork $artwork): void {
if ($artwork->published_at === null) {
return;
}
$publishedAt = $artwork->published_at->copy();
if ($artwork->created_at === null || ! $artwork->created_at->equalTo($publishedAt)) {
$artwork->created_at = $publishedAt;
}
});
static::deleting(function (Artwork $artwork): void {
if (! method_exists($artwork, 'isForceDeleting') || ! $artwork->isForceDeleting()) {
return;

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContentModerationActionLog extends Model
{
protected $table = 'content_moderation_action_logs';
public $timestamps = false;
protected $fillable = [
'finding_id',
'target_type',
'target_id',
'action_type',
'actor_type',
'actor_id',
'old_status',
'new_status',
'old_visibility',
'new_visibility',
'notes',
'meta_json',
'created_at',
];
protected $casts = [
'finding_id' => 'integer',
'target_id' => 'integer',
'actor_id' => 'integer',
'meta_json' => 'array',
'created_at' => 'datetime',
];
public function finding(): BelongsTo
{
return $this->belongsTo(ContentModerationFinding::class, 'finding_id');
}
public function actor(): BelongsTo
{
return $this->belongsTo(User::class, 'actor_id');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContentModerationAiSuggestion extends Model
{
protected $table = 'content_moderation_ai_suggestions';
public $timestamps = false;
protected $fillable = [
'finding_id',
'provider',
'suggested_label',
'suggested_action',
'confidence',
'explanation',
'campaign_tags_json',
'raw_response_json',
'created_at',
];
protected $casts = [
'finding_id' => 'integer',
'confidence' => 'integer',
'campaign_tags_json' => 'array',
'raw_response_json' => 'array',
'created_at' => 'datetime',
];
public function finding(): BelongsTo
{
return $this->belongsTo(ContentModerationFinding::class, 'finding_id');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContentModerationCluster extends Model
{
protected $table = 'content_moderation_clusters';
protected $fillable = [
'campaign_key',
'cluster_reason',
'review_bucket',
'escalation_status',
'cluster_score',
'findings_count',
'unique_users_count',
'unique_domains_count',
'latest_finding_at',
'summary_json',
];
protected $casts = [
'cluster_score' => 'integer',
'findings_count' => 'integer',
'unique_users_count' => 'integer',
'unique_domains_count' => 'integer',
'latest_finding_at' => 'datetime',
'summary_json' => 'array',
];
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use App\Enums\ModerationDomainStatus;
use Illuminate\Database\Eloquent\Model;
class ContentModerationDomain extends Model
{
protected $table = 'content_moderation_domains';
protected $fillable = [
'domain',
'status',
'times_seen',
'times_flagged',
'times_confirmed_spam',
'linked_users_count',
'linked_findings_count',
'linked_clusters_count',
'first_seen_at',
'last_seen_at',
'top_keywords_json',
'top_content_types_json',
'false_positive_count',
'notes',
];
protected $casts = [
'status' => ModerationDomainStatus::class,
'times_seen' => 'integer',
'times_flagged' => 'integer',
'times_confirmed_spam' => 'integer',
'linked_users_count' => 'integer',
'linked_findings_count' => 'integer',
'linked_clusters_count' => 'integer',
'first_seen_at' => 'datetime',
'last_seen_at' => 'datetime',
'top_keywords_json' => 'array',
'top_content_types_json' => 'array',
'false_positive_count' => 'integer',
];
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContentModerationFeedback extends Model
{
protected $table = 'content_moderation_feedback';
public $timestamps = false;
protected $fillable = [
'finding_id',
'feedback_type',
'actor_id',
'notes',
'meta_json',
'created_at',
];
protected $casts = [
'finding_id' => 'integer',
'actor_id' => 'integer',
'meta_json' => 'array',
'created_at' => 'datetime',
];
public function finding(): BelongsTo
{
return $this->belongsTo(ContentModerationFinding::class, 'finding_id');
}
public function actor(): BelongsTo
{
return $this->belongsTo(User::class, 'actor_id');
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Models;
use App\Enums\ModerationEscalationStatus;
use App\Enums\ModerationContentType;
use App\Enums\ModerationSeverity;
use App\Enums\ModerationStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property ModerationContentType $content_type
* @property int $content_id
* @property int|null $artwork_id
* @property int|null $user_id
* @property ModerationStatus $status
* @property ModerationSeverity $severity
* @property int $score
* @property string $content_hash
* @property string $scanner_version
* @property array|null $reasons_json
* @property array|null $matched_links_json
* @property array|null $matched_domains_json
* @property array|null $matched_keywords_json
* @property string|null $content_snapshot
* @property int|null $reviewed_by
* @property \Illuminate\Support\Carbon|null $reviewed_at
* @property string|null $action_taken
* @property string|null $admin_notes
* @property string|null $content_hash_normalized
* @property string|null $group_key
* @property array|null $rule_hits_json
* @property array|null $domain_ids_json
* @property int|null $user_risk_score
* @property bool $is_auto_hidden
* @property string|null $auto_action_taken
* @property \Illuminate\Support\Carbon|null $auto_hidden_at
* @property int|null $resolved_by
* @property \Illuminate\Support\Carbon|null $resolved_at
* @property int|null $restored_by
* @property \Illuminate\Support\Carbon|null $restored_at
* @property string|null $content_target_type
* @property int|null $content_target_id
* @property string|null $campaign_key
* @property int|null $cluster_score
* @property string|null $cluster_reason
* @property int|null $priority_score
* @property string|null $ai_provider
* @property string|null $ai_label
* @property int|null $ai_confidence
* @property string|null $ai_explanation
* @property bool $is_false_positive
* @property int $false_positive_count
* @property string|null $policy_name
* @property string|null $review_bucket
* @property ModerationEscalationStatus $escalation_status
* @property array|null $score_breakdown_json
*/
class ContentModerationFinding extends Model
{
protected $table = 'content_moderation_findings';
protected $fillable = [
'content_type',
'content_id',
'artwork_id',
'user_id',
'status',
'severity',
'score',
'content_hash',
'scanner_version',
'reasons_json',
'matched_links_json',
'matched_domains_json',
'matched_keywords_json',
'content_snapshot',
'reviewed_by',
'reviewed_at',
'action_taken',
'admin_notes',
'content_hash_normalized',
'group_key',
'rule_hits_json',
'domain_ids_json',
'user_risk_score',
'is_auto_hidden',
'auto_action_taken',
'auto_hidden_at',
'resolved_by',
'resolved_at',
'restored_by',
'restored_at',
'content_target_type',
'content_target_id',
'campaign_key',
'cluster_score',
'cluster_reason',
'priority_score',
'ai_provider',
'ai_label',
'ai_confidence',
'ai_explanation',
'is_false_positive',
'false_positive_count',
'policy_name',
'review_bucket',
'escalation_status',
'score_breakdown_json',
];
protected $casts = [
'content_type' => ModerationContentType::class,
'status' => ModerationStatus::class,
'severity' => ModerationSeverity::class,
'score' => 'integer',
'reasons_json' => 'array',
'matched_links_json' => 'array',
'matched_domains_json' => 'array',
'matched_keywords_json' => 'array',
'rule_hits_json' => 'array',
'domain_ids_json' => 'array',
'user_risk_score' => 'integer',
'is_auto_hidden' => 'boolean',
'reviewed_at' => 'datetime',
'auto_hidden_at' => 'datetime',
'resolved_at' => 'datetime',
'restored_at' => 'datetime',
'content_target_id' => 'integer',
'cluster_score' => 'integer',
'priority_score' => 'integer',
'ai_confidence' => 'integer',
'is_false_positive' => 'boolean',
'false_positive_count' => 'integer',
'escalation_status' => ModerationEscalationStatus::class,
'score_breakdown_json' => 'array',
];
public function artwork(): BelongsTo
{
return $this->belongsTo(Artwork::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function reviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by');
}
public function resolver(): BelongsTo
{
return $this->belongsTo(User::class, 'resolved_by');
}
public function restorer(): BelongsTo
{
return $this->belongsTo(User::class, 'restored_by');
}
public function actionLogs(): HasMany
{
return $this->hasMany(ContentModerationActionLog::class, 'finding_id')->orderByDesc('created_at');
}
public function aiSuggestions(): HasMany
{
return $this->hasMany(ContentModerationAiSuggestion::class, 'finding_id')->orderByDesc('created_at');
}
public function feedback(): HasMany
{
return $this->hasMany(ContentModerationFeedback::class, 'finding_id')->orderByDesc('created_at');
}
public function isPending(): bool
{
return $this->status === ModerationStatus::Pending;
}
public function isReviewed(): bool
{
return in_array($this->status, [
ModerationStatus::ReviewedSafe,
ModerationStatus::ConfirmedSpam,
ModerationStatus::Resolved,
], true);
}
public function hasMatchedDomains(): bool
{
return ! empty($this->matched_domains_json);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Enums\ModerationRuleType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContentModerationRule extends Model
{
protected $table = 'content_moderation_rules';
protected $fillable = [
'type',
'value',
'enabled',
'weight',
'notes',
'created_by',
];
protected $casts = [
'type' => ModerationRuleType::class,
'enabled' => 'boolean',
'weight' => 'integer',
];
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@@ -36,12 +36,14 @@ class DashboardPreference extends Model
protected $fillable = [
'user_id',
'pinned_spaces',
'studio_preferences',
];
protected function casts(): array
{
return [
'pinned_spaces' => 'array',
'studio_preferences' => 'array',
];
}

View File

@@ -28,6 +28,7 @@ class NovaCard extends Model
public const VISIBILITY_PRIVATE = 'private';
public const STATUS_DRAFT = 'draft';
public const STATUS_SCHEDULED = 'scheduled';
public const STATUS_PROCESSING = 'processing';
public const STATUS_PUBLISHED = 'published';
public const STATUS_HIDDEN = 'hidden';
@@ -85,6 +86,8 @@ class NovaCard extends Model
'allow_export',
'original_creator_id',
'published_at',
'scheduled_for',
'scheduling_timezone',
'last_engaged_at',
'last_ranked_at',
'last_rendered_at',
@@ -114,6 +117,7 @@ class NovaCard extends Model
'allow_background_reuse' => 'boolean',
'allow_export' => 'boolean',
'published_at' => 'datetime',
'scheduled_for' => 'datetime',
'last_engaged_at' => 'datetime',
'last_ranked_at' => 'datetime',
'last_rendered_at' => 'datetime',
@@ -245,6 +249,12 @@ class NovaCard extends Model
return null;
}
// Prefer an explicit CDN URL so images are served through the CDN edge layer.
$cdnBase = (string) env('FILES_CDN_URL', '');
if ($cdnBase !== '') {
return rtrim($cdnBase, '/') . '/' . ltrim($this->preview_path, '/');
}
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($this->preview_path);
}
@@ -259,6 +269,11 @@ class NovaCard extends Model
return $this->previewUrl();
}
$cdnBase = (string) env('FILES_CDN_URL', '');
if ($cdnBase !== '') {
return rtrim($cdnBase, '/') . '/' . ltrim($ogPath, '/');
}
return Storage::disk((string) config('nova_cards.storage.public_disk', 'public'))->url($ogPath);
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserSocialLink extends Model
{
protected $table = 'user_social_links';
protected $fillable = [
'user_id',
'platform',
'url',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}