fix a talk bug for t xxx > yyy
[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 DXCluster;
19 use DXProtVars;
20 use DXCommandmode;
21 use DXLog;
22 use Spot;
23 use DXProtout;
24 use DXDebug;
25 use Filter;
26 use Local;
27 use DXDb;
28 use AnnTalk;
29 use Geomag;
30 use WCY;
31 use Time::HiRes qw(gettimeofday tv_interval);
32 use BadWords;
33
34 use strict;
35 use vars qw($me $pc11_max_age $pc23_max_age
36                         $last_hour %pings %rcmds
37                         %nodehops @baddx $baddxfn 
38                         $allowzero $decode_dk0wcy $send_opernam @checklist);
39
40 $me = undef;                                    # the channel id for this cluster
41 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
42 $pc23_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc23
43
44 $last_hour = time;                              # last time I did an hourly periodic update
45 %pings = ();                    # outstanding ping requests outbound
46 %rcmds = ();                    # outstanding rcmd requests outbound
47 %nodehops = ();                 # node specific hop control
48 @baddx = ();                    # list of illegal spotted callsigns
49
50
51 $baddxfn = "$main::data/baddx.pl";
52
53 @checklist = 
54 (
55  [ qw(c c m bp bc c) ],                 # pc10
56  [ qw(f m d t m c c h) ],               # pc11
57  [ qw(c bc m bp bm p h) ],              # pc12
58  [ qw(c h) ],                                   # 
59  [ qw(c h) ],                                   # 
60  [ qw(c m h) ],                                 # 
61  undef ,                                                # pc16 has to be validated manually
62  [ qw(c c h) ],                                 # pc17
63  [ qw(m n) ],                                   # pc18
64  undef ,                                                # pc19 has to be validated manually
65  undef ,                                                # pc20 no validation
66  [ qw(c m h) ],                                 # pc21
67  undef ,                                                # pc22 no validation
68  [ qw(d n n n n m c c h) ],             # pc23
69  [ qw(c p h) ],                                 # pc24
70  [ qw(c c n n) ],                               # pc25
71  [ qw(f m d t m c c bc) ],              # pc26
72  [ qw(d n n n n m c c bc) ],    # pc27
73  [ qw(c c c c d t p m bp n p bp bc) ], # pc28
74  [ qw(c c n m) ],                               # pc29
75  [ qw(c c n) ],                                 # pc30
76  [ qw(c c n) ],                                 # pc31
77  [ qw(c c n) ],                                 # pc32
78  [ qw(c c n) ],                                 # pc33
79  [ qw(c c m) ],                                 # pc34
80  [ qw(c c m) ],                                 # pc35
81  [ qw(c c m) ],                                 # pc36
82  [ qw(c c n m) ],                               # pc37
83  undef,                                                 # pc38 not interested
84  [ qw(c m) ],                                   # pc39
85  [ qw(c c m p n) ],                             # pc40
86  [ qw(c n m h) ],                               # pc41
87  [ qw(c c n) ],                                 # pc42
88  undef,                                                 # pc43 don't handle it
89  [ qw(c c n m m c) ],                   # pc44
90  [ qw(c c n m) ],                               # pc45
91  [ qw(c c n) ],                                 # pc46
92  undef,                                                 # pc47
93  undef,                                                 # pc48
94  [ qw(c m h) ],                                 # pc49
95  [ qw(c n h) ],                                 # pc50
96  [ qw(c c n) ],                                 # pc51
97  undef,
98  undef,
99  undef,
100  undef,
101  undef,
102  undef,
103  undef,
104  undef,
105  undef,                                                 # pc60
106  undef,
107  undef,
108  undef,
109  undef,
110  undef,
111  undef,
112  undef,
113  undef,
114  undef,
115  undef,                                                 # pc70
116  undef,
117  undef,
118  [ qw(d n n n n n n m m m c c h) ],     # pc73
119  undef,
120  undef,
121  undef,
122  undef,
123  undef,
124  undef,
125  undef,                                                 # pc80
126  undef,
127  undef,
128  undef,
129  [ qw(c c c m) ],                               # pc84
130  [ qw(c c c m) ],                               # pc85
131 );
132
133 # use the entry in the check list to check the field list presented
134 # return OK if line NOT in check list (for now)
135 sub check
136 {
137         my $n = shift;
138         $n -= 10;
139         return 0 if $n < 0 || $n > @checklist; 
140         my $ref = $checklist[$n];
141         return 0 unless ref $ref;
142         
143         my $i;
144         shift;    # not interested in the first field
145         for ($i = 0; $i < @$ref; $i++) {
146                 my ($blank, $act) = $$ref[$i] =~ /^(b?)(\w)$/;
147                 return 0 unless $act;
148                 next if $blank && $_[$i] =~ /^[ \*]$/;
149                 if ($act eq 'c') {
150                         return $i+1 unless is_callsign($_[$i]);
151                 } elsif ($act eq 'm') {
152                         return $i+1 unless is_pctext($_[$i]);
153                 } elsif ($act eq 'p') {
154                         return $i+1 unless is_pcflag($_[$i]);
155                 } elsif ($act eq 'f') {
156                         return $i+1 unless is_freq($_[$i]);
157                 } elsif ($act eq 'n') {
158                         return $i+1 unless $_[$i] =~ /^[\d ]+$/;
159                 } elsif ($act eq 'h') {
160                         return $i+1 unless $_[$i] =~ /^H\d\d?$/;
161                 } elsif ($act eq 'd') {
162                         return $i+1 unless $_[$i] =~ /^\s*\d+-\w\w\w-[12][90]\d\d$/;
163                 } elsif ($act eq 't') {
164                         return $i+1 unless $_[$i] =~ /^[012]\d[012345]\dZ$/;
165                 }
166         }
167         return 0;
168 }
169
170 sub init
171 {
172         my $user = DXUser->get($main::mycall);
173         $DXProt::myprot_version += $main::version*100;
174         $me = DXProt->new($main::mycall, 0, $user); 
175         $me->{here} = 1;
176         $me->{state} = "indifferent";
177         do "$main::data/hop_table.pl" if -e "$main::data/hop_table.pl";
178         confess $@ if $@;
179         $me->{sort} = 'S';    # S for spider
180
181         # now prime the spot and wwv  duplicates file with data
182 #    my @today = Julian::unixtoj(time);
183 #       for (Spot::readfile(@today), Spot::readfile(Julian::sub(@today, 1))) {
184 #               Spot::dup(@{$_}[0..3]);
185 #       }
186 #       for (Geomag::readfile(time)) {
187 #               Geomag::dup(@{$_}[1..5]);
188 #       }
189
190         # load the baddx file
191         do "$baddxfn" if -e "$baddxfn";
192         print "$@\n" if $@;
193 }
194
195 #
196 # obtain a new connection this is derived from dxchannel
197 #
198
199 sub new 
200 {
201         my $self = DXChannel::alloc(@_);
202         return $self;
203 }
204
205 # this is how a pc connection starts (for an incoming connection)
206 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
207 # all the crap that comes between).
208 sub start
209 {
210         my ($self, $line, $sort) = @_;
211         my $call = $self->{call};
212         my $user = $self->{user};
213         
214         # remember type of connection
215         $self->{consort} = $line;
216         $self->{outbound} = $sort eq 'O';
217         $self->{priv} = $user->priv || 1;     # other clusters can always be 'normal' users
218         $self->{lang} = $user->lang || 'en';
219         $self->{isolate} = $user->{isolate};
220         $self->{consort} = $line;       # save the connection type
221         $self->{here} = 1;
222
223         # get the INPUT filters (these only pertain to Clusters)
224         $self->{inspotfilter} = Filter::read_in('spots', $call, 1);
225         $self->{inwwvfilter} = Filter::read_in('wwv', $call, 1);
226         $self->{inwcyfilter} = Filter::read_in('wcy', $call, 1);
227         $self->{inannfilter} = Filter::read_in('ann', $call, 1);
228         
229         # set unbuffered and no echo
230         $self->send_now('B',"0");
231         $self->send_now('E',"0");
232         
233         # ping neighbour node stuff
234         my $ping = $user->pingint;
235         $ping = 5*60 unless defined $ping;
236         $self->{pingint} = $ping;
237         $self->{nopings} = $user->nopings || 2;
238         $self->{pingtime} = [ ];
239         $self->{pingave} = 0;
240
241         # send initialisation string
242         unless ($self->{outbound}) {
243 #               $self->send(pc38()) if DXNode->get_all();
244                 $self->send(pc18());
245                 $self->{lastping} = $main::systime;
246         } else {
247                 # remove from outstanding connects queue
248                 @main::outstanding_connects = grep {$_->{call} ne $call} @main::outstanding_connects;
249                 $self->{lastping} = $main::systime + $self->pingint / 2;
250         }
251         $self->state('init');
252         $self->pc50_t(time);
253
254         # send info to all logged in thingies
255         $self->tell_login('loginn');
256
257         Log('DXProt', "$call connected");
258 }
259
260 #
261 # This is the normal pcxx despatcher
262 #
263 sub normal
264 {
265         my ($self, $line) = @_;
266         my @field = split /\^/, $line;
267         return unless @field;
268         
269         pop @field if $field[-1] eq '~';
270         
271 #       print join(',', @field), "\n";
272                                                 
273         # ignore any lines that don't start with PC
274         return if !$field[0] =~ /^PC/;
275         
276         # process PC frames
277         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
278         return unless $pcno;
279         return if $pcno < 10 || $pcno > 99;
280
281         # check for and dump bad protocol messages
282         my $n = check($pcno, @field);
283         if ($n) {
284                 dbg('chan', "bad field $n, dumped (" . parray($checklist[$pcno-10]) . ")");
285                 return;
286         }
287
288         # local processing 1
289         my $pcr;
290         eval {
291                 $pcr = Local::pcprot($self, $pcno, @field);
292         };
293 #       dbg('local', "Local::pcprot error $@") if $@;
294         return if $pcr;
295         
296  SWITCH: {
297                 if ($pcno == 10) {              # incoming talk
298
299                         # will we allow it at all?
300                         my @bad;
301                         if (@bad = BadWords::check($field[3])) {
302                                 dbg('chan', "Bad words: @bad, dropped" );
303                                 return;
304                         }
305
306                         # is it for me or one of mine?
307                         my ($to, $via, $call, $dxchan);
308                         if ($field[5] gt ' ') {
309                                 $call = $via = $field[2];
310                                 $to = $field[5];
311                         } else {
312                                 $call = $to = $field[2];
313                         }
314                         $dxchan = DXChannel->get($call);
315                         if ($dxchan && $dxchan->is_user) {
316                                 $field[3] =~ s/\%5E/^/g;
317                                 $dxchan->talk($field[1], $to, $via, $field[3]);
318                         } else {
319                                 $self->route($field[2], $line); # relay it on its way
320                         }
321                         return;
322                 }
323                 
324                 if ($pcno == 11 || $pcno == 26) { # dx spot
325
326                         # route 'foreign' pc26s 
327                         if ($pcno == 26) {
328                                 if ($field[7] ne $main::mycall) {
329                                         $self->route($field[7], $line);
330                                         return;
331                                 }
332                         }
333                         
334                         # if this is a 'nodx' node then ignore it
335                         if (grep $field[7] =~ /^$_/,  @DXProt::nodx_node) {
336                                 dbg('chan', "Bad DXNode, dropped");
337                                 return;
338                         }
339                         
340                         # convert the date to a unix date
341                         my $d = cltounix($field[3], $field[4]);
342                         # bang out (and don't pass on) if date is invalid or the spot is too old (or too young)
343                         if (!$d || ($pcno == 11 && ($d < $main::systime - $pc11_max_age || $d > $main::systime + 900))) {
344                                 dbg('chan', "Spot ignored, invalid date or out of range ($field[3] $field[4])\n");
345                                 return;
346                         }
347
348                         # is it 'baddx'
349                         if (grep $field[2] eq $_, @baddx) {
350                                 dbg('chan', "Bad DX spot, ignored");
351                                 return;
352                         }
353                         
354                         # do some de-duping
355                         $field[5] =~ s/^\s+//;      # take any leading blanks off
356                         if (Spot::dup($field[1], $field[2], $d, $field[5])) {
357                                 dbg('chan', "Duplicate Spot ignored\n");
358                                 return;
359                         }
360                         my @bad;
361                         if (@bad = BadWords::check($field[5])) {
362                                 dbg('chan', "Bad words: @bad, dropped" );
363                                 return;
364                         }
365                         
366                         my @spot = Spot::add($field[1], $field[2], $d, $field[5], $field[6], $field[7]);
367
368             #
369                         # @spot at this point contains:-
370             # freq, spotted call, time, text, spotter, spotted cc, spotters cc, orig node
371                         # then  spotted itu, spotted cq, spotters itu, spotters cq
372                         # you should be able to route on any of these
373             #
374                         
375                         # fix up qra locators of known users 
376                         my $user = DXUser->get_current($spot[4]);
377                         if ($user) {
378                                 my $qra = $user->qra;
379                                 unless ($qra && DXBearing::is_qra($qra)) {
380                                         my $lat = $user->lat;
381                                         my $long = $user->long;
382                                         if (defined $lat && defined $long) {
383                                                 $user->qra(DXBearing::lltoqra($lat, $long)); 
384                                                 $user->put;
385                                         }
386                                 }
387
388                                 # send a remote command to a distant cluster if it is visible and there is no
389                                 # qra locator and we havn't done it for a month.
390
391                                 unless ($user->qra) {
392                                         my $node;
393                                         my $to = $user->homenode;
394                                         my $last = $user->lastoper || 0;
395                                         if ($send_opernam && $main::systime > $last + $DXUser::lastoperinterval && $to && ($node = DXCluster->get_exact($to)) ) {
396                                                 my $cmd = "forward/opernam $spot[4]";
397                                                 # send the rcmd but we aren't interested in the replies...
398                                                 if ($node && $node->dxchan && $node->dxchan->is_clx) {
399                                                         route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
400                                                 } else {
401                                                         route(undef, $to, pc34($main::mycall, $to, $cmd));
402                                                 }
403                                                 if ($to ne $field[7]) {
404                                                         $to = $field[7];
405                                                         $node = DXCluster->get_exact($to);
406                                                         if ($node && $node->dxchan && $node->dxchan->is_clx) {
407                                                                 route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
408                                                         } else {
409                                                                 route(undef, $to, pc34($main::mycall, $to, $cmd));
410                                                         }
411                                                 }
412                                                 $user->lastoper($main::systime);
413                                                 $user->put;
414                                         }
415                                 }
416                         }
417                                 
418                         # local processing 
419                         my $r;
420                         eval {
421                                 $r = Local::spot($self, @spot);
422                         };
423 #                       dbg('local', "Local::spot1 error $@") if $@;
424                         return if $r;
425
426                         # DON'T be silly and send on PC26s!
427                         return if $pcno == 26;
428
429                         # send out the filtered spots
430                         send_dx_spot($self, $line, @spot) if @spot;
431                         return;
432                 }
433                 
434                 if ($pcno == 12) {              # announces
435                         # announce duplicate checking
436                         $field[3] =~ s/^\s+//;  # remove leading blanks
437                         if (AnnTalk::dup($field[1], $field[2], $field[3])) {
438                                 dbg('chan', "Duplicate Announce ignored");
439                                 return;
440                         }
441
442                         my @bad;
443                         if (@bad = BadWords::check($field[3])) {
444                                 dbg('chan', "Bad words: @bad, dropped" );
445                                 return;
446                         }
447                         if ($field[2] eq '*' || $field[2] eq $main::mycall) {
448                                 
449                                 # global ann filtering on INPUT
450                                 if ($self->{inannfilter}) {
451                                         my ($filter, $hops) = Filter::it($self->{inannfilter}, @field[1..6], $self->{call} );
452                                         unless ($filter) {
453                                                 dbg('chan', "Rejected by filter");
454                                                 return;
455                                         }
456                                 }
457
458                                 # send it
459                                 $self->send_announce($line, @field[1..6]);
460                         } else {
461                                 $self->route($field[2], $line);
462                         }
463                         
464                         return;
465                 }
466                 
467                 if ($pcno == 13) {
468                         last SWITCH;
469                 }
470                 if ($pcno == 14) {
471                         last SWITCH;
472                 }
473                 if ($pcno == 15) {
474                         last SWITCH;
475                 }
476                 
477                 if ($pcno == 16) {              # add a user
478                         my $node = DXCluster->get_exact($field[1]); 
479                         my $dxchan;
480                         if (!$node && ($dxchan = DXChannel->get($field[1]))) {
481                                 # add it to the node table if it isn't present and it's
482                                 # connected locally
483                                 $node = DXNode->new($dxchan, $field[1], 0, 1, 5400);
484                                 broadcast_ak1a(pc19($dxchan, $node), $dxchan, $self) unless $dxchan->{isolate};
485                                 
486                         }
487                         return unless $node; # ignore if havn't seen a PC19 for this one yet
488                         return unless $node->isa('DXNode');
489                         if ($node->dxchan != $self) {
490                                 dbg('chan', "LOOP: $field[1] came in on wrong channel");
491                                 return;
492                         }
493                         if (($dxchan = DXChannel->get($field[1])) && $dxchan != $self) {
494                                 dbg('chan', "LOOP: $field[1] connected locally");
495                                 return;
496                         }
497                         my $i;
498                                                 
499                         for ($i = 2; $i < $#field; $i++) {
500                                 my ($call, $confmode, $here) = $field[$i] =~ /^(\S+) (\S) (\d)/o;
501                                 next if !$call || length $call < 3 || length $call > 8;
502                                 next if !$confmode;
503                                 $call = uc $call;
504                                 next if DXCluster->get_exact($call); # we already have this (loop?)
505                                 
506                                 $confmode = $confmode eq '*';
507                                 DXNodeuser->new($self, $node, $call, $confmode, $here);
508                                 
509                                 # add this station to the user database, if required
510                                 $call =~ s/-\d+$//o;        # remove ssid for users
511                                 my $user = DXUser->get_current($call);
512                                 $user = DXUser->new($call) if !$user;
513                                 $user->homenode($node->call) if !$user->homenode;
514                                 $user->node($node->call);
515                                 $user->lastin($main::systime) unless DXChannel->get($call);
516                                 $user->put;
517                         }
518                         
519                         # queue up any messages (look for privates only)
520                         DXMsg::queue_msg(1) if $self->state eq 'normal';     
521                         last SWITCH;
522                 }
523                 
524                 if ($pcno == 17) {              # remove a user
525                         my $node = DXCluster->get_exact($field[2]);
526                         my $dxchan;
527                         if (!$node && ($dxchan = DXChannel->get($field[2]))) {
528                                 # add it to the node table if it isn't present and it's
529                                 # connected locally
530                                 $node = DXNode->new($dxchan, $field[2], 0, 1, 5400);
531                                 broadcast_ak1a(pc19($dxchan, $node), $dxchan, $self) unless $dxchan->{isolate};
532                                 return;
533                         }
534                         return unless $node;
535                         return unless $node->isa('DXNode');
536                         if ($node->dxchan != $self) {
537                                 dbg('chan', "LOOP: $field[2] came in on wrong channel");
538                                 return;
539                         }
540                         if (($dxchan = DXChannel->get($field[2])) && $dxchan != $self) {
541                                 dbg('chan', "LOOP: $field[2] connected locally");
542                                 return;
543                         }
544                         my $ref = DXCluster->get_exact($field[1]);
545                         $ref->del() if $ref;
546                         last SWITCH;
547                 }
548                 
549                 if ($pcno == 18) {              # link request
550                         $self->state('init');   
551
552                         # first clear out any nodes on this dxchannel
553                         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
554                         foreach my $node (@gonenodes) {
555                                 next if $node->dxchan == $DXProt::me;
556                                 broadcast_ak1a(pc21($node->call, 'Gone, re-init') , $self) unless $self->{isolate}; 
557                                 $node->del();
558                         }
559                         $self->send_local_config();
560                         $self->send(pc20());
561                         return;             # we don't pass these on
562                 }
563                 
564                 if ($pcno == 19) {              # incoming cluster list
565                         my $i;
566                         my $newline = "PC19^";
567                         for ($i = 1; $i < $#field-1; $i += 4) {
568                                 my $here = $field[$i];
569                                 my $call = uc $field[$i+1];
570                                 my $confmode = $field[$i+2];
571                                 my $ver = $field[$i+3];
572
573                                 $ver = 5400 if !$ver && $allowzero;
574                                 
575                                 # now check the call over
576                                 my $node = DXCluster->get_exact($call);
577                                 if ($node) {
578                                         my $dxchan;
579                                         if (($dxchan = DXChannel->get($call)) && $dxchan != $self) {
580                                                 dbg('chan', "LOOP: $call connected locally");
581                                         }
582                                     if ($node->dxchan != $self) {
583                                                 dbg('chan', "LOOP: $call come in on wrong channel");
584                                                 next;
585                                         }
586                                         dbg('chan', "already have $call");
587                                         next;
588                                 }
589                                 
590                                 # check for sane parameters
591                                 next if $ver < 5000; # only works with version 5 software
592                                 next if length $call < 3; # min 3 letter callsigns
593
594                                 # add it to the nodes table and outgoing line
595                                 $newline .= "$here^$call^$confmode^$ver^";
596                                 DXNode->new($self, $call, $confmode, $here, $ver);
597                                 
598                                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
599                                 my $mref = DXMsg::get_busy($call);
600                                 $mref->stop_msg($call) if $mref;
601                                 
602                                 # add this station to the user database, if required (don't remove SSID from nodes)
603                                 my $user = DXUser->get_current($call);
604                                 if (!$user) {
605                                         $user = DXUser->new($call);
606                                         $user->sort('A');
607                                         $user->priv(1);                   # I have relented and defaulted nodes
608                                         $self->{priv} = 1;                # to user RCMDs allowed
609                                         $user->homenode($call);
610                                         $user->node($call);
611                                 }
612                                 $user->lastin($main::systime) unless DXChannel->get($call);
613                                 $user->put;
614                         }
615                         
616                         return if $newline eq "PC19^";
617
618                         # add hop count 
619                         $newline .=  get_hops(19) . "^";
620                         $line = $newline;
621                         last SWITCH;
622                 }
623                 
624                 if ($pcno == 20) {              # send local configuration
625                         $self->send_local_config();
626                         $self->send(pc22());
627                         $self->state('normal');
628                         return;
629                 }
630                 
631                 if ($pcno == 21) {              # delete a cluster from the list
632                         my $call = uc $field[1];
633                         if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
634                                 my $node = DXCluster->get_exact($call);
635                                 if ($node) {
636                                         if ($call eq $self->{call}) {
637                                                 dbg('chan', "LOOP: Trying to disconnect myself with PC21");
638                                                 return;
639                                         } 
640                                         if ($node->dxchan != $self) {
641                                                 dbg('chan', "LOOP: $call come in on wrong channel");
642                                                 return;
643                                         }
644                                         my $dxchan;
645                                         if ($dxchan = DXChannel->get($call)) {
646                                                 dbg('chan', "LOOP: $call connected locally");
647                                                 return;
648                                         }
649                                         $node->del();
650                                 } else {
651                                         dbg('chan', "$call not in table, dropped");
652                                         return;
653                                 }
654                         }
655                         last SWITCH;
656                 }
657                 
658                 if ($pcno == 22) {
659                         $self->state('normal');
660                         return;
661                 }
662                                 
663                 if ($pcno == 23 || $pcno == 27) { # WWV info
664                         
665                         # route 'foreign' pc27s 
666                         if ($pcno == 27) {
667                                 if ($field[8] ne $main::mycall) {
668                                         $self->route($field[8], $line);
669                                         return;
670                                 }
671                         }
672
673                         # do some de-duping
674                         my $d = cltounix($field[1], sprintf("%02d18Z", $field[2]));
675                         my $sfi = unpad($field[3]);
676                         my $k = unpad($field[4]);
677                         my $i = unpad($field[5]);
678                         my ($r) = $field[6] =~ /R=(\d+)/;
679                         $r = 0 unless $r;
680                         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $field[2] < 0 || $field[2] > 23) {
681                                 dbg('chan', "WWV Date ($field[1] $field[2]) out of range");
682                                 return;
683                         }
684                         if (Geomag::dup($d,$sfi,$k,$i,$field[6])) {
685                                 dbg('chan', "Dup WWV Spot ignored\n");
686                                 return;
687                         }
688                         $field[7] =~ s/-\d+$//o;            # remove spotter's ssid
689                 
690                         my $wwv = Geomag::update($d, $field[2], $sfi, $k, $i, @field[6..8], $r);
691
692                         my $rep;
693                         eval {
694                                 $rep = Local::wwv($self, $field[1], $field[2], $sfi, $k, $i, @field[6..8], $r);
695                         };
696 #                       dbg('local', "Local::wwv2 error $@") if $@;
697                         return if $rep;
698
699                         # DON'T be silly and send on PC27s!
700                         return if $pcno == 27;
701
702                         # broadcast to the eager world
703                         send_wwv_spot($self, $line, $d, $field[2], $sfi, $k, $i, @field[6..8]);
704                         return;
705                 }
706                 
707                 if ($pcno == 24) {              # set here status
708                         my $call = uc $field[1];
709                         my $ref = DXCluster->get_exact($call);
710                         $ref->here($field[2]) if $ref;
711                         last SWITCH;
712                 }
713                 
714                 if ($pcno == 25) {      # merge request
715                         if ($field[1] ne $main::mycall) {
716                                 $self->route($field[1], $line);
717                                 return;
718                         }
719                         if ($field[2] eq $main::mycall) {
720                                 dbg('chan', "Trying to merge to myself, ignored");
721                                 return;
722                         }
723
724                         Log('DXProt', "Merge request for $field[3] spots and $field[4] WWV from $field[1]");
725                         
726                         # spots
727                         if ($field[3] > 0) {
728                                 my @in = reverse Spot::search(1, undef, undef, 0, $field[3]);
729                                 my $in;
730                                 foreach $in (@in) {
731                                         $self->send(pc26(@{$in}[0..4], $field[2]));
732                                 }
733                         }
734
735                         # wwv
736                         if ($field[4] > 0) {
737                                 my @in = reverse Geomag::search(0, $field[4], time, 1);
738                                 my $in;
739                                 foreach $in (@in) {
740                                         $self->send(pc27(@{$in}[0..5], $field[2]));
741                                 }
742                         }
743                         return;
744                 }
745
746                 if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42 || $pcno == 49) { # mail/file handling
747                         if ($pcno == 49 || $field[1] eq $main::mycall) {
748                                 DXMsg::process($self, $line);
749                         } else {
750                                 $self->route($field[1], $line) unless $self->is_clx;
751                         }
752                         return;
753                 }
754                 
755                 if ($pcno == 34 || $pcno == 36) { # remote commands (incoming)
756                         if ($field[1] eq $main::mycall) {
757                                 my $ref = DXUser->get_current($field[2]);
758                                 my $cref = DXCluster->get($field[2]);
759                                 Log('rcmd', 'in', $ref->{priv}, $field[2], $field[3]);
760                                 unless (!$cref || !$ref || $cref->mynode->call ne $ref->homenode) {    # not allowed to relay RCMDS!
761                                         if ($ref->{priv}) {     # you have to have SOME privilege, the commands have further filtering
762                                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
763                                                 my $oldpriv = $self->{priv};
764                                                 $self->{priv} = $ref->{priv};     # assume the user's privilege level
765                                                 my @in = (DXCommandmode::run_cmd($self, $field[3]));
766                                                 $self->{priv} = $oldpriv;
767                                                 for (@in) {
768                                                         s/\s*$//og;
769                                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:$_"));
770                                                         Log('rcmd', 'out', $field[2], $_);
771                                                 }
772                                                 delete $self->{remotecmd};
773                                         } else {
774                                                 $self->send(pc35($main::mycall, $field[2], "$main::mycall:sorry...!"));
775                                         }
776                                 } else {
777                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:your attempt is logged, Tut tut tut...!"));
778                                 }
779                         } else {
780                                 my $ref = DXUser->get_current($field[1]);
781                                 if ($ref && $ref->is_clx) {
782                                         route($field[1], pc84($field[2], $field[1], $field[2], $field[3]));
783                                 } else {
784                                         $self->route($field[1], $line);
785                                 }
786                         }
787                         return;
788                 }
789                 
790                 if ($pcno == 35) {              # remote command replies
791                         if ($field[1] eq $main::mycall) {
792                                 my $s = $rcmds{$field[2]};
793                                 if ($s) {
794                                         my $dxchan = DXChannel->get($s->{call});
795                                         $dxchan->send($field[3]) if $dxchan;
796                                         delete $rcmds{$field[2]} if !$dxchan;
797                                 } else {
798                                         # send unsolicited ones to the sysop
799                                         my $dxchan = DXChannel->get($main::myalias);
800                                         $dxchan->send($field[3]) if $dxchan;
801                                 }
802                         } else {
803                                 my $ref = DXUser->get_current($field[1]);
804                                 if ($ref && $ref->is_clx) {
805                                         route($field[1], pc85($field[2], $field[1], $field[2], $field[3]));
806                                 } else {
807                                         $self->route($field[1], $line);
808                                 }
809                         }
810                         return;
811                 }
812                 
813                 # for pc 37 see 44 onwards
814
815                 if ($pcno == 38) {              # node connected list from neighbour
816                         return;
817                 }
818                 
819                 if ($pcno == 39) {              # incoming disconnect
820                         $self->disconnect(1);
821                         return;
822                 }
823                 
824                 if ($pcno == 41) {              # user info
825                         # add this station to the user database, if required
826                         my $user = DXUser->get_current($field[1]);
827                         if (!$user) {
828                                 # then try without an SSID
829                                 $field[1] =~ s/-\d+$//o;
830                                 $user = DXUser->get_current($field[1]);
831                         }
832                         $user = DXUser->new($field[1]) if !$user;
833                         
834                         if ($field[2] == 1) {
835                                 $user->name($field[3]);
836                         } elsif ($field[2] == 2) {
837                                 $user->qth($field[3]);
838                         } elsif ($field[2] == 3) {
839                                 my ($lat, $long) = DXBearing::stoll($field[3]);
840                                 $user->lat($lat);
841                                 $user->long($long);
842                                 $user->qra(DXBearing::lltoqra($lat, $long)) unless $user->qra && DXBearing::is_qra($user->qra);
843                         } elsif ($field[2] == 4) {
844                                 $user->homenode($field[3]);
845                         }
846                         $user->lastoper($main::systime);   # to cut down on excessive for/opers being generated
847                         $user->put;
848                         last SWITCH;
849                 }
850                 if ($pcno == 43) {
851                         last SWITCH;
852                 }
853                 if ($pcno == 37 || $pcno == 44 || $pcno == 45 || $pcno == 46 || $pcno == 47 || $pcno == 48) {
854                         DXDb::process($self, $line);
855                         return;
856                 }
857                 
858                 if ($pcno == 50) {              # keep alive/user list
859                         my $node = DXCluster->get_exact($field[1]);
860                         if ($node) {
861                                 return unless $node->isa('DXNode');
862                                 return unless $node->dxchan == $self;
863                                 $node->update_users($field[2]);
864                         }
865                         last SWITCH;
866                 }
867                 
868                 if ($pcno == 51) {              # incoming ping requests/answers
869                         
870                         # is it for us?
871                         if ($field[1] eq $main::mycall) {
872                                 my $flag = $field[3];
873                                 if ($flag == 1) {
874                                         $self->send(pc51($field[2], $field[1], '0'));
875                                 } else {
876                                         # it's a reply, look in the ping list for this one
877                                         my $ref = $pings{$field[2]};
878                                         if ($ref) {
879                                                 my $tochan =  DXChannel->get($field[2]);
880                                                 while (@$ref) {
881                                                         my $r = shift @$ref;
882                                                         my $dxchan = DXChannel->get($r->{call});
883                                                         next unless $dxchan;
884                                                         my $t = tv_interval($r->{t}, [ gettimeofday ]);
885                                                         if ($dxchan->is_user) {
886                                                                 my $s = sprintf "%.2f", $t; 
887                                                                 my $ave = sprintf "%.2f", $tochan ? ($tochan->{pingave} || $t) : $t;
888                                                                 $dxchan->send($dxchan->msg('pingi', $field[2], $s, $ave))
889                                                         } elsif ($dxchan->is_node) {
890                                                                 if ($tochan) {
891                                                                         $tochan->{nopings} = 2; # pump up the timer
892                                                                         push @{$tochan->{pingtime}}, $t;
893                                                                         shift @{$tochan->{pingtime}} if @{$tochan->{pingtime}} > 6;
894                                                                         my $st;
895                                                                         for (@{$tochan->{pingtime}}) {
896                                                                                 $st += $_;
897                                                                         }
898                                                                         $tochan->{pingave} = $st / @{$tochan->{pingtime}};
899                                                                 }
900                                                         } 
901                                                 }
902                                         }
903                                 }
904                         } else {
905                                 # route down an appropriate thingy
906                                 $self->route($field[1], $line);
907                         }
908                         return;
909                 }
910
911                 if ($pcno == 75) {              # dunno but route it
912                         if ($field[1] ne $main::mycall) {
913                                 $self->route($field[1], $line);
914                         }
915                         return;
916                 }
917
918                 if ($pcno == 73) {  # WCY broadcasts
919                         
920                         # do some de-duping
921                         my $d = cltounix($field[1], sprintf("%02d18Z", $field[2]));
922                         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $field[2] < 0 || $field[2] > 23) {
923                                 dbg('chan', "WCY Date ($field[1] $field[2]) out of range");
924                                 return;
925                         }
926                         @field = map { unpad($_) } @field;
927                         if (WCY::dup($d,@field[3..7])) {
928                                 dbg('chan', "Dup WCY Spot ignored\n");
929                                 return;
930                         }
931                 
932                         my $wcy = WCY::update($d, @field[2..12]);
933
934                         my $rep;
935                         eval {
936                                 $rep = Local::wwv($self, @field[1..12]);
937                         };
938                         # dbg('local', "Local::wcy error $@") if $@;
939                         return if $rep;
940
941                         # broadcast to the eager world
942                         send_wcy_spot($self, $line, $d, @field[2..12]);
943                         return;
944                 }
945
946                 if ($pcno == 84) { # remote commands (incoming)
947                         if ($field[1] eq $main::mycall) {
948                                 my $ref = DXUser->get_current($field[2]);
949                                 my $cref = DXCluster->get($field[2]);
950                                 Log('rcmd', 'in', $ref->{priv}, $field[2], $field[4]);
951                                 unless ($field[4] =~ /rcmd/i || !$cref || !$ref || $cref->mynode->call ne $ref->homenode) {    # not allowed to relay RCMDS!
952                                         if ($ref->{priv}) {     # you have to have SOME privilege, the commands have further filtering
953                                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
954                                                 my $oldpriv = $self->{priv};
955                                                 $self->{priv} = $ref->{priv};     # assume the user's privilege level
956                                                 my @in = (DXCommandmode::run_cmd($self, $field[4]));
957                                                 $self->{priv} = $oldpriv;
958                                                 for (@in) {
959                                                         s/\s*$//og;
960                                                         $self->send(pc85($main::mycall, $field[2], $field[3], "$main::mycall:$_"));
961                                                         Log('rcmd', 'out', $field[2], $_);
962                                                 }
963                                                 delete $self->{remotecmd};
964                                         } else {
965                                                 $self->send(pc85($main::mycall, $field[2], $field[3], "$main::mycall:sorry...!"));
966                                         }
967                                 } else {
968                                         $self->send(pc85($main::mycall, $field[2], $field[3],"$main::mycall:your attempt is logged, Tut tut tut...!"));
969                                 }
970                         } else {
971                                 my $ref = DXUser->get_current($field[1]);
972                                 if ($ref && $ref->is_clx) {
973                                         $self->route($field[1], $line);
974                                 } else {
975                                         route($field[1], pc34($field[2], $field[1], $field[4]));
976                                 }
977                         }
978                         return;
979                 }
980
981                 if ($pcno == 85) {              # remote command replies
982                         if ($field[1] eq $main::mycall) {
983                                 my $dxchan = DXChannel->get($field[3]);
984                                 if ($dxchan) {
985                                         $dxchan->send($field[4]);
986                                 } else {
987                                         my $s = $rcmds{$field[2]};
988                                         if ($s) {
989                                                 $dxchan = DXChannel->get($s->{call});
990                                                 $dxchan->send($field[4]) if $dxchan;
991                                                 delete $rcmds{$field[2]} if !$dxchan;
992                                         } else {
993                                                 # send unsolicited ones to the sysop
994                                                 my $dxchan = DXChannel->get($main::myalias);
995                                                 $dxchan->send($field[4]) if $dxchan;
996                                         }
997                                 }
998                         } else {
999                                 my $ref = DXUser->get_current($field[1]);
1000                                 if ($ref && $ref->is_clx) {
1001                                         $self->route($field[1], $line);
1002                                 } else {
1003                                         route($field[1], pc35($field[2], $field[1], $field[4]));
1004                                 }
1005                         }
1006                         return;
1007                 }
1008         }
1009          
1010         # if get here then rebroadcast the thing with its Hop count decremented (if
1011         # there is one). If it has a hop count and it decrements to zero then don't
1012         # rebroadcast it.
1013         #
1014         # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
1015         #        REBROADCAST!!!!
1016         #
1017          
1018         unless ($self->{isolate}) {
1019                 broadcast_ak1a($line, $self); # send it to everyone but me
1020         }
1021 }
1022
1023 #
1024 # This is called from inside the main cluster processing loop and is used
1025 # for despatching commands that are doing some long processing job
1026 #
1027 sub process
1028 {
1029         my $t = time;
1030         my @dxchan = DXChannel->get_all();
1031         my $dxchan;
1032         
1033         foreach $dxchan (@dxchan) {
1034                 next unless $dxchan->is_node();
1035                 next if $dxchan == $me;
1036                 
1037                 # send a pc50 out on this channel
1038                 if ($t >= $dxchan->pc50_t + $DXProt::pc50_interval) {
1039                         $dxchan->send(pc50(scalar DXChannel::get_all_users));
1040                         $dxchan->pc50_t($t);
1041                 } 
1042
1043                 # send a ping out on this channel
1044                 if ($dxchan->{pingint} && $t >= $dxchan->{pingint} + $dxchan->{lastping}) {
1045                         if ($dxchan->{nopings} <= 0) {
1046                                 $dxchan->disconnect;
1047                         } else {
1048                                 addping($main::mycall, $dxchan->call);
1049                                 $dxchan->{nopings} -= 1;
1050                                 $dxchan->{lastping} = $t;
1051                         }
1052                 }
1053         }
1054         
1055         my $key;
1056         my $val;
1057         my $cutoff;
1058         if ($main::systime - 3600 > $last_hour) {
1059 #               Spot::process;
1060 #               Geomag::process;
1061 #               AnnTalk::process;
1062                 $last_hour = $main::systime;
1063         }
1064 }
1065
1066 #
1067 # finish up a pc context
1068 #
1069 sub finish
1070 {
1071         my $self = shift;
1072         my $call = $self->call;
1073         my $conn = shift;
1074         my $ref = DXCluster->get_exact($call);
1075         
1076         # unbusy and stop and outgoing mail
1077         my $mref = DXMsg::get_busy($call);
1078         $mref->stop_msg($call) if $mref;
1079         
1080         # broadcast to all other nodes that all the nodes connected to via me are gone
1081         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
1082         my $node;
1083         
1084         foreach $node (@gonenodes) {
1085                 next if $node->call eq $call;
1086                 broadcast_ak1a(pc21($node->call, 'Gone') , $self) unless $self->{isolate}; 
1087                 $node->del();
1088         }
1089
1090         # remove outstanding pings
1091         delete $pings{$call};
1092         
1093         # now broadcast to all other ak1a nodes that I have gone
1094         broadcast_ak1a(pc21($call, 'Gone.'), $self) unless $self->{isolate};
1095
1096         # I was the last node visited
1097     $self->user->node($main::mycall);
1098
1099         # send info to all logged in thingies
1100         $self->tell_login('logoutn');
1101
1102         Log('DXProt', $call . " Disconnected");
1103         $ref->del() if $ref;
1104 }
1105
1106 #
1107 # some active measures
1108 #
1109 sub send_dx_spot
1110 {
1111         my $self = shift;
1112         my $line = shift;
1113         my @dxchan = DXChannel->get_all();
1114         my $dxchan;
1115         
1116         # send it if it isn't the except list and isn't isolated and still has a hop count
1117         # taking into account filtering and so on
1118         foreach $dxchan (@dxchan) {
1119                 my $routeit;
1120                 my ($filter, $hops);
1121
1122                 if ($dxchan->{spotfilter}) {
1123                     ($filter, $hops) = Filter::it($dxchan->{spotfilter}, @_, $self->{call} );
1124                         next unless $filter;
1125                 }
1126                 
1127                 if ($dxchan->is_node) {
1128                         next if $dxchan == $self;
1129                         if ($hops) {
1130                                 $routeit = $line;
1131                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1132                         } else {
1133                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
1134                                 next unless $routeit;
1135                         }
1136                         if ($filter) {
1137                                 $dxchan->send($routeit) if $routeit;
1138                         } else {
1139                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
1140                         }
1141                 } elsif ($dxchan->is_user && $dxchan->{dx}) {
1142                         my $buf = Spot::formatb($dxchan->{user}->wantgrid, $_[0], $_[1], $_[2], $_[3], $_[4]);
1143                         $buf .= "\a\a" if $dxchan->{beep};
1144                         $buf =~ s/\%5E/^/g;
1145                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'talk') {
1146                                 $dxchan->send($buf);
1147                         } else {
1148                                 $dxchan->delay($buf);
1149                         }
1150                 }                                       
1151         }
1152 }
1153
1154 sub send_wwv_spot
1155 {
1156         my $self = shift;
1157         my $line = shift;
1158         my @dxchan = DXChannel->get_all();
1159         my $dxchan;
1160         
1161         # send it if it isn't the except list and isn't isolated and still has a hop count
1162         # taking into account filtering and so on
1163         foreach $dxchan (@dxchan) {
1164                 my $routeit;
1165                 my ($filter, $hops);
1166
1167                 if ($dxchan->{wwvfilter}) {
1168                          ($filter, $hops) = Filter::it($dxchan->{wwvfilter}, @_, $self->{call} );
1169                          next unless $filter;
1170                 }
1171                 if ($dxchan->is_node) {
1172                         next if $dxchan == $self;
1173                         if ($hops) {
1174                                 $routeit = $line;
1175                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1176                         } else {
1177                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
1178                                 next unless $routeit;
1179                         }
1180                         if ($filter) {
1181                                 $dxchan->send($routeit) if $routeit;
1182                         } else {
1183                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
1184                                 
1185                         }
1186                 } elsif ($dxchan->is_user && $dxchan->{wwv}) {
1187                         my $buf = "WWV de $_[6] <$_[1]>:   SFI=$_[2], A=$_[3], K=$_[4], $_[5]";
1188                         $buf .= "\a\a" if $dxchan->{beep};
1189                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'talk') {
1190                                 $dxchan->send($buf);
1191                         } else {
1192                                 $dxchan->delay($buf);
1193                         }
1194                 }                                       
1195         }
1196 }
1197
1198 sub send_wcy_spot
1199 {
1200         my $self = shift;
1201         my $line = shift;
1202         my @dxchan = DXChannel->get_all();
1203         my $dxchan;
1204         
1205         # send it if it isn't the except list and isn't isolated and still has a hop count
1206         # taking into account filtering and so on
1207         foreach $dxchan (@dxchan) {
1208                 my $routeit;
1209                 my ($filter, $hops);
1210
1211                 if ($dxchan->{wcyfilter}) {
1212                          ($filter, $hops) = Filter::it($dxchan->{wcyfilter}, @_, $self->{call} );
1213                          next unless $filter;
1214                 }
1215                 if ($dxchan->is_clx || $dxchan->is_spider) {
1216                         next if $dxchan == $self;
1217                         if ($hops) {
1218                                 $routeit = $line;
1219                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1220                         } else {
1221                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
1222                                 next unless $routeit;
1223                         }
1224                         if ($filter) {
1225                                 $dxchan->send($routeit) if $routeit;
1226                         } else {
1227                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
1228                         }
1229                 } elsif ($dxchan->is_user && $dxchan->{wcy}) {
1230                         my $buf = "WCY de $_[10] <$_[1]> : K=$_[4] expK=$_[5] A=$_[3] R=$_[6] SFI=$_[2] SA=$_[7] GMF=$_[8] Au=$_[9]";
1231                         $buf .= "\a\a" if $dxchan->{beep};
1232                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'talk') {
1233                                 $dxchan->send($buf);
1234                         } else {
1235                                 $dxchan->delay($buf);
1236                         }
1237                 }                                       
1238         }
1239 }
1240
1241 # send an announce
1242 sub send_announce
1243 {
1244         my $self = shift;
1245         my $line = shift;
1246         my @dxchan = DXChannel->get_all();
1247         my $dxchan;
1248         my $text = unpad($_[2]);
1249         my $target;
1250         my $to = 'To ';
1251                                 
1252         if ($_[3] eq '*') {     # sysops
1253                 $target = "SYSOP";
1254         } elsif ($_[3] gt ' ') { # speciality list handling
1255                 my ($name) = split /\./, $_[3]; 
1256                 $target = "$name"; # put the rest in later (if bothered) 
1257         } 
1258         
1259         if ($_[5] eq '1') {
1260                 $target = "WX"; 
1261                 $to = '';
1262         }
1263         $target = "All" if !$target;
1264         
1265         Log('ann', $target, $_[0], $text);
1266
1267         # send it if it isn't the except list and isn't isolated and still has a hop count
1268         # taking into account filtering and so on
1269         foreach $dxchan (@dxchan) {
1270                 my $routeit;
1271                 my ($filter, $hops);
1272
1273                 if ($dxchan->{annfilter}) {
1274                         ($filter, $hops) = Filter::it($dxchan->{annfilter}, @_, $self->{call} );
1275                         next unless $filter;
1276                 } 
1277                 if ($dxchan->is_node && $_[1] ne $main::mycall) {  # i.e not specifically routed to me
1278                         next if $dxchan == $self;
1279                         if ($hops) {
1280                                 $routeit = $line;
1281                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1282                         } else {
1283                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
1284                                 next unless $routeit;
1285                         }
1286                         if ($filter) {
1287                                 $dxchan->send($routeit) if $routeit;
1288                         } else {
1289                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
1290                                 
1291                         }
1292                 } elsif ($dxchan->is_user) {
1293                         unless ($dxchan->{ann}) {
1294                                 next if $_[0] ne $main::myalias && $_[0] ne $main::mycall;
1295                         }
1296                         next if $target eq 'SYSOP' && $dxchan->{priv} < 5;
1297                         my $buf = "$to$target de $_[0]: $text";
1298                         $buf =~ s/\%5E/^/g;
1299                         $buf .= "\a\a" if $dxchan->{beep};
1300                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'talk') {
1301                                 $dxchan->send($buf);
1302                         } else {
1303                                 $dxchan->delay($buf);
1304                         }
1305                 }                                       
1306         }
1307 }
1308
1309 sub send_local_config
1310 {
1311         my $self = shift;
1312         my $n;
1313         my @nodes;
1314         my @localnodes;
1315         my @remotenodes;
1316                 
1317         # send our nodes
1318         if ($self->{isolate}) {
1319                 @localnodes = (DXCluster->get_exact($main::mycall));
1320         } else {
1321                 # create a list of all the nodes that are not connected to this connection
1322                 # and are not themselves isolated, this to make sure that isolated nodes
1323         # don't appear outside of this node
1324                 @nodes = DXNode::get_all();
1325                 @nodes = grep { $_->{call} ne $main::mycall } @nodes;
1326                 @nodes = grep { $_->dxchan != $self } @nodes if @nodes;
1327                 @nodes = grep { !$_->dxchan->{isolate} } @nodes if @nodes;
1328                 @localnodes = grep { $_->dxchan->{call} eq $_->{call} } @nodes if @nodes;
1329                 unshift @localnodes, DXCluster->get_exact($main::mycall);
1330                 @remotenodes = grep { $_->dxchan->{call} ne $_->{call} } @nodes if @nodes;
1331         }
1332
1333         my @s = $me->pc19(@localnodes, @remotenodes);
1334         for (@s) {
1335                 my $routeit = adjust_hops($self, $_);
1336                 $self->send($routeit) if $routeit;
1337         }
1338         
1339         # get all the users connected on the above nodes and send them out
1340         foreach $n (@localnodes, @remotenodes) {
1341                 my @users = values %{$n->list};
1342                 my @s = pc16($n, @users);
1343                 for (@s) {
1344                         my $routeit = adjust_hops($self, $_);
1345                         $self->send($routeit) if $routeit;
1346                 }
1347         }
1348 }
1349
1350 #
1351 # route a message down an appropriate interface for a callsign
1352 #
1353 # is called route(to, pcline);
1354 #
1355 sub route
1356 {
1357         my ($self, $call, $line) = @_;
1358         my $cl = DXCluster->get_exact($call);
1359         if ($cl) {       # don't route it back down itself
1360                 if (ref $self && $call eq $self->{call}) {
1361                         dbg('chan', "Trying to route back to source, dropped");
1362                         return;
1363                 }
1364                 my $hops;
1365                 my $dxchan = $cl->{dxchan};
1366                 if ($dxchan) {
1367                         my $routeit = adjust_hops($dxchan, $line);   # adjust its hop count by node name
1368                         if ($routeit) {
1369                                 $dxchan->send($routeit) if $dxchan;
1370                         }
1371                 }
1372         }
1373 }
1374
1375 # broadcast a message to all clusters taking into account isolation
1376 # [except those mentioned after buffer]
1377 sub broadcast_ak1a
1378 {
1379         my $s = shift;                          # the line to be rebroadcast
1380         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1381         my @dxchan = DXChannel::get_all_nodes();
1382         my $dxchan;
1383         
1384         # send it if it isn't the except list and isn't isolated and still has a hop count
1385         foreach $dxchan (@dxchan) {
1386                 next if grep $dxchan == $_, @except;
1387                 my $routeit = adjust_hops($dxchan, $s);      # adjust its hop count by node name
1388                 $dxchan->send($routeit) unless $dxchan->{isolate} || !$routeit;
1389         }
1390 }
1391
1392 # broadcast a message to all clusters ignoring isolation
1393 # [except those mentioned after buffer]
1394 sub broadcast_all_ak1a
1395 {
1396         my $s = shift;                          # the line to be rebroadcast
1397         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1398         my @dxchan = DXChannel::get_all_nodes();
1399         my $dxchan;
1400         
1401         # send it if it isn't the except list and isn't isolated and still has a hop count
1402         foreach $dxchan (@dxchan) {
1403                 next if grep $dxchan == $_, @except;
1404                 my $routeit = adjust_hops($dxchan, $s);      # adjust its hop count by node name
1405                 $dxchan->send($routeit);
1406         }
1407 }
1408
1409 # broadcast to all users
1410 # storing the spot or whatever until it is in a state to receive it
1411 sub broadcast_users
1412 {
1413         my $s = shift;                          # the line to be rebroadcast
1414         my $sort = shift;           # the type of transmission
1415         my $fref = shift;           # a reference to an object to filter on
1416         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1417         my @dxchan = DXChannel::get_all_users();
1418         my $dxchan;
1419         my @out;
1420         
1421         foreach $dxchan (@dxchan) {
1422                 next if grep $dxchan == $_, @except;
1423                 push @out, $dxchan;
1424         }
1425         broadcast_list($s, $sort, $fref, @out);
1426 }
1427
1428 # broadcast to a list of users
1429 sub broadcast_list
1430 {
1431         my $s = shift;
1432         my $sort = shift;
1433         my $fref = shift;
1434         my $dxchan;
1435         
1436         foreach $dxchan (@_) {
1437                 my $filter = 1;
1438                 
1439                 if ($sort eq 'dx') {
1440                     next unless $dxchan->{dx};
1441                         ($filter) = Filter::it($dxchan->{spotfilter}, @{$fref}) if ref $fref;
1442                         next unless $filter;
1443                 }
1444                 next if $sort eq 'ann' && !$dxchan->{ann};
1445                 next if $sort eq 'wwv' && !$dxchan->{wwv};
1446                 next if $sort eq 'wcy' && !$dxchan->{wcy};
1447                 next if $sort eq 'wx' && !$dxchan->{wx};
1448
1449                 $s =~ s/\a//og unless $dxchan->{beep};
1450
1451                 if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'talk') {
1452                         $dxchan->send($s);      
1453                 } else {
1454                         $dxchan->delay($s);
1455                 }
1456         }
1457 }
1458
1459
1460 #
1461 # obtain the hops from the list for this callsign and pc no 
1462 #
1463
1464 sub get_hops
1465 {
1466         my $pcno = shift;
1467         my $hops = $DXProt::hopcount{$pcno};
1468         $hops = $DXProt::def_hopcount if !$hops;
1469         return "H$hops";       
1470 }
1471
1472
1473 # adjust the hop count on a per node basis using the user loadable 
1474 # hop table if available or else decrement an existing one
1475 #
1476
1477 sub adjust_hops
1478 {
1479         my $self = shift;
1480         my $s = shift;
1481         my $call = $self->{call};
1482         my $hops;
1483         
1484         if (($hops) = $s =~ /\^H(\d+)\^~?$/o) {
1485                 my ($pcno) = $s =~ /^PC(\d\d)/o;
1486                 confess "$call called adjust_hops with '$s'" unless $pcno;
1487                 my $ref = $nodehops{$call} if %nodehops;
1488                 if ($ref) {
1489                         my $newhops = $ref->{$pcno};
1490                         return "" if defined $newhops && $newhops == 0;
1491                         $newhops = $ref->{default} unless $newhops;
1492                         return "" if defined $newhops && $newhops == 0;
1493                         $newhops = $hops if !$newhops;
1494                         $s =~ s/\^H(\d+)(\^~?)$/\^H$newhops$2/ if $newhops;
1495                 } else {
1496                         # simply decrement it
1497                         $hops--;
1498                         return "" if !$hops;
1499                         $s =~ s/\^H(\d+)(\^~?)$/\^H$hops$2/ if $hops;
1500                 }
1501         }
1502         return $s;
1503 }
1504
1505
1506 # load hop tables
1507 #
1508 sub load_hops
1509 {
1510         my $self = shift;
1511         return $self->msg('lh1') unless -e "$main::data/hop_table.pl";
1512         do "$main::data/hop_table.pl";
1513         return $@ if $@;
1514         return 0;
1515 }
1516
1517
1518 # add a ping request to the ping queues
1519 sub addping
1520 {
1521         my ($from, $to) = @_;
1522         my $ref = $pings{$to} || [];
1523         my $r = {};
1524         $r->{call} = $from;
1525         $r->{t} = [ gettimeofday ];
1526         route(undef, $to, pc51($to, $main::mycall, 1));
1527         push @$ref, $r;
1528         $pings{$to} = $ref;
1529 }
1530
1531 # add a rcmd request to the rcmd queues
1532 sub addrcmd
1533 {
1534         my ($self, $to, $cmd) = @_;
1535
1536         my $r = {};
1537         $r->{call} = $self->{call};
1538         $r->{t} = $main::systime;
1539         $r->{cmd} = $cmd;
1540         $rcmds{$to} = $r;
1541
1542         my $ref = DXCluster->get_exact($to);
1543     if ($ref && $ref->dxchan && $ref->dxchan->is_clx) {
1544                 route(undef, $to, pc84($main::mycall, $to, $self->{call}, $cmd));
1545         } else {
1546                 route(undef, $to, pc34($main::mycall, $to, $cmd));
1547         }
1548 }
1549
1550 sub disconnect
1551 {
1552         my $self = shift;
1553         my $nopc39 = shift;
1554
1555         if ($self->{conn} && !$nopc39) {
1556                 $self->send_now("D", DXProt::pc39($main::mycall, $self->msg('disc1', "System Op")));
1557         }
1558
1559         $self->SUPER::disconnect;
1560 }
1561
1562
1563
1564 # send a talk message to this thingy
1565 #
1566 sub talk
1567 {
1568         my ($self, $from, $to, $via, $line) = @_;
1569         
1570         $line =~ s/\^/\\5E/g;                   # remove any ^ characters
1571         $self->send(DXProt::pc10($from, $to, $via, $line));
1572         Log('talk', $self->call, $from, $via?$via:$main::mycall, $line);
1573 }
1574 1;
1575 __END__