Implement creator studio and upload updates
This commit is contained in:
@@ -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;
|
||||
|
||||
47
app/Models/ContentModerationActionLog.php
Normal file
47
app/Models/ContentModerationActionLog.php
Normal 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');
|
||||
}
|
||||
}
|
||||
38
app/Models/ContentModerationAiSuggestion.php
Normal file
38
app/Models/ContentModerationAiSuggestion.php
Normal 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');
|
||||
}
|
||||
}
|
||||
32
app/Models/ContentModerationCluster.php
Normal file
32
app/Models/ContentModerationCluster.php
Normal 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',
|
||||
];
|
||||
}
|
||||
43
app/Models/ContentModerationDomain.php
Normal file
43
app/Models/ContentModerationDomain.php
Normal 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',
|
||||
];
|
||||
}
|
||||
39
app/Models/ContentModerationFeedback.php
Normal file
39
app/Models/ContentModerationFeedback.php
Normal 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');
|
||||
}
|
||||
}
|
||||
199
app/Models/ContentModerationFinding.php
Normal file
199
app/Models/ContentModerationFinding.php
Normal 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);
|
||||
}
|
||||
}
|
||||
32
app/Models/ContentModerationRule.php
Normal file
32
app/Models/ContentModerationRule.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
22
app/Models/UserSocialLink.php
Normal file
22
app/Models/UserSocialLink.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user