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