Added filefix %AREA

This commit is contained in:
Deon George 2024-11-26 22:05:40 +11:00
parent 39034dbbb0
commit d58ed8842b
8 changed files with 403 additions and 46 deletions

View File

@ -78,7 +78,7 @@ class Area extends Base
->whereNotNull('export_at') ->whereNotNull('export_at')
->whereNull('sent_at') ->whereNull('sent_at')
->orderBy('echomails.datetime') ->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(); ->delete();
Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x)); Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x));

View File

@ -0,0 +1,144 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Jobs\FilefixRescan;
// Filearea Processing Command
class Area extends Base
{
private const LOGKEY = 'FFA';
private const command = '%AREA';
public static function help(): array
{
return [
self::command.' [-|+]<FILEAREA> [R|D=<DAYS>]',
' 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);
}
}
}

View File

@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Notification;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base; use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Notifications\Netmails\Filefix\AreaList as AreaListNotification; use App\Notifications\Netmails\Filefix\AreaList as AreaListNotification;
// LIST - List echoareas in a domain // LIST - List fileareas in a domain
class AreaList extends Base class AreaList extends Base
{ {
private const LOGKEY = 'AFS'; private const LOGKEY = 'AFS';

View File

@ -51,6 +51,9 @@ class Rescan extends Command
$eo = Echoarea::where('name',$this->argument('area'))->sole(); $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')) if ($this->option('queue'))
AreafixRescan::dispatch($ao,$eo,$this->argument('days'))->onQueue($this->option('queuename')); AreafixRescan::dispatch($ao,$eo,$this->argument('days'))->onQueue($this->option('queuename'));
else else

View File

@ -2,10 +2,10 @@
namespace App\Console\Commands\Filefix; namespace App\Console\Commands\Filefix;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\{Address,Filearea,File}; use App\Jobs\FilefixRescan;
use App\Models\{Address,Filearea};
class Rescan extends Command class Rescan extends Command
{ {
@ -14,7 +14,13 @@ class Rescan extends Command
* *
* @var string * @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. * The console command description.
@ -31,6 +37,9 @@ class Rescan extends Command
*/ */
public function handle(): int 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')); $ao = Address::findFtn($this->argument('ftn'));
if (! $ao) if (! $ao)
@ -41,49 +50,14 @@ class Rescan extends Command
throw new \Exception('Areaname is required'); throw new \Exception('Areaname is required');
$fao = Filearea::where('name',$this->argument('area'))->sole(); $fao = Filearea::where('name',$this->argument('area'))->sole();
if ($fao->domain_id !== $ao->zone->domain_id) 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)); 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 ($this->option('queue'))
if (! $ao->fileareas->contains($fao->id)) FilefixRescan::dispatch($ao,$fao,$this->argument('days'))->onQueue($this->option('queuename'));
throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$fao->name)); else
FilefixRescan::dispatchSync($ao,$fao,$this->argument('days'));
// 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));
}
}
}
return self::SUCCESS; return self::SUCCESS;
} }

168
app/Jobs/FilefixRescan.php Normal file
View File

@ -0,0 +1,168 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\Skip;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Models\{Address,Filearea,File};
use App\Notifications\Netmails\Filefix\Scan;
class FilefixRescan implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JAR';
private Address $ao; // System address
private Filearea $fao; // Domain we are processing
private int $days;
private bool $rescan;
/**
* Create a new job instance.
*/
public function __construct(Address $ao,Filearea $fao,int $days=30,bool $rescan=FALSE)
{
$this->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));
}
}

View File

@ -16,7 +16,7 @@ class Scan extends Netmails
private const LOGKEY = 'ACS'; private const LOGKEY = 'ACS';
/** /**
* Reply to a areafix, commands unknown. * Notification on a (re)scan request.
* *
* @param Collection $result * @param Collection $result
*/ */

View File

@ -0,0 +1,68 @@
<?php
namespace App\Notifications\Netmails\Filefix;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Notifications\Netmails;
use App\Models\Netmail;
use App\Traits\{MessagePath,PageTemplate};
class Scan extends Netmails
{
use MessagePath,PageTemplate;
private const LOGKEY = 'FCS';
/**
* Notification on a (re)scan request.
*
* @param Collection $result
*/
public function __construct(private Collection $result)
{
parent::__construct();
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return Netmail
* @throws \Exception
*/
public function toNetmail(object $notifiable): Netmail
{
$o = $this->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;
}
}