clrghouz/app/Models/Address.php

456 lines
12 KiB
PHP

<?php
namespace App\Models;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use App\Classes\FTN\Packet;
use App\Http\Controllers\DomainController;
use App\Traits\{ScopeActive,UsePostgres};
/**
* @todo Need to stop this from happening:
* In this example nn:3/1 can be defined 3 different ways.
* + id | zone_id | region_id | host_id | node_id | point_id | status | role | system_id | hub_id
* + ----+---------+-----------+---------+---------+----------+--------+------+-----------+--------
* + 533 | 6 | 3 | 0 | 1 | 0 | | 4 | 1 |
* + 534 | 6 | n | 3 | 1 | 0 | | 2 | 1 |
* + 535 | 6 | 0 | 3 | 1 | 0 | | 2 | 1 |
*/
class Address extends Model
{
use ScopeActive,SoftDeletes,UsePostgres;
/* SCOPES */
public function scopeFTNOrder($query)
{
return $query
->orderBy('region_id')
->orderBy('host_id')
->orderBy('node_id')
->orderBy('point_id');
}
/* RELATIONS */
/**
* Find children dependent on this record
*/
public function children()
{
// We have no session data for this address, by definition it has no children
if (! $this->session('sespass'))
return $this->hasMany(self::class,'id','void');
if (! $this->session('default')) {
switch ($this->role) {
case DomainController::NODE_ZC:
$children = self::select('addresses.*')
->where('zone_id',$this->zone_id);
break;
case DomainController::NODE_RC:
$children = self::select('addresses.*')
->where('zone_id',$this->zone_id)
->where('region_id',$this->region_id);
break;
case DomainController::NODE_NC:
$children = self::select('addresses.*')
->where('zone_id',$this->zone_id)
->where('region_id',$this->region_id)
->where('host_id',$this->host_id);
break;
case DomainController::NODE_HC:
// Identify our children.
$children = self::select('addresses.*')
->where('hub_id',$this->id);
break;
case DomainController::NODE_UNKNOWN:
case DomainController::NODE_ACTIVE:
case DomainController::NODE_POINT:
// Nodes dont have children, but must return a relationship instance
return $this->hasOne(self::class,NULL,'void');
default:
throw new Exception('Unknown role: '.serialize($this->role));
}
} else {
$children = self::select('addresses.*')
->where('zone_id',$this->zone_id);
}
// Remove any children that we have session details for (SAME AS HC)
$sessions = self::select('hubnodes.*')
->join('system_zone',['system_zone.system_id'=>'addresses.system_id','system_zone.zone_id'=>'addresses.zone_id'])
->join('addresses as hubnodes',['hubnodes.zone_id'=>'addresses.zone_id','hubnodes.id'=>'addresses.id'])
->where('addresses.zone_id',$this->zone_id)
->where('addresses.system_id','<>',$this->system_id)
->whereIN('addresses.system_id',$children->get()->pluck('system_id'));
// For each of the session, identify their children
$session_kids = collect();
foreach ($sessions->get() as $so)
$session_kids = $session_kids->merge(($x=$so->children) ? $x->pluck('id') : []);
// ZC's receive all mail, except for defined nodes, and defined hubs/hosts/rcs
return $this->hasMany(self::class,'zone_id','zone_id')
->whereIn('id',$children->get()->pluck('id')->toArray())
->whereNotIn('id',$sessions->get()->pluck('id')->toArray())
->whereNotIn('id',$session_kids->toArray())
->where('system_id','<>',$this->system_id)
->select('addresses.*')
->orderBy('region_id')
->orderBy('host_id')
->orderBy('node_id')
->orderBy('point_id')
->with(['zone.domain']);
}
public function echoareas()
{
return $this->belongsToMany(Echoarea::class)
->withPivot(['subscribed']);
}
/**
* Who we send this systems mail to.
*
* @return Address|null
* @throws Exception
*/
public function parent(): ?Address
{
// If we are have session password, then we dont have a parent
if ($this->session('sespass'))
return $this;
switch ($this->role) {
// ZCs dont have parents, but we may have a default
case DomainController::NODE_ZC:
if (($x=$this->zone->systems->where('pivot.default',TRUE))->count())
return $x->first()->match($this->zone)->first();
else
return NULL;
// RC
case DomainController::NODE_RC:
$parent = self::where('zone_id',$this->zone_id)
->where('region_id',0)
->where('host_id',0)
->where('node_id',0)
->single();
break;
// Hosts
case DomainController::NODE_NC:
// See if we have a RC
$parent = self::where('zone_id',$this->zone_id)
->where('region_id',$this->region_id)
->where('host_id',0)
->where('node_id',0)
->single();
if (! $parent) {
// See if we have a RC
$parent = self::where('zone_id',$this->zone_id)
->where('region_id',0)
->where('host_id',0)
->where('node_id',0)
->single();
}
break;
// Hubs
case DomainController::NODE_HC:
// Normal Nodes
case DomainController::NODE_ACTIVE:
// If we are a child of a hub, then check our hub
$parent = (($this->hub_id)
? self::where('id',$this->hub_id)
: self::where('zone_id',$this->zone_id)
->where('region_id',$this->region_id)
->where('host_id',$this->host_id)
->where('node_id',0))
->single();
break;
case DomainController::NODE_UNKNOWN:
case DomainController::NODE_POINT:
case DomainController::NODE_DOWN:
// @todo Points - if the boss is defined, we should return it.
return NULL;
default:
throw new Exception('Unknown role: '.serialize($this->role));
}
return $parent?->parent();
}
public function system()
{
return $this->belongsTo(System::class);
}
public function zone()
{
return $this->belongsTo(Zone::class);
}
/* ATTRIBUTES */
/**
* Render the node name in full 5D
*
* @return string
*/
public function getFTNAttribute(): string
{
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
{
return sprintf('%d:%d/%d',$this->zone->zone_id,$this->host_id ?: $this->region_id,$this->node_id);
}
public function getFTN4DAttribute(): string
{
return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id);
}
public function getRoleNameAttribute(): string
{
switch ($this->role) {
case DomainController::NODE_ACTIVE:
return 'NODE';
case DomainController::NODE_ZC:
return 'ZC';
case DomainController::NODE_RC:
return 'RC';
case DomainController::NODE_NC:
return 'NC';
case DomainController::NODE_HC:
return 'HUB';
case DomainController::NODE_POINT:
return 'POINT';
default:
return '?';
}
}
/* METHODS */
/**
* Find a record in the DB for a node string, eg: 10:1/1.0
*
* @param string $ftn
* @return Address|null
* @throws Exception
*/
public static function findFTN(string $ftn): ?self
{
$ftn = self::parseFTN($ftn);
// Are we looking for a region address
if (($ftn['f'] === 0) && $ftn['p'] === 0) {
$o = (new self)->active()
->select('addresses.*')
->where('zones.zone_id',$ftn['z'])
->where(function($q) use ($ftn) {
return $q
->where(function($q) use ($ftn) {
return $q->where('region_id',$ftn['n'])
->where('host_id',0);
});
})
->where('node_id',$ftn['f'])
->where('point_id',$ftn['p'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('addresses.active',TRUE)
->when($ftn['d'],function($query,$domain) {
$query->where('domains.name',$domain);
})
->when((! $ftn['d']),function($query) {
$query->where('zones.default',TRUE);
})
->single();
if ($o && $o->system->active)
return $o;
}
$o = (new self)->active()
->select('addresses.*')
->where('zones.zone_id',$ftn['z'])
->where(function($q) use ($ftn) {
return $q->where(function($qq) use ($ftn) {
return $qq
->where('region_id',$ftn['n'])
->where('host_id',0);
})
->orWhere(function($qq) use ($ftn) {
return $qq
->where('host_id',$ftn['n']);
});
})
->where('node_id',$ftn['f'])
->where('point_id',$ftn['p'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('addresses.active',TRUE)
->when($ftn['d'],function($query,$domain) {
$query->where('domains.name',$domain);
})
->when((! $ftn['d']),function($query) {
$query->where('zones.default',TRUE);
})
->single();
return ($o && $o->system->active) ? $o : NULL;
}
/**
* Get echomail for this node
*
* @return Packet|null
*/
public function getEchomail(): ?Packet
{
$pkt = NULL;
$echomail = DB::table('address_echomail')
->select('echomail_id')
->where('address_id',$this->id)
->whereNull('sent_date')
->get();
if (($x=Echomail::select('*')
->whereIn('_id',$echomail->pluck('echomail_id')))
->count())
{
$pkt = $this->getPacket($x->get());
DB::table('address_echomail')
->whereIn('echomail_id',$echomail->pluck('echomail_id'))
->where('address_id',$this->id)
->whereNull('sent_date')
->update(['sent_date'=>Carbon::now()]);
}
return $pkt;
}
/**
* Get netmail for this node (including it's children)
*
* @return Packet|null
*/
public function getNetmail(): ?Packet
{
if (($x=Netmail::whereIn('tftn_id',(($x=$this->children) ? $x->pluck('id') : collect())->push($this->id))
->where(function($q) {
return $q->whereNull('sent')
->orWhere('sent',FALSE);
}))
->whereNull('local')
->count())
{
return $this->getPacket($x->get());
}
return NULL;
}
/**
* Return a packet of mail
*
* @param Collection $c
* @return Packet
*/
private function getPacket(Collection $c): Packet
{
$o = new Packet($this);
foreach ($c as $oo) {
$o->addMail($oo->packet($this));
$oo->packet = $o->name;
$oo->sent = TRUE;
$oo->sent_at = Carbon::now();
$oo->save();
}
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
{
// http://ftsc.org/docs/frl-1028.002
if (! preg_match('#^([0-9]+):([0-9]+)/([0-9]+)(.([0-9]+))?(@([a-z0-9\-_~]{0,8}))?$#',strtolower($ftn),$matches))
throw new Exception('Invalid FTN: '.$ftn);
// Check our numbers are correct.
foreach ([1,2,3] as $i) {
if ((! is_numeric($matches[$i])) || ($matches[$i] > DomainController::NUMBER_MAX))
throw new Exception('Invalid FTN: '.$ftn);
}
if (isset($matches[5]) AND ((! is_numeric($matches[$i])) || ($matches[5] > DomainController::NUMBER_MAX)))
throw new Exception('Invalid FTN: '.$ftn);
return [
'z'=>(int)$matches[1],
'n'=>(int)$matches[2],
'f'=>(int)$matches[3],
'p'=>isset($matches[5]) && $matches[5] ? (int)$matches[5] : 0,
'd'=>$matches[7] ?? NULL
];
}
/**
* Retrieve the address session details/passwords
*
* @param string $type
* @return string|null
*/
public function session(string $type): ?string
{
return ($x=$this->system->sessions->where('id',$this->zone_id)->first()) ? $x->pivot->{$type} : NULL;
}
}