<?php12namespace Pterodactyl\Models;34use Illuminate\Notifications\Notifiable;5use Illuminate\Database\Query\JoinClause;6use Znck\Eloquent\Traits\BelongsToThrough;7use Pterodactyl\Contracts\Models\Identifiable;8use Illuminate\Database\Eloquent\Relations\HasOne;9use Illuminate\Database\Eloquent\Relations\HasMany;10use Pterodactyl\Models\Traits\HasRealtimeIdentifier;11use Illuminate\Database\Eloquent\Relations\BelongsTo;12use Illuminate\Database\Eloquent\Factories\HasFactory;13use Illuminate\Database\Eloquent\Relations\MorphToMany;14use Illuminate\Database\Eloquent\Relations\HasManyThrough;15use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;1617/**18* \Pterodactyl\Models\Server.19*20* @property int $id21* @property string|null $external_id22* @property string $uuid23* @property string $uuidShort24* @property int $node_id25* @property string $name26* @property string $description27* @property string|null $status28* @property bool $skip_scripts29* @property int $owner_id30* @property int $memory31* @property int $swap32* @property int $disk33* @property int $io34* @property int $cpu35* @property string|null $threads36* @property bool $oom_disabled37* @property int $allocation_id38* @property int $nest_id39* @property int $egg_id40* @property string $startup41* @property string $image42* @property int|null $allocation_limit43* @property int|null $database_limit44* @property int $backup_limit45* @property \Illuminate\Support\Carbon|null $created_at46* @property \Illuminate\Support\Carbon|null $updated_at47* @property \Illuminate\Support\Carbon|null $installed_at48* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ActivityLog[] $activity49* @property int|null $activity_count50* @property Allocation|null $allocation51* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Allocation[] $allocations52* @property int|null $allocations_count53* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Backup[] $backups54* @property int|null $backups_count55* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Database[] $databases56* @property int|null $databases_count57* @property Egg|null $egg58* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Mount[] $mounts59* @property int|null $mounts_count60* @property Nest $nest61* @property Node $node62* @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications63* @property int|null $notifications_count64* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Schedule[] $schedules65* @property int|null $schedules_count66* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Subuser[] $subusers67* @property int|null $subusers_count68* @property ServerTransfer|null $transfer69* @property User $user70* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\EggVariable[] $variables71* @property int|null $variables_count72*73* @method static \Database\Factories\ServerFactory factory(...$parameters)74* @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery()75* @method static \Illuminate\Database\Eloquent\Builder|Server newQuery()76* @method static \Illuminate\Database\Eloquent\Builder|Server query()77* @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationId($value)78* @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationLimit($value)79* @method static \Illuminate\Database\Eloquent\Builder|Server whereBackupLimit($value)80* @method static \Illuminate\Database\Eloquent\Builder|Server whereCpu($value)81* @method static \Illuminate\Database\Eloquent\Builder|Server whereCreatedAt($value)82* @method static \Illuminate\Database\Eloquent\Builder|Server whereDatabaseLimit($value)83* @method static \Illuminate\Database\Eloquent\Builder|Server whereDescription($value)84* @method static \Illuminate\Database\Eloquent\Builder|Server whereDisk($value)85* @method static \Illuminate\Database\Eloquent\Builder|Server whereEggId($value)86* @method static \Illuminate\Database\Eloquent\Builder|Server whereExternalId($value)87* @method static \Illuminate\Database\Eloquent\Builder|Server whereId($value)88* @method static \Illuminate\Database\Eloquent\Builder|Server whereImage($value)89* @method static \Illuminate\Database\Eloquent\Builder|Server whereIo($value)90* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)91* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)92* @method static \Illuminate\Database\Eloquent\Builder|Server whereNestId($value)93* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)94* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)95* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)96* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)97* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)98* @method static \Illuminate\Database\Eloquent\Builder|Server whereStatus($value)99* @method static \Illuminate\Database\Eloquent\Builder|Server whereSwap($value)100* @method static \Illuminate\Database\Eloquent\Builder|Server whereThreads($value)101* @method static \Illuminate\Database\Eloquent\Builder|Server whereUpdatedAt($value)102* @method static \Illuminate\Database\Eloquent\Builder|Server whereUuid($value)103* @method static \Illuminate\Database\Eloquent\Builder|Server whereUuidShort($value)104*105* @mixin \Eloquent106*/107#[Attributes\Identifiable('serv')]108class Server extends Model implements Identifiable109{110/** @use HasFactory<\Database\Factories\ServerFactory> */111use HasFactory;112use BelongsToThrough;113use Notifiable;114use HasRealtimeIdentifier;115116/**117* The resource name for this model when it is transformed into an118* API representation using fractal.119*/120public const RESOURCE_NAME = 'server';121public const STATUS_INSTALLING = 'installing';122public const STATUS_INSTALL_FAILED = 'install_failed';123public const STATUS_REINSTALL_FAILED = 'reinstall_failed';124public const STATUS_SUSPENDED = 'suspended';125public const STATUS_RESTORING_BACKUP = 'restoring_backup';126127/**128* The table associated with the model.129*/130protected $table = 'servers';131132/**133* Default values when creating the model. We want to switch to disabling OOM killer134* on server instances unless the user specifies otherwise in the request.135*/136protected $attributes = [137'status' => self::STATUS_INSTALLING,138'oom_disabled' => true,139'installed_at' => null,140];141142/**143* The default relationships to load for all server models.144*/145protected $with = ['allocation'];146147/**148* Fields that are not mass assignable.149*/150protected $guarded = ['id', self::CREATED_AT, self::UPDATED_AT, 'deleted_at', 'installed_at'];151152public static array $validationRules = [153'external_id' => 'sometimes|nullable|string|between:1,191|unique:servers',154'owner_id' => 'required|integer|exists:users,id',155'name' => 'required|string|min:1|max:191',156'node_id' => 'required|exists:nodes,id',157'description' => 'string',158'status' => 'nullable|string',159'memory' => 'required|numeric|min:0',160'swap' => 'required|numeric|min:-1',161'io' => 'required|numeric|between:10,1000',162'cpu' => 'required|numeric|min:0',163'threads' => 'nullable|regex:/^[0-9-,]+$/',164'oom_disabled' => 'sometimes|boolean',165'disk' => 'required|numeric|min:0',166'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',167'nest_id' => 'required|exists:nests,id',168'egg_id' => 'required|exists:eggs,id',169'startup' => 'required|string',170'skip_scripts' => 'sometimes|boolean',171'image' => ['required', 'string', 'max:191', 'regex:/^~?[\w\.\/\-:@ ]*$/'],172'database_limit' => 'present|nullable|integer|min:0',173'allocation_limit' => 'sometimes|nullable|integer|min:0',174'backup_limit' => 'present|nullable|integer|min:0',175];176177/**178* Cast values to correct type.179*/180protected $casts = [181'node_id' => 'integer',182'skip_scripts' => 'boolean',183'owner_id' => 'integer',184'memory' => 'integer',185'swap' => 'integer',186'disk' => 'integer',187'io' => 'integer',188'cpu' => 'integer',189'oom_disabled' => 'boolean',190'allocation_id' => 'integer',191'nest_id' => 'integer',192'egg_id' => 'integer',193'database_limit' => 'integer',194'allocation_limit' => 'integer',195'backup_limit' => 'integer',196self::CREATED_AT => 'datetime',197self::UPDATED_AT => 'datetime',198'deleted_at' => 'datetime',199'installed_at' => 'datetime',200];201202/**203* Returns the format for server allocations when communicating with the Daemon.204*/205public function getAllocationMappings(): array206{207return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) {208return $item->pluck('port');209})->toArray();210}211212public function isInstalled(): bool213{214return $this->status !== self::STATUS_INSTALLING && $this->status !== self::STATUS_INSTALL_FAILED;215}216217public function isSuspended(): bool218{219return $this->status === self::STATUS_SUSPENDED;220}221222/**223* Gets the user who owns the server.224*225* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Pterodactyl\Models\User, $this>226*/227public function user(): BelongsTo228{229return $this->belongsTo(User::class, 'owner_id');230}231232/**233* Gets the subusers associated with a server.234*235* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\Subuser, $this>236*/237public function subusers(): HasMany238{239return $this->hasMany(Subuser::class, 'server_id', 'id');240}241242/**243* Gets the default allocation for a server.244*245* @return \Illuminate\Database\Eloquent\Relations\HasOne<\Pterodactyl\Models\Allocation, $this>246*/247public function allocation(): HasOne248{249return $this->hasOne(Allocation::class, 'id', 'allocation_id');250}251252/**253* Gets all allocations associated with this server.254*255* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\Allocation, $this>256*/257public function allocations(): HasMany258{259return $this->hasMany(Allocation::class, 'server_id');260}261262/**263* Gets information for the nest associated with this server.264*265* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Pterodactyl\Models\Nest, $this>266*/267public function nest(): BelongsTo268{269return $this->belongsTo(Nest::class);270}271272/**273* Gets information for the egg associated with this server.274*275* @return \Illuminate\Database\Eloquent\Relations\HasOne<\Pterodactyl\Models\Egg, $this>276*/277public function egg(): HasOne278{279return $this->hasOne(Egg::class, 'id', 'egg_id');280}281282/**283* Gets information for the service variables associated with this server.284*285* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\EggVariable, $this>286*/287public function variables(): HasMany288{289return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id')290->select(['egg_variables.*', 'server_variables.variable_value as server_value'])291->leftJoin('server_variables', function (JoinClause $join) {292// Don't forget to join against the server ID as well since the way we're using this relationship293// would actually return all the variables and their values for _all_ servers using that egg,294// rather than only the server for this model.295//296// @see https://github.com/pterodactyl/panel/issues/2250297$join->on('server_variables.variable_id', 'egg_variables.id')298->where('server_variables.server_id', $this->id);299});300}301302/**303* Gets information for the node associated with this server.304*305* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Pterodactyl\Models\Node, $this>306*/307public function node(): BelongsTo308{309return $this->belongsTo(Node::class);310}311312/**313* Gets information for the tasks associated with this server.314*315* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\Schedule, $this>316*/317public function schedules(): HasMany318{319return $this->hasMany(Schedule::class);320}321322/**323* Gets all databases associated with a server.324*325* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\Database, $this>326*/327public function databases(): HasMany328{329return $this->hasMany(Database::class);330}331332/**333* Returns the location that a server belongs to.334*335* @return \Znck\Eloquent\Relations\BelongsToThrough<\Pterodactyl\Models\Location, \Pterodactyl\Models\Node>336*337* @throws \Exception338*/339public function location(): \Znck\Eloquent\Relations\BelongsToThrough340{341return $this->belongsToThrough(Location::class, Node::class); // @phpstan-ignore return.type342}343344/**345* Returns the associated server transfer.346*347* @return \Illuminate\Database\Eloquent\Relations\HasOne<\Pterodactyl\Models\ServerTransfer, $this>348*/349public function transfer(): HasOne350{351return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');352}353354/**355* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\Backup, $this>356*/357public function backups(): HasMany358{359return $this->hasMany(Backup::class);360}361362/**363* Returns all mounts that have this server has mounted.364*365* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough<\Pterodactyl\Models\Mount, \Pterodactyl\Models\MountServer, $this>366*/367public function mounts(): HasManyThrough368{369return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id');370}371372/**373* Returns all of the activity log entries where the server is the subject.374*375* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<\Pterodactyl\Models\ActivityLog, $this>376*/377public function activity(): MorphToMany378{379return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');380}381382/**383* Checks if the server is currently in a user-accessible state. If not, an384* exception is raised. This should be called whenever something needs to make385* sure the server is not in a weird state that should block user access.386*387* @throws ServerStateConflictException388*/389public function validateCurrentState()390{391if (392$this->isSuspended()393|| $this->node->isUnderMaintenance()394|| !$this->isInstalled()395|| $this->status === self::STATUS_RESTORING_BACKUP396|| !is_null($this->transfer)397) {398throw new ServerStateConflictException($this);399}400}401402/**403* Checks if the server is currently in a transferable state. If not, an404* exception is raised. This should be called whenever something needs to make405* sure the server is able to be transferred and is not currently being transferred406* or installed.407*/408public function validateTransferState()409{410if (411!$this->isInstalled()412|| $this->status === self::STATUS_RESTORING_BACKUP413|| !is_null($this->transfer)414) {415throw new ServerStateConflictException($this);416}417}418}419420421