'array', 'is_verified' => 'boolean', 'artworks_count' => 'integer', 'collections_count' => 'integer', 'followers_count' => 'integer', 'founded_at' => 'datetime', 'last_activity_at' => 'datetime', ]; public function owner(): BelongsTo { return $this->belongsTo(User::class, 'owner_user_id'); } public function getRouteKeyName(): string { return 'slug'; } public function members(): HasMany { return $this->hasMany(GroupMember::class); } public function invitations(): HasMany { return $this->hasMany(GroupInvitation::class); } public function follows(): HasMany { return $this->hasMany(GroupFollow::class); } public function artworks(): HasMany { return $this->hasMany(Artwork::class); } public function collections(): HasMany { return $this->hasMany(Collection::class); } public function joinRequests(): HasMany { return $this->hasMany(GroupJoinRequest::class); } public function posts(): HasMany { return $this->hasMany(GroupPost::class); } public function recruitmentProfile(): HasOne { return $this->hasOne(GroupRecruitmentProfile::class); } public function projects(): HasMany { return $this->hasMany(GroupProject::class); } public function releases(): HasMany { return $this->hasMany(GroupRelease::class); } public function challenges(): HasMany { return $this->hasMany(GroupChallenge::class); } public function events(): HasMany { return $this->hasMany(GroupEvent::class); } public function assets(): HasMany { return $this->hasMany(GroupAsset::class); } public function activityItems(): HasMany { return $this->hasMany(GroupActivityItem::class); } public function contributorStats(): HasMany { return $this->hasMany(GroupContributorStat::class); } public function badges(): HasMany { return $this->hasMany(GroupBadge::class); } public function memberBadges(): HasMany { return $this->hasMany(GroupMemberBadge::class); } public function discoveryMetric(): HasOne { return $this->hasOne(GroupDiscoveryMetric::class); } public function historyEntries(): HasMany { return $this->hasMany(GroupHistory::class); } public function scopePublic(Builder $query): Builder { return $query ->where('visibility', self::VISIBILITY_PUBLIC) ->where('status', self::LIFECYCLE_ACTIVE); } public static function acceptedVisibilityValues(): array { return [self::VISIBILITY_PUBLIC, self::VISIBILITY_PRIVATE, self::VISIBILITY_UNLISTED]; } public static function acceptedMembershipPolicies(): array { return [self::MEMBERSHIP_INVITE_ONLY, self::MEMBERSHIP_REQUEST_TO_JOIN, self::MEMBERSHIP_OPEN]; } public static function normalizeMemberRole(string $role): string { $normalized = strtolower(trim($role)); return $normalized === self::ROLE_CONTRIBUTOR ? self::ROLE_MEMBER : $normalized; } public static function displayRole(?string $role): ?string { if ($role === null) { return null; } return self::normalizeMemberRole($role) === self::ROLE_MEMBER ? self::ROLE_CONTRIBUTOR : self::normalizeMemberRole($role); } public function isOwnedBy(User|int|null $user): bool { $userId = $user instanceof User ? $user->id : $user; return $userId !== null && (int) $userId === (int) $this->owner_user_id; } public function isPubliclyVisible(): bool { return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true) && $this->status !== self::LIFECYCLE_SUSPENDED; } public function isOperational(): bool { return $this->status === self::LIFECYCLE_ACTIVE; } public function canArchive(User $user): bool { return $this->isOwnedBy($user); } public function canViewStudio(User $user): bool { if ($this->status === self::LIFECYCLE_SUSPENDED) { return false; } return $this->hasActiveMember($user); } public function activeRoleFor(User|int|null $user): ?string { $userId = $user instanceof User ? $user->id : $user; if ($userId === null) { return null; } if ($this->isOwnedBy($userId)) { return self::ROLE_OWNER; } $members = $this->relationLoaded('members') ? $this->members : $this->members()->where('status', self::STATUS_ACTIVE)->get(); return $members ->first(fn (GroupMember $member): bool => (int) $member->user_id === (int) $userId && $member->status === self::STATUS_ACTIVE) ?->role; } public function hasActiveMember(User|int|null $user): bool { return $this->activeRoleFor($user) !== null; } public function activeMembershipFor(User|int|null $user): ?GroupMember { $userId = $user instanceof User ? $user->id : $user; if ($userId === null || $this->isOwnedBy($userId)) { return null; } $members = $this->relationLoaded('members') ? $this->members : $this->members()->where('status', self::STATUS_ACTIVE)->get(); return $members->first( fn (GroupMember $member): bool => (int) $member->user_id === (int) $userId && $member->status === self::STATUS_ACTIVE ); } public function permissionOverridesFor(User|int|null $user): array { if ($this->isOwnedBy($user)) { return collect(self::allowedPermissionOverrides()) ->mapWithKeys(fn (string $permission): array => [$permission => true]) ->all(); } $member = $this->activeMembershipFor($user); if (! $member) { return []; } return collect($member->permission_overrides_json ?? []) ->mapWithKeys(function ($override): array { if (is_array($override)) { $key = trim((string) ($override['key'] ?? '')); if ($key === '' || ! in_array($key, self::allowedPermissionOverrides(), true)) { return []; } return [$key => (bool) ($override['is_allowed'] ?? false)]; } $key = trim((string) $override); if ($key === '' || ! in_array($key, self::allowedPermissionOverrides(), true)) { return []; } return [$key => true]; }) ->all(); } public function hasPermission(User|int|null $user, string $permission): bool { return $this->permissionOverridesFor($user)[$permission] ?? false; } public function hasDeniedPermission(User|int|null $user, string $permission): bool { $overrides = $this->permissionOverridesFor($user); return array_key_exists($permission, $overrides) && $overrides[$permission] === false; } public static function permissionKeys(): array { return self::allowedPermissionOverrides(); } public function canBeViewedBy(?User $user): bool { if ($this->isPubliclyVisible()) { return true; } return $user !== null && $this->hasActiveMember($user); } public function canManage(User $user): bool { return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true); } public function canManageMembers(User $user): bool { return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true); } public function canPublishArtworks(User $user): bool { return $this->isOperational() && in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true); } public function canCreateArtworkDrafts(User $user): bool { return $this->isOperational() && $this->hasActiveMember($user); } public function canSubmitArtworkForReview(User $user): bool { return $this->isOperational() && $this->hasActiveMember($user); } public function canReviewSubmissions(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_REVIEW_SUBMISSIONS)) { return false; } $role = $this->activeRoleFor($user); return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_REVIEW_SUBMISSIONS); } public function canRequestJoin(?User $user): bool { if (! $this->isOperational() || $user === null || $this->hasActiveMember($user)) { return false; } return in_array($this->membership_policy, [self::MEMBERSHIP_REQUEST_TO_JOIN, self::MEMBERSHIP_OPEN], true); } public function canReviewJoinRequests(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_REVIEW_JOIN_REQUESTS)) { return false; } $role = $this->activeRoleFor($user); return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_REVIEW_JOIN_REQUESTS); } public function canManageRecruitment(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_RECRUITMENT)) { return false; } $role = $this->activeRoleFor($user); return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_RECRUITMENT); } public function canManagePosts(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_POSTS)) { return false; } $role = $this->activeRoleFor($user); return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_POSTS); } public function canPublishPosts(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_POSTS)) { return false; } $role = $this->activeRoleFor($user); return in_array($role, [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_PUBLISH_POSTS) || $this->canManagePosts($user); } public function canPinPosts(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_PIN_POSTS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_PIN_POSTS); } public function canManageMemberPermissions(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_MEMBER_PERMISSIONS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_MEMBER_PERMISSIONS); } public function canManageEvents(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_EVENTS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_EVENTS); } public function canManageChallenges(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_CHALLENGES)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_CHALLENGES); } public function canManageProjects(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_PROJECTS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_PROJECTS); } public function canManageReleases(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_RELEASES)) { return false; } return $this->canManageProjects($user) || $this->hasPermission($user, self::PERMISSION_MANAGE_RELEASES); } public function canPublishReleases(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_RELEASES)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_PUBLISH_RELEASES) || $this->canManageReleases($user); } public function canManageMilestones(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_MILESTONES)) { return false; } return $this->canManageProjects($user) || $this->canManageReleases($user) || $this->hasPermission($user, self::PERMISSION_MANAGE_MILESTONES); } public function canViewReputationDashboard(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_REPUTATION_DASHBOARD)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_VIEW_REPUTATION_DASHBOARD); } public function canManageBadges(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_BADGES)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_BADGES); } public function canViewInternalTrustMetrics(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS); } public function canFeatureReleases(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_FEATURE_RELEASES)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_FEATURE_RELEASES); } public function canAssignReleaseLead(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_ASSIGN_RELEASE_LEAD)) { return false; } return $this->canManageReleases($user) || $this->hasPermission($user, self::PERMISSION_ASSIGN_RELEASE_LEAD); } public function canManageAssets(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_ASSETS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_ASSETS); } public function canFeatureChallengeEntries(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_FEATURE_CHALLENGE_ENTRIES)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_FEATURE_CHALLENGE_ENTRIES); } public function canPublishEventUpdates(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_PUBLISH_EVENT_UPDATES)) { return false; } return $this->canManageEvents($user) || $this->hasPermission($user, self::PERMISSION_PUBLISH_EVENT_UPDATES); } public function canAttachAssetsToProjects(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS)) { return false; } return $this->canManageProjects($user) || $this->hasPermission($user, self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS); } public function canViewInternalAssets(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_VIEW_INTERNAL_ASSETS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN, self::ROLE_EDITOR], true) || $this->hasPermission($user, self::PERMISSION_VIEW_INTERNAL_ASSETS); } public function canPinActivity(User $user): bool { if (! $this->isOperational()) { return false; } if ($this->hasDeniedPermission($user, self::PERMISSION_MANAGE_ACTIVITY_PINS)) { return false; } return in_array($this->activeRoleFor($user), [self::ROLE_OWNER, self::ROLE_ADMIN], true) || $this->hasPermission($user, self::PERMISSION_MANAGE_ACTIVITY_PINS); } public static function allowedPermissionOverrides(): array { return [ self::PERMISSION_REVIEW_JOIN_REQUESTS, self::PERMISSION_REVIEW_SUBMISSIONS, self::PERMISSION_MANAGE_RECRUITMENT, self::PERMISSION_MANAGE_POSTS, self::PERMISSION_PUBLISH_POSTS, self::PERMISSION_PIN_POSTS, self::PERMISSION_MANAGE_MEMBER_PERMISSIONS, self::PERMISSION_MANAGE_EVENTS, self::PERMISSION_MANAGE_CHALLENGES, self::PERMISSION_MANAGE_PROJECTS, self::PERMISSION_MANAGE_RELEASES, self::PERMISSION_PUBLISH_RELEASES, self::PERMISSION_MANAGE_MILESTONES, self::PERMISSION_VIEW_REPUTATION_DASHBOARD, self::PERMISSION_MANAGE_BADGES, self::PERMISSION_VIEW_INTERNAL_TRUST_METRICS, self::PERMISSION_FEATURE_RELEASES, self::PERMISSION_ASSIGN_RELEASE_LEAD, self::PERMISSION_MANAGE_ASSETS, self::PERMISSION_FEATURE_CHALLENGE_ENTRIES, self::PERMISSION_PUBLISH_EVENT_UPDATES, self::PERMISSION_ATTACH_ASSETS_TO_PROJECTS, self::PERMISSION_VIEW_INTERNAL_ASSETS, self::PERMISSION_MANAGE_ACTIVITY_PINS, ]; } public function canManageCollections(User $user): bool { return $this->isOperational() && $this->canPublishArtworks($user); } public function avatarUrl(): ?string { return $this->assetUrl($this->avatar_path); } public function bannerUrl(): ?string { return $this->assetUrl($this->banner_path); } public function publicUrl(): string { return route('groups.show', ['group' => $this->slug]); } public function shouldBeSearchable(): bool { return $this->visibility === self::VISIBILITY_PUBLIC && $this->status === self::LIFECYCLE_ACTIVE; } public function toSearchableArray(): array { $recruitment = $this->relationLoaded('recruitmentProfile') ? $this->recruitmentProfile : $this->recruitmentProfile()->first(); $memberNames = $this->members() ->with('user:id,name,username') ->where('status', self::STATUS_ACTIVE) ->limit(12) ->get() ->map(fn (GroupMember $member): string => (string) ($member->user?->name ?: $member->user?->username ?: '')) ->filter() ->values() ->all(); return [ 'id' => (int) $this->id, 'name' => (string) $this->name, 'slug' => (string) $this->slug, 'headline' => (string) ($this->headline ?? ''), 'bio' => (string) ($this->bio ?? ''), 'type' => (string) ($this->type ?? ''), 'visibility' => (string) $this->visibility, 'status' => (string) ($this->status ?? self::LIFECYCLE_ACTIVE), 'artworks_count' => (int) ($this->artworks_count ?? 0), 'followers_count' => (int) ($this->followers_count ?? 0), 'is_recruiting' => (bool) ($recruitment?->is_recruiting ?? false), 'recruitment_headline' => (string) ($recruitment?->headline ?? ''), 'recruitment_roles' => array_values(array_filter($recruitment?->roles_json ?? [])), 'recruitment_skills' => array_values(array_filter($recruitment?->skills_json ?? [])), 'release_titles' => $this->releases()->where('visibility', GroupRelease::VISIBILITY_PUBLIC)->latest('published_at')->limit(6)->pluck('title')->filter()->values()->all(), 'project_titles' => $this->projects()->where('visibility', GroupProject::VISIBILITY_PUBLIC)->latest('updated_at')->limit(6)->pluck('title')->filter()->values()->all(), 'challenge_titles' => $this->challenges()->where('visibility', GroupChallenge::VISIBILITY_PUBLIC)->latest('updated_at')->limit(6)->pluck('title')->filter()->values()->all(), 'event_titles' => $this->events()->where('visibility', GroupEvent::VISIBILITY_PUBLIC)->latest('start_at')->limit(6)->pluck('title')->filter()->values()->all(), 'badge_keys' => $this->badges()->latest('awarded_at')->limit(6)->pluck('badge_key')->filter()->values()->all(), 'member_names' => $memberNames, ]; } private function assetUrl(?string $path): ?string { $trimmed = trim((string) $path); if ($trimmed === '') { return null; } if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { return $trimmed; } return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($trimmed, '/'); } }