]> gb7djk.dxcluster.net Git - spider.git/blob - perl/DXCommandmode.pm
make sure DXCron::spawn records output
[spider.git] / perl / DXCommandmode.pm
1 #!/usr/bin/perl
2 #
3 # This module impliments the user facing command mode for a dx cluster
4 #
5 # Copyright (c) 1998 Dirk Koopman G1TLH
6 #
7 #
8
9
10 package DXCommandmode;
11
12 #use POSIX;
13
14 @ISA = qw(DXChannel);
15
16 use POSIX qw(:math_h);
17 use DXUtil;
18 use DXChannel;
19 use DXUser;
20 use DXVars;
21 use DXDebug;
22 use DXM;
23 use DXLog;
24 use DXLogPrint;
25 use DXBearing;
26 use CmdAlias;
27 use Filter;
28 use Minimuf;
29 use DXDb;
30 use AnnTalk;
31 use WCY;
32 use Sun;
33 use Internet;
34 use Script;
35 use QSL;
36 use DB_File;
37 use VE7CC;
38 use DXXml;
39 use AsyncMsg;
40 use JSON;
41
42 use Mojo::IOLoop;
43 use Mojo::IOLoop::ForkCall;
44 use Mojo::UserAgent;
45
46 use strict;
47 use vars qw(%Cache %cmd_cache $errstr %aliases $scriptbase %nothereslug
48         $maxbadcount $msgpolltime $default_pagelth $cmdimportdir);
49
50 %Cache = ();                                    # cache of dynamically loaded routine's mod times
51 %cmd_cache = ();                                # cache of short names
52 $errstr = ();                                   # error string from eval
53 %aliases = ();                                  # aliases for (parts of) commands
54 $scriptbase = "$main::root/scripts"; # the place where all users start scripts go
55 $maxbadcount = 3;                               # no of bad words allowed before disconnection
56 $msgpolltime = 3600;                    # the time between polls for new messages 
57 $cmdimportdir = "$main::root/cmd_import"; # the base directory for importing command scripts 
58                                           # this does not exist as default, you need to create it manually
59 #
60
61 #
62 # obtain a new connection this is derived from dxchannel
63 #
64
65 sub new 
66 {
67         my $self = DXChannel::alloc(@_);
68
69         # routing, this must go out here to prevent race condx
70         my $pkg = shift;
71         my $call = shift;
72 #       my @rout = $main::routeroot->add_user($call, Route::here(1));
73         DXProt::_add_thingy($main::routeroot, [$call, 0, 0, 1, undef, undef, $self->{conn}->peerhost], );
74
75         # ALWAYS output the user
76         my $ref = Route::User::get($call);
77         if ($ref) {
78                 $main::me->route_pc16($main::mycall, undef, $main::routeroot, $ref);
79                 $main::me->route_pc92a($main::mycall, undef, $main::routeroot, $ref) unless $DXProt::pc92_slug_changes;
80         }
81
82         return $self;
83 }
84
85 # this is how a a connection starts, you get a hello message and the motd with
86 # possibly some other messages asking you to set various things up if you are
87 # new (or nearly new and slacking) user.
88
89 sub start
90
91         my ($self, $line, $sort) = @_;
92         my $user = $self->{user};
93         my $call = $self->{call};
94         my $name = $user->{name};
95         
96         # log it
97         my $host = $self->{conn}->peerhost;
98         $host ||= "AGW Port #$self->{conn}->{agwport}" if exists $self->{conn}->{agwport};
99         $host ||= "unknown";
100         LogDbg('DXCommand', "$call connected from $host");
101
102         $self->{name} = $name ? $name : $call;
103         $self->send($self->msg('l2',$self->{name}));
104         $self->state('prompt');         # a bit of room for further expansion, passwords etc
105         $self->{priv} = $user->priv || 0;
106         $self->{lang} = $user->lang || $main::lang || 'en';
107         my $pagelth = $user->pagelth;
108         $pagelth = $default_pagelth unless defined $pagelth;
109         $self->{pagelth} = $pagelth;
110         ($self->{width}) = $line =~ /width=(\d+)/; $line =~ s/\s*width=\d+\s*//;
111         $self->{width} = 80 unless $self->{width} && $self->{width} > 80;
112         $self->{consort} = $line;       # save the connection type
113         
114         # set some necessary flags on the user if they are connecting
115         $self->{beep} = $user->wantbeep;
116         $self->{ann} = $user->wantann;
117         $self->{wwv} = $user->wantwwv;
118         $self->{wcy} = $user->wantwcy;
119         $self->{talk} = $user->wanttalk;
120         $self->{wx} = $user->wantwx;
121         $self->{dx} = $user->wantdx;
122         $self->{logininfo} = $user->wantlogininfo;
123         $self->{ann_talk} = $user->wantann_talk;
124         $self->{here} = 1;
125         $self->{prompt} = $user->prompt if $user->prompt;
126         $self->{lastmsgpoll} = 0;
127
128         # sort out new dx spot stuff
129         $user->wantdxcq(0) unless defined $user->{wantdxcq};
130         $user->wantdxitu(0) unless defined $user->{wantdxitu};  
131         $user->wantusstate(0) unless defined $user->{wantusstate};
132
133         # sort out registration
134         if ($main::reqreg == 1) {
135                 $self->{registered} = $user->registered;
136         } elsif ($main::reqreg == 2) {
137                 $self->{registered} = !$user->registered;
138         } else {
139                 $self->{registered} = 1;
140         }
141
142         # send the relevant MOTD
143         $self->send_motd;
144
145         # sort out privilege reduction
146         $self->{priv} = 0 if $line =~ /^(ax|te)/ && !$self->conn->{usedpasswd};
147
148         # get the filters
149         my $nossid = $call;
150         $nossid =~ s/-\d+$//;
151         
152         $self->{spotsfilter} = Filter::read_in('spots', $call, 0) 
153                 || Filter::read_in('spots', $nossid, 0)
154                         || Filter::read_in('spots', 'user_default', 0);
155         $self->{wwvfilter} = Filter::read_in('wwv', $call, 0) 
156                 || Filter::read_in('wwv', $nossid, 0) 
157                         || Filter::read_in('wwv', 'user_default', 0);
158         $self->{wcyfilter} = Filter::read_in('wcy', $call, 0) 
159                 || Filter::read_in('wcy', $nossid, 0) 
160                         || Filter::read_in('wcy', 'user_default', 0);
161         $self->{annfilter} = Filter::read_in('ann', $call, 0) 
162                 || Filter::read_in('ann', $nossid, 0) 
163                         || Filter::read_in('ann', 'user_default', 0) ;
164
165         # clean up qra locators
166         my $qra = $user->qra;
167         $qra = undef if ($qra && !DXBearing::is_qra($qra));
168         unless ($qra) {
169                 my $lat = $user->lat;
170                 my $long = $user->long;
171                 $user->qra(DXBearing::lltoqra($lat, $long)) if (defined $lat && defined $long);  
172         }
173
174         # decide on echo
175         my $echo = $user->wantecho;
176         unless ($echo) {
177                 $self->send_now('E', "0");
178                 $self->send($self->msg('echow'));
179                 $self->conn->echo($echo) if $self->conn->can('echo');
180         }
181         
182         $self->tell_login('loginu');
183         $self->tell_buddies('loginb');
184         
185         # do we need to send a forward/opernam?
186         my $lastoper = $user->lastoper || 0;
187         my $homenode = $user->homenode || ""; 
188         if ($homenode eq $main::mycall && $main::systime >= $lastoper + $DXUser::lastoperinterval) {
189                 run_cmd($main::me, "forward/opernam $call");
190                 $user->lastoper($main::systime + ((int rand(10)) * 86400));
191         }
192
193         # run a script send the output to the punter
194         my $script = new Script(lc $call) || new Script('user_default');
195         $script->run($self) if $script;
196
197         # send cluster info
198         my $info = Route::cluster();
199         $self->send("Cluster:$info");
200
201         # send prompts for qth, name and things
202         $self->send($self->msg('namee1')) if !$user->name;
203         $self->send($self->msg('qthe1')) if !$user->qth;
204         $self->send($self->msg('qll')) if !$user->qra || (!$user->lat && !$user->long);
205         $self->send($self->msg('hnodee1')) if !$user->qth;
206         $self->send($self->msg('m9')) if DXMsg::for_me($call);
207
208         # send out any buddy messages for other people that are online
209         foreach my $call (@{$user->buddies}) {
210                 my $ref = Route::User::get($call);
211                 if ($ref) {
212                         foreach my $node ($ref->parents) {
213                                 $self->send($self->msg($node eq $main::mycall ? 'loginb' : 'loginbn', $call, $node));
214                         } 
215                 }
216         }
217
218         $self->lastmsgpoll($main::systime);
219         $self->prompt;
220 }
221
222 #
223 # This is the normal command prompt driver
224 #
225
226 sub normal
227 {
228         my $self = shift;
229         my $cmdline = shift;
230         my @ans;
231
232         # save this for them's that need it
233         my $rawline = $cmdline;
234         
235         # remove leading and trailing spaces
236         $cmdline =~ s/^\s*(.*)\s*$/$1/;
237         
238         if ($self->{state} eq 'page') {
239                 my $i = $self->{pagelth};
240                 my $ref = $self->{pagedata};
241                 my $tot = @$ref;
242                 
243                 # abort if we get a line starting in with a
244                 if ($cmdline =~ /^a/io) {
245                         undef $ref;
246                         $i = 0;
247                 }
248         
249                 # send a tranche of data
250                 while ($i-- > 0 && @$ref) {
251                         my $line = shift @$ref;
252                         $line =~ s/\s+$//o;     # why am having to do this? 
253                         $self->send($line);
254                 }
255                 
256                 # reset state if none or else chuck out an intermediate prompt
257                 if ($ref && @$ref) {
258                         $tot -= $self->{pagelth};
259                         $self->send($self->msg('page', $tot));
260                 } else {
261                         $self->state('prompt');
262                 }
263         } elsif ($self->{state} eq 'sysop') {
264                 my $passwd = $self->{user}->passwd;
265                 if ($passwd) {
266                         my @pw = grep {$_ !~ /\s/} split //, $passwd;
267                         my @l = @{$self->{passwd}};
268                         my $str = "$pw[$l[0]].*$pw[$l[1]].*$pw[$l[2]].*$pw[$l[3]].*$pw[$l[4]]";
269                         if ($cmdline =~ /$str/) {
270                                 $self->{priv} = $self->{user}->priv;
271                         } else {
272                                 $self->send($self->msg('sorry'));
273                         }
274                 } else {
275                         $self->send($self->msg('sorry'));
276                 }
277                 $self->state('prompt');
278         } elsif ($self->{state} eq 'passwd') {
279                 my $passwd = $self->{user}->passwd;
280                 if ($passwd && $cmdline eq $passwd) {
281                         $self->send($self->msg('pw1'));
282                         $self->state('passwd1');
283                 } else {
284                         $self->conn->{echo} = $self->conn->{decho};
285                         delete $self->conn->{decho};
286                         $self->send($self->msg('sorry'));
287                         $self->state('prompt');
288                 }
289         } elsif ($self->{state} eq 'passwd1') {
290                 $self->{passwd} = $cmdline;
291                 $self->send($self->msg('pw2'));
292                 $self->state('passwd2');
293         } elsif ($self->{state} eq 'passwd2') {
294                 if ($cmdline eq $self->{passwd}) {
295                         $self->{user}->passwd($cmdline);
296                         $self->send($self->msg('pw3'));
297                 } else {
298                         $self->send($self->msg('pw4'));
299                 }
300                 $self->conn->{echo} = $self->conn->{decho};
301                 delete $self->conn->{decho};
302                 $self->state('prompt');
303         } elsif ($self->{state} eq 'talk' || $self->{state} eq 'chat') {
304                 if ($cmdline =~ m{^(?:/EX|/ABORT)}i) {
305                         for (@{$self->{talklist}}) {
306                                 if ($self->{state} eq 'talk') {
307                                         $self->send_talks($_,  $self->msg('talkend'));
308                                 } else {
309                                         $self->local_send('C', $self->msg('chatend', $_));
310                                 }
311                         }
312                         $self->state('prompt');
313                         delete $self->{talklist};
314                 } elsif ($cmdline =~ m|^/+\w+|) {
315                         $cmdline =~ s|^/||;
316                         my $sendit = $cmdline =~ s|^/+||;
317                         my @in = $self->run_cmd($cmdline);
318                         $self->send_ans(@in);
319                         if ($sendit && $self->{talklist} && @{$self->{talklist}}) {
320                                 foreach my $l (@in) {
321                                         my @bad;
322                                         if (@bad = BadWords::check($l)) {
323                                                 $self->badcount(($self->badcount||0) + @bad);
324                                                 LogDbg('DXCommand', "$self->{call} swore: $l with words:" . join(',', @bad) . ")");
325                                         } else {
326                                                 for (@{$self->{talklist}}) {
327                                                         if ($self->{state} eq 'talk') {
328                                                                 $self->send_talks($_, $l);
329                                                         } else {
330                                                                 send_chats($self, $_, $l)
331                                                         }
332                                                 }
333                                         }
334                                 }
335                         }
336                         $self->send($self->{state} eq 'talk' ? $self->talk_prompt : $self->chat_prompt);
337                 } elsif ($self->{talklist} && @{$self->{talklist}}) {
338                         # send what has been said to whoever is in this person's talk list
339                         my @bad;
340                         if (@bad = BadWords::check($cmdline)) {
341                                 $self->badcount(($self->badcount||0) + @bad);
342                                 LogDbg('DXCommand', "$self->{call} swore: $cmdline with words:" . join(',', @bad) . ")");
343                         } else {
344                                 for (@{$self->{talklist}}) {
345                                         if ($self->{state} eq 'talk') {
346                                                 $self->send_talks($_, $rawline);
347                                         } else {
348                                                 send_chats($self, $_, $rawline);
349                                         }
350                                 }
351                         }
352                         $self->send($self->talk_prompt) if $self->{state} eq 'talk';
353                         $self->send($self->chat_prompt) if $self->{state} eq 'chat';
354                 } else {
355                         # for safety
356                         $self->state('prompt');
357                 }
358         } elsif (my $func = $self->{func}) {
359                 no strict 'refs';
360                 my @ans;
361                 if (ref $self->{edit}) {
362                         eval { @ans = $self->{edit}->$func($self, $rawline)};
363                 } else {
364                         eval {  @ans = &{$self->{func}}($self, $rawline) };
365                 }
366                 if ($@) {
367                         $self->send_ans("Syserr: on stored func $self->{func}", $@);
368                         delete $self->{func};
369                         $self->state('prompt');
370                         undef $@;
371                 }
372                 $self->send_ans(@ans);
373         } else {
374                 $self->send_ans(run_cmd($self, $cmdline));
375         } 
376
377         # check for excessive swearing
378         if ($self->{badcount} && $self->{badcount} >= $maxbadcount) {
379                 LogDbg('DXCommand', "$self->{call} logged out for excessive swearing");
380                 $self->disconnect;
381                 return;
382         }
383
384         # send a prompt only if we are in a prompt state
385         $self->prompt() if $self->{state} =~ /^prompt/o;
386 }
387
388 # send out the talk messages taking into account vias and connectivity
389 sub send_talks
390 {
391         my ($self, $ent, $line) = @_;
392         
393         my ($to, $via) = $ent =~ /(\S+)>(\S+)/;
394         $to = $ent unless $to;
395         my $call = $via && $via ne '*' ? $via : $to;
396         my $clref = Route::get($call);
397         my $dxchan = $clref->dxchan if $clref;
398         if ($dxchan) {
399                 $dxchan->talk($self->{call}, $to, undef, $line);
400         } else {
401                 $self->send($self->msg('disc2', $via ? $via : $to));
402                 my @l = grep { $_ ne $ent } @{$self->{talklist}};
403                 if (@l) {
404                         $self->{talklist} = \@l;
405                 } else {
406                         delete $self->{talklist};
407                         $self->state('prompt');
408                 }
409         }
410 }
411
412 sub send_chats
413 {
414         my $self = shift;
415         my $target = shift;
416         my $text = shift;
417
418         my $msgid = DXProt::nextchatmsgid();
419         $text = "#$msgid $text";
420         $main::me->normal(DXProt::pc93($target, $self->{call}, undef, $text));
421 }
422
423 sub special_prompt
424 {
425         my $self = shift;
426         my $prompt = shift;
427         my @call;
428         for (@{$self->{talklist}}) {
429                 my ($to, $via) = /(\S+)>(\S+)/;
430                 $to = $_ unless $to;
431                 push @call, $to;
432         }
433         return $self->msg($prompt, join(',', @call));
434 }
435
436 sub talk_prompt
437 {
438         my $self = shift;
439         return $self->special_prompt('talkprompt');
440 }
441
442 sub chat_prompt
443 {
444         my $self = shift;
445         return $self->special_prompt('chatprompt');
446 }
447
448 #
449 # send a load of stuff to a command user with page prompting
450 # and stuff
451 #
452
453 sub send_ans
454 {
455         my $self = shift;
456         
457         if ($self->{pagelth} && @_ > $self->{pagelth}) {
458                 my $i;
459                 for ($i = $self->{pagelth}; $i-- > 0; ) {
460                         my $line = shift @_;
461                         $line =~ s/\s+$//o;     # why am having to do this? 
462                         $self->send($line);
463                 }
464                 $self->{pagedata} =  [ @_ ];
465                 $self->state('page');
466                 $self->send($self->msg('page', scalar @_));
467         } else {
468                 for (@_) {
469                         if (defined $_) {
470                                 $self->send($_);
471                         } else {
472                                 $self->send('');
473                         }
474                 }
475         } 
476 }
477
478
479 # this is the thing that runs the command, it is done like this for the 
480 # benefit of remote command execution
481 #
482
483 sub run_cmd
484 {
485         my $self = shift;
486         my $user = $self->{user};
487         my $call = $self->{call};
488         my $cmdline = shift;
489         my @ans;
490         
491         return () if length $cmdline == 0;
492         
493         # split the command line up into parts, the first part is the command
494         my ($cmd, $args) = split /\s+/, $cmdline, 2;
495         $args = "" unless defined $args;
496                 
497         if ($cmd) {
498
499                 # check cmd
500                 if ($cmd =~ m|^/| || $cmd =~ m|[^-?\w/]|) {
501                         LogDbg('DXCommand', "cmd: invalid characters in '$cmd'");
502                         return $self->_error_out('e1');
503                 }
504
505                 # strip out // on command only
506                 $cmd =~ s|//|/|g;
507                                         
508                 my ($path, $fcmd);
509                         
510                 dbg("cmd: $cmd") if isdbg('command');
511                         
512                 # alias it if possible
513                 my $acmd = CmdAlias::get_cmd($cmd);
514                 if ($acmd) {
515                         ($cmd, $args) = split /\s+/, "$acmd $args", 2;
516                         $args = "" unless defined $args;
517                         dbg("cmd: aliased $cmd $args") if isdbg('command');
518                 }
519                         
520                 # first expand out the entry to a command
521                 ($path, $fcmd) = search($main::localcmd, $cmd, "pl");
522                 ($path, $fcmd) = search($main::cmd, $cmd, "pl") unless $path && $fcmd;
523
524                 if ($path && $cmd) {
525                         dbg("cmd: path $cmd cmd: $fcmd") if isdbg('command');
526                         
527                         my $package = find_cmd_name($path, $fcmd);
528                         return ($@) if $@;
529                                 
530                         if ($package && $self->can("${package}::handle")) {
531                                 no strict 'refs';
532                                 dbg("cmd: package $package") if isdbg('command');
533                                 eval { @ans = &{"${package}::handle"}($self, $args) };
534                                 return (DXDebug::shortmess($@)) if $@;
535                         } else {
536                                 dbg("cmd: $package not present") if isdbg('command');
537                                 return $self->_error_out('e1');
538                         }
539                 } else {
540                         dbg("cmd: $cmd not found") if isdbg('command');
541                         return $self->_error_out('e1');
542                 }
543         }
544         
545         my $ok = shift @ans;
546         if ($ok) {
547                 delete $self->{errors};
548         } else {
549                 if (++$self->{errors} > $DXChannel::maxerrors) {
550                         $self->send($self->msg('e26'));
551                         $self->disconnect;
552                         return ();
553                 }
554         }
555         return map {s/([^\s])\s+$/$1/; $_} @ans;
556 }
557
558 #
559 # This is called from inside the main cluster processing loop and is used
560 # for despatching commands that are doing some long processing job
561 #
562 sub process
563 {
564         my $t = time;
565         my @dxchan = DXChannel::get_all();
566         my $dxchan;
567         
568         foreach $dxchan (@dxchan) {
569                 next unless $dxchan->{sort} eq 'U';  
570         
571                 # send a outstanding message prompt if required
572                 if ($t >= $dxchan->lastmsgpoll + $msgpolltime) {
573                         $dxchan->send($dxchan->msg('m9')) if DXMsg::for_me($dxchan->call);
574                         $dxchan->lastmsgpoll($t);
575                 }
576                 
577                 # send a prompt if no activity out on this channel
578                 if ($t >= $dxchan->t + $main::user_interval) {
579                         $dxchan->prompt() if $dxchan->{state} =~ /^prompt/o;
580                         $dxchan->t($t);
581                 }
582         }
583
584         while (my ($k, $v) = each %nothereslug) {
585                 if ($main::systime >= $v + 300) {
586                         delete $nothereslug{$k};
587                 }
588         }
589
590         import_cmd();
591 }
592
593 #
594 # finish up a user context
595 #
596 sub disconnect
597 {
598         my $self = shift;
599         my $call = $self->call;
600
601         return if $self->{disconnecting}++;
602
603         delete $self->{senddbg};
604
605         my $uref = Route::User::get($call);
606         my @rout;
607         if ($uref) {
608 #               @rout = $main::routeroot->del_user($uref);
609                 @rout = DXProt::_del_thingy($main::routeroot, [$call, 0]);
610
611                 dbg("B/C PC17 on $main::mycall for: $call") if isdbg('route');
612
613                 # issue a pc17 to everybody interested
614                 $main::me->route_pc17($main::mycall, undef, $main::routeroot, $uref);
615                 $main::me->route_pc92d($main::mycall, undef, $main::routeroot, $uref) unless $DXProt::pc92_slug_changes;
616         } else {
617                 confess "trying to disconnect a non existant user $call";
618         }
619
620         # I was the last node visited
621     $self->user->node($main::mycall);
622                 
623         # send info to all logged in thingies
624         $self->tell_login('logoutu');
625         $self->tell_buddies('logoutb');
626
627         LogDbg('DXCommand', "$call disconnected");
628
629         $self->SUPER::disconnect;
630 }
631
632 #
633 # short cut to output a prompt
634 #
635
636 sub prompt
637 {
638         my $self = shift;
639
640         return if $self->{gtk};         # 'cos prompts are not a concept that applies here
641         
642         my $call = $self->call;
643         my $date = cldate($main::systime);
644         my $time = ztime($main::systime);
645         my $prompt = $self->{prompt} || $self->msg('pr');
646
647         $call = "($call)" unless $self->here;
648         $prompt =~ s/\%C/$call/g;
649         $prompt =~ s/\%D/$date/g;
650         $prompt =~ s/\%T/$time/g;
651         $prompt =~ s/\%M/$main::mycall/g;
652         
653         $self->send($prompt);
654 }
655
656 # broadcast a message to all users [except those mentioned after buffer]
657 sub broadcast
658 {
659         my $pkg = shift;                        # ignored
660         my $s = shift;                          # the line to be rebroadcast
661         
662     foreach my $dxchan (DXChannel::get_all()) {
663                 next unless $dxchan->{sort} eq 'U'; # only interested in user channels  
664                 next if grep $dxchan == $_, @_;
665                 $dxchan->send($s);                      # send it
666         }
667 }
668
669 # gimme all the users
670 sub get_all
671 {
672         return grep {$_->{sort} eq 'U'} DXChannel::get_all();
673 }
674
675 # run a script for this user
676 sub run_script
677 {
678         my $self = shift;
679         my $silent = shift || 0;
680         
681 }
682
683 #
684 # search for the command in the cache of short->long form commands
685 #
686
687 sub search
688 {
689         my ($path, $short_cmd, $suffix) = @_;
690         my ($apath, $acmd);
691         
692         # commands are lower case
693         $short_cmd = lc $short_cmd;
694         dbg("command: $path $short_cmd\n") if isdbg('command');
695
696         # do some checking for funny characters
697         return () if $short_cmd =~ /\/$/;
698
699         # return immediately if we have it
700         ($apath, $acmd) = split ',', $cmd_cache{$short_cmd} if $cmd_cache{$short_cmd};
701         if ($apath && $acmd) {
702                 dbg("cached $short_cmd = ($apath, $acmd)\n") if isdbg('command');
703                 return ($apath, $acmd);
704         }
705         
706         # if not guess
707         my @parts = split '/', $short_cmd;
708         my $dirfn;
709         my $curdir = $path;
710         
711         while (my $p = shift @parts) {
712                 opendir(D, $curdir) or confess "can't open $curdir $!";
713                 my @ls = readdir D;
714                 closedir D;
715
716                 # if this isn't the last part
717                 if (@parts) {
718                         my $found;
719                         foreach my $l (sort @ls) {
720                                 next if $l =~ /^\./;
721                                 if ((-d "$curdir/$l") && $p eq substr($l, 0, length $p)) {
722                                         dbg("got dir: $curdir/$l\n") if isdbg('command');
723                                         $dirfn .= "$l/";
724                                         $curdir .= "/$l";
725                                         $found++;
726                                         last;
727                                 }
728                         }
729                         # only proceed if we find the directory asked for
730                         return () unless $found;
731                 } else {
732                         foreach my $l (sort @ls) {
733                                 next if $l =~ /^\./;
734                                 next unless $l =~ /\.$suffix$/;
735                                 if ($p eq substr($l, 0, length $p)) {
736                                         $l =~ s/\.$suffix$//;
737                                         $dirfn = "" unless $dirfn;
738                                         $cmd_cache{$short_cmd} = join(',', ($path, "$dirfn$l")); # cache it
739                                         dbg("got path: $path cmd: $dirfn$l\n") if isdbg('command');
740                                         return ($path, "$dirfn$l");
741                                 }
742                         }
743                 }
744         }
745
746         return ();  
747 }  
748
749 # clear the command name cache
750 sub clear_cmd_cache
751 {
752         no strict 'refs';
753         
754         for my $k (keys %Cache) {
755                 unless ($k =~ /cmd_cache/) {
756                         dbg("Undefining cmd $k") if isdbg('command');
757                         undef $DXCommandmode::{"${k}::"};
758                 }
759         }
760         %cmd_cache = ();
761         %Cache = ( cmd_clear_cmd_cache  => $Cache{cmd_clear_cmd_cache} );
762 }
763
764 #
765 # the persistant execution of things from the command directories
766 #
767 #
768 # This allows perl programs to call functions dynamically
769
770 # This has been nicked directly from the perlembed pages
771 #
772 #require Devel::Symdump;  
773
774 sub valid_package_name {
775         my $string = shift;
776         $string =~ s|([^A-Za-z0-9_/])|sprintf("_%2x",unpack("C",$1))|eg;
777         
778         $string =~ s|/|_|g;
779         return "cmd_$string";
780 }
781
782
783 # this bit of magic finds a command in the offered directory
784 sub find_cmd_name {
785         my $path = shift;
786         my $cmdname = shift;
787         my $package = valid_package_name($cmdname);
788         my $filename = "$path/$cmdname.pl";
789         my $mtime = -M $filename;
790         
791         # return if we can't find it
792         $errstr = undef;
793         unless (defined $mtime) {
794                 $errstr = DXM::msg('e1');
795                 return undef;
796         }
797         
798         if(exists $Cache{$package} && exists $Cache{$package}->{mtime} && $Cache{$package}->{mtime} <= $mtime) {
799                 #we have compiled this subroutine already,
800                 #it has not been updated on disk, nothing left to do
801                 #print STDERR "already compiled $package->handler\n";
802                 dbg("find_cmd_name: $package cached") if isdbg('command');
803         } else {
804
805                 my $sub = readfilestr($filename);
806                 unless ($sub) {
807                         $errstr = "Syserr: can't open '$filename' $!";
808                         return undef;
809                 };
810                 
811                 #wrap the code into a subroutine inside our unique package
812                 my $eval = qq(package DXCommandmode::$package; use POSIX qw{:math_h}; use DXLog; use DXDebug; use DXUser; use DXUtil; our \@ISA = qw{DXCommandmode}; );
813
814
815                 if ($sub =~ m|\s*sub\s+handle\n|) {
816                         $eval .= $sub;
817                 } else {
818                         $eval .= qq(sub handle { $sub });
819                 }
820                 
821                 if (isdbg('eval')) {
822                         my @list = split /\n/, $eval;
823                         my $line;
824                         for (@list) {
825                                 dbg($_ . "\n") if isdbg('eval');
826                         }
827                 }
828                 
829                 # get rid of any existing sub and try to compile the new one
830                 no strict 'refs';
831
832                 if (exists $Cache{$package}) {
833                         dbg("find_cmd_name: Redefining $package") if isdbg('command');
834                         undef $DXCommandmode::{"${package}::"};
835                         delete $Cache{$package};
836                 } else {
837                         dbg("find_cmd_name: Defining $package") if isdbg('command');
838                 }
839
840                 eval $eval;
841
842                 $Cache{$package} = {mtime => $mtime } unless $@;
843         }
844
845         return "DXCommandmode::$package";
846 }
847
848 sub send
849 {
850         my $self = shift;
851         if ($self->{gtk}) {
852                 for (@_) {
853                         $self->SUPER::send(dd(['cmd',$_]));
854                 }
855         } else {
856                 $self->SUPER::send(@_);
857         }
858 }
859
860 sub local_send
861 {
862         my ($self, $let, $buf) = @_;
863         if ($self->{state} eq 'prompt' || $self->{state} eq 'talk' || $self->{state} eq 'chat') {
864                 if ($self->{enhanced}) {
865                         $self->send_later($let, $buf);
866                 } else {
867                         $self->send($buf);
868                 }
869         } else {
870                 $self->delay($buf);
871         }
872 }
873
874 # send a talk message here
875 sub talk
876 {
877         my ($self, $from, $to, $via, $line, $onode) = @_;
878         $line =~ s/\\5E/\^/g;
879         if ($self->{talk}) {
880                 if ($self->{gtk}) {
881                         $self->local_send('T', dd(['talk',$to,$from,$via,$line]));
882                 } else {
883                         $self->local_send('T', "$to de $from: $line");
884                 }
885         }
886         Log('talk', $to, $from, '<' . ($onode || '*'), $line);
887         # send a 'not here' message if required
888         unless ($self->{here} && $from ne $to) {
889                 my $key = "$to$from";
890                 unless (exists $nothereslug{$key}) {
891                         my ($ref, $dxchan);
892                         if (($ref = Route::get($from)) && ($dxchan = $ref->dxchan)) {
893                                 my $name = $self->user->name || $to;
894                                 my $s = $self->user->nothere || $dxchan->msg('nothere', $name);
895                                 $nothereslug{$key} = $main::systime;
896                                 $dxchan->talk($to, $from, undef, $s);
897                         }
898                 }
899         }
900 }
901
902 # send an announce
903 sub announce
904 {
905         my $self = shift;
906         my $line = shift;
907         my $isolate = shift;
908         my $to = shift;
909         my $target = shift;
910         my $text = shift;
911         my ($filter, $hops);
912
913         if (!$self->{ann_talk} && $to ne $self->{call}) {
914                 my $call = AnnTalk::is_talk_candidate($_[0], $text);
915                 return if $call;
916         }
917
918         if ($self->{annfilter}) {
919                 ($filter, $hops) = $self->{annfilter}->it(@_ );
920                 return unless $filter;
921         }
922
923         unless ($self->{ann}) {
924                 return if $_[0] ne $main::myalias && $_[0] ne $main::mycall;
925         }
926         return if $target eq 'SYSOP' && $self->{priv} < 5;
927         my $buf;
928         if ($self->{gtk}) {
929                 $buf = dd(['ann', $to, $target, $text, @_])
930         } else {
931                 $buf = "$to$target de $_[0]: $text";
932                 $buf =~ s/\%5E/^/g;
933                 $buf .= "\a\a" if $self->{beep};
934         }
935         $self->local_send($target eq 'WX' ? 'W' : 'N', $buf);
936 }
937
938 # send a chat
939 sub chat
940 {
941         my $self = shift;
942         my $line = shift;
943         my $isolate = shift;
944         my $target = shift;
945         my $to = shift;
946         my $text = shift;
947         my ($filter, $hops);
948
949         return unless grep uc $_ eq $target, @{$self->{user}->{group}};
950         
951         $text =~ s/^\#\d+ //;
952         my $buf;
953         if ($self->{gtk}) {
954                 $buf = dd(['chat', $to, $target, $text, @_])
955         } else {
956                 $buf = "$target de $_[0]: $text";
957                 $buf =~ s/\%5E/^/g;
958                 $buf .= "\a\a" if $self->{beep};
959         }
960         $self->local_send('C', $buf);
961 }
962
963 sub format_dx_spot
964 {
965         my $self = shift;
966
967         my $t = ztime($_[2]);
968         my $loc = '';
969         my $clth = $self->{consort} eq 'local' ? 29 : 30;
970         my $comment = substr (($_[3] || ''), 0, $clth);
971         $comment .= ' ' x ($clth - length($comment));
972         if ($self->{user}->wantgrid) {
973                 my $ref = DXUser::get_current($_[4]);
974                 if ($ref) {
975                         $loc = $ref->qra || '';
976                         $loc = ' ' . substr($loc, 0, 4) if $loc;
977                 }
978         }
979
980         if ($self->{user}->wantdxitu) {
981                 $loc = ' ' . sprintf("%2d", $_[10]) if defined $_[10];
982                 $comment = substr($comment, 0,  $self->{consort} eq 'local' ? 26 : 27) . ' ' . sprintf("%2d", $_[8]) if defined $_[8]; 
983         } elsif ($self->{user}->wantdxcq) {
984                 $loc = ' ' . sprintf("%2d", $_[11]) if defined $_[11];
985                 $comment = substr($comment, 0,  $self->{consort} eq 'local' ? 26 : 27) . ' ' . sprintf("%2d", $_[9]) if defined $_[9]; 
986         } elsif ($self->{user}->wantusstate) {
987                 $loc = ' ' . $_[13] if $_[13];
988                 $comment = substr($comment, 0,  $self->{consort} eq 'local' ? 26 : 27) . ' ' . $_[12] if $_[12]; 
989         }
990
991         return sprintf "DX de %-7.7s%11.1f  %-12.12s %-s $t$loc", "$_[4]:", $_[0], $_[1], $comment;
992 }
993
994 # send a dx spot
995 sub dx_spot
996 {
997         my $self = shift;
998         my $line = shift;
999         my $isolate = shift;
1000         return unless $self->{dx};
1001
1002         my ($filter, $hops);
1003
1004         if ($self->{spotsfilter}) {
1005                 ($filter, $hops) = $self->{spotsfilter}->it(@_ );
1006                 return unless $filter;
1007         }
1008
1009         dbg('spot: "' . join('","', @_) . '"') if isdbg('dxspot');
1010
1011         my $buf;
1012         if ($self->{ve7cc}) {
1013                 $buf = VE7CC::dx_spot($self, @_);
1014         } elsif ($self->{gtk}) {
1015                 my ($dxloc, $byloc);
1016
1017                 my $ref = DXUser::get_current($_[4]);
1018                 if ($ref) {
1019                         $byloc = $ref->qra;
1020                         $byloc = substr($byloc, 0, 4) if $byloc;
1021                 }
1022
1023                 my $spot = $_[1];
1024                 $spot =~ s|/\w{1,4}$||;
1025                 $ref = DXUser::get_current($spot);
1026                 if ($ref) {
1027                         $dxloc = $ref->qra;
1028                         $dxloc = substr($dxloc, 0, 4) if $dxloc;
1029                 }
1030                 $buf = dd(['dx', @_, ($dxloc||''), ($byloc||'')]);
1031                 
1032         } else {
1033                 $buf = $self->format_dx_spot(@_);
1034                 $buf .= "\a\a" if $self->{beep};
1035                 $buf =~ s/\%5E/^/g;
1036         }
1037
1038         $self->local_send('X', $buf);
1039 }
1040
1041 sub wwv
1042 {
1043         my $self = shift;
1044         my $line = shift;
1045         my $isolate = shift;
1046         my ($filter, $hops);
1047
1048         return unless $self->{wwv};
1049         
1050         if ($self->{wwvfilter}) {
1051                 ($filter, $hops) = $self->{wwvfilter}->it(@_[7..$#_] );
1052                 return unless $filter;
1053         }
1054
1055         my $buf;
1056         if ($self->{gtk}) {
1057                 $buf = dd(['wwv', @_])
1058         } else {
1059                 $buf = "WWV de $_[6] <$_[1]>:   SFI=$_[2], A=$_[3], K=$_[4], $_[5]";
1060                 $buf .= "\a\a" if $self->{beep};
1061         }
1062         
1063         $self->local_send('V', $buf);
1064 }
1065
1066 sub wcy
1067 {
1068         my $self = shift;
1069         my $line = shift;
1070         my $isolate = shift;
1071         my ($filter, $hops);
1072
1073         return unless $self->{wcy};
1074         
1075         if ($self->{wcyfilter}) {
1076                 ($filter, $hops) = $self->{wcyfilter}->it(@_ );
1077                 return unless $filter;
1078         }
1079
1080         my $buf;
1081         if ($self->{gtk}) {
1082                 $buf = dd(['wcy', @_])
1083         } else {
1084                 $buf = "WCY de $_[10] <$_[1]> : K=$_[4] expK=$_[5] A=$_[3] R=$_[6] SFI=$_[2] SA=$_[7] GMF=$_[8] Au=$_[9]";
1085                 $buf .= "\a\a" if $self->{beep};
1086         }
1087         $self->local_send('Y', $buf);
1088 }
1089
1090 # broadcast debug stuff to all interested parties
1091 sub broadcast_debug
1092 {
1093         my $s = shift;                          # the line to be rebroadcast
1094         
1095         foreach my $dxchan (DXChannel::get_all_users) {
1096                 next unless $dxchan->{enhanced} && $dxchan->{senddbg};
1097                 if ($dxchan->{gtk}) {
1098                         $dxchan->send_later('L', dd(['db', $s]));
1099                 } else {
1100                         $dxchan->send_later('L', $s);
1101                 }
1102         }
1103 }
1104
1105 sub do_entry_stuff
1106 {
1107         my $self = shift;
1108         my $line = shift;
1109         my @out;
1110         
1111         if ($self->state eq 'enterbody') {
1112                 my $loc = $self->{loc} || confess "local var gone missing" ;
1113                 if ($line eq "\032" || $line eq '%1A' || uc $line eq "/EX") {
1114                         no strict 'refs';
1115                         push @out, &{$loc->{endaction}}($self);          # like this for < 5.8.0
1116                         $self->func(undef);
1117                         $self->state('prompt');
1118                 } elsif ($line eq "\031" || uc $line eq "/ABORT" || uc $line eq "/QUIT") {
1119                         push @out, $self->msg('m10');
1120                         delete $loc->{lines};
1121                         delete $self->{loc};
1122                         $self->func(undef);
1123                         $self->state('prompt');
1124                 } else {
1125                         push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1126                         # i.e. it ain't and end or abort, therefore store the line
1127                 }
1128         } else {
1129                 confess "Invalid state $self->{state}";
1130         }
1131         return @out;
1132 }
1133
1134 sub store_startup_script
1135 {
1136         my $self = shift;
1137         my $loc = $self->{loc} || confess "local var gone missing" ;
1138         my @out;
1139         my $call = $loc->{call} || confess "callsign gone missing";
1140         confess "lines array gone missing" unless ref $loc->{lines};
1141         my $r = Script::store($call, $loc->{lines});
1142         if (defined $r) {
1143                 if ($r) {
1144                         push @out, $self->msg('m19', $call, $r);
1145                 } else {
1146                         push @out, $self->msg('m20', $call);
1147                 }
1148         } else {
1149                 push @out, "error opening startup script $call $!";
1150         } 
1151         return @out;
1152 }
1153
1154 # Import any commands contained in any files in import_cmd directory
1155 #
1156 # If the filename has a recogisable callsign as some delimited part
1157 # of it, then this is the user the command will be run as. 
1158 #
1159 sub import_cmd
1160 {
1161         # are there any to do in this directory?
1162         return unless -d $cmdimportdir;
1163         unless (opendir(DIR, $cmdimportdir)) {
1164                 LogDbg('err', "can\'t open $cmdimportdir $!");
1165                 return;
1166         } 
1167
1168         my @names = readdir(DIR);
1169         closedir(DIR);
1170         my $name;
1171
1172         return unless @names;
1173         
1174         foreach $name (@names) {
1175                 next if $name =~ /^\./;
1176
1177                 my $s = Script->new($name, $cmdimportdir);
1178                 if ($s) {
1179                         LogDbg('DXCommand', "Run import cmd file $name");
1180                         my @cat = split /[^A-Za-z0-9]+/, $name;
1181                         my ($call) = grep {is_callsign(uc $_)} @cat;
1182                         $call ||= $main::mycall;
1183                         $call = uc $call;
1184                         my @out;
1185                         
1186                         
1187                         $s->inscript(0);        # switch off script checks
1188                         
1189                         if ($call eq $main::mycall) {
1190                                 @out = $s->run($main::me, 1);
1191                         } else {
1192                                 my $dxchan = DXChannel::get($call);
1193                             if ($dxchan) {
1194                                         @out = $s->run($dxchan, 1);
1195                                 } else {
1196                                         my $u = DXUser::get($call);
1197                                         if ($u) {
1198                                                 $dxchan = $main::me;
1199                                                 my $old = $dxchan->{call};
1200                                                 my $priv = $dxchan->{priv};
1201                                                 my $user = $dxchan->{user};
1202                                                 $dxchan->{call} = $call;
1203                                                 $dxchan->{priv} = $u->priv;
1204                                                 $dxchan->{user} = $u;
1205                                                 @out = $s->run($dxchan, 1);
1206                                                 $dxchan->{call} = $old;
1207                                                 $dxchan->{priv} = $priv;
1208                                                 $dxchan->{user} = $user;
1209                                         } else {
1210                                                 LogDbg('err', "Trying to run import cmd for non-existant user $call");
1211                                         }
1212                                 }
1213                         }
1214                         $s->erase;
1215                         for (@out) {
1216                                 LogDbg('DXCommand', "Import cmd $name/$call: $_");
1217                         }
1218                 } else {
1219                         LogDbg('err', "Failed to open $cmdimportdir/$name $!");
1220                         unlink "$cmdimportdir/$name";
1221                 }
1222         }
1223 }
1224
1225 sub print_find_reply
1226 {
1227         my ($self, $node, $target, $flag, $ms) = @_;
1228         my $sort = $flag == 2 ? "External" : "Local";
1229         $self->send("$sort $target found at $node in $ms ms" );
1230 }
1231
1232 # send the most relevant motd
1233 sub send_motd
1234 {
1235         my $self = shift;
1236         my $motd;
1237
1238         unless ($self->{registered}) {
1239                 $motd = "${main::motd}_nor_$self->{lang}";
1240                 $motd = "${main::motd}_nor" unless -e $motd;
1241         }
1242         $motd = "${main::motd}_$self->{lang}" unless $motd && -e $motd;
1243         $motd = $main::motd unless $motd && -e $motd;
1244         if ($self->conn->ax25) {
1245                 if ($motd) {
1246                         $motd = "${motd}_ax25" if -e "${motd}_ax25";
1247                 } else {
1248                         $motd = "${main::motd}_ax25" if -e "${main::motd}_ax25";
1249                 }
1250         }
1251         $self->send_file($motd) if -e $motd;
1252 }
1253
1254 # Punt off a long running command into a separate process
1255 #
1256 # This is called from commands to run some potentially long running
1257 # function. The process forks and then runs the function and returns
1258 # the result back to the cmd. 
1259 #
1260 # NOTE: this merely forks the current process and then runs the cmd in that (current) context.
1261 #       IT DOES NOT START UP SOME NEW PROGRAM AND RELIES ON THE FACT THAT IT IS RUNNING DXSPIDER 
1262 #       THE CURRENT CONTEXT!!
1263
1264 # call: $self->spawn_cmd(\<function>, [cb => sub{...}], [prefix => "cmd> "], [progress => 0|1], [args => [...]]);
1265 sub spawn_cmd
1266 {
1267         my $self = shift;
1268         my $cmdref = shift;
1269         my $call = $self->{call};
1270         my %args = @_;
1271         my @out;
1272         
1273         my $cb = delete $args{cb};
1274         my $prefix = delete $args{prefix};
1275         my $progress = delete $args{progress};
1276         my $args = delete $args{args} || [];
1277
1278         no strict 'refs';
1279                 
1280         my $fc = Mojo::IOLoop::ForkCall->new;
1281         $fc->serializer(\&encode_json);
1282         $fc->deserializer(\&decode_json);
1283         $fc->run(
1284                          sub {my @args = @_; my @res = $cmdref->(@args); return @res},
1285                          $args,
1286                          sub {
1287                                  my ($fc, $err, @res) = @_; 
1288                                  my $dxchan = DXChannel::get($call);
1289                                  return unless $dxchan;
1290
1291                                  if (defined $err) {
1292                                          my $s = "DXCommand::spawn_cmd: call $call error $err";
1293                                          dbg($s) if isdbg('chan');
1294                                          $dxchan->send($s);
1295                                          return;
1296                                  }
1297                                  if ($cb) {
1298                                          $cb->($dxchan, @res);
1299                                  } else {
1300                                          return unless @res;
1301                                          if (defined $prefix) {
1302                                                  $dxchan->send(map {"$prefix$_"} @res);
1303                                          } else {
1304                                                  $dxchan->send(@res);
1305                                          }
1306                                  }
1307                          });
1308         return @out;
1309 }
1310
1311 1;
1312 __END__