clrghouz/app/Models/Netmail.php

236 lines
6.0 KiB
PHP

<?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,CompressedStringOrNull,UTF8StringOrNull};
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 = [
'to' => UTF8StringOrNull::class,
'from' => UTF8StringOrNull::class,
'subject' => UTF8StringOrNull::class,
'origin' => UTF8StringOrNull::class,
'tearline' => UTF8StringOrNull::class,
'tagline' => UTF8StringOrNull::class,
'datetime' => 'datetime:Y-m-d H:i:s',
'kludges' => CollectionOrNull::class,
'msg' => CompressedStringOrNull::class,
'msg_src' => CompressedStringOrNull::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);
}
}