id)); $model->active = FALSE; $model->save(); }); } /** * Create an FTN address associated with a system * * @param string $address * @param System $so * @return Address|null * @throws \Exception */ public static function createFTN(string $address,System $so): ?self { $ftn = self::parseFTN($address); if (! $ftn['d']) { // See if we have a default domain for this domain if ($ftn['z']) { $zo = Zone::where('zone_id',$ftn['z'])->where('default',TRUE)->single(); if ($zo) $ftn['d'] = $zo->domain->name; else { Log::alert(sprintf('%s:! Refusing to create address [%s] no domain available',self::LOGKEY,$address)); return NULL; } } } Log::debug(sprintf('%s:- Creating AKA [%s] for [%s]',self::LOGKEY,$address,$so->name)); // Check Domain exists Domain::unguard(); $do = Domain::firstOrNew(['name'=>$ftn['d']]); Domain::reguard(); if (! $do->exists) { $do->public = TRUE; $do->active = TRUE; $do->notes = 'Auto created'; $do->save(); } // If the zone is zero, see if this is a flatten domain, and if so, find an NC if (($ftn['z'] === 0) && $do->flatten) { $nc = self::findZone($do,$ftn['n'],0,0); if ($nc) { Log::info(sprintf('%s:- Setting zone to [%d]',self::LOGKEY,$nc->zone->zone_id)); $ftn['z'] = $nc->zone->zone_id; } } // Create zone Zone::unguard(); $zo = Zone::firstOrNew(['domain_id'=>$do->id,'zone_id'=>$ftn['z']]); Zone::reguard(); if (! $zo->exists) { $zo->active = TRUE; $zo->notes = 'Auto created'; $zo->system_id = System::createUnknownSystem()->id; $do->zones()->save($zo); } if (! $zo->active || ! $do->active) { Log::alert(sprintf('%s:! Refusing to create address [%s] in disabled zone or domain',self::LOGKEY,$address)); return NULL; } // Create Address, assigned to $so $o = new self; $o->active = TRUE; $o->zone_id = $zo->id; $o->region_id = $ftn['r']; $o->host_id = $ftn['n']; $o->node_id = $ftn['f']; $o->point_id = $ftn['p']; try { $so->addresses()->save($o); } catch (QueryException $e) { Log::error(sprintf('%s:! ERROR creating address [%s] (%s)',self::LOGKEY,$o->ftn,get_class($e))); return NULL; } return $o; } /** * Find a record in the DB for a node string, eg: 10:1/1.0 * * @param string $address * @param bool $trashed * @param bool $recent * @return Address|null * @throws \Exception */ public static function findFTN(string $address,bool $trashed=FALSE,bool $recent=FALSE): ?self { $ftn = self::parseFTN($address); $o = NULL; $query = (new self) ->select('addresses.*') ->join('zones',['zones.id'=>'addresses.zone_id']) ->join('domains',['domains.id'=>'zones.domain_id']) ->when($trashed,function($query) use ($recent) { return $query->withTrashed() ->orderBy('updated_at','DESC') ->when($recent,fn($query)=>$query->where(fn($query)=>$query ->where('deleted_at','>=',Carbon::now()->subMonth())->orWhereNull('deleted_at'))); },function($query) { $query->active(); }) ->where('zones.zone_id',$ftn['z']) ->where('node_id',$ftn['f']) ->where('point_id',$ftn['p']) ->when($ftn['d'],function($query,$domain) { $query->where('domains.name',$domain); },function($query) { $query->where('zones.default',TRUE); }) ->orderBy('created_at','DESC'); $q = $query->clone(); // Are we looking for a region address if (($ftn['f'] === 0) && ($ftn['p'] === 0)) $o = $query ->where('region_id',$ftn['n']) ->where('host_id',$ftn['n']) ->with(['system:id,active,name,address,pkt_msgs,last_session,hold']) ->first(); // Look for a normal address if (! $o) $o = $q ->where(function($q) use ($ftn) { return $q ->where(function($q) use ($ftn) { return $q ->where('region_id',$ftn['n']) ->where('host_id',$ftn['n']); }) ->orWhere('host_id',$ftn['n']); }) ->with(['system:id,active,name,address,pkt_msgs,last_session,hold']) ->first(); // Check and see if we are a flattened domain, our address might be available with a different zone. // This occurs when we are parsing 2D addresses from SEEN-BY, but we have the zone if (! $o && ($ftn['p'] === 0)) { if ($ftn['d']) $do = Domain::select('flatten')->where(['name'=>$ftn['d']])->single(); else { $zo = Zone::where('zone_id',$ftn['z'])->where('default',TRUE)->with(['domain:id,flatten'])->single(); $do = $zo?->domain; } if ($do && $do->flatten && (($ftn['z'] === 0) || $do->zones->pluck('zone_id')->contains($ftn['z']))) $o = self::findZone($do,$ftn['n'],$ftn['f'],$ftn['p'],$trashed); } return ($o && ($trashed || $o->system->active)) ? $o : NULL; } public static function newFTN(string $address): self { $ftn = self::parseFTN($address); $do = $ftn['d'] ? Domain::where('name',$ftn['d'])->single() : NULL; $o = new self; $o->region_id = $ftn['r']; $o->host_id = $ftn['n']; $o->node_id = $ftn['f']; $o->point_id = $ftn['p']; $zo = Zone::where('zone_id',$ftn['z']) ->when($do,fn($query)=>$query->where('domain_id',$do->id)) ->single(); $o->zone_id = $zo?->id; if (($ftn['z'] === 0) || (! $zo)) { Log::alert(sprintf('%s:! newFTN was parsed an FTN [%s] with a zero zone, adding empty zone in domain',self::LOGKEY,$address)); $zo = new Zone; $zo->domain_id = $do?->id; } $o->zone()->associate($zo); return $o; } /** * This is to find an address for a domain (like fidonet), which is technically 2D even though it uses multiple zones. * * This was implemented to identify seenby and path kludges * * @param Domain $do * @param int $host * @param int $node * @param int $point * @param bool $trashed * @return self|null * @throws \Exception */ public static function findZone(Domain $do,int $host,int $node,int $point,bool $trashed=FALSE): ?self { if (! $do->flatten) throw new \Exception(sprintf('Domain is not set with flatten: %d',$do->id)); $zones = $do->zones->pluck('zone_id'); $o = (new self) ->select('addresses.*') ->join('zones',['zones.id'=>'addresses.zone_id']) ->when($trashed,function($query) { $query->withTrashed(); },function($query) { $query->active(); }) ->whereIN('zones.zone_id',$zones) ->where(function($q) use ($host) { return $q ->where(function($q) use ($host) { return $q->where('region_id',$host) ->where('host_id',$host); }) ->orWhere('host_id',$host); }) ->where('node_id',$node) ->where('point_id',$point) ->where('zones.domain_id',$do->id) ->single(); return $o; } /** * Parse a string and split it out as an FTN array * * @param string $ftn * @return array * @throws \Exception */ public static function parseFTN(string $ftn): array { if (! preg_match(sprintf('#^%s$#',self::ftn_regex),strtolower($ftn),$matches)) throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - regex failed',serialize($ftn))); // Check our numbers are correct. foreach ([1,2,3] as $i) if ((! is_numeric($matches[$i])) || ($matches[$i] > self::ADDRESS_FIELD_MAX)) throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - zone, host, or node address invalid [%d]',$ftn,$matches[$i])); if ((! empty($matches[4])) AND ((! is_numeric($matches[$i])) || ($matches[4] > self::ADDRESS_FIELD_MAX))) throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - point address invalid [%d]',$ftn,$matches[4])); // Work out region $region_id = NULL; $zone_id = NULL; // We can only work out region/zone if we have a domain - this is for 2D parsing if ($matches[5] ?? NULL) { $o = new self; $o->host_id = (int)$matches[2]; $o->node_id = (int)$matches[3]; $o->point_id = empty($matches[4]) ? 0 : (int)$matches[4]; if ($matches[1] !== "0") { $zo = Zone::select('zones.*') ->where('zone_id',$matches[1]) ->join('domains',['domains.id'=>'zones.domain_id']) ->where('domains.name',$matches[5]) ->single(); // Try and find out the zone from the host_id } else { $zo = Zone::select('zones.*') ->where(fn($query)=>$query->where('host_id',$matches[2])->orWhere('region_id',$matches[2])) ->join('domains',['domains.id'=>'zones.domain_id']) ->join('addresses',['addresses.zone_id'=>'zones.id']) ->where('domains.name',$matches[5]) ->first(); } $o->zone_id = $zo?->id; $parent = $o->parent(); $region_id = $parent?->region_id; $zone_id = $parent?->zone->zone_id; } return [ 'z' => (int)($zone_id ?: $matches[1]), 'r' => (int)$region_id, 'n' => (int)$matches[2], 'f' => (int)$matches[3], 'p' => empty($matches[4]) ? 0 : (int)$matches[4], 'd' => $matches[5] ?? NULL ]; } /* SCOPES */ /** * An FTN is active only if the address, zone, domain is also active * * @param $query * @return mixed * @note zones and domains needs to be joined in the base call, or use FTN() */ public function scopeActiveFTN($query) { return $query ->where('zones.active',TRUE) ->where('domains.active',TRUE) ->active(); } /** * Select to support returning FTN address * * @param $query * @return void */ public function scopeFTN($query) { return $query ->select(['addresses.id','region_id','host_id','node_id','point_id','addresses.zone_id','addresses.active','role','security','addresses.system_id','addresses.active','validated','deleted_at']) ->join('zones',['zones.id'=>'addresses.zone_id']) ->join('domains',['domains.id'=>'zones.domain_id']) ->orderBy('domains.name') ->orderBy('region_id') ->orderBy('host_id') ->orderBy('node_id') ->orderBy('point_id') ->with([ 'zone:zones.id,domain_id,zone_id,active', 'zone.domain:domains.id,name,active,public', ]); } /** @deprecated use FTN() */ public function scopeFTNOrder($query) { return $query ->orderBy('region_id') ->orderBy('host_id') ->orderBy('node_id') ->orderBy('point_id'); } public function scopeFTN2DOrder($query) { return $query ->orderBy('host_id') ->orderBy('node_id') ->orderBy('point_id'); } /** * Return a list of addresses and the amount of uncollected echomail * * @param $query * @return mixed */ public function scopeUncollectedEchomail($query) { return $query ->join('echomail_seenby',['echomail_seenby.address_id'=>'addresses.id']) ->join('echomails',['echomails.id'=>'echomail_seenby.echomail_id']) ->whereNotNull('export_at') ->whereNull('sent_at') ->whereNull('echomails.deleted_at') ->groupBy('addresses.id'); } public function scopeUncollectedEchomailTotal($query) { return $query ->select([ 'addresses.id', 'zone_id', 'host_id', 'node_id', 'point_id', 'system_id', DB::raw('count(addresses.id) as uncollected_echomail'), DB::raw('0 as uncollected_netmail'), DB::raw('0 as uncollected_files'), ]) ->UncollectedEchomail(); } /** * Return a list of addresses and the amount of uncollected files * * @param $query * @return mixed */ public function scopeUncollectedFiles($query) { return $query ->join('file_seenby',['file_seenby.address_id'=>'addresses.id']) ->join('files',['files.id'=>'file_seenby.file_id']) ->whereNotNull('export_at') ->whereNull('sent_at') ->whereNull('files.deleted_at') ->groupBy('addresses.id'); } public function scopeUncollectedFilesTotal($query) { return $query ->select([ 'addresses.id', 'zone_id', 'host_id', 'node_id', 'point_id', 'system_id', DB::raw('0 as uncollected_echomail'), DB::raw('0 as uncollected_netmail'), DB::raw('count(addresses.id) as uncollected_files') ]) ->UncollectedFiles(); } public function scopeUncollectedNetmail($query) { return $query ->join('netmails',['netmails.tftn_id'=>'addresses.id']) ->where(function($query) { return $query->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_INTRANSIT)) ->orWhereRaw(sprintf('(flags & %d) > 0',Message::FLAG_LOCAL)); }) ->whereRaw(sprintf('(flags & %d) = 0',Message::FLAG_SENT)) ->whereNull('sent_pkt') ->whereNull('sent_at') ->whereNull('netmails.deleted_at') ->groupBy('addresses.id'); } /** * Return a list of addresses and the amount of uncollected netmail * * @param $query * @return mixed */ public function scopeUncollectedNetmailTotal($query) { return $query ->select([ 'addresses.id', 'zone_id', 'host_id', 'node_id', 'point_id', 'system_id', DB::raw('0 as uncollected_echomail'), DB::raw('count(addresses.id) as uncollected_netmail'), DB::raw('0 as uncollected_files') ]) ->UncollectedNetmail(); } /* RELATIONS */ public function domain() { return $this->hasOneThrough(Domain::class,Zone::class,'id','id','zone_id','domain_id'); } public function dynamics() { return $this->hasMany(Dynamic::class); } /** * Echoareas this address is subscribed to * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function echoareas() { return $this->belongsToMany(Echoarea::class) ->using(AddressEchoarea::class) ->orderBy('name') ->withPivot(['subscribed']); } public function echomail_from() { return $this->hasMany(Echomail::class,'fftn_id','id') ->orderBy('datetime','DESC') ->limit(10); } /** * Echomails that this address has seen * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany * @todo Rework echomail_seenby to not have a specific seenby recorded for the fftn_id, but automatically include it when generating seenbys. */ public function echomail_seen() { return $this->belongsToMany(Echomail::class,'echomail_seenby') ->withPivot(['export_at','sent_at','sent_pkt']); } /** * Files that this address has seen * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function file_seen(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(File::class,'file_seenby') ->withPivot(['sent_at','export_at']); } /** * Fileareas this address is subscribed to * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function fileareas(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(Filearea::class) ->orderBy('name') ->withPivot(['subscribed']); } /** * If we are a hub, if role === NODE_HC and child entries have us as their hub_id * * @return HasMany */ public function nodes_hub(): HasMany { return $this->hasMany(Address::class,'hub_id','id') ->select(['id','addresses.zone_id','region_id','host_id','node_id','point_id','system_id']) ->active() ->FTNorder() ->with([ 'zone:zones.id,domain_id,zone_id', 'zone.domain:domains.id,name', ]); } /** * Return the nodes that belong to this NC/RC/ZC * * @return HasMany */ public function nodes_net(): HasMany { return HasMany::noConstraints( fn()=>$this->newHasMany( (new self)->newQuery() ->where('zone_id',$this->zone_id) ->where('host_id',$this->host_id) ->where('point_id',0) ->whereNot('id',$this->id) ->active() ->FTNorder() ->with(['zone:id,domain_id,zone_id']), $this, NULL, ($this->role_id === self::NODE_NC) ? 'id' : NULL) ); } /** * If we are a boss node, return our children. * * @return HasMany */ public function nodes_point(): HasMany { return HasMany::noConstraints( fn()=>$this->newHasMany( (new self)->newQuery() ->where('zone_id',$this->zone_id) ->where('host_id',$this->host_id) ->where('node_id',$this->node_id) ->where('point_id','>',0) ->whereNot('id',$this->id) ->active() ->FTNorder() ->with(['zone:id,domain_id,zone_id']), $this, NULL, ($this->role_id !== self::NODE_POINT) ? 'id' : NULL) ); } public function nodes_region(): HasMany { return HasMany::noConstraints( fn()=>$this->newHasMany( (new self)->newQuery() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->whereNot('id',$this->id) ->active() ->FTNorder() ->with(['zone:id,domain_id,zone_id']), $this, NULL, ($this->role_id === self::NODE_RC) ? 'id' : NULL) ); } public function nodes_zone(): HasMany { return HasMany::noConstraints( fn()=>$this->newHasMany( (new self)->newQuery() ->where('zone_id',$this->zone_id) ->whereNot('id',$this->id) ->active() ->FTNorder() ->with(['zone:id,domain_id,zone_id']), $this, NULL, ($this->role_id === self::NODE_ZC) ? 'id' : NULL) ); } public function system() { return $this->belongsTo(System::class); } public function uplink_hub() { return $this->belongsTo(Address::class,'hub_id','id'); } public function zone() { return $this->belongsTo(Zone::class); } /* ATTRIBUTES */ /** * Return if this address is active * * @param bool $value * @return bool */ public function getActiveAttribute(bool $value): bool { return $value && $this->getActiveDomainAttribute(); } public function getActiveDomainAttribute(): bool { return $this->zone->active && $this->zone->domain->active; } /** * Render the node name in full 5D * * @return string */ public function getFTNAttribute(): string { if (! $this->relationLoaded('zone')) $this->load(['zone:id,domain_id,zone_id','zone.domain:domains.id,name']); return sprintf('%s@%s',$this->getFTN4DAttribute(),$this->zone->domain->name); } public function getFTN2DAttribute(): string { return sprintf('%d/%d',$this->host_id ?: $this->region_id,$this->node_id); } public function getFTN3DAttribute(): string { if (! $this->relationLoaded('zone')) $this->load(['zone:id,domain_id,zone_id']); if (! $this->zone) throw new InvalidFTNException(sprintf('Invalid Zone for FTN address [%d/%d.%d@%s]',$this->host_id ?: $this->region_id,$this->node_id,$this->point_id,$this->domain?->name)); return sprintf('%d:%s',$this->zone->zone_id,$this->getFTN2DAttribute()); } public function getFTN4DAttribute(): string { if (! $this->relationLoaded('zone')) $this->load(['zone:id,domain_id,zone_id']); return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id); } public function getIsDefaultRouteAttribute(): bool { return (! is_null($x=$this->session('default'))) && $x; } public function getIsDownAttribute(): bool { return $this->role & self::NODE_DOWN; } public function getIsHostedAttribute(): bool { return strlen($this->getPassSessionAttribute() ?: '') > 0; } public function getIsHoldAttribute(): bool { return $this->role & self::NODE_HOLD; } public function getIsOwnedAttribute(): bool { return $this->system->is_owned; } public function getIsPrivateAttribute(): bool { return (! $this->system->address); } /** * Determine this address' role (without status) * * @return int * @see \App\Http\Requests\AddressAdd::class */ public function getRoleIdAttribute(): int { static $warn = FALSE; $val = ($this->role & self::NODE_ALL); $role = $this->ftn_role(); if ($this->isRoleOverride($role)) { if (! $warn) { $warn = TRUE; Log::alert(sprintf('%s:! Address ROLE [%d] is not consistent with what is expected [%d] for [%s]',self::LOGKEY,$val,$role,$this->ftn)); } return $val; } else return $role; } /** * Return a name for the role (without status) * * @return string */ public function getRoleNameAttribute(): string { if ($this->getIsDownAttribute()) return 'DOWN'; if ($this->getIsHoldAttribute()) return 'HOLD'; if ($this->getIsPrivateAttribute()) return 'PVT'; switch ($this->role_id) { case self::NODE_ZC: return 'ZC'; case self::NODE_RC: return 'RC'; case self::NODE_NC: return 'NC'; case self::NODE_HC: return 'HUB'; case self::NODE_NN: return 'NODE'; case self::NODE_POINT: return 'POINT'; default: return $this->role_id; } } public function getPassFixAttribute(): ?string { return strtoupper($this->session('fixpass')); } public function getPassPacketAttribute(): ?string { return strtoupper($this->session('pktpass')); } public function getPassSessionAttribute(): ?string { return $this->session('sespass'); } public function getPassTicAttribute(): ?string { return strtoupper($this->session('ticpass')); } /* METHODS */ /** * Check the user's activation code for this address is correct * * @param User $uo * @param string $code * @return bool */ public function activation_check(User $uo,string $code): bool { try { Log::info(sprintf('%s:Checking Activation code [%s] is valid for user [%d]',self::LOGKEY,$code,$uo->id)); return ($code === $this->activation_set($uo)); } catch (\Exception $e) { Log::error(sprintf('%s:! Activation code [%s] invalid for user [%d]',self::LOGKEY,$code,$uo->id)); return FALSE; } } /** * Create an activation code for this address * * @param User $uo * @return string */ public function activation_set(User $uo): string { return sprintf('%x:%s', $this->id, substr(md5(sprintf('%d:%x',$uo->id,timew($this->updated_at))),0,10) ); } /** * This is the children of this record, as per normal FTN routing ZC -> RC -> NC -> HUB -> Node -> Point * * This a ZC would return all records for the Zone, * An RC would only return records in the region, etc * * @return Collection * @see self::parent() */ public function children(): Collection { // If we are a point, our parent is the boss switch ($this->role_id) { case self::NODE_NN: // Normal Nodes $o = self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id) ->where('node_id',$this->node_id); break; case self::NODE_HC: // Hubs $o = self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id) ->where('hub_id',$this->id) ->where('id','<>',$this->id) ->get(); // Need to add in points of this hub's nodes return $o->merge( self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id) ->whereIn('node_id',$o->pluck('node_id')) ->where('point_id','<>',0) ->get() ); case self::NODE_NC: // Nets $o = self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id); break; case self::NODE_RC: // Regions $o = self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id); break; case self::NODE_ZC: // Zone $o = self::active() ->where('zone_id',$this->zone_id); break; default: return new Collection; } return $o ->where('id','<>',$this->id) ->get(); } /** * Contrast to children(), this takes into account authentication details, and we route mail to this * address (because we have session details) and it's children. * * @return Collection * @throws \Exception * @see self::children() * @see self::uplink() */ public function downlinks(): Collection { // We have no session data for this address, (and its not our address), by definition it has no children if (! $this->is_hosted && (! our_address()->pluck('id')->contains($this->id))) return new Collection; // If this system is not marked to default route for this address if (! $this->is_default_route) { $children = $this->children() ->push($this); // We route everything for this domain } else { $children = self::select('addresses.*') ->join('zones',['zones.id'=>'addresses.zone_id']) ->where('addresses.id','<>',$this->id) ->where('domain_id',$this->zone->domain_id) ->with(['zone.domain']) ->active() ->FTNorder() ->get(); } // If there are no children if (! $children->count()) return new Collection; // Exclude links and their children. $exclude = collect(); foreach (our_nodes($this->zone->domain)->diff([$this]) as $o) { // We only exclude downlink children if ($o->role_id < $this->role_id) continue; // If this address is in our list, remove it and it's children $exclude = $exclude->merge($o->children()); $exclude->push($o); } return $children->diff($exclude); } /** * List of all our nodes and their children * * @return \Illuminate\Support\Collection * @throws \Exception * @deprecated use children() */ public function downstream(): \Illuminate\Support\Collection { return $this->downlinks()->transform(function($item) { return $item->nodes()->push($item); })->flatten(); } /** * Files waiting to be sent to this system * * @return Collection */ public function dynamicWaiting(): Collection { return $this->dynamics() ->where('next_at','<=',Carbon::now()) ->where('active',TRUE) ->get(); } /** * Echomail waiting to be sent to this system * * @return Builder */ public function echomailWaiting(): Builder { return Echomail::select('echomails.*') ->join('echomail_seenby',['echomail_seenby.echomail_id'=>'echomails.id']) ->where('address_id',$this->id) ->whereNull('echomails.deleted_at') ->whereNotNull('export_at') ->whereNull('sent_at') ->orderby('id') ->with([ 'tagline:id,value', 'tearline:id,value', 'origin:id,value', 'echoarea:id,name,domain_id', 'echoarea.domain:id,name', 'fftn:id,zone_id,region_id,host_id,node_id,point_id', 'fftn.zone:id,domain_id,zone_id', 'fftn.zone.domain:id,name', ]); } /** * Files waiting to be sent to this system * * @return Collection */ public function filesWaiting(): Collection { return File::select('files.*') ->join('file_seenby',['file_seenby.file_id'=>'files.id']) ->where('address_id',$this->id) ->whereNull('files.deleted_at') ->whereNotNull('export_at') ->whereNull('sent_at') ->orderby('id') ->with([ 'filearea:id,name,domain_id', 'filearea.domain:id,name', 'fftn:id,zone_id,region_id,host_id,node_id,point_id', 'fftn.zone:id,domain_id,zone_id', 'fftn.zone.domain:id,name', ]) ->get(); } /** * Work out what role this FTN should have * * @return int|null */ private function ftn_role(): ?int { $role = NULL; // If we have a point address, we're a point if ($this->point_id) $role = self::NODE_POINT; // If we have a node_id, we're either a Node or a Hub elseif ($this->node_id) { $role = ($this->nodes_hub->count()) ? self::NODE_HC : ((($this->role & Address::NODE_ALL) === self::NODE_HC) ? self::NODE_HC : self::NODE_NN); // point_id and node_id are zero // If our region_id !== host_id, and are not zero, and node_id/point_id === 0, we are an NC } elseif (($this->region_id !== $this->host_id) && $this->host_id) { $role = self::NODE_NC; // point_id and node_id are zero } elseif (($this->region_id === $this->host_id) && $this->host_id) { $role = self::NODE_RC; // point_id and node_id are zero } elseif (($this->region_id === $this->host_id) && (! $this->host_id)) { $role = self::NODE_ZC; } if (isset($this->region_id) && is_null($role)) Log::alert(sprintf('%s:! Address ROLE [%d] could not be determined for [%s]',self::LOGKEY,($this->role & Address::NODE_ALL),$this->ftn)); return $role; } /** * Get echomail for this node * * @return Packet|null * @throws \Exception * @todo If we export to uplink hubs without our address in the seenby, they should send the message back to * us with their seenby's. */ public function getEchomail(): ?Packet { if ($count=($num=$this->echomailWaiting())->count()) { Log::info(sprintf('%s:= Got [%d] echomails for [%s] for sending',self::LOGKEY,$count,$this->ftn)); // Limit to max messages if ($count > $this->system->pkt_msgs) Log::notice(sprintf('%s:= Only sending [%d] echomails for [%s]',self::LOGKEY,$this->system->pkt_msgs,$this->ftn)); return $this->system->packet($this)->mail($num->take($this->system->pkt_msgs)->get()); } return NULL; } /** * Get netmail for this node (including it's children) * * @return Packet|null * @throws \Exception */ public function getNetmail(): ?Packet { if ($count=($num=$this->netmailAlertWaiting())->count()) { Log::info(sprintf('%s:= Packaging [%d] netmail alerts to [%s]',self::LOGKEY,$count,$this->ftn)); $msgs = $num->get(); // Find any message that defines a packet password $pass = $msgs ->map(function($item) { $passpos = strpos($item->subject,':'); return (($passpos > 0) && ($passpos <= 8)) ? substr($item->subject,0,$passpos) : NULL; }) ->filter() ->pop(); Log::debug(sprintf('%s:= Overwriting system packet password with [%s] for [%s]',self::LOGKEY,$pass,$this->ftn)); return $this->system->packet($this,$pass)->mail( $msgs->filter(fn($item)=>preg_match("/^{$pass}:/",$item->subject)) ->transform(function($item) use ($pass) { $item->subject = preg_replace("/^{$pass}:/",'',$item->subject); return $item; }) ); } if ($count=($num=$this->netmailWaiting())->count()) { Log::info(sprintf('%s:= Got [%d] netmails for [%s] for sending',self::LOGKEY,$count,$this->ftn)); // Limit to max messages if ($count > $this->system->pkt_msgs) Log::alert(sprintf('%s:= Only sending [%d] netmails for [%s]',self::LOGKEY,$this->system->pkt_msgs,$this->ftn)); return $this->system->packet($this)->mail($num->take($this->system->pkt_msgs)->get()); } return NULL; } public function isRoleOverride(int $role=NULL): bool { $val = ($this->role & self::NODE_ALL); $role = $role ?: $this->ftn_role(); return ($val && ($role !== $val)) || (! $role); } /** * Netmail waiting to be sent to this system * * @return Builder * @throws \Exception */ public function netmailWaiting(): Builder { // Addresses that our downstream of this address, except anybody that has session details with us $ours = our_nodes($this->zone->domain)->pluck('id'); $addresses = $this->downstream() ->filter(fn($item)=>! $ours->contains($item->id)) ->merge($this->system->match($this->zone,Address::NODE_ALL)); $netmails = $this ->UncollectedNetmail() ->select('netmails.id') ->whereIn('addresses.id',$addresses->pluck('id')) ->groupBy(['netmails.id']) ->get(); return Netmail::whereIn('id',$netmails->pluck('id')); } /** * Netmail alerts waiting to be sent to this system * * @return Builder * @throws \Exception * @note The packet password to use is on the subject line for these alerts */ public function netmailAlertWaiting(): Builder { $netmails = $this ->UncollectedNetmail() ->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_LOCAL)) ->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_PKTPASSWD)) ->whereRaw(sprintf('(flags & %d) = 0',Message::FLAG_SENT)) ->select('netmails.id') ->whereIn('addresses.id',$this->downlinks()->add($this)->pluck('id')) ->groupBy(['netmails.id']) ->get(); return Netmail::whereIn('id',$netmails->pluck('id')); } public function nodes(): Collection { switch ($this->role_id) { case self::NODE_NN: return $this->nodes_point; case self::NODE_HC: return $this->nodes_hub; case self::NODE_NC: return $this->nodes_net; case self::NODE_RC: return $this->nodes_region; case self::NODE_ZC: return $this->nodes_zone; default: return new Collection; } } /** * Find the immediate parent for this address, as per normal FTN routing ZC <- RC <- NC <- HUB <- Node <- Point * * @return Address|null * @throws \Exception * @see self::children() */ public function parent(): ?Address { // If we are a point, our parent is the boss switch ($this->role_id) { case self::NODE_POINT: // BOSS Node return self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id) ->where('node_id',$this->node_id) ->where('point_id',0) ->single(); case self::NODE_NN: // HUB if it exists, otherwise NC if ($this->uplink_hub) return $this->uplink_hub; // Else fall through case self::NODE_HC: // NC return self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->host_id) ->where('node_id',0) ->where('point_id',0) ->single(); case self::NODE_NC: // RC return self::active() ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) ->where('host_id',$this->region_id) ->where('node_id',0) ->where('point_id',0) ->single(); case self::NODE_RC: // ZC return self::active() ->where('zone_id',$this->zone_id) ->where('region_id',0) ->where('node_id',0) ->where('point_id',0) ->single(); default: return NULL; } } /** * Retrieve the address session details/passwords * * @param string $type * @return string|null */ private function session(string $type): ?string { return ($this->exists && ($x=$this->system->sessions->where('id',$this->zone_id)->first())) ? ($x->pivot->{$type} ?: '') : NULL; } /** * Contrast to parent(), this takes into account authentication details, and we route mail to this * address (because we have session details) with that uplink. * * @return Address|$this|null * @throws \Exception * @see self::parent() * @see self::downlinks() */ public function uplink(): ?Address { // If it is our address if (our_address()->contains($this)) return NULL; // If we have session password, then we are the parent if ($this->is_hosted) return $this; // Traverse up our parents until we have one with session details if ($x=$this->parent()?->uplink()) { return $x; // See if we have a node registered as the default route for this zone } else { $sz = SystemZone::whereIn('zone_id',$this->domain->zones->pluck('id')) ->where('default',TRUE) ->single(); return $sz?->system->akas->sortBy('security')->last(); } } }