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