add chat_import stuff
[spider.git] / perl / DXProt.pm
1 #!/usr/bin/perl
2 #
3 # This module impliments the protocal mode for a dx cluster
4 #
5 # Copyright (c) 1998 Dirk Koopman G1TLH
6 #
7 # $Id$
8
9
10 package DXProt;
11
12 @ISA = qw(DXChannel);
13
14 use DXUtil;
15 use DXChannel;
16 use DXUser;
17 use DXM;
18 use DXProtVars;
19 use DXCommandmode;
20 use DXLog;
21 use Spot;
22 use DXProtout;
23 use DXDebug;
24 use Filter;
25 use Local;
26 use DXDb;
27 use AnnTalk;
28 use Geomag;
29 use WCY;
30 use Time::HiRes qw(gettimeofday tv_interval);
31 use BadWords;
32 use DXHash;
33 use Route;
34 use Route::Node;
35 use Script;
36 use Investigate;
37 use RouteDB;
38
39
40 use strict;
41
42 use vars qw($VERSION $BRANCH);
43 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /(\d+)\.(\d+)/ );
44 $BRANCH = sprintf( "%d.%03d", q$Revision$ =~ /\d+\.\d+\.(\d+)\.(\d+)/  || (0,0));
45 $main::build += $VERSION;
46 $main::branch += $BRANCH;
47
48 use vars qw($pc11_max_age $pc23_max_age $last_pc50 $eph_restime $eph_info_restime $eph_pc34_restime
49                         $last_hour $last10 %eph  %pings %rcmds $ann_to_talk
50                         $pingint $obscount %pc19list $chatdupeage $chatimportfn
51                         $investigation_int $pc19_version 
52                         %nodehops $baddx $badspotter $badnode $censorpc $rspfcheck
53                         $allowzero $decode_dk0wcy $send_opernam @checklist);
54
55 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
56 $pc23_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc23
57
58 $last_hour = time;                              # last time I did an hourly periodic update
59 %pings = ();                    # outstanding ping requests outbound
60 %rcmds = ();                    # outstanding rcmd requests outbound
61 %nodehops = ();                 # node specific hop control
62 %pc19list = ();                                 # list of outstanding PC19s that haven't had PC16s on them
63
64 $censorpc = 1;                                  # Do a BadWords::check on text fields and reject things
65                                                                 # loads of 'bad things'
66 $baddx = new DXHash "baddx";
67 $badspotter = new DXHash "badspotter";
68 $badnode = new DXHash "badnode";
69 $last10 = $last_pc50 = time;
70 $ann_to_talk = 1;
71 $rspfcheck = 1;
72 $eph_restime = 180;
73 $eph_info_restime = 60*60;
74 $eph_pc34_restime = 30;
75 $pingint = 5*60;
76 $obscount = 2;
77 $chatdupeage = 20 * 60 * 60;
78 $chatimportfn = "$main::root/chat_import";
79 $investigation_int = 12*60*60;  # time between checks to see if we can see this node
80 $pc19_version = 5466;                   # the visible version no for outgoing PC19s generated from pc59
81
82 @checklist = 
83 (
84  [ qw(i c c m bp bc c) ],                       # pc10
85  [ qw(i f m d t m c c h) ],             # pc11
86  [ qw(i c bm m bm bm p h) ],            # pc12
87  [ qw(i c h) ],                                 # 
88  [ qw(i c h) ],                                 # 
89  [ qw(i c m h) ],                                       # 
90  undef ,                                                # pc16 has to be validated manually
91  [ qw(i c c h) ],                                       # pc17
92  [ qw(i m n) ],                                 # pc18
93  undef ,                                                # pc19 has to be validated manually
94  undef ,                                                # pc20 no validation
95  [ qw(i c m h) ],                                       # pc21
96  undef ,                                                # pc22 no validation
97  [ qw(i d n n n n m c c h) ],           # pc23
98  [ qw(i c p h) ],                                       # pc24
99  [ qw(i c c n n) ],                             # pc25
100  [ qw(i f m d t m c c bc) ],            # pc26
101  [ qw(i d n n n n m c c bc) ],  # pc27
102  [ qw(i c c m c d t p m bp n p bp bc) ], # pc28
103  [ qw(i c c n m) ],                             # pc29
104  [ qw(i c c n) ],                                       # pc30
105  [ qw(i c c n) ],                                       # pc31
106  [ qw(i c c n) ],                                       # pc32
107  [ qw(i c c n) ],                                       # pc33
108  [ qw(i c c m) ],                                       # pc34
109  [ qw(i c c m) ],                                       # pc35
110  [ qw(i c c m) ],                                       # pc36
111  [ qw(i c c n m) ],                             # pc37
112  undef,                                                 # pc38 not interested
113  [ qw(i c m) ],                                 # pc39
114  [ qw(i c c m p n) ],                           # pc40
115  [ qw(i c n m h) ],                             # pc41
116  [ qw(i c c n) ],                                       # pc42
117  undef,                                                 # pc43 don't handle it
118  [ qw(i c c n m m c) ],                 # pc44
119  [ qw(i c c n m) ],                             # pc45
120  [ qw(i c c n) ],                                       # pc46
121  undef,                                                 # pc47
122  undef,                                                 # pc48
123  [ qw(i c m h) ],                                       # pc49
124  [ qw(i c n h) ],                                       # pc50
125  [ qw(i c c n) ],                                       # pc51
126  undef,
127  undef,
128  undef,
129  undef,
130  undef,
131  undef,
132  undef,
133  undef,
134  undef,                                                 # pc60
135  undef,
136  undef,
137  undef,
138  undef,
139  undef,
140  undef,
141  undef,
142  undef,
143  undef,
144  undef,                                                 # pc70
145  undef,
146  undef,
147  [ qw(i d n n n n n n m m m c c h) ],   # pc73
148  undef,
149  undef,
150  undef,
151  undef,
152  undef,
153  undef,
154  undef,                                                 # pc80
155  undef,
156  undef,
157  undef,
158  [ qw(i c c c m) ],                             # pc84
159  [ qw(i c c c m) ],                             # pc85
160  undef,
161  undef,
162  undef,
163  undef,
164  [ qw(i c n) ],                                 # pc90
165 );
166
167 # use the entry in the check list to check the field list presented
168 # return OK if line NOT in check list (for now)
169 sub check
170 {
171         my $n = shift;
172         $n -= 10;
173         return 0 if $n < 0 || $n > @checklist; 
174         my $ref = $checklist[$n];
175         return 0 unless ref $ref;
176         
177         my $i;
178         for ($i = 1; $i < @$ref; $i++) {
179                 my ($blank, $act) = $$ref[$i] =~ /^(b?)(\w)$/;
180                 return 0 unless $act;
181                 next if $blank && $_[$i] =~ /^[ \*]$/;
182                 if ($act eq 'c') {
183                         return $i unless is_callsign($_[$i]);
184                 } elsif ($act eq 'i') {                 
185                         ;                                       # do nothing
186                 } elsif ($act eq 'm') {
187                         return $i unless is_pctext($_[$i]);
188                 } elsif ($act eq 'p') {
189                         return $i unless is_pcflag($_[$i]);
190                 } elsif ($act eq 'f') {
191                         return $i unless is_freq($_[$i]);
192                 } elsif ($act eq 'n') {
193                         return $i unless $_[$i] =~ /^[\d ]+$/;
194                 } elsif ($act eq 'h') {
195                         return $i unless $_[$i] =~ /^H\d\d?$/;
196                 } elsif ($act eq 'd') {
197                         return $i unless $_[$i] =~ /^\s*\d+-\w\w\w-[12][90]\d\d$/;
198                 } elsif ($act eq 't') {
199                         return $i unless $_[$i] =~ /^[012]\d[012345]\dZ$/;
200                 } 
201         }
202         return 0;
203 }
204
205 sub init
206 {
207         do "$main::data/hop_table.pl" if -e "$main::data/hop_table.pl";
208         confess $@ if $@;
209 }
210
211 #
212 # obtain a new connection this is derived from dxchannel
213 #
214
215 sub new 
216 {
217         my $self = DXChannel::alloc(@_);
218
219         # add this node to the table, the values get filled in later
220         my $pkg = shift;
221         my $call = shift;
222         $main::routeroot->add($call, '5000', Route::here(1)) if $call ne $main::mycall;
223
224         return $self;
225 }
226
227 # this is how a pc connection starts (for an incoming connection)
228 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
229 # all the crap that comes between).
230 sub start
231 {
232         my ($self, $line, $sort) = @_;
233         my $call = $self->{call};
234         my $user = $self->{user};
235
236         # log it
237         my $host = $self->{conn}->{peerhost} || "unknown";
238         Log('DXProt', "$call connected from $host");
239         
240         # remember type of connection
241         $self->{consort} = $line;
242         $self->{outbound} = $sort eq 'O';
243         my $priv = $user->priv;
244         $priv = $user->priv(1) unless $priv;
245         $self->{priv} = $priv;     # other clusters can always be 'normal' users
246         $self->{lang} = $user->lang || 'en';
247         $self->{isolate} = $user->{isolate};
248         $self->{consort} = $line;       # save the connection type
249         $self->{here} = 1;
250         $self->{width} = 80;
251
252         # sort out registration
253         $self->{registered} = 1;
254
255         # get the output filters
256         $self->{spotsfilter} = Filter::read_in('spots', $call, 0) || Filter::read_in('spots', 'node_default', 0);
257         $self->{wwvfilter} = Filter::read_in('wwv', $call, 0) || Filter::read_in('wwv', 'node_default', 0);
258         $self->{wcyfilter} = Filter::read_in('wcy', $call, 0) || Filter::read_in('wcy', 'node_default', 0);
259         $self->{annfilter} = Filter::read_in('ann', $call, 0) || Filter::read_in('ann', 'node_default', 0) ;
260         $self->{routefilter} = Filter::read_in('route', $call, 0) || Filter::read_in('route', 'node_default', 0) unless $self->{isolate} ;
261
262
263         # get the INPUT filters (these only pertain to Clusters)
264         $self->{inspotsfilter} = Filter::read_in('spots', $call, 1) || Filter::read_in('spots', 'node_default', 1);
265         $self->{inwwvfilter} = Filter::read_in('wwv', $call, 1) || Filter::read_in('wwv', 'node_default', 1);
266         $self->{inwcyfilter} = Filter::read_in('wcy', $call, 1) || Filter::read_in('wcy', 'node_default', 1);
267         $self->{inannfilter} = Filter::read_in('ann', $call, 1) || Filter::read_in('ann', 'node_default', 1);
268         $self->{inroutefilter} = Filter::read_in('route', $call, 1) || Filter::read_in('route', 'node_default', 1) unless $self->{isolate};
269         
270         # set unbuffered and no echo
271         $self->send_now('B',"0");
272         $self->send_now('E',"0");
273         $self->conn->echo(0) if $self->conn->can('echo');
274         
275         # ping neighbour node stuff
276         my $ping = $user->pingint;
277         $ping = $pingint unless defined $ping;
278         $self->{pingint} = $ping;
279         $self->{nopings} = $user->nopings || $obscount;
280         $self->{pingtime} = [ ];
281         $self->{pingave} = 999;
282         $self->{metric} ||= 100;
283         $self->{lastping} = $main::systime;
284
285         # send initialisation string
286         unless ($self->{outbound}) {
287                 $self->sendinit;
288         }
289         
290         $self->state('init');
291         $self->{pc50_t} = $main::systime;
292
293         # send info to all logged in thingies
294         $self->tell_login('loginn');
295
296         # run a script send the output to the debug file
297         my $script = new Script(lc $call) || new Script('node_default');
298         $script->run($self) if $script;
299 }
300
301 #
302 # send outgoing 'challenge'
303 #
304
305 sub sendinit
306 {
307         my $self = shift;
308         $self->send(pc18());
309 }
310
311 sub removepc90
312 {
313         $_[0] =~ s/^PC90\^[-A-Z0-9]+\^\d+\^//;
314         $_[0] =~ s/^PC91\^[-A-Z0-9]+\^\d+\^[-A-Z0-9]+\^//;
315 }
316
317 #sub send
318 #{
319 #       my $self = shift;
320 #       while (@_) {
321 #               my $line = shift;
322 #               $self->SUPER::send($line);
323 #       }
324 #}
325
326 #
327 # This is the normal pcxx despatcher
328 #
329 sub normal
330 {
331         my ($self, $line) = @_;
332
333         # remove any incoming PC90 frames
334         removepc90($line);
335
336         my @field = split /\^/, $line;
337         return unless @field;
338         
339         pop @field if $field[-1] eq '~';
340         
341 #       print join(',', @field), "\n";
342                                                 
343         
344         # process PC frames, this will fail unless the frame starts PCnn
345         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
346         unless (defined $pcno && $pcno >= 10 && $pcno <= 99) {
347                 dbg("PCPROT: unknown protocol") if isdbg('chanerr');
348                 return;
349         }
350
351         # check for and dump bad protocol messages
352         my $n = check($pcno, @field);
353         if ($n) {
354                 dbg("PCPROT: bad field $n, dumped (" . parray($checklist[$pcno-10]) . ")") if isdbg('chanerr');
355                 return;
356         }
357
358         my $origin = $self->{call};
359         no strict 'subs';
360         my $sub = "handle_$pcno";
361
362         if ($self->can($sub)) {
363                 $self->$sub($pcno, $line, $origin, @field);
364         } else {
365                 $self->handle_default($pcno, $line, $origin, @field);
366         }
367 }
368         
369 # incoming talk commands
370 sub handle_10
371 {
372         my $self = shift;
373         my $pcno = shift;
374         my $line = shift;
375         my $origin = shift;
376
377         # rsfp check
378         return if $rspfcheck and !$self->rspfcheck(0, $_[6], $_[1]);
379                         
380         # will we allow it at all?
381         if ($censorpc) {
382                 my @bad;
383                 if (@bad = BadWords::check($_[3])) {
384                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
385                         return;
386                 }
387         }
388
389         # is it for me or one of mine?
390         my ($from, $to, $via, $call, $dxchan);
391         $from = $_[1];
392         if ($_[5] gt ' ') {
393                 $via = $_[2];
394                 $to = $_[5];
395         } else {
396                 $to = $_[2];
397         }
398
399         # if this is a 'nodx' node then ignore it
400         if ($badnode->in($_[6]) || ($via && $badnode->in($via))) {
401                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
402                 return;
403         }
404
405         # if this is a 'bad spotter' user then ignore it
406         my $nossid = $from;
407         $nossid =~ s/-\d+$//;
408         if ($badspotter->in($nossid)) {
409                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
410                 return;
411         }
412
413         # if we are converting announces to talk is it a dup?
414         if ($ann_to_talk) {
415                 if (AnnTalk::is_talk_candidate($from, $_[3]) && AnnTalk::dup($from, $to, $_[3])) {
416                         dbg("DXPROT: Dupe talk from announce, dropped") if isdbg('chanerr');
417                         return;
418                 }
419         }
420
421         # remember a route to this node and also the node on which this user is
422         RouteDB::update($_[6], $self->{call});
423 #       RouteDB::update($to, $_[6]);
424
425         # it is here and logged on
426         $dxchan = DXChannel->get($main::myalias) if $to eq $main::mycall;
427         $dxchan = DXChannel->get($to) unless $dxchan;
428         if ($dxchan && $dxchan->is_user) {
429                 $_[3] =~ s/\%5E/^/g;
430                 $dxchan->talk($from, $to, $via, $_[3]);
431                 return;
432         }
433
434         # is it elsewhere, visible on the cluster via the to address?
435         # note: this discards the via unless the to address is on
436         # the via address
437         my ($ref, $vref);
438         if ($ref = Route::get($to)) {
439                 $vref = Route::Node::get($via) if $via;
440                 $vref = undef unless $vref && grep $to eq $_, $vref->users;
441                 $ref->dxchan->talk($from, $to, $vref ? $via : undef, $_[3], $_[6]);
442                 return;
443         }
444
445         # can we see an interface to send it down?
446         
447         # not visible here, send a message of condolence
448         $vref = undef;
449         $ref = Route::get($from);
450         $vref = $ref = Route::Node::get($_[6]) unless $ref; 
451         if ($ref) {
452                 $dxchan = $ref->dxchan;
453                 $dxchan->talk($main::mycall, $from, $vref ? $vref->call : undef, $dxchan->msg('talknh', $to) );
454         }
455 }
456
457 # DX Spot handling
458 sub handle_11
459 {
460         my $self = shift;
461         my $pcno = shift;
462         my $line = shift;
463         my $origin = shift;
464
465         # route 'foreign' pc26s 
466         if ($pcno == 26) {
467                 if ($_[7] ne $main::mycall) {
468                         $self->route($_[7], $line);
469                         return;
470                 }
471         }
472                         
473         # rsfp check
474         #                       return if $rspfcheck and !$self->rspfcheck(1, $_[7], $_[6]);
475
476         # if this is a 'nodx' node then ignore it
477         if ($badnode->in($_[7])) {
478                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
479                 return;
480         }
481                         
482         # if this is a 'bad spotter' user then ignore it
483         my $nossid = $_[6];
484         $nossid =~ s/-\d+$//;
485         if ($badspotter->in($nossid)) {
486                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
487                 return;
488         }
489                         
490         # convert the date to a unix date
491         my $d = cltounix($_[3], $_[4]);
492         # bang out (and don't pass on) if date is invalid or the spot is too old (or too young)
493         if (!$d || ($pcno == 11 && ($d < $main::systime - $pc11_max_age || $d > $main::systime + 900))) {
494                 dbg("PCPROT: Spot ignored, invalid date or out of range ($_[3] $_[4])\n") if isdbg('chanerr');
495                 return;
496         }
497
498         # is it 'baddx'
499         if ($baddx->in($_[2]) || BadWords::check($_[2]) || $_[2] =~ /COCK/) {
500                 dbg("PCPROT: Bad DX spot, ignored") if isdbg('chanerr');
501                 return;
502         }
503                         
504         # do some de-duping
505         $_[5] =~ s/^\s+//;                      # take any leading blanks off
506         $_[2] = unpad($_[2]);           # take off leading and trailing blanks from spotted callsign
507         if ($_[2] =~ /BUST\w*$/) {
508                 dbg("PCPROT: useless 'BUSTED' spot") if isdbg('chanerr');
509                 return;
510         }
511         if ($censorpc) {
512                 my @bad;
513                 if (@bad = BadWords::check($_[5])) {
514                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
515                         return;
516                 }
517         }
518
519         # remember a route
520 #       RouteDB::update($_[7], $self->{call});
521 #       RouteDB::update($_[6], $_[7]);
522         
523         my @spot = Spot::prepare($_[1], $_[2], $d, $_[5], $_[6], $_[7]);
524         # global spot filtering on INPUT
525         if ($self->{inspotsfilter}) {
526                 my ($filter, $hops) = $self->{inspotsfilter}->it(@spot);
527                 unless ($filter) {
528                         dbg("PCPROT: Rejected by input spot filter") if isdbg('chanerr');
529                         return;
530                 }
531         }
532
533         # this goes after the input filtering, but before the add
534         # so that if it is input filtered, it isn't added to the dup
535         # list. This allows it to come in from a "legitimate" source
536         if (Spot::dup($_[1], $_[2], $d, $_[5], $_[6])) {
537                 dbg("PCPROT: Duplicate Spot ignored\n") if isdbg('chanerr');
538                 return;
539         }
540
541         # add it 
542         Spot::add(@spot);
543
544         #
545         # @spot at this point contains:-
546         # freq, spotted call, time, text, spotter, spotted cc, spotters cc, orig node
547         # then  spotted itu, spotted cq, spotters itu, spotters cq
548         # you should be able to route on any of these
549         #
550                         
551         # fix up qra locators of known users 
552         my $user = DXUser->get_current($spot[4]);
553         if ($user) {
554                 my $qra = $user->qra;
555                 unless ($qra && is_qra($qra)) {
556                         my $lat = $user->lat;
557                         my $long = $user->long;
558                         if (defined $lat && defined $long) {
559                                 $user->qra(DXBearing::lltoqra($lat, $long)); 
560                                 $user->put;
561                         }
562                 }
563
564                 # send a remote command to a distant cluster if it is visible and there is no
565                 # qra locator and we havn't done it for a month.
566
567                 unless ($user->qra) {
568                         my $node;
569                         my $to = $user->homenode;
570                         my $last = $user->lastoper || 0;
571                         if ($send_opernam && $to && $to ne $main::mycall && $main::systime > $last + $DXUser::lastoperinterval && ($node = Route::Node::get($to)) ) {
572                                 my $cmd = "forward/opernam $spot[4]";
573                                 # send the rcmd but we aren't interested in the replies...
574                                 my $dxchan = $node->dxchan;
575                                 if ($dxchan && $dxchan->is_clx) {
576                                         route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
577                                 } else {
578                                         route(undef, $to, pc34($main::mycall, $to, $cmd));
579                                 }
580                                 if ($to ne $_[7]) {
581                                         $to = $_[7];
582                                         $node = Route::Node::get($to);
583                                         if ($node) {
584                                                 $dxchan = $node->dxchan;
585                                                 if ($dxchan && $dxchan->is_clx) {
586                                                         route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
587                                                 } else {
588                                                         route(undef, $to, pc34($main::mycall, $to, $cmd));
589                                                 }
590                                         }
591                                 }
592                                 $user->lastoper($main::systime);
593                                 $user->put;
594                         }
595                 }
596         }
597                                 
598         # local processing 
599         my $r;
600         eval {
601                 $r = Local::spot($self, @spot);
602         };
603         #                       dbg("Local::spot1 error $@") if isdbg('local') if $@;
604         return if $r;
605
606         # DON'T be silly and send on PC26s!
607         return if $pcno == 26;
608
609         # send out the filtered spots
610         send_dx_spot($self, $line, @spot) if @spot;
611 }
612                 
613 # announces
614 sub handle_12
615 {
616         my $self = shift;
617         my $pcno = shift;
618         my $line = shift;
619         my $origin = shift;
620
621         #                       return if $rspfcheck and !$self->rspfcheck(1, $_[5], $_[1]);
622
623         # announce duplicate checking
624         $_[3] =~ s/^\s+//;                      # remove leading blanks
625
626         if ($censorpc) {
627                 my @bad;
628                 if (@bad = BadWords::check($_[3])) {
629                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
630                         return;
631                 }
632         }
633
634         # if this is a 'nodx' node then ignore it
635         if ($badnode->in($_[5])) {
636                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
637                 return;
638         }
639
640         # if this is a 'bad spotter' user then ignore it
641         my $nossid = $_[1];
642         $nossid =~ s/-\d+$//;
643         if ($badspotter->in($nossid)) {
644                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
645                 return;
646         }
647
648
649         my $dxchan;
650         
651         if ((($dxchan = DXChannel->get($_[2])) && $dxchan->is_user) || $_[4] =~ /^[\#\w.]+$/){
652                 $self->send_chat($line, @_[1..6]);
653         } elsif ($_[2] eq '*' || $_[2] eq $main::mycall) {
654
655                 # remember a route
656 #               RouteDB::update($_[5], $self->{call});
657 #               RouteDB::update($_[1], $_[5]);
658
659                 # ignore something that looks like a chat line coming in with sysop
660                 # flag - this is a kludge...
661                 if ($_[3] =~ /^\#\d+ / && $_[4] eq '*') {
662                         dbg('PCPROT: Probable chat rewrite, dropped') if isdbg('chanerr');
663                         return;
664                 }
665
666                 # here's a bit of fun, convert incoming ann with a callsign in the first word
667                 # or one saying 'to <call>' to a talk if we can route to the recipient
668                 if ($ann_to_talk) {
669                         my $call = AnnTalk::is_talk_candidate($_[1], $_[3]);
670                         if ($call) {
671                                 my $ref = Route::get($call);
672                                 if ($ref) {
673                                         $dxchan = $ref->dxchan;
674                                         $dxchan->talk($_[1], $call, undef, $_[3], $_[5]) if $dxchan != $self;
675                                         return;
676                                 }
677                         }
678                 }
679         
680                 # send it
681                 $self->send_announce($line, @_[1..6]);
682         } else {
683                 $self->route($_[2], $line);
684         }
685 }
686                 
687 # incoming user         
688 sub handle_16
689 {
690         my $self = shift;
691         my $pcno = shift;
692         my $line = shift;
693         my $origin = shift;
694
695         # general checks
696         my $dxchan;
697         my $ncall = $_[1];
698         my $newline = "PC16^";
699                         
700         # dos I want users from this channel?
701         unless ($self->user->wantpc16) {
702                 dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
703                 return;
704         }
705         # is it me?
706         if ($ncall eq $main::mycall) {
707                 dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
708                 return;
709         }
710
711         RouteDB::update($ncall, $self->{call});
712
713         # do we believe this call? 
714         unless ($ncall eq $self->{call} || $self->is_believed($ncall)) {
715                 if (my $ivp = Investigate::get($ncall, $self->{call})) {
716                         $ivp->store_pcxx($pcno,$line,$origin,@_);
717                 } else {
718                         dbg("PCPROT: We don't believe $ncall on $self->{call}") if isdbg('chanerr');
719                 }
720                 return;
721         }
722
723         if (eph_dup($line)) {
724                 dbg("PCPROT: dup PC16 detected") if isdbg('chanerr');
725                 return;
726         }
727
728         my $parent = Route::Node::get($ncall); 
729
730         # if there is a parent, proceed, otherwise if there is a latent PC19 in the PC19list, 
731         # fix it up in the routing tables and issue it forth before the PC16
732         unless ($parent) {
733                 my $nl = $pc19list{$ncall};
734
735                 if ($nl && @_ > 3) { # 3 because of the hop count!
736
737                         # this is a new (remembered) node, now attach it to me if it isn't in filtered
738                         # and we haven't disallowed it
739                         my $user = DXUser->get_current($ncall);
740                         if (!$user) {
741                                 $user = DXUser->new($ncall);
742                                 $user->sort('A');
743                                 $user->priv(1); # I have relented and defaulted nodes
744                                 $user->lockout(1);
745                                 $user->homenode($ncall);
746                                 $user->node($ncall);
747                         }
748
749                         my $wantpc19 = $user->wantroutepc19;
750                         if ($wantpc19 || !defined $wantpc19) {
751                                 my $new = Route->new($ncall); # throw away
752                                 if ($self->in_filter_route($new)) {
753                                         my @nrout;
754                                         for (@$nl) {
755                                                 $parent = Route::Node::get($_->[0]);
756                                                 $dxchan = $parent->dxchan if $parent;
757                                                 if ($dxchan && $dxchan ne $self) {
758                                                         dbg("PCPROT: PC19 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
759                                                         $parent = undef;
760                                                 }
761                                                 if ($parent) {
762                                                         my $r = $parent->add($ncall, $_->[1], $_->[2]);
763                                                         push @nrout, $r unless @nrout;
764                                                 }
765                                         }
766                                         $user->wantroutepc19(1) unless defined $wantpc19; # for now we work on the basis that pc16 = real route 
767                                         $user->lastin($main::systime) unless DXChannel->get($ncall);
768                                         $user->put;
769                                                 
770                                         # route the pc19 - this will cause 'stuttering PC19s' for a while
771                                         $self->route_pc19($origin, $line, @nrout) if @nrout ;
772                                         $parent = Route::Node::get($ncall);
773                                         unless ($parent) {
774                                                 dbg("PCPROT: lost $ncall after sending PC19 for it?");
775                                                 return;
776                                         }
777                                 } else {
778                                         return;
779                                 }
780                                 delete $pc19list{$ncall};
781                         }
782                 } else {
783                         dbg("PCPROT: Node $ncall not in config") if isdbg('chanerr');
784                         return;
785                 }
786         } else {
787                                 
788                 $dxchan = $parent->dxchan;
789                 if ($dxchan && $dxchan ne $self) {
790                         dbg("PCPROT: PC16 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
791                         return;
792                 }
793
794                 # input filter if required
795                 return unless $self->in_filter_route($parent);
796         }
797
798         my $i;
799         my @rout;
800         for ($i = 2; $i < $#_; $i++) {
801                 my ($call, $conf, $here) = $_[$i] =~ /^(\S+) (\S) (\d)/o;
802                 next unless $call && $conf && defined $here && is_callsign($call);
803                 next if $call eq $main::mycall;
804
805                 eph_del_regex("^PC17\\^$call\\^$ncall");
806                                 
807                 $conf = $conf eq '*';
808
809                 # reject this if we think it is a node already
810                 my $r = Route::Node::get($call);
811                 my $u = DXUser->get_current($call) unless $r;
812                 if ($r || ($u && $u->is_node)) {
813                         dbg("PCPROT: $call is a node") if isdbg('chanerr');
814                         next;
815                 }
816                                 
817                 $r = Route::User::get($call);
818                 my $flags = Route::here($here)|Route::conf($conf);
819                                 
820                 if ($r) {
821                         my $au = $r->addparent($parent);                                        
822                         if ($r->flags != $flags) {
823                                 $r->flags($flags);
824                                 $au = $r;
825                         }
826                         push @rout, $r if $au;
827                 } else {
828                         push @rout, $parent->add_user($call, $flags);
829                 }
830                 
831                                 
832                 # add this station to the user database, if required
833                 $call =~ s/-\d+$//o;    # remove ssid for users
834                 my $user = DXUser->get_current($call);
835                 $user = DXUser->new($call) if !$user;
836                 $user->homenode($parent->call) if !$user->homenode;
837                 $user->node($parent->call);
838                 $user->lastin($main::systime) unless DXChannel->get($call);
839                 $user->put;
840         }
841         $self->route_pc16($origin, $line, $parent, @rout) if @rout;
842 }
843                 
844 # remove a user
845 sub handle_17
846 {
847         my $self = shift;
848         my $pcno = shift;
849         my $line = shift;
850         my $origin = shift;
851         my $dxchan;
852         my $ncall = $_[2];
853         my $ucall = $_[1];
854
855         eph_del_regex("^PC16\\^$ncall.*$ucall");
856                         
857         # do I want users from this channel?
858         unless ($self->user->wantpc16) {
859                 dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
860                 return;
861         }
862         if ($ncall eq $main::mycall) {
863                 dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
864                 return;
865         }
866
867         RouteDB::delete($ncall, $self->{call});
868
869         # do we believe this call? 
870         unless ($ncall eq $self->{call} || $self->is_believed($ncall)) {
871                 if (my $ivp = Investigate::get($ncall, $self->{call})) {
872                         $ivp->store_pcxx($pcno,$line,$origin,@_);
873                 } else {
874                         dbg("PCPROT: We don't believe $ncall on $self->{call}") if isdbg('chanerr');
875                 }
876                 return;
877         }
878
879         my $uref = Route::User::get($ucall);
880         unless ($uref) {
881                 dbg("PCPROT: Route::User $ucall not in config") if isdbg('chanerr');
882         }
883         my $parent = Route::Node::get($ncall);
884         unless ($parent) {
885                 dbg("PCPROT: Route::Node $ncall not in config") if isdbg('chanerr');
886         }                       
887
888         $dxchan = $parent->dxchan if $parent;
889         if ($dxchan && $dxchan ne $self) {
890                 dbg("PCPROT: PC17 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
891                 return;
892         }
893
894         # input filter if required and then remove user if present
895         if ($parent) {
896 #               return unless $self->in_filter_route($parent);  
897                 $parent->del_user($uref) if $uref;
898         } else {
899                 $parent = Route->new($ncall);  # throw away
900         }
901
902         if (eph_dup($line)) {
903                 dbg("PCPROT: dup PC17 detected") if isdbg('chanerr');
904                 return;
905         }
906
907         $uref = Route->new($ucall) unless $uref; # throw away
908         $self->route_pc17($origin, $line, $parent, $uref);
909 }
910                 
911 # link request
912 sub handle_18
913 {
914         my $self = shift;
915         my $pcno = shift;
916         my $line = shift;
917         my $origin = shift;
918         $self->state('init');   
919
920         # record the type and version offered
921         if ($_[1] =~ /DXSpider Version: (\d+\.\d+) Build: (\d+\.\d+)/) {
922                 $self->version(53 + $1);
923                 $self->user->version(53 + $1);
924                 $self->build(0 + $2);
925                 $self->user->build(0 + $2);
926                 unless ($self->is_spider) {
927                         $self->user->sort('S');
928                         $self->user->put;
929                         $self->sort('S');
930                 }
931         } else {
932                 $self->version(50.0);
933                 $self->version($_[2] / 100) if $_[2] && $_[2] =~ /^\d+$/;
934                 $self->user->version($self->version);
935         }
936
937         # first clear out any nodes on this dxchannel
938         my $parent = Route::Node::get($self->{call});
939         my @rout = $parent->del_nodes;
940         $self->route_pc21($origin, $line, @rout, $parent) if @rout;
941         $self->send_local_config();
942         $self->send(pc20());
943 }
944                 
945 # incoming cluster list
946 sub handle_19
947 {
948         my $self = shift;
949         my $pcno = shift;
950         my $line = shift;
951         my $origin = shift;
952
953         my $i;
954         my $newline = "PC19^";
955
956         # new routing list
957         my @rout;
958
959         # first get the INTERFACE node
960         my $parent = Route::Node::get($self->{call});
961         unless ($parent) {
962                 dbg("DXPROT: my parent $self->{call} has disappeared");
963                 $self->disconnect;
964                 return;
965         }
966
967         # if the origin isn't the same as the INTERFACE, then reparent, creating nodes as necessary
968         if ($origin ne $self->call) {
969                 my $op = Route::Node::get($origin);
970                 unless ($op) {
971                         $op = $parent->add($origin, 5000, Route::here(1));
972                         my $user = DXUser->get_current($origin);
973                         if (!$user) {
974                                 $user = DXUser->new($origin);
975                                 $user->priv(1);         # I have relented and defaulted nodes
976                                 $user->lockout(1);
977                                 $user->homenode($origin);
978                                 $user->node($origin);
979                                 $user->wantroutepc19(1);
980                         }
981                         $user->sort('A') unless $user->is_node;
982                         $user->put;
983                 }
984                 $parent = $op;
985         }
986
987         # parse the PC19
988         for ($i = 1; $i < $#_-1; $i += 4) {
989                 my $here = $_[$i];
990                 my $call = uc $_[$i+1];
991                 my $conf = $_[$i+2];
992                 my $ver = $_[$i+3];
993                 next unless defined $here && defined $conf && is_callsign($call);
994
995                 eph_del_regex("^PC(?:21\\^$call|17\\^[^\\^]+\\^$call)");
996                                 
997                 # check for sane parameters
998                 #                               $ver = 5000 if $ver eq '0000';
999                 next if $ver < 5000;    # only works with version 5 software
1000                 next if length $call < 3; # min 3 letter callsigns
1001                 next if $call eq $main::mycall;
1002
1003                 # check that this PC19 isn't trying to alter the wrong dxchan
1004                 my $dxchan = DXChannel->get($call);
1005                 if ($dxchan && $dxchan != $self) {
1006                         dbg("PCPROT: PC19 from $self->{call} trying to alter wrong locally connected $call, ignored!") if isdbg('chanerr');
1007                         next;
1008                 }
1009
1010                 # add this station to the user database, if required (don't remove SSID from nodes)
1011                 my $user = DXUser->get_current($call);
1012                 if (!$user) {
1013                         $user = DXUser->new($call);
1014                         $user->priv(1);         # I have relented and defaulted nodes
1015                         $user->lockout(1);
1016                         $user->homenode($call);
1017                         $user->node($call);
1018                 }
1019                 $user->sort('A') unless $user->is_node;
1020
1021                 RouteDB::update($call, $self->{call});
1022
1023                 # do we believe this call?
1024                 my $genline = "PC19^$here^$call^$conf^$ver^$_[-1]^"; 
1025                 unless ($call eq $self->{call} || $self->is_believed($call)) {
1026                         my $pt = $user->lastping($self->{call}) || 0;
1027                         if ($pt+$investigation_int < $main::systime && !Investigate::get($call, $self->{call})) {
1028                                 my $ivp  = Investigate->new($call, $self->{call});
1029                                 $ivp->version($ver);
1030                                 $ivp->here($here);
1031                                 $ivp->store_pcxx($pcno,$genline,$origin,'PC19',$here,$call,$conf,$ver,$_[-1]);
1032                         } else {
1033                                 dbg("PCPROT: We don't believe $call on $self->{call}") if isdbg('chanerr');
1034                         }
1035                         $user->put;
1036                         next;
1037                 }
1038
1039                 if (eph_dup($genline)) {
1040                         dbg("PCPROT: dup PC19 for $call detected") if isdbg('chanerr');
1041                         next;
1042                 }
1043
1044                 my $r = Route::Node::get($call);
1045                 my $flags = Route::here($here)|Route::conf($conf);
1046
1047                 # modify the routing table if it is in it, otherwise store it in the pc19list for now
1048                 if ($r) {
1049                         my $ar;
1050                         if ($call ne $parent->call) {
1051                                 if ($self->in_filter_route($r)) {
1052                                         $ar = $parent->add($call, $ver, $flags);
1053                                         push @rout, $ar if $ar;
1054                                 } else {
1055                                         next;
1056                                 }
1057                         }
1058                         if ($r->version ne $ver || $r->flags != $flags) {
1059                                 $r->version($ver);
1060                                 $r->flags($flags);
1061                                 push @rout, $r unless $ar;
1062                         }
1063                 } else {
1064
1065                         # if he is directly connected or allowed then add him, otherwise store him up for later
1066                         if ($call eq $self->{call} || $user->wantroutepc19) {
1067                                 my $new = Route->new($call); # throw away
1068                                 if ($self->in_filter_route($new)) {
1069                                         my $ar = $parent->add($call, $ver, $flags);
1070                                         $user->wantroutepc19(1) unless defined $user->wantroutepc19;
1071                                         push @rout, $ar if $ar;
1072                                 } else {
1073                                         next;
1074                                 }
1075                         } else {
1076                                 $pc19list{$call} = [] unless exists $pc19list{$call};
1077                                 my $nl = $pc19list{$call};
1078                                 push @{$pc19list{$call}}, [$self->{call}, $ver, $flags] unless grep $_->[0] eq $self->{call}, @$nl;
1079                         }
1080                 }
1081
1082                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
1083                 my $mref = DXMsg::get_busy($call);
1084                 $mref->stop_msg($call) if $mref;
1085                                 
1086                 $user->lastin($main::systime) unless DXChannel->get($call);
1087                 $user->put;
1088         }
1089
1090
1091         $self->route_pc19($origin, $line, @rout) if @rout;
1092 }
1093                 
1094 # send local configuration
1095 sub handle_20
1096 {
1097         my $self = shift;
1098         my $pcno = shift;
1099         my $line = shift;
1100         my $origin = shift;
1101         $self->send_local_config();
1102         $self->send(pc22());
1103         $self->state('normal');
1104         $self->{lastping} = 0;
1105 }
1106                 
1107 # delete a cluster from the list
1108 sub handle_21
1109 {
1110         my $self = shift;
1111         my $pcno = shift;
1112         my $line = shift;
1113         my $origin = shift;
1114         my $call = uc $_[1];
1115
1116         eph_del_regex("^PC1[679].*$call");
1117                         
1118         # if I get a PC21 from the same callsign as self then treat it
1119         # as a PC39: I have gone away
1120         if ($call eq $self->call) {
1121                 $self->disconnect(1);
1122                 return;
1123         }
1124
1125         RouteDB::delete($call, $self->{call});
1126
1127         # check if we believe this
1128         unless ($call eq $self->{call} || $self->is_believed($call)) {
1129                 if (my $ivp = Investigate::get($call, $self->{call})) {
1130                         $ivp->store_pcxx($pcno,$line,$origin,@_);
1131                 } else {
1132                         dbg("PCPROT: We don't believe $call on $self->{call}") if isdbg('chanerr');
1133                 }
1134                 return;
1135         }
1136
1137         # check to see if we are in the pc19list, if we are then don't bother with any of
1138         # this routing table manipulation, just remove it from the list and dump it
1139         my @rout;
1140         if (my $nl = $pc19list{$call}) {
1141                 $pc19list{$call} = [ grep {$_->[0] ne $self->{call}} @$nl ];
1142                 delete $pc19list{$call} unless @{$pc19list{$call}};
1143         } else {
1144                                 
1145                 my $parent = Route::Node::get($self->{call});
1146                 unless ($parent) {
1147                         dbg("DXPROT: my parent $self->{call} has disappeared");
1148                         $self->disconnect;
1149                         return;
1150                 }
1151                 if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
1152                         my $node = Route::Node::get($call);
1153                         if ($node) {
1154                                                 
1155                                 my $dxchan = DXChannel->get($call);
1156                                 if ($dxchan && $dxchan != $self) {
1157                                         dbg("PCPROT: PC21 from $self->{call} trying to alter locally connected $call, ignored!") if isdbg('chanerr');
1158                                         return;
1159                                 }
1160                                                 
1161                                 # input filter it
1162                                 return unless $self->in_filter_route($node);
1163                                                 
1164                                 # routing objects
1165                                 push @rout, $node->del($parent);
1166                         }
1167                 } else {
1168                         dbg("PCPROT: I WILL _NOT_ be disconnected!") if isdbg('chanerr');
1169                         return;
1170                 }
1171         }
1172
1173         $self->route_pc21($origin, $line, @rout) if @rout;
1174 }
1175                 
1176
1177 sub handle_22
1178 {
1179         my $self = shift;
1180         my $pcno = shift;
1181         my $line = shift;
1182         my $origin = shift;
1183         $self->state('normal');
1184         $self->{lastping} = 0;
1185 }
1186                                 
1187 # WWV info
1188 sub handle_23
1189 {
1190         my $self = shift;
1191         my $pcno = shift;
1192         my $line = shift;
1193         my $origin = shift;
1194                         
1195         # route foreign' pc27s 
1196         if ($pcno == 27) {
1197                 if ($_[8] ne $main::mycall) {
1198                         $self->route($_[8], $line);
1199                         return;
1200                 }
1201         }
1202
1203         # only do a rspf check on PC23 (not 27)
1204         if ($pcno == 23) {
1205                 return if $rspfcheck and !$self->rspfcheck(1, $_[8], $_[7])
1206         }
1207
1208         # do some de-duping
1209         my $d = cltounix($_[1], sprintf("%02d18Z", $_[2]));
1210         my $sfi = unpad($_[3]);
1211         my $k = unpad($_[4]);
1212         my $i = unpad($_[5]);
1213         my ($r) = $_[6] =~ /R=(\d+)/;
1214         $r = 0 unless $r;
1215         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $_[2] < 0 || $_[2] > 23) {
1216                 dbg("PCPROT: WWV Date ($_[1] $_[2]) out of range") if isdbg('chanerr');
1217                 return;
1218         }
1219         if (Geomag::dup($d,$sfi,$k,$i,$_[6])) {
1220                 dbg("PCPROT: Dup WWV Spot ignored\n") if isdbg('chanerr');
1221                 return;
1222         }
1223         $_[7] =~ s/-\d+$//o;            # remove spotter's ssid
1224                 
1225         my $wwv = Geomag::update($d, $_[2], $sfi, $k, $i, @_[6..8], $r);
1226
1227         my $rep;
1228         eval {
1229                 $rep = Local::wwv($self, $_[1], $_[2], $sfi, $k, $i, @_[6..8], $r);
1230         };
1231         #                       dbg("Local::wwv2 error $@") if isdbg('local') if $@;
1232         return if $rep;
1233
1234         # DON'T be silly and send on PC27s!
1235         return if $pcno == 27;
1236
1237         # broadcast to the eager world
1238         send_wwv_spot($self, $line, $d, $_[2], $sfi, $k, $i, @_[6..8]);
1239 }
1240                 
1241 # set here status
1242 sub handle_24
1243 {
1244         my $self = shift;
1245         my $pcno = shift;
1246         my $line = shift;
1247         my $origin = shift;
1248         my $call = uc $_[1];
1249         my ($nref, $uref);
1250         $nref = Route::Node::get($call);
1251         $uref = Route::User::get($call);
1252         return unless $nref || $uref; # if we don't know where they are, it's pointless sending it on
1253                         
1254         if (eph_dup($line)) {
1255                 dbg("PCPROT: Dup PC24 ignored\n") if isdbg('chanerr');
1256                 return;
1257         }
1258         
1259         $nref->here($_[2]) if $nref;
1260         $uref->here($_[2]) if $uref;
1261         my $ref = $nref || $uref;
1262         return unless $self->in_filter_route($ref);
1263
1264         $self->route_pc24($origin, $line, $ref, $_[3]);
1265 }
1266                 
1267 # merge request
1268 sub handle_25
1269 {
1270         my $self = shift;
1271         my $pcno = shift;
1272         my $line = shift;
1273         my $origin = shift;
1274         if ($_[1] ne $main::mycall) {
1275                 $self->route($_[1], $line);
1276                 return;
1277         }
1278         if ($_[2] eq $main::mycall) {
1279                 dbg("PCPROT: Trying to merge to myself, ignored") if isdbg('chanerr');
1280                 return;
1281         }
1282
1283         Log('DXProt', "Merge request for $_[3] spots and $_[4] WWV from $_[2]");
1284                         
1285         # spots
1286         if ($_[3] > 0) {
1287                 my @in = reverse Spot::search(1, undef, undef, 0, $_[3]);
1288                 my $in;
1289                 foreach $in (@in) {
1290                         $self->send(pc26(@{$in}[0..4], $_[2]));
1291                 }
1292         }
1293
1294         # wwv
1295         if ($_[4] > 0) {
1296                 my @in = reverse Geomag::search(0, $_[4], time, 1);
1297                 my $in;
1298                 foreach $in (@in) {
1299                         $self->send(pc27(@{$in}[0..5], $_[2]));
1300                 }
1301         }
1302 }
1303
1304 sub handle_26 {goto &handle_11}
1305 sub handle_27 {goto &handle_23}
1306
1307 # mail/file handling
1308 sub handle_28
1309 {
1310         my $self = shift;
1311         my $pcno = shift;
1312         my $line = shift;
1313         my $origin = shift;
1314         if ($_[1] eq $main::mycall) {
1315                 no strict 'refs';
1316                 my $sub = "DXMsg::handle_$pcno";
1317                 &$sub($self, @_);
1318         } else {
1319                 $self->route($_[1], $line) unless $self->is_clx;
1320         }
1321 }
1322
1323 sub handle_29 {goto &handle_28}
1324 sub handle_30 {goto &handle_28}
1325 sub handle_31 {goto &handle_28}
1326 sub handle_32 {goto &handle_28}
1327 sub handle_33 {goto &handle_28}
1328                 
1329 sub handle_34
1330 {
1331         my $self = shift;
1332         my $pcno = shift;
1333         my $line = shift;
1334         my $origin = shift;
1335         if (eph_dup($line, $eph_pc34_restime)) {
1336                 dbg("PCPROT: dupe PC34, ignored") if isdbg('chanerr');
1337         } else {
1338                 $self->process_rcmd($_[1], $_[2], $_[2], $_[3]);
1339         }
1340 }
1341                 
1342 # remote command replies
1343 sub handle_35
1344 {
1345         my $self = shift;
1346         my $pcno = shift;
1347         my $line = shift;
1348         my $origin = shift;
1349         eph_del_regex("^PC35\\^$_[2]\\^$_[1]\\^");
1350         $self->process_rcmd_reply($_[1], $_[2], $_[1], $_[3]);
1351 }
1352                 
1353 sub handle_36 {goto &handle_34}
1354
1355 # database stuff
1356 sub handle_37
1357 {
1358         my $self = shift;
1359         my $pcno = shift;
1360         my $line = shift;
1361         my $origin = shift;
1362         if ($_[1] eq $main::mycall) {
1363                 no strict 'refs';
1364                 my $sub = "DXDb::handle_$pcno";
1365                 &$sub($self, @_);
1366         } else {
1367                 $self->route($_[1], $line) unless $self->is_clx;
1368         }
1369 }
1370
1371 # node connected list from neighbour
1372 sub handle_38
1373 {
1374         my $self = shift;
1375         my $pcno = shift;
1376         my $line = shift;
1377         my $origin = shift;
1378 }
1379                 
1380 # incoming disconnect
1381 sub handle_39
1382 {
1383         my $self = shift;
1384         my $pcno = shift;
1385         my $line = shift;
1386         my $origin = shift;
1387         if ($_[1] eq $self->{call}) {
1388                 $self->disconnect(1);
1389         } else {
1390                 dbg("PCPROT: came in on wrong channel") if isdbg('chanerr');
1391         }
1392 }
1393
1394 sub handle_40 {goto &handle_28}
1395                 
1396 # user info
1397 sub handle_41
1398 {
1399         my $self = shift;
1400         my $pcno = shift;
1401         my $line = shift;
1402         my $origin = shift;
1403         my $call = $_[1];
1404
1405         my $l = $line;
1406         $l =~ s/[\x00-\x20\x7f-\xff]+//g; # remove all funny characters and spaces for dup checking
1407         if (eph_dup($l, $eph_info_restime)) {
1408                 dbg("PCPROT: dup PC41, ignored") if isdbg('chanerr');
1409                 return;
1410         }
1411                         
1412         # input filter if required
1413         #                       my $ref = Route::get($call) || Route->new($call);
1414         #                       return unless $self->in_filter_route($ref);
1415
1416         if ($_[3] eq $_[2] || $_[3] =~ /^\s*$/) {
1417                 dbg('PCPROT: invalid value') if isdbg('chanerr');
1418                 return;
1419         }
1420
1421         # add this station to the user database, if required
1422         my $user = DXUser->get_current($call);
1423         $user = DXUser->new($call) unless $user;
1424                         
1425         if ($_[2] == 1) {
1426                 $user->name($_[3]);
1427         } elsif ($_[2] == 2) {
1428                 $user->qth($_[3]);
1429         } elsif ($_[2] == 3) {
1430                 if (is_latlong($_[3])) {
1431                         my ($lat, $long) = DXBearing::stoll($_[3]);
1432                         $user->lat($lat);
1433                         $user->long($long);
1434                         $user->qra(DXBearing::lltoqra($lat, $long));
1435                 } else {
1436                         dbg('PCPROT: not a valid lat/long') if isdbg('chanerr');
1437                         return;
1438                 }
1439         } elsif ($_[2] == 4) {
1440                 $user->homenode($_[3]);
1441         } elsif ($_[2] == 5) {
1442                 if (is_qra(uc $_[3])) {
1443                         my ($lat, $long) = DXBearing::qratoll(uc $_[3]);
1444                         $user->lat($lat);
1445                         $user->long($long);
1446                         $user->qra(uc $_[3]);
1447                 } else {
1448                         dbg('PCPROT: not a valid QRA locator') if isdbg('chanerr');
1449                         return;
1450                 }
1451         }
1452         $user->lastoper($main::systime); # to cut down on excessive for/opers being generated
1453         $user->put;
1454
1455         unless ($self->{isolate}) {
1456                 DXChannel::broadcast_nodes($line, $self); # send it to everyone but me
1457         }
1458
1459         #  perhaps this IS what we want after all
1460         #                       $self->route_pc41($ref, $call, $_[2], $_[3], $_[4]);
1461 }
1462
1463 sub handle_42 {goto &handle_28}
1464
1465
1466 # database
1467 sub handle_44 {goto &handle_37}
1468 sub handle_45 {goto &handle_37}
1469 sub handle_46 {goto &handle_37}
1470 sub handle_47 {goto &handle_37}
1471 sub handle_48 {goto &handle_37}
1472                 
1473 # message and database
1474 sub handle_49
1475 {
1476         my $self = shift;
1477         my $pcno = shift;
1478         my $line = shift;
1479         my $origin = shift;
1480
1481         if (eph_dup($line)) {
1482                 dbg("PCPROT: Dup PC49 ignored\n") if isdbg('chanerr');
1483                 return;
1484         }
1485         
1486         if ($_[1] eq $main::mycall) {
1487                 DXMsg::handle_49($self, @_);
1488         } else {
1489                 $self->route($_[1], $line) unless $self->is_clx;
1490         }
1491 }
1492
1493 # keep alive/user list
1494 sub handle_50
1495 {
1496         my $self = shift;
1497         my $pcno = shift;
1498         my $line = shift;
1499         my $origin = shift;
1500
1501         my $call = $_[1];
1502
1503         RouteDB::update($call, $self->{call});
1504
1505         my $node = Route::Node::get($call);
1506         if ($node) {
1507                 return unless $node->call eq $self->{call};
1508                 $node->usercount($_[2]);
1509
1510                 # input filter if required
1511                 return unless $self->in_filter_route($node);
1512
1513                 $self->route_pc50($origin, $line, $node, $_[2], $_[3]) unless eph_dup($line);
1514         }
1515 }
1516                 
1517 # incoming ping requests/answers
1518 sub handle_51
1519 {
1520         my $self = shift;
1521         my $pcno = shift;
1522         my $line = shift;
1523         my $origin = shift;
1524         my $to = $_[1];
1525         my $from = $_[2];
1526         my $flag = $_[3];
1527
1528                         
1529         # is it for us?
1530         if ($to eq $main::mycall) {
1531                 if ($flag == 1) {
1532                         $self->send(pc51($from, $to, '0'));
1533                 } else {
1534                         # it's a reply, look in the ping list for this one
1535                         my $ref = $pings{$from};
1536                         if ($ref) {
1537                                 my $tochan =  DXChannel->get($from);
1538                                 while (@$ref) {
1539                                         my $r = shift @$ref;
1540                                         my $dxchan = DXChannel->get($r->{call});
1541                                         next unless $dxchan;
1542                                         my $t = tv_interval($r->{t}, [ gettimeofday ]);
1543                                         if ($dxchan->is_user) {
1544                                                 my $s = sprintf "%.2f", $t; 
1545                                                 my $ave = sprintf "%.2f", $tochan ? ($tochan->{pingave} || $t) : $t;
1546                                                 $dxchan->send($dxchan->msg('pingi', $from, $s, $ave))
1547                                         } elsif ($dxchan->is_node) {
1548                                                 if ($tochan) {
1549                                                         my $nopings = $tochan->user->nopings || $obscount;
1550                                                         push @{$tochan->{pingtime}}, $t;
1551                                                         shift @{$tochan->{pingtime}} if @{$tochan->{pingtime}} > 6;
1552                                                         
1553                                                         # cope with a missed ping, this means you must set the pingint large enough
1554                                                         if ($t > $tochan->{pingint}  && $t < 2 * $tochan->{pingint} ) {
1555                                                                 $t -= $tochan->{pingint};
1556                                                         }
1557                                                         
1558                                                         # calc smoothed RTT a la TCP
1559                                                         if (@{$tochan->{pingtime}} == 1) {
1560                                                                 $tochan->{pingave} = $t;
1561                                                         } else {
1562                                                                 $tochan->{pingave} = $tochan->{pingave} + (($t - $tochan->{pingave}) / 6);
1563                                                         }
1564                                                         $tochan->{nopings} = $nopings; # pump up the timer
1565                                                         if (my $ivp = Investigate::get($from, $self->{call})) {
1566                                                                 $ivp->handle_ping;
1567                                                         }
1568                                                 } elsif (my $rref = Route::Node::get($r->{call})) {
1569                                                         if (my $ivp = Investigate::get($from, $self->{call})) {
1570                                                                 $ivp->handle_ping;
1571                                                         }
1572                                                 }
1573                                         }
1574                                 }
1575                         }
1576                 }
1577         } else {
1578
1579                 RouteDB::update($from, $self->{call});
1580
1581                 if (eph_dup($line)) {
1582                         dbg("PCPROT: dup PC51 detected") if isdbg('chanerr');
1583                         return;
1584                 }
1585                 # route down an appropriate thingy
1586                 $self->route($to, $line);
1587         }
1588 }
1589
1590 # dunno but route it
1591 sub handle_75
1592 {
1593         my $self = shift;
1594         my $pcno = shift;
1595         my $line = shift;
1596         my $origin = shift;
1597         my $call = $_[1];
1598         if ($call ne $main::mycall) {
1599                 $self->route($call, $line);
1600         }
1601 }
1602
1603 # WCY broadcasts
1604 sub handle_73
1605 {
1606         my $self = shift;
1607         my $pcno = shift;
1608         my $line = shift;
1609         my $origin = shift;
1610         my $call = $_[1];
1611                         
1612         # do some de-duping
1613         my $d = cltounix($call, sprintf("%02d18Z", $_[2]));
1614         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $_[2] < 0 || $_[2] > 23) {
1615                 dbg("PCPROT: WCY Date ($call $_[2]) out of range") if isdbg('chanerr');
1616                 return;
1617         }
1618         @_ = map { unpad($_) } @_;
1619         if (WCY::dup($d)) {
1620                 dbg("PCPROT: Dup WCY Spot ignored\n") if isdbg('chanerr');
1621                 return;
1622         }
1623                 
1624         my $wcy = WCY::update($d, @_[2..12]);
1625
1626         my $rep;
1627         eval {
1628                 $rep = Local::wcy($self, @_[1..12]);
1629         };
1630         # dbg("Local::wcy error $@") if isdbg('local') if $@;
1631         return if $rep;
1632
1633         # broadcast to the eager world
1634         send_wcy_spot($self, $line, $d, @_[2..12]);
1635 }
1636
1637 # remote commands (incoming)
1638 sub handle_84
1639 {
1640         my $self = shift;
1641         my $pcno = shift;
1642         my $line = shift;
1643         my $origin = shift;
1644         $self->process_rcmd($_[1], $_[2], $_[3], $_[4]);
1645 }
1646
1647 # remote command replies
1648 sub handle_85
1649 {
1650         my $self = shift;
1651         my $pcno = shift;
1652         my $line = shift;
1653         my $origin = shift;
1654         $self->process_rcmd_reply($_[1], $_[2], $_[3], $_[4]);
1655 }
1656
1657 # if get here then rebroadcast the thing with its Hop count decremented (if
1658 # there is one). If it has a hop count and it decrements to zero then don't
1659 # rebroadcast it.
1660 #
1661 # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
1662 #        REBROADCAST!!!!
1663 #
1664
1665 sub handle_default
1666 {
1667         my $self = shift;
1668         my $pcno = shift;
1669         my $line = shift;
1670         my $origin = shift;
1671
1672         if (eph_dup($line)) {
1673                 dbg("PCPROT: Ephemeral dup, dropped") if isdbg('chanerr');
1674         } else {
1675                 unless ($self->{isolate}) {
1676                         DXChannel::broadcast_nodes($line, $self) if $line =~ /\^H\d+\^?~?$/; # send it to everyone but me
1677                 }
1678         }
1679 }
1680
1681 #
1682 # This is called from inside the main cluster processing loop and is used
1683 # for despatching commands that are doing some long processing job
1684 #
1685 sub process
1686 {
1687         my $t = time;
1688         my @dxchan = DXChannel->get_all();
1689         my $dxchan;
1690         my $pc50s;
1691         
1692         # send out a pc50 on EVERY channel all at once
1693         if ($t >= $last_pc50 + $DXProt::pc50_interval) {
1694                 $pc50s = pc50($main::me, scalar DXChannel::get_all_users);
1695                 eph_dup($pc50s);
1696                 $last_pc50 = $t;
1697         }
1698
1699         foreach $dxchan (@dxchan) {
1700                 next unless $dxchan->is_node();
1701                 next if $dxchan == $main::me;
1702
1703                 # send the pc50
1704                 $dxchan->send($pc50s) if $pc50s;
1705                 
1706                 # send a ping out on this channel
1707                 if ($dxchan->{pingint} && $t >= $dxchan->{pingint} + $dxchan->{lastping}) {
1708                         if ($dxchan->{nopings} <= 0) {
1709                                 $dxchan->disconnect;
1710                         } else {
1711                                 addping($main::mycall, $dxchan->call);
1712                                 $dxchan->{nopings} -= 1;
1713                                 $dxchan->{lastping} = $t;
1714                         }
1715                 }
1716         }
1717
1718         Investigate::process();
1719
1720         # every ten seconds
1721         if ($t - $last10 >= 10) {       
1722                 # clean out ephemera 
1723
1724                 eph_clean();
1725                 import_chat();
1726                 
1727
1728                 $last10 = $t;
1729         }
1730         
1731         if ($main::systime - 3600 > $last_hour) {
1732                 $last_hour = $main::systime;
1733         }
1734 }
1735
1736 #
1737 # finish up a pc context
1738 #
1739
1740 #
1741 # some active measures
1742 #
1743
1744
1745 sub send_dx_spot
1746 {
1747         my $self = shift;
1748         my $line = shift;
1749         my @dxchan = DXChannel->get_all();
1750         my $dxchan;
1751         
1752         # send it if it isn't the except list and isn't isolated and still has a hop count
1753         # taking into account filtering and so on
1754         foreach $dxchan (@dxchan) {
1755                 next if $dxchan == $main::me;
1756                 next if $dxchan == $self && $self->is_node;
1757                 $dxchan->dx_spot($line, $self->{isolate}, @_, $self->{call});
1758         }
1759 }
1760
1761 sub dx_spot
1762 {
1763         my $self = shift;
1764         my $line = shift;
1765         my $isolate = shift;
1766         my ($filter, $hops);
1767
1768         if ($self->{spotsfilter}) {
1769                 ($filter, $hops) = $self->{spotsfilter}->it(@_);
1770                 return unless $filter;
1771         }
1772         send_prot_line($self, $filter, $hops, $isolate, $line);
1773 }
1774
1775 sub send_prot_line
1776 {
1777         my ($self, $filter, $hops, $isolate, $line) = @_;
1778         my $routeit;
1779
1780
1781         if ($hops) {
1782                 $routeit = $line;
1783                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1784         } else {
1785                 $routeit = adjust_hops($self, $line);  # adjust its hop count by node name
1786                 return unless $routeit;
1787         }
1788         if ($filter) {
1789                 $self->send($routeit);
1790         } else {
1791                 $self->send($routeit) unless $self->{isolate} || $isolate;
1792         }
1793 }
1794
1795
1796 sub send_wwv_spot
1797 {
1798         my $self = shift;
1799         my $line = shift;
1800         my @dxchan = DXChannel->get_all();
1801         my $dxchan;
1802         my @dxcc = ((Prefix::cty_data($_[6]))[0..2], (Prefix::cty_data($_[7]))[0..2]);
1803
1804         # send it if it isn't the except list and isn't isolated and still has a hop count
1805         # taking into account filtering and so on
1806         foreach $dxchan (@dxchan) {
1807                 next if $dxchan == $main::me;
1808                 next if $dxchan == $self && $self->is_node;
1809                 my $routeit;
1810                 my ($filter, $hops);
1811
1812                 $dxchan->wwv($line, $self->{isolate}, @_, $self->{call}, @dxcc);
1813         }
1814 }
1815
1816 sub wwv
1817 {
1818         my $self = shift;
1819         my $line = shift;
1820         my $isolate = shift;
1821         my ($filter, $hops);
1822         
1823         if ($self->{wwvfilter}) {
1824                 ($filter, $hops) = $self->{wwvfilter}->it(@_);
1825                 return unless $filter;
1826         }
1827         send_prot_line($self, $filter, $hops, $isolate, $line)
1828 }
1829
1830 sub send_wcy_spot
1831 {
1832         my $self = shift;
1833         my $line = shift;
1834         my @dxchan = DXChannel->get_all();
1835         my $dxchan;
1836         my @dxcc = ((Prefix::cty_data($_[10]))[0..2], (Prefix::cty_data($_[11]))[0..2]);
1837         
1838         # send it if it isn't the except list and isn't isolated and still has a hop count
1839         # taking into account filtering and so on
1840         foreach $dxchan (@dxchan) {
1841                 next if $dxchan == $main::me;
1842                 next if $dxchan == $self;
1843
1844                 $dxchan->wcy($line, $self->{isolate}, @_, $self->{call}, @dxcc);
1845         }
1846 }
1847
1848 sub wcy
1849 {
1850         my $self = shift;
1851         my $line = shift;
1852         my $isolate = shift;
1853         my ($filter, $hops);
1854
1855         if ($self->{wcyfilter}) {
1856                 ($filter, $hops) = $self->{wcyfilter}->it(@_);
1857                 return unless $filter;
1858         }
1859         send_prot_line($self, $filter, $hops, $isolate, $line) if $self->is_clx || $self->is_spider || $self->is_dxnet;
1860 }
1861
1862 # send an announce
1863 sub send_announce
1864 {
1865         my $self = shift;
1866         my $line = shift;
1867         my @dxchan = DXChannel->get_all();
1868         my $dxchan;
1869         my $target;
1870         my $to = 'To ';
1871         my $text = unpad($_[2]);
1872                                 
1873         if ($_[3] eq '*') {     # sysops
1874                 $target = "SYSOP";
1875         } elsif ($_[3] gt ' ') { # speciality list handling
1876                 my ($name) = split /\./, $_[3]; 
1877                 $target = "$name"; # put the rest in later (if bothered) 
1878         } 
1879         
1880         if ($_[5] eq '1') {
1881                 $target = "WX"; 
1882                 $to = '';
1883         }
1884         $target = "ALL" if !$target;
1885
1886
1887         # obtain country codes etc 
1888         my @a = Prefix::cty_data($_[0]);
1889         my @b = Prefix::cty_data($_[4]);
1890         if ($self->{inannfilter}) {
1891                 my ($filter, $hops) = 
1892                         $self->{inannfilter}->it(@_, $self->{call}, 
1893                                                                          @a[0..2],
1894                                                                          @b[0..2], $a[3], $b[3]);
1895                 unless ($filter) {
1896                         dbg("PCPROT: Rejected by input announce filter") if isdbg('chanerr');
1897                         return;
1898                 }
1899         }
1900
1901         if (AnnTalk::dup($_[0], $_[1], $_[2])) {
1902                 dbg("PCPROT: Duplicate Announce ignored") if isdbg('chanerr');
1903                 return;
1904         }
1905
1906         Log('ann', $target, $_[0], $text);
1907
1908         # send it if it isn't the except list and isn't isolated and still has a hop count
1909         # taking into account filtering and so on
1910         foreach $dxchan (@dxchan) {
1911                 next if $dxchan == $main::me;
1912                 next if $dxchan == $self && $self->is_node;
1913                 $dxchan->announce($line, $self->{isolate}, $to, $target, $text, @_, $self->{call},
1914                                                   @a[0..2], @b[0..2]);
1915         }
1916 }
1917
1918 my $msgid = 0;
1919
1920 sub nextchatmsgid
1921 {
1922         $msgid++;
1923         $msgid = 1 if $msgid > 999;
1924         return $msgid;
1925 }
1926
1927 # send a chat line
1928 sub send_chat
1929 {
1930         my $self = shift;
1931         my $line = shift;
1932         my @dxchan = DXChannel->get_all();
1933         my $dxchan;
1934         my $target = $_[3];
1935         my $text = unpad($_[2]);
1936         my $ak1a_line;
1937                                 
1938         # munge the group and recast the line if required
1939         if ($target =~ s/\.LST$//) {
1940                 $ak1a_line = $line;
1941         }
1942         
1943         # obtain country codes etc 
1944         my @a = Prefix::cty_data($_[0]);
1945         my @b = Prefix::cty_data($_[4]);
1946         if ($self->{inannfilter}) {
1947                 my ($filter, $hops) = 
1948                         $self->{inannfilter}->it(@_, $self->{call}, 
1949                                                                          @a[0..2],
1950                                                                          @b[0..2], $a[3], $b[3]);
1951                 unless ($filter) {
1952                         dbg("PCPROT: Rejected by input announce filter") if isdbg('chanerr');
1953                         return;
1954                 }
1955         }
1956
1957         if (AnnTalk::dup($_[0], $_[1], $_[2], $chatdupeage)) {
1958                 dbg("PCPROT: Duplicate Announce ignored") if isdbg('chanerr');
1959                 return;
1960         }
1961
1962
1963         Log('chat', $target, $_[0], $text);
1964
1965         # send it if it isn't the except list and isn't isolated and still has a hop count
1966         # taking into account filtering and so on
1967         foreach $dxchan (@dxchan) {
1968                 my $is_ak1a = $dxchan->is_ak1a;
1969                 
1970                 if ($dxchan->is_node) {
1971                         next if $dxchan == $main::me;
1972                         next if $dxchan == $self;
1973                         next unless $dxchan->is_spider || $is_ak1a;
1974                         next if $target eq 'LOCAL';
1975                         if (!$ak1a_line && $is_ak1a) {
1976                                 $ak1a_line = DXProt::pc12($_[0], $text, $_[1], "$target.LST");
1977                         }
1978                 }
1979                 
1980                 $dxchan->chat($is_ak1a ? $ak1a_line : $line, $self->{isolate}, $target, $_[1], 
1981                                           $text, @_, $self->{call}, @a[0..2], @b[0..2]);
1982         }
1983 }
1984
1985 sub announce
1986 {
1987         my $self = shift;
1988         my $line = shift;
1989         my $isolate = shift;
1990         my $to = shift;
1991         my $target = shift;
1992         my $text = shift;
1993         my ($filter, $hops);
1994
1995         if ($self->{annfilter}) {
1996                 ($filter, $hops) = $self->{annfilter}->it(@_);
1997                 return unless $filter;
1998         }
1999         send_prot_line($self, $filter, $hops, $isolate, $line) unless $_[1] eq $main::mycall;
2000 }
2001
2002 sub chat
2003 {
2004         goto &announce;
2005 }
2006
2007
2008 sub send_local_config
2009 {
2010         my $self = shift;
2011         my $node;
2012         my @nodes;
2013         my @localnodes;
2014         my @remotenodes;
2015
2016         dbg('DXProt::send_local_config') if isdbg('trace');
2017         
2018         # send our nodes
2019         if ($self->{isolate}) {
2020                 @localnodes = ( $main::routeroot );
2021                 $self->send_route($main::mycall, \&pc19, 1, $main::routeroot);
2022         } else {
2023                 # create a list of all the nodes that are not connected to this connection
2024                 # and are not themselves isolated, this to make sure that isolated nodes
2025         # don't appear outside of this node
2026
2027                 # send locally connected nodes
2028                 my @dxchan = grep { $_->call ne $main::mycall && $_ != $self && !$_->{isolate} } DXChannel::get_all_nodes();
2029                 @localnodes = map { my $r = Route::Node::get($_->{call}); $r ? $r : () } @dxchan if @dxchan;
2030                 $self->send_route($main::mycall, \&pc19, scalar(@localnodes)+1, $main::routeroot, @localnodes);
2031
2032                 my $node;
2033                 my @rawintcalls = map { $_->nodes } @localnodes if @localnodes;
2034                 my @intcalls;
2035                 for $node (@rawintcalls) {
2036                         push @intcalls, $node unless grep $node eq $_, @intcalls; 
2037                 }
2038                 my $ref = Route::Node::get($self->{call});
2039                 my @rnodes = $ref->nodes;
2040                 for $node (@intcalls) {
2041                         push @remotenodes, Route::Node::get($node) unless grep $node eq $_, @rnodes, @remotenodes;
2042                 }
2043                 $self->send_route($main::mycall, \&pc19, scalar(@remotenodes), @remotenodes);
2044         }
2045         
2046         # get all the users connected on the above nodes and send them out
2047         foreach $node ($main::routeroot, @localnodes, @remotenodes) {
2048                 if ($node) {
2049                         my @rout = map {my $r = Route::User::get($_); $r ? ($r) : ()} $node->users;
2050                         $self->send_route($main::mycall, \&pc16, 1, $node, @rout) if @rout && $self->user->wantsendpc16;
2051                 } else {
2052                         dbg("sent a null value") if isdbg('chanerr');
2053                 }
2054         }
2055 }
2056
2057 #
2058 # route a message down an appropriate interface for a callsign
2059 #
2060 # is called route(to, pcline);
2061 #
2062
2063 sub route
2064 {
2065         my ($self, $call, $line) = @_;
2066
2067         if (ref $self && $call eq $self->{call}) {
2068                 dbg("PCPROT: Trying to route back to source, dropped") if isdbg('chanerr');
2069                 return;
2070         }
2071
2072         # always send it down the local interface if available
2073         my $dxchan = DXChannel->get($call);
2074         if ($dxchan) {
2075                 dbg("route: $call -> $dxchan->{call} direct" ) if isdbg('route');
2076         } else {
2077                 my $cl = Route::get($call);
2078                 $dxchan = $cl->dxchan if $cl;
2079                 if (ref $dxchan) {
2080                         if (ref $self && $dxchan eq $self) {
2081                                 dbg("PCPROT: Trying to route back to source, dropped") if isdbg('chanerr');
2082                                 return;
2083                         }
2084                         dbg("route: $call -> $dxchan->{call} using normal route" ) if isdbg('route');
2085                 }
2086         }
2087
2088         # try the backstop method
2089         unless ($dxchan) {
2090                 my $rcall = RouteDB::get($call);
2091                 if ($rcall) {
2092                         if ($self && $rcall eq $self->{call}) {
2093                                 dbg("PCPROT: Trying to route back to source, dropped") if isdbg('chanerr');
2094                                 return;
2095                         }
2096                         $dxchan = DXChannel->get($rcall);
2097                         dbg("route: $call -> $rcall using RouteDB" ) if isdbg('route') && $dxchan;
2098                 }
2099         }
2100
2101         if ($dxchan) {
2102                 my $routeit = adjust_hops($dxchan, $line);   # adjust its hop count by node name
2103                 if ($routeit) {
2104                         $dxchan->send($routeit) unless $dxchan == $main::me;
2105                 }
2106         } else {
2107                 dbg("PCPROT: No route available, dropped") if isdbg('chanerr');
2108         }
2109 }
2110
2111 #
2112 # obtain the hops from the list for this callsign and pc no 
2113 #
2114
2115 sub get_hops
2116 {
2117         my $pcno = shift;
2118         my $hops = $DXProt::hopcount{$pcno};
2119         $hops = $DXProt::def_hopcount if !$hops;
2120         return "H$hops";       
2121 }
2122
2123
2124 # adjust the hop count on a per node basis using the user loadable 
2125 # hop table if available or else decrement an existing one
2126 #
2127
2128 sub adjust_hops
2129 {
2130         my $self = shift;
2131         my $s = shift;
2132         my $call = $self->{call};
2133         my $hops;
2134         
2135         if (($hops) = $s =~ /\^H(\d+)\^~?$/o) {
2136                 my ($pcno) = $s =~ /^PC(\d\d)/o;
2137                 confess "$call called adjust_hops with '$s'" unless $pcno;
2138                 my $ref = $nodehops{$call} if %nodehops;
2139                 if ($ref) {
2140                         my $newhops = $ref->{$pcno};
2141                         return "" if defined $newhops && $newhops == 0;
2142                         $newhops = $ref->{default} unless $newhops;
2143                         return "" if defined $newhops && $newhops == 0;
2144                         $newhops = $hops if !$newhops;
2145                         $s =~ s/\^H(\d+)(\^~?)$/\^H$newhops$2/ if $newhops;
2146                 } else {
2147                         # simply decrement it
2148                         $hops--;
2149                         return "" if !$hops;
2150                         $s =~ s/\^H(\d+)(\^~?)$/\^H$hops$2/ if $hops;
2151                 }
2152         }
2153         return $s;
2154 }
2155
2156
2157 # load hop tables
2158 #
2159 sub load_hops
2160 {
2161         my $self = shift;
2162         return $self->msg('lh1') unless -e "$main::data/hop_table.pl";
2163         do "$main::data/hop_table.pl";
2164         return $@ if $@;
2165         return ();
2166 }
2167
2168
2169 # add a ping request to the ping queues
2170 sub addping
2171 {
2172         my ($from, $to, $via) = @_;
2173         my $ref = $pings{$to} || [];
2174         my $r = {};
2175         $r->{call} = $from;
2176         $r->{t} = [ gettimeofday ];
2177         if ($via && (my $dxchan = DXChannel->get($via))) {
2178                 $dxchan->send(pc51($to, $main::mycall, 1));
2179         } else {
2180                 route(undef, $to, pc51($to, $main::mycall, 1));
2181         }
2182         push @$ref, $r;
2183         $pings{$to} = $ref;
2184         my $u = DXUser->get_current($to);
2185         if ($u) {
2186                 $u->lastping(($via || $from), $main::systime);
2187                 $u->put;
2188         }
2189 }
2190
2191 sub process_rcmd
2192 {
2193         my ($self, $tonode, $fromnode, $user, $cmd) = @_;
2194         if ($tonode eq $main::mycall) {
2195                 my $ref = DXUser->get_current($fromnode);
2196                 my $cref = Route::Node::get($fromnode);
2197                 Log('rcmd', 'in', $ref->{priv}, $fromnode, $cmd);
2198                 if ($cmd !~ /^\s*rcmd/i && $cref && $ref && $cref->call eq $ref->homenode) { # not allowed to relay RCMDS!
2199                         if ($ref->{priv}) {             # you have to have SOME privilege, the commands have further filtering
2200                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
2201                                 my $oldpriv = $self->{priv};
2202                                 $self->{priv} = $ref->{priv}; # assume the user's privilege level
2203                                 my @in = (DXCommandmode::run_cmd($self, $cmd));
2204                                 $self->{priv} = $oldpriv;
2205                                 $self->send_rcmd_reply($main::mycall, $fromnode, $user, @in);
2206                                 delete $self->{remotecmd};
2207                         } else {
2208                                 $self->send_rcmd_reply($main::mycall, $fromnode, $user, "sorry...!");
2209                         }
2210                 } else {
2211                         $self->send_rcmd_reply($main::mycall, $fromnode, $user, "your attempt is logged, Tut tut tut...!");
2212                 }
2213         } else {
2214                 my $ref = DXUser->get_current($tonode);
2215                 if ($ref && $ref->is_clx) {
2216                         $self->route($tonode, pc84($fromnode, $tonode, $user, $cmd));
2217                 } else {
2218                         $self->route($tonode, pc34($fromnode, $tonode, $cmd));
2219                 }
2220         }
2221 }
2222
2223 sub process_rcmd_reply
2224 {
2225         my ($self, $tonode, $fromnode, $user, $line) = @_;
2226         if ($tonode eq $main::mycall) {
2227                 my $s = $rcmds{$fromnode};
2228                 if ($s) {
2229                         my $dxchan = DXChannel->get($s->{call});
2230                         my $ref = $user eq $tonode ? $dxchan : (DXChannel->get($user) || $dxchan);
2231                         $ref->send($line) if $ref;
2232                         delete $rcmds{$fromnode} if !$dxchan;
2233                 } else {
2234                         # send unsolicited ones to the sysop
2235                         my $dxchan = DXChannel->get($main::myalias);
2236                         $dxchan->send($line) if $dxchan;
2237                 }
2238         } else {
2239                 my $ref = DXUser->get_current($tonode);
2240                 if ($ref && $ref->is_clx) {
2241                         $self->route($tonode, pc85($fromnode, $tonode, $user, $line));
2242                 } else {
2243                         $self->route($tonode, pc35($fromnode, $tonode, $line));
2244                 }
2245         }
2246 }
2247
2248 sub send_rcmd_reply
2249 {
2250         my $self = shift;
2251         my $tonode = shift;
2252         my $fromnode = shift;
2253         my $user = shift;
2254         while (@_) {
2255                 my $line = shift;
2256                 $line =~ s/\s*$//;
2257                 Log('rcmd', 'out', $fromnode, $line);
2258                 if ($self->is_clx) {
2259                         $self->send(pc85($main::mycall, $fromnode, $user, "$main::mycall:$line"));
2260                 } else {
2261                         $self->send(pc35($main::mycall, $fromnode, "$main::mycall:$line"));
2262                 }
2263         }
2264 }
2265
2266 # add a rcmd request to the rcmd queues
2267 sub addrcmd
2268 {
2269         my ($self, $to, $cmd) = @_;
2270
2271         my $r = {};
2272         $r->{call} = $self->{call};
2273         $r->{t} = $main::systime;
2274         $r->{cmd} = $cmd;
2275         $rcmds{$to} = $r;
2276         
2277         my $ref = Route::Node::get($to);
2278         my $dxchan = $ref->dxchan;
2279         if ($dxchan && $dxchan->is_clx) {
2280                 route(undef, $to, pc84($main::mycall, $to, $self->{call}, $cmd));
2281         } else {
2282                 route(undef, $to, pc34($main::mycall, $to, $cmd));
2283         }
2284 }
2285
2286 sub disconnect
2287 {
2288         my $self = shift;
2289         my $pc39flag = shift;
2290         my $call = $self->call;
2291
2292         return if $self->{disconnecting}++;
2293         
2294         unless ($pc39flag && $pc39flag == 1) {
2295                 $self->send_now("D", DXProt::pc39($main::mycall, $self->msg('disc1', "System Op")));
2296         }
2297
2298         # get rid of any PC16/17/19
2299         eph_del_regex("^PC1[679]*$call");
2300
2301         # do routing stuff, remove me from routing table
2302         my $node = Route::Node::get($call);
2303         my @rout;
2304         if ($node) {
2305                 @rout = $node->del($main::routeroot);
2306                 
2307                 # and all my ephemera as well
2308                 for (@rout) {
2309                         my $c = $_->call;
2310                         eph_del_regex("^PC1[679].*$c");
2311                 }
2312         }
2313
2314         RouteDB::delete_interface($call);
2315         
2316         # remove them from the pc19list as well
2317         while (my ($k,$v) = each %pc19list) {
2318                 my @l = grep {$_->[0] ne $call} @{$pc19list{$k}};
2319                 if (@l) {
2320                         $pc19list{$k} = \@l;
2321                 } else {
2322                         delete $pc19list{$k};
2323                 }
2324                 
2325                 # and the ephemera
2326                 eph_del_regex("^PC1[679].*$k");
2327         }
2328
2329         # unbusy and stop and outgoing mail
2330         my $mref = DXMsg::get_busy($call);
2331         $mref->stop_msg($call) if $mref;
2332         
2333         # broadcast to all other nodes that all the nodes connected to via me are gone
2334         unless ($pc39flag && $pc39flag == 2) {
2335                 $self->route_pc21($main::mycall, undef, @rout) if @rout;
2336         }
2337
2338         # remove outstanding pings
2339         delete $pings{$call};
2340         
2341         # I was the last node visited
2342     $self->user->node($main::mycall);
2343
2344         # send info to all logged in thingies
2345         $self->tell_login('logoutn');
2346
2347         Log('DXProt', $call . " Disconnected");
2348
2349         $self->SUPER::disconnect;
2350 }
2351
2352
2353
2354 # send a talk message to this thingy
2355 #
2356 sub talk
2357 {
2358         my ($self, $from, $to, $via, $line, $origin) = @_;
2359         
2360         $line =~ s/\^/\\5E/g;                   # remove any ^ characters
2361         $self->send(DXProt::pc10($from, $to, $via, $line, $origin));
2362         Log('talk', $to, $from, $via?$via:$self->call, $line) unless $origin && $origin ne $main::mycall;
2363 }
2364
2365 # send it if it isn't the except list and isn't isolated and still has a hop count
2366 # taking into account filtering and so on
2367
2368 sub send_route
2369 {
2370         my $self = shift;
2371         my $origin = shift;
2372         my $generate = shift;
2373         my $no = shift;     # the no of things to filter on 
2374         my $routeit;
2375         my ($filter, $hops);
2376         my @rin;
2377         
2378         for (; @_ && $no; $no--) {
2379                 my $r = shift;
2380                 
2381                 if (!$self->{isolate} && $self->{routefilter}) {
2382                         $filter = undef;
2383                         if ($r) {
2384                                 ($filter, $hops) = $self->{routefilter}->it($self->{call}, $self->{dxcc}, $self->{itu}, $self->{cq}, $r->call, $r->dxcc, $r->itu, $r->cq, $self->{state}, $r->{state});
2385                                 if ($filter) {
2386                                         push @rin, $r;
2387                                 } else {
2388                                         dbg("DXPROT: $self->{call}/" . $r->call . " rejected by output filter") if isdbg('chanerr');
2389                                 }
2390                         } else {
2391                                 dbg("was sent a null value") if isdbg('chanerr');
2392                         }
2393                 } else {
2394                         push @rin, $r unless $self->{isolate} && $r->call ne $main::mycall;
2395                 }
2396         }
2397         if (@rin) {
2398                 foreach my $line (&$generate(@rin, @_)) {
2399                         if ($hops) {
2400                                 $routeit = $line;
2401                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
2402                         } else {
2403                                 $routeit = adjust_hops($self, $line);  # adjust its hop count by node name
2404                                 next unless $routeit;
2405                         }
2406                         
2407                         $self->send($routeit);
2408                 }
2409         }
2410 }
2411
2412 sub broadcast_route
2413 {
2414         my $self = shift;
2415         my $origin = shift;
2416         my $generate = shift;
2417         my $line = shift;
2418         my @dxchan = DXChannel::get_all_nodes();
2419         my $dxchan;
2420         
2421         unless ($self->{isolate}) {
2422                 foreach $dxchan (@dxchan) {
2423                         next if $dxchan == $self;
2424                         next if $dxchan == $main::me;
2425                         next unless $dxchan->isa('DXProt');
2426                         next if ($generate == \&pc16 || $generate==\&pc17) && !$dxchan->user->wantsendpc16;
2427  
2428                         $dxchan->send_route($origin, $generate, @_);
2429                 }
2430         }
2431 }
2432
2433 sub route_pc16
2434 {
2435         my $self = shift;
2436         return unless $self->user->wantpc16;
2437         my $origin = shift;
2438         my $line = shift;
2439         broadcast_route($self, $origin, \&pc16, $line, 1, @_);
2440 }
2441
2442 sub route_pc17
2443 {
2444         my $self = shift;
2445         return unless $self->user->wantpc16;
2446         my $origin = shift;
2447         my $line = shift;
2448         broadcast_route($self, $origin, \&pc17, $line, 1, @_);
2449 }
2450
2451 sub route_pc19
2452 {
2453         my $self = shift;
2454         my $origin = shift;
2455         my $line = shift;
2456         broadcast_route($self, $origin, \&pc19, $line, scalar @_, @_);
2457 }
2458
2459 sub route_pc21
2460 {
2461         my $self = shift;
2462         my $origin = shift;
2463         my $line = shift;
2464         broadcast_route($self, $origin, \&pc21, $line, scalar @_, @_);
2465 }
2466
2467 sub route_pc24
2468 {
2469         my $self = shift;
2470         my $origin = shift;
2471         my $line = shift;
2472         broadcast_route($self, $origin, \&pc24, $line, 1, @_);
2473 }
2474
2475 sub route_pc41
2476 {
2477         my $self = shift;
2478         my $origin = shift;
2479         my $line = shift;
2480         broadcast_route($self, $origin, \&pc41, $line, 1, @_);
2481 }
2482
2483 sub route_pc50
2484 {
2485         my $self = shift;
2486         my $origin = shift;
2487         my $line = shift;
2488         broadcast_route($self, $origin, \&pc50, $line, 1, @_);
2489 }
2490
2491 sub in_filter_route
2492 {
2493         my $self = shift;
2494         my $r = shift;
2495         my ($filter, $hops) = (1, 1);
2496         
2497         if ($self->{inroutefilter}) {
2498                 ($filter, $hops) = $self->{inroutefilter}->it($self->{call}, $self->{dxcc}, $self->{itu}, $self->{cq}, $r->call, $r->dxcc, $r->itu, $r->cq, $self->state, $r->state);
2499                 dbg("PCPROT: $self->{call}/" . $r->call . ' rejected by in_filter_route') if !$filter && isdbg('chanerr');
2500         }
2501         return $filter;
2502 }
2503
2504 sub eph_dup
2505 {
2506         my $s = shift;
2507         my $t = shift || $eph_restime;
2508         my $r;
2509
2510         # chop the end off
2511         $s =~ s/\^H\d\d?\^?\~?$//;
2512         $r = 1 if exists $eph{$s};    # pump up the dup if it keeps circulating
2513         $eph{$s} = $main::systime + $t;
2514         dbg("PCPROT: emphemeral duplicate") if $r && isdbg('chanerr'); 
2515         return $r;
2516 }
2517
2518 sub eph_del_regex
2519 {
2520         my $regex = shift;
2521         my ($key, $val);
2522         while (($key, $val) = each %eph) {
2523                 if ($key =~ m{$regex}) {
2524                         delete $eph{$key};
2525                 }
2526         }
2527 }
2528
2529 sub eph_clean
2530 {
2531         my ($key, $val);
2532         
2533         while (($key, $val) = each %eph) {
2534                 if ($main::systime >= $val) {
2535                         delete $eph{$key};
2536                 }
2537         }
2538 }
2539
2540 sub eph_list
2541 {
2542         my ($key, $val);
2543         my @out;
2544
2545         while (($key, $val) = each %eph) {
2546                 push @out, $key, $val;
2547         }
2548         return @out;
2549 }
2550
2551 sub run_cmd
2552 {
2553         goto &DXCommandmode::run_cmd;
2554 }
2555
2556
2557 # import any msgs in the chat directory
2558 # the messages are sent to the chat group which forms the
2559 # the first part of the name (eg: solar.1243.txt would be
2560 # sent to chat group SOLAR)
2561
2562 # Each message found is sent: one non-blank line to one chat
2563 # message. So 4 lines = 4 chat messages.
2564
2565 # The special name LOCAL is for local users ANN
2566 # The special name ALL is for ANN/FULL
2567 # The special name SYSOP is for ANN/SYSOP
2568 #
2569 sub import_chat
2570 {
2571         # are there any to do in this directory?
2572         return unless -d $chatimportfn;
2573         unless (opendir(DIR, $chatimportfn)) {
2574                 dbg("can\'t open $chatimportfn $!") if isdbg('msg');
2575                 Log('msg', "can\'t open $chatimportfn $!");
2576                 return;
2577         } 
2578
2579         my @names = readdir(DIR);
2580         closedir(DIR);
2581         my $name;
2582         foreach $name (@names) {
2583                 next if $name =~ /^\./;
2584                 my $splitit = $name =~ /^split/;
2585                 my $fn = "$chatimportfn/$name";
2586                 next unless -f $fn;
2587                 unless (open(MSG, $fn)) {
2588                         dbg("can\'t open import file $fn $!") if isdbg('msg');
2589                         Log('msg', "can\'t open import file $fn $!");
2590                         unlink($fn);
2591                         next;
2592                 }
2593                 my @msg = map { s/\r?\n$//; $_ } <MSG>;
2594                 close(MSG);
2595                 unlink($fn);
2596
2597                 my @cat = split /\./, $name;
2598                 my $target = uc $cat[0];
2599
2600                 foreach my $text (@msg) {
2601                         next unless $text && $text !~ /^\s*#/;
2602                         if ($target eq 'ALL' || $target eq 'LOCAL' || $target eq 'SYSOP') {
2603                                 my $sysopflag = $target eq 'SYSOP' ? '*' : ' ';
2604                                 if ($target ne 'LOCAL') {
2605                                         send_announce($main::me, pc12($main::mycall, $text, '*', $sysopflag), $main::mycall, '*', $text, $sysopflag, $main::mycall, '0');
2606                                 } else {
2607                                         Log('ann', 'LOCAL', $main::mycall, $text);
2608                                         DXChannel::broadcast_list("To LOCAL de ${main::mycall}: $text\a", 'ann', undef, DXCommandmode->get_all());
2609                                 }
2610                         } else {
2611                                 my $msgid = nextchatmsgid();
2612                                 $text = "#$msgid $text";
2613                                 send_chat($main::me, pc12($main::mycall, $text, '*', $target), $main::mycall, '*', $text, $target, $main::mycall, '0');
2614                         }
2615                 }
2616         }
2617 }
2618
2619 1;
2620 __END__