<?php namespace App\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Casts\{CollectionOrNull,CompressedString}; use App\Interfaces\Packet; use App\Pivots\ViaPivot; use App\Traits\{MessageAttributes,MsgID}; final class Netmail extends Model implements Packet { use SoftDeletes,MsgID,MessageAttributes; private const LOGKEY = 'MN-'; private const PATH_REGEX = '/^([0-9]+:[0-9]+\/[0-9]+(\..*)?)\s+@([0-9.a-zA-Z]+)\s+(.*)$/'; /** * Kludges that we absorb in this model */ private const kludges = [ 'MSGID:'=>'msgid', 'REPLY:'=>'replyid', 'Via' => 'set_path', ]; protected $casts = [ 'datetime' => 'datetime:Y-m-d H:i:s', 'kludges' => CollectionOrNull::class, 'msg' => CompressedString::class, 'msg_src' => CompressedString::class, 'sent_at' => 'datetime:Y-m-d H:i:s', ]; public function __set($key,$value) { switch ($key) { case 'kludges': if (! count($value)) return; if (array_key_exists($value[0],self::kludges)) { $this->{self::kludges[$value[0]]} = $value[1]; } else { $this->kludges->put($value[0],$value[1]); } break; case 'set_fftn': case 'set_tftn': // Values that we pass to boot() to record how we got this netmail case 'set_pkt': case 'set_recvtime': case 'set_sender': // @todo We'll normalise these values when saving the netmail case 'set_tagline': case 'set_tearline': case 'set_origin': $this->set->put($key,$value); break; // The path the netmail went through to get here case 'set_path': if (! $this->set->has($key)) $this->set->put($key,collect()); $this->set->get($key)->push($value); break; default: parent::__set($key,$value); } } public static function boot() { parent::boot(); static::creating(function($model) { if (isset($model->errors) && $model->errors->count()) throw new \Exception('Cannot save, validation errors exist'); }); static::created(function($model) { $nodes = collect(); // Parse PATH // <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number] if ($model->set->has('set_path')) { foreach ($model->set->get('set_path') as $line) { $m = []; if (preg_match(self::PATH_REGEX,$line,$m)) { // Address // @todo Do we need to add a domain here, since the path line may not include one $ao = Address::findFTN($m[1]); // Time $t = []; $datetime = ''; if (! preg_match('/^([0-9]+\.[0-9]+)(\.?(.*))?$/',$m[3],$t)) Log::alert(sprintf('%s:! Unable to determine time from [%s]',self::LOGKEY,$m[3])); else $datetime = Carbon::createFromFormat('Ymd.His',$t[1],$t[3] ?? ''); if (! $ao) { Log::alert(sprintf('%s:! Undefined Node [%s] in netmail path.',self::LOGKEY,$m[1])); //$rogue->push(['node'=>$m[1],'datetime'=>$datetime,'program'=>$m[4]]); } else { $nodes->push(['node'=>$ao,'datetime'=>$datetime,'program'=>$m[4]]); } } } // If there are no details (Mystic), we'll create a blank } elseif ($model->set->has('set_sender')) { $nodes->push(['node'=>$model->set->get('set_sender'),'datetime'=>Carbon::now(),'program'=>'Unknown']); } // Save the Path $ppoid = NULL; foreach ($nodes as $path) { $po = DB::select('INSERT INTO netmail_path (netmail_id,address_id,parent_id,datetime,program) VALUES (?,?,?,?,?) RETURNING id',[ $model->id, $path['node']->id, $ppoid, (string)$path['datetime'], $path['program'], ]); $ppoid = $po[0]->id; } // Our last node in the path is our sender if ($nodes->count() && $model->set->has('set_pkt') && $model->set->has('set_sender') && $model->set->has('set_recvtime')) { DB::update('UPDATE netmail_path set recv_pkt=?,recv_at=?,recv_id=? where address_id=? and netmail_id=?',[ $model->set->get('set_pkt'), $model->set->get('set_recvtime'), $model->set->get('set_sender')->id, Arr::get($nodes->last(),'node')->id, $model->id, ]); } // Save our origin, tearline & tagline if ($model->set->has('set_tagline')) $model->tagline = $model->set->get('set_tagline'); if ($model->set->has('set_tearline')) $model->tearline = $model->set->get('set_tearline'); if ($model->set->has('set_origin')) $model->origin = $model->set->get('set_origin'); $model->save(); }); } /* RELATIONS */ public function fftn() { return $this ->belongsTo(Address::class) ->withTrashed(); } public function path() { return $this->belongsToMany(Address::class,'netmail_path') ->withPivot(['id','parent_id','datetime','program','recv_pkt','recv_id']) ->orderBy('netmail_path.id') ->using(ViaPivot::class); } public function tftn() { return $this ->belongsTo(Address::class) ->withTrashed(); } /* ATTRIBUTES */ /** * Enable rendering the path even if the model hasnt been saved * * @return Collection */ public function getPathAttribute(): Collection { return ((! $this->exists) && $this->set->has('set_path')) ? $this->set->get('set_path')->map(function($item) { $m = []; preg_match(self::PATH_REGEX,$item,$m); return $m[1]; }) : $this->getRelationValue('path'); } /* METHODS */ /** * Render the via line * * @param Address $ao * @return string * @throws \Exception */ public function via(Address $ao): string { if (! $ao->pivot) throw new \Exception('Cannot render the via line without an address record without a path pivot'); return sprintf('%s @%s.UTC %s', $ao->ftn3d, $ao->pivot->datetime->format('Ymd.His'), $ao->pivot->program); } }