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