<?php namespace App\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use App\Classes\FTN\Packet; use App\Jobs\AddressPoll; class System extends Model { use HasFactory; public const default = 'Discovered System'; protected $casts = [ 'last_session' => 'datetime:Y-m-d H:i:s' ]; /* STATIC */ public static function createUnknownSystem(): self { self::unguard(); $so = self::firstOrCreate([ 'name' => self::default, 'sysop' => 'Unknown', 'location' => 'Unknown', 'active' => TRUE, ]); self::reguard(); return $so; } /* SCOPES */ /** * Only query active records * @todo test false action */ public function scopeActive($query) { $uo = Auth::user(); return $query ->when( $uo && ! $uo->isAdmin(), fn($query)=> $query ->whereIn('id',$uo->systems->pluck('id')) ->orWhere($this->getTable().'.active',TRUE), fn($query)=>$query->where($this->getTable().'.active',TRUE)) ->orderBy('name'); } /* RELATIONS */ /** * All addresses assigned to a system, including addresses pending deletion * @return mixed */ public function addresses() { return $this->hasMany(Address::class) ->FTN() ->withTrashed(); } /** * System addresses that are active * * @return mixed */ public function akas() { return $this->hasMany(Address::class) ->FTN() ->ActiveFTN(); } public function mailers() { return $this->belongsToMany(Mailer::class) ->withPivot(['last_poll','port','attempts','active']); } public function mailer_preferred() { return $this->mailers() ->preferred(); } public function logs() { return $this->hasMany(SystemLog::class); } public function logs_recent() { return $this->hasMany(SystemLog::class) ->orderby('created_at','DESC') ->limit(10); } /** * Session Passwords for system * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function sessions() { return $this->belongsToMany(Zone::class) ->select(['zones.id','zones.zone_id','domain_id','zones.active']) ->join('domains',['domains.id'=>'zones.domain_id']) ->withPivot(['sespass','pktpass','ticpass','fixpass','zt_ipv4','zt_ipv6','default']) ->orderBy('domains.name'); } /** * If this system is configured as this host * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function setup() { return $this->hasOne(Setup::class); } public function users() { return $this->belongsToMany(User::class); } /** * This system is the ZC for the following zones */ public function zcs() { return $this->hasMany(Zone::class) ->select(['zones.id','zone_id','domain_id','system_id','zones.active']) ->join('domains',['domains.id'=>'zones.domain_id']) ->orderBy('domains.name') ->orderBy('zone_id') ->with([ 'domain:id,name,active', ]); } /** * Zones a system has addresses for * * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ public function zones() { return $this->hasManyThrough(Zone::class,Address::class,'system_id','id','id','zone_id'); } /* ATTRIBUTES */ public function getAccessMethodAttribute(): string { switch ($this->method) { case 23: return sprintf('telnet://%s:%s',$this->address,$this->port); case 22: return sprintf('ssh://%s:%s',$this->address,$this->port); case 513: return sprintf('rlogin://%s:%s',$this->address,$this->port); default: return $this->method ? sprintf('%s:%s',$this->address,$this->port) : 'No access method available.'; } } public function getAccessMailerAttribute(): string { switch (($x=$this->mailer_preferred()->first())?->name) { case 'BINKP': return sprintf('binkp://%s:%s',$this->address,$x->pivot->port); case 'EMSI': return sprintf('emsi://%s:%s',$this->address,$x->pivot->port); default: return $x?->name ? sprintf('%s:%s',$this->address,$x->pivot->port) : 'No mailer available.'; } } public function getBatchFilesAttribute(?int $val): int { return $val ?: Setup::findOrFail(config('app.id'))->batch_files; } public function getIsOwnedAttribute(): bool { return $this->users->count(); } public function getLastSeenAttribute(): ?Carbon { return $this->logs_recent->first()?->created_at; } public function getPktMsgsAttribute(?int $val): int { return $val ?: Setup::findOrFail(config('app.id'))->msgs_pkt; } /* METHODS */ public function addresses_common(): Collection { $our = our_address()->pluck('zone.domain_id')->unique(); // Return our akas, filter with our_addresses() return $this->addresses->filter(fn($item)=>$our->contains($item->zone->domain_id)); } /** * Return the ACTIVE addresses that are common with our addresses * * @return Collection * @throws \Exception */ public function aka_common(): Collection { $our = our_address()->pluck('zone.domain_id')->unique(); // Return our akas, filter with our_addresses() return $this->akas->filter(fn($item)=>$our->contains($item->zone->domain_id)); } /** * Return the ACTIVE addresses that we auth with * * @return Collection * @throws \Exception */ public function aka_known(): Collection { return $this->aka_common() ->filter(fn($item)=>$this->sessions->contains($item->zone_id)); } /** * Return the ACTIVE addresses in the same networks as us, but dont auth here * * @return Collection * @throws \Exception */ public function aka_unknown(): Collection { return $this->aka_common() ->filter(fn($item)=>! $this->sessions->contains($item->zone_id)); } /** * Return the AKAs that are in networks not common with us * * @return Collection * @throws \Exception */ public function aka_uncommon(): Collection { $our = our_address()->pluck('zone.domain_id')->unique(); // Return our akas, filter with our_addresses() return $this->akas->filter(fn($item)=>! $our->contains($item->zone->domain_id)); } public function echoareas() { return Echoarea::select('echoareas.*') ->join('address_echoarea',['address_echoarea.echoarea_id'=>'echoareas.id']) ->join('addresses',['addresses.id'=>'address_echoarea.address_id']) ->where('addresses.system_id',$this->id); } public function fileareas() { return Filearea::select('fileareas.*') ->join('address_filearea',['address_filearea.filearea_id'=>'fileareas.id']) ->join('addresses',['addresses.id'=>'address_filearea.address_id']) ->where('addresses.system_id',$this->id); } /** * Return the system name, or role name for the zone * * @param Address $o * @return string */ public function full_name(Address $o): string { switch ($o->role_id) { case Address::NODE_ZC; return sprintf('ZC-%s-%05d',$o->zone->domain->name,$o->zone->zone_id); case Address::NODE_RC; return sprintf('RC-%s-%05d',$o->zone->domain->name,$o->region_id); case Address::NODE_NC; return sprintf('NC-%s-%05d',$o->zone->domain->name,$o->host_id); case Address::NODE_HC; case Address::NODE_NN; default: return $this->name; } } public function inDomain(Domain $o): bool { return $this->addresses ->filter(fn($item)=>$item->zone->domain_id === $o->id) ->count() > 0; } /** * Return the system's address in the same zone * This function can filter based on the address type needed. * * @param Zone $o * @param int $type * @return Collection */ public function match(Zone $o,int $type=(Address::NODE_NC|Address::NODE_HC|Address::NODE_NN|Address::NODE_POINT)): Collection { $akas = $this->akas ->where(function($item) use($o) { return ($item->zone_id === $o->id) || ($item->zone->domain_id === $o->domain_id); }); return ($akas->count() > 1) ? $akas->filter(function($item) use ($type) { return $item->role_id & $type;}) : $akas; } /** * Return the packet that this system uses * * @param Address $ao * @param string|null $password * @return Packet * @throws \Exception */ public function packet(Address $ao,string $password=NULL): Packet { if ($ao->system_id !== $this->id) throw new \Exception('Packet for [%s] is not for system [%d]',$ao->ftn,$this->id); return (new (collect(Packet::PACKET_TYPES) ->get($this->pkt_type ?: config('fido.packet_default')))) ->for($ao) ->password($password); } public function poll(): ?Job { return Job::where('queue',AddressPoll::QUEUE) ->get() ->where(fn($item)=>$this->akas->pluck('id')->contains($item->command->address->id)) ->last(); } }