diff --git a/app/Classes/FTN/Process/Netmail/Robot/Areafix/Area.php b/app/Classes/FTN/Process/Netmail/Robot/Areafix/Area.php index 99b39ef..195e50d 100644 --- a/app/Classes/FTN/Process/Netmail/Robot/Areafix/Area.php +++ b/app/Classes/FTN/Process/Netmail/Robot/Areafix/Area.php @@ -78,7 +78,7 @@ class Area extends Base ->whereNotNull('export_at') ->whereNull('sent_at') ->orderBy('echomails.datetime') - ->skip($this->mo->fftn->system->pkt_msgs) + ->skip($this->mo->fftn->system->pkt_msgs) // Might already being sent in this session ->delete(); Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x)); diff --git a/app/Classes/FTN/Process/Netmail/Robot/Filefix/Area.php b/app/Classes/FTN/Process/Netmail/Robot/Filefix/Area.php new file mode 100644 index 0000000..8914a6f --- /dev/null +++ b/app/Classes/FTN/Process/Netmail/Robot/Filefix/Area.php @@ -0,0 +1,144 @@ + [R|D=]', + ' Use the area command to subscribe (+) or unsubscribe (-) to a FILEAREA', + ' Arguments:', + ' - FILEAREA (required) name of area to subscribe or unsubscribe', + ' - D=DAYS (optional) number of days to resend files from this area that you', + ' havent already received (useful if you are resubscribing to an area and', + ' have received files in the past)', + ' - R=DAYS (optional) number of days to resend files from this area (even if', + ' it was sent to you previously)', + ' Notes:', + ' * "+" is optional, and is implied if "-" is not used', + ' * "R" and "D" options only apply to subscribing', + ]; + } + + public function process(): string + { + $command = self::command.' '.join(' ',$this->arguments); + + // If command starts with '-', its to unsubscribe + if (str_starts_with($this->arguments[0],'-')) { + $sub = FALSE; + $area = substr($this->arguments[0],1); + + } elseif (str_starts_with($this->arguments[0],'+')) { + $sub = TRUE; + $area = substr($this->arguments[0],1); + + } else { + $sub = TRUE; + $area = $this->arguments[0]; + } + + Log::debug(sprintf('%s:- Processing [%s] for [%s]',self::LOGKEY,$sub ? 'ADD' : 'REMOVE',$area)); + + // Drop the area from the arguments, the rest are options + array_shift($this->arguments); + + // Area exists + if ($fa=$this->mo->fftn->domain->fileareas->where('name',$area)->pop()) { + // If already subscribed + if ($nea=$this->mo->fftn->fileareas->where('name',$area)->pop()) { + // requesting to subscribe "You already are since..., arguments ignored + if ($sub) { + Log::debug(sprintf('%s:- FTN [%s] ALREADY subscribed to [%s] since [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$nea->pivot->subscribed->format('Y-m-d H:i'))); + + return sprintf('%-25s <-- ALREADY subscribed since %s',$command,$nea->pivot->subscribed->format('Y-m-d H:i')); + + // requesting to unsubscribe + } else { + $this->mo->fftn->fileareas()->detach($fa->id); + + // Remove sub, clear queue + $x = DB::table('file_seenby') + ->where('address_id',$this->mo->fftn->id) + ->join('files',['files.id'=>'file_seenby.file_id']) + ->where('filearea_id',$nea->id) + ->whereNotNull('export_at') + ->whereNull('sent_at') + ->orderBy('files.datetime') + ->skip(1) // Might already being sent in this session + ->delete(); + + Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x)); + + return sprintf('%-25s <-- UNSUBSCRIBED, CLEARED [%d] FILES from queue',$command,$x); + } + + // If not subscribed + } else { + // requesting to subscribe, subsubsribe and rescan if arguments + if ($sub) { + $this->mo->fftn->fileareas()->attach([$fa->id=>['subscribed'=>Carbon::now()]]); + + // If we have arguments, they are to rescan + if (count($this->arguments) === 1) { + $m = []; + if (preg_match('/^([DR])=([0-9]+)$/',$this->arguments[0],$m)) { + switch ($m[1]) { + // Scan + case 'D': + FilefixRescan::dispatch($this->mo->fftn,$fa,$m[2]) + ->onQueue('mail'); + + return sprintf('%-25s <-- SUBSCRIBED, RESCAN [%d] DAYS queued',$command,$m[2]); + + // Scan + case 'R': + FilefixRescan::dispatch($this->mo->fftn,$fa,$m[2],TRUE) + ->onQueue('mail'); + + return sprintf('%-25s <-- SUBSCRIBED, FORCE RESCAN [%d] DAYS queued',$command,$m[2]); + } + } + + return sprintf('%-25s <-- SUBSCRIBED, INVALID OPTIONS',$command); + + } elseif (count($this->arguments) > 1) { + Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s], extra commands [%s] ignored',self::LOGKEY,$this->mo->fftn->ftn,$area,join('|',$this->arguments))); + + return sprintf('%-25s <-- SUBSCRIBED, OPTIONS IGNORED',$command); + + } else { + Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area)); + + return sprintf('%-25s <-- SUBSCRIBED',$command); + } + + // If not subscribed, "you arent subscribed, arguments ignored" + } else { + Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area)); + + return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command); + } + } + + } else { + Log::debug(sprintf('%s:- FTN [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area)); + + return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command); + } + } +} \ No newline at end of file diff --git a/app/Classes/FTN/Process/Netmail/Robot/Filefix/AreaList.php b/app/Classes/FTN/Process/Netmail/Robot/Filefix/AreaList.php index 48b9eb4..a71423d 100644 --- a/app/Classes/FTN/Process/Netmail/Robot/Filefix/AreaList.php +++ b/app/Classes/FTN/Process/Netmail/Robot/Filefix/AreaList.php @@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Notification; use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base; use App\Notifications\Netmails\Filefix\AreaList as AreaListNotification; -// LIST - List echoareas in a domain +// LIST - List fileareas in a domain class AreaList extends Base { private const LOGKEY = 'AFS'; diff --git a/app/Console/Commands/Areafix/Rescan.php b/app/Console/Commands/Areafix/Rescan.php index 70c2731..fcd4ed7 100644 --- a/app/Console/Commands/Areafix/Rescan.php +++ b/app/Console/Commands/Areafix/Rescan.php @@ -51,6 +51,9 @@ class Rescan extends Command $eo = Echoarea::where('name',$this->argument('area'))->sole(); + if ($eo->domain_id !== $ao->zone->domain_id) + throw new \Exception(sprintf('Echo area [%s] is not in domain [%s] for FTN [%s]',$eo->name,$ao->zone->domain->name,$ao->ftn)); + if ($this->option('queue')) AreafixRescan::dispatch($ao,$eo,$this->argument('days'))->onQueue($this->option('queuename')); else diff --git a/app/Console/Commands/Filefix/Rescan.php b/app/Console/Commands/Filefix/Rescan.php index afe0180..b82c292 100644 --- a/app/Console/Commands/Filefix/Rescan.php +++ b/app/Console/Commands/Filefix/Rescan.php @@ -2,10 +2,10 @@ namespace App\Console\Commands\Filefix; -use Carbon\Carbon; use Illuminate\Console\Command; -use App\Models\{Address,Filearea,File}; +use App\Jobs\FilefixRescan; +use App\Models\{Address,Filearea}; class Rescan extends Command { @@ -14,7 +14,13 @@ class Rescan extends Command * * @var string */ - protected $signature = 'filefix:rescan {ftn} {area} {file?}'; + protected $signature = 'filefix:rescan' + .' {ftn : FTN Address}' + .' {area : Echoarea Tag}' + .' {days? : Limit to files received days ago}' + .' {--j|queue : Queue the Job}' + .' {--Q|queuename=default : Queue on queue}' + .' {--R|export : Re-export previously sent files}'; /** * The console command description. @@ -31,6 +37,9 @@ class Rescan extends Command */ public function handle(): int { + if (($this->argument('days')) && (! is_numeric($this->argument('days')))) + throw new \Exception('Days must be numeric: '.$this->argument('days')); + $ao = Address::findFtn($this->argument('ftn')); if (! $ao) @@ -41,49 +50,14 @@ class Rescan extends Command throw new \Exception('Areaname is required'); $fao = Filearea::where('name',$this->argument('area'))->sole(); + if ($fao->domain_id !== $ao->zone->domain_id) throw new \Exception(sprintf('File area [%s] is not in domain [%s] for FTN [%s]',$fao->name,$ao->zone->domain->name,$ao->ftn)); - // Check that the user is subscribed - if (! $ao->fileareas->contains($fao->id)) - throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$fao->name)); - - // Check that an FTN can read the area - if (! $fao->can_read($ao->security)) - throw new \Exception(sprintf('FTN [%s] doesnt have permission to receive [%s]',$ao->ftn,$fao->name)); - - foreach (File::select('id') - ->where('filearea_id',$fao->id) - ->when($this->argument('file'),function($query) { - return $query->where('name','=',$this->argument('file')); - }) - ->orderBy('datetime') - ->cursor() as $fo) { - - // File hasnt been exported before - if (! $fo->seenby->count()) { - $fo->seenby()->attach($ao->id,['export_at'=>Carbon::now()]); - $this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d)); - - } else { - $export = $fo->seenby->where('id',$ao->id)->pop(); - - // File is pending export - if ($export && $export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) { - $this->warn(sprintf('Not exporting [%d] already queued for [%s]',$fo->id,$ao->ftn3d)); - - // File has been exported - } elseif ($export) { - $fo->seenby()->updateExistingPivot($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]); - $this->info(sprintf('Re-exported [%d] to [%s]',$fo->id,$ao->ftn3d)); - - // File has not been exported - } else { - $fo->seenby()->attach($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]); - $this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d)); - } - } - } + if ($this->option('queue')) + FilefixRescan::dispatch($ao,$fao,$this->argument('days'))->onQueue($this->option('queuename')); + else + FilefixRescan::dispatchSync($ao,$fao,$this->argument('days')); return self::SUCCESS; } diff --git a/app/Jobs/FilefixRescan.php b/app/Jobs/FilefixRescan.php new file mode 100644 index 0000000..910943e --- /dev/null +++ b/app/Jobs/FilefixRescan.php @@ -0,0 +1,168 @@ +ao = $ao->withoutRelations(); + $this->fao = $fao->withoutRelations(); + $this->days = $days; + $this->rescan = $rescan; + } + + public function __get(string $key): mixed + { + switch ($key) { + case 'jobname': + return sprintf('%s %s (%d)',$this->ao->ftn,$this->fao->name,$this->days); + + default: + $this->fail('Unkown key:'.$key); + return NULL; + } + } + + public function middleware(): array + { + return [ + Skip::when(function(): bool { + if ($this->fao->domain_id !== $this->ao->zone->domain_id) { + Log::error(sprintf('%s:! File area [%s] is not in domain [%s] for FTN [%s]',self::LOGKEY,$this->fao->name,$this->ao->zone->domain->name,$this->ao->ftn)); + + return TRUE; + + // Check that the user is subscribed + } elseif (! $this->ao->fileareas->contains($this->fao->id)) { + Log::error(sprintf('%s:! FTN [%s] is not subscribed to [%s]',self::LOGKEY,$this->ao->ftn,$this->fao->name)); + + return TRUE; + + // Check that an FTN can read the area + } elseif (! $this->fao->can_read($this->ao->security)) { + Log::error(sprintf('%s:! FTN [%s] doesnt have permission to receive [%s]',self::LOGKEY,$this->ao->ftn,$this->fao->name)); + + return TRUE; + } + + return FALSE; + }) + ]; + } + + public function handle(): void + { + $c = 0; + $s = 0; + + $earliest = NULL; + $latest = NULL; + + foreach (File::select(['id','datetime']) + ->where('filearea_id',$this->fao->id) + ->where('datetime','>=', + Carbon::now() + ->subDays($this->days) + ->startOfDay() + ) + ->orderBy('datetime') + ->cursor() as $fo) { + + // File hasnt been exported before + if (! $fo->seenby->count()) { + $fo->seenby()->attach($this->ao->id,['export_at'=>Carbon::now()]); + $c++; + + if (($fo->datetime < $earliest) || (! $earliest)) + $earliest = $fo->datetime; + + if (($latest < $fo->datetime) || (! $latest)) + $latest = $fo->datetime; + + Log::debug(sprintf('Exported [%d] FILE (%s) dated (%s) to [%s]',$fo->id,$fo->name,$fo->datetime->format('Y-m-d H:i:s'),$this->ao->ftn3d)); + + } else { + $export = $fo->seenby->where('id',$this->ao->id)->pop(); + + if ($export) { + // File is pending export + if ($export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) { + $s++; + Log::debug(sprintf('Not exporting [%d] FILE (%s) dated (%s) already queued for [%s]',$fo->id,$fo->name,$fo->datetime->format('Y-m-d H:i:s'),$this->ao->ftn3d)); + + // File has been exported + } elseif ($this->rescan) { + $fo->seenby()->updateExistingPivot($this->ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]); + $c++; + + if (($fo->datetime < $earliest) || (! $earliest)) + $earliest = $fo->datetime; + + if (($latest < $fo->datetime) || (! $latest)) + $latest = $fo->datetime; + + Log::debug(sprintf('Re-exported [%d] FILE (%s) dated (%s) to [%s]',$fo->id,$fo->name,$fo->datetime,$this->ao->ftn3d)); + + } else { + $s++; + Log::debug(sprintf('Not resending previously sent message [%d], FILE (%s) - sent on [%s]', + $fo->id, + $fo->name, + $export->pivot->sent_at ?: '-', + )); + } + + // File has not been exported + } else { + $fo->seenby()->attach($this->ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]); + $c++; + + if (($fo->datetime < $earliest) || (! $earliest)) + $earliest = $fo->datetime; + + if (($latest < $fo->datetime) || (! $latest)) + $latest = $fo->datetime; + + Log::debug(sprintf('Exported [%d] to [%s]',$fo->id,$this->ao->ftn3d)); + } + } + } + + Notification::route('netmail',$this->ao) + ->notify(new Scan(collect([ + 'area'=>$this->fao->name, + 'queued'=>$c, + 'skipped'=>$s, + 'earliest'=>$earliest, + 'latest'=>$latest, + ]))); + + Log::info(sprintf('%s:= Queued [%d], Skipped [%d] files for [%s] in [%s]',self::LOGKEY,$c,$s,$this->ao->ftn,$this->fao->name)); + } +} \ No newline at end of file diff --git a/app/Notifications/Netmails/Areafix/Scan.php b/app/Notifications/Netmails/Areafix/Scan.php index 9b4f0c2..7c65885 100644 --- a/app/Notifications/Netmails/Areafix/Scan.php +++ b/app/Notifications/Netmails/Areafix/Scan.php @@ -16,7 +16,7 @@ class Scan extends Netmails private const LOGKEY = 'ACS'; /** - * Reply to a areafix, commands unknown. + * Notification on a (re)scan request. * * @param Collection $result */ diff --git a/app/Notifications/Netmails/Filefix/Scan.php b/app/Notifications/Netmails/Filefix/Scan.php new file mode 100644 index 0000000..3d0088c --- /dev/null +++ b/app/Notifications/Netmails/Filefix/Scan.php @@ -0,0 +1,68 @@ +setupNetmail($notifiable); + $ao = $notifiable->routeNotificationFor(static::via); + + Log::info(sprintf('%s:+ Responding to filefix for a node [%s] SCAN processed',self::LOGKEY,$ao->ftn)); + + $o->subject = 'Filefix - Scan Results'; + + // Message + $msg = $this->page(FALSE,'Filefix'); + + $msg->addText("A filefix (re)Scan has completed:\r\r"); + + $msg->addText(sprintf("Area: %s, Queued: %d, Skipped: %d\r\r", + $this->result->get('area'), + $this->result->get('queued'), + $this->result->get('skipped'), + )); + + if ($x=$this->result->get('earliest')) + $msg->addText(sprintf("The earliest file being sent: %s.\r",$x)); + + if (($x=$this->result->get('latest')) && ($this->result->get('earliest') !== $x)) + $msg->addText(sprintf("The latest file being sent: %s.\r",$x)); + + $o->msg = $msg->render(); + $o->set_tagline = 'Why did the chicken cross the road? The robot programmed it.'; + + $o->save(); + + return $o; + } +} \ No newline at end of file