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