1. Crossed fingers, got rid of the instabilities caused by execing programs
[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 Local;
26
27 use Carp;
28
29 use strict;
30 use vars qw($me $pc11_max_age $pc11_dup_age $pc23_dup_age %spotdup %wwvdup $last_hour %pings %rcmds %nodehops);
31
32 $me = undef;                                    # the channel id for this cluster
33 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
34 $pc11_dup_age = 24*3600;                # the maximum time to keep the spot dup list for
35 $pc23_dup_age = 24*3600;                # the maximum time to keep the wwv dup list for
36 %spotdup = ();                              # the pc11 and 26 dup hash 
37 %wwvdup = ();                               # the pc23 and 27 dup hash 
38 $last_hour = time;                              # last time I did an hourly periodic update
39 %pings = ();                    # outstanding ping requests outbound
40 %rcmds = ();                    # outstanding rcmd requests outbound
41 %nodehops = ();                 # node specific hop control
42
43
44 sub init
45 {
46         my $user = DXUser->get($main::mycall);
47         $DXProt::myprot_version += $main::version*100;
48         $me = DXProt->new($main::mycall, 0, $user); 
49         $me->{here} = 1;
50         $me->{state} = "indifferent";
51         do "$main::data/hop_table.pl" if -e "$main::data/hop_table.pl";
52         confess $@ if $@;
53         #  $me->{sort} = 'M';    # M for me
54
55         # now prime the spot duplicates file with today's and yesterday's data
56     my @today = Julian::unixtoj(time);
57         my @spots = Spot::readfile(@today);
58         @today = Julian::sub(@today, 1);
59         push @spots, Spot::readfile(@today);
60         for (@spots) {
61                 my $dupkey = "$_->[0]$_->[1]$_->[2]$_->[3]$_->[4]";
62                 $spotdup{$dupkey} = $_->[2];
63         }
64
65         # now prime the wwv duplicates file with just this month's data
66         my @wwv = Geomag::readfile(time);
67         for (@wwv) {
68                 my $dupkey = "$_->[1].$_->[2]$_->[3]$_->[4]";
69                 $wwvdup{$dupkey} = $_->[1];
70         }
71
72 }
73
74 #
75 # obtain a new connection this is derived from dxchannel
76 #
77
78 sub new 
79 {
80         my $self = DXChannel::alloc(@_);
81         $self->{'sort'} = 'A';          # in absence of how to find out what sort of an object I am
82         return $self;
83 }
84
85 # this is how a pc connection starts (for an incoming connection)
86 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
87 # all the crap that comes between).
88 sub start
89 {
90         my ($self, $line, $sort) = @_;
91         my $call = $self->{call};
92         my $user = $self->{user};
93         
94         # remember type of connection
95         $self->{consort} = $line;
96         $self->{outbound} = $sort eq 'O';
97         $self->{priv} = $user->priv;
98         $self->{lang} = $user->lang;
99         $self->{isolate} = $user->{isolate};
100         $self->{consort} = $line;       # save the connection type
101         $self->{here} = 1;
102         
103         # set unbuffered
104         $self->send_now('B',"0");
105         
106         # send initialisation string
107         if (!$self->{outbound}) {
108                 $self->send(pc38()) if DXNode->get_all();
109                 $self->send(pc18());
110         }
111         $self->state('init');
112         $self->pc50_t(time);
113
114         Log('DXProt', "$call connected");
115 }
116
117 #
118 # This is the normal pcxx despatcher
119 #
120 sub normal
121 {
122         my ($self, $line) = @_;
123         my @field = split /[\^\~]/, $line;
124         
125         # ignore any lines that don't start with PC
126         return if !$field[0] =~ /^PC/;
127         
128         # process PC frames
129         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
130         return unless $pcno;
131         return if $pcno < 10 || $pcno > 51;
132         
133         # local processing 1
134         my $pcr;
135         eval {
136                 $pcr = Local::pcprot($self, $pcno, @field);
137         };
138         dbg('local', "Local::pcprot error $@") if $@;
139         return if $pcr;
140         
141  SWITCH: {
142                 if ($pcno == 10) {              # incoming talk
143                         
144                         # is it for me or one of mine?
145                         my $call = ($field[5] gt ' ') ? $field[5] : $field[2];
146                         if ($call eq $main::mycall || grep $_ eq $call, get_all_user_calls()) {
147                                 
148                                 # yes, it is
149                                 my $text = unpad($field[3]);
150                                 Log('talk', $call, $field[1], $field[6], $text);
151                                 $call = $main::myalias if $call eq $main::mycall;
152                                 my $ref = DXChannel->get($call);
153                                 $ref->send("$call de $field[1]: $text") if $ref;
154                         } else {
155                                 route($field[2], $line); # relay it on its way
156                         }
157                         return;
158                 }
159                 
160                 if ($pcno == 11 || $pcno == 26) { # dx spot
161                         
162                         # if this is a 'nodx' node then ignore it
163                         last SWITCH if grep $field[7] =~ /^$_/,  @DXProt::nodx_node;
164                         
165                         # convert the date to a unix date
166                         my $d = cltounix($field[3], $field[4]);
167                         # bang out (and don't pass on) if date is invalid or the spot is too old
168                         if (!$d || ($pcno == 11 && $d < $main::systime - $pc11_max_age)) {
169                                 dbg('chan', "Spot ignored, invalid date or too old\n");
170                                 return;
171                         }
172
173                         # strip off the leading & trailing spaces from the comment
174                         my $text = unpad($field[5]);
175                         
176                         # store it away
177                         my $spotter = $field[6];
178                         $spotter =~ s/-\d+$//o; # strip off the ssid from the spotter
179                         
180                         # do some de-duping
181                         my $freq = $field[1] - 0;
182                         my $dupkey = "$freq$field[2]$d$text$spotter";
183                         if ($spotdup{$dupkey}) {
184                                 dbg('chan', "Duplicate Spot ignored\n");
185                                 return;
186                         }
187                         
188                         $spotdup{$dupkey} = $d;
189                         
190                         my $spot = Spot::add($freq, $field[2], $d, $text, $spotter, $field[7]);
191                         
192                         # local processing 
193                         my $r;
194                         eval {
195                                 $r = Local::spot1($self, $freq, $field[2], $d, $text, $spotter, $field[7]);
196                         };
197                         dbg('local', "Local::spot1 error $@") if $@;
198                         return if $r;
199
200                         # send orf to the users
201                         if ($spot && $pcno == 11) {
202                                 my $buf = Spot::formatb($field[1], $field[2], $d, $text, $spotter);
203                                 broadcast_users("$buf\a\a");
204                         }
205
206                         # DON'T be silly and send on PC26s!
207                         return if $pcno == 26;
208                         
209                         last SWITCH;
210                 }
211                 
212                 if ($pcno == 12) {              # announces
213                         
214                         if ($field[2] eq '*' || $field[2] eq $main::mycall) {
215                                 
216                                 # strip leading and trailing stuff
217                                 my $text = unpad($field[3]);
218                                 my $target;
219                                 my $to = 'To ';
220                                 my @list;
221                                 
222                                 if ($field[4] eq '*') { # sysops
223                                         $target = "SYSOP";
224                                         @list = map { $_->priv >= 5 ? $_ : () } get_all_users();
225                                 } elsif ($field[4] gt ' ') { # speciality list handling
226                                         my ($name) = split /\./, $field[4]; 
227                                         $target = "$name"; # put the rest in later (if bothered) 
228                                 } 
229                                 
230                                 if ($field[6] eq '1') {
231                                         $target = "WX"; 
232                                         $to = '';
233                                 }
234                                 $target = "All" if !$target;
235                                 
236                                 if (@list > 0) {
237                                         broadcast_list("$to$target de $field[1]: $text", @list);
238                                 } else {
239                                         broadcast_users("$target de $field[1]: $text");
240                                 }
241                                 Log('ann', $target, $field[1], $text);
242                                 
243                                 return if $field[2] eq $main::mycall; # it's routed to me
244                         } else {
245                                 route($field[2], $line);
246                                 return;                 # only on a routed one
247                         }
248                         
249                         last SWITCH;
250                 }
251                 
252                 if ($pcno == 13) {
253                         last SWITCH;
254                 }
255                 if ($pcno == 14) {
256                         last SWITCH;
257                 }
258                 if ($pcno == 15) {
259                         last SWITCH;
260                 }
261                 
262                 if ($pcno == 16) {              # add a user
263                         my $node = DXCluster->get_exact($field[1]); 
264                         last SWITCH if !$node; # ignore if havn't seen a PC19 for this one yet
265                         last SWITCH unless $node->isa('DXNode');
266                         my $i;
267                         
268                         
269                         for ($i = 2; $i < $#field; $i++) {
270                                 my ($call, $confmode, $here) = $field[$i] =~ /^(\S+) (\S) (\d)/o;
271                                 next if length $call < 3 || length $call > 8;
272                                 next if !$confmode;
273                                 $call = uc $call;
274                                 next if DXCluster->get_exact($call); # we already have this (loop?)
275                                 
276                                 $confmode = $confmode eq '*';
277                                 DXNodeuser->new($self, $node, $call, $confmode, $here);
278                                 
279                                 # add this station to the user database, if required
280                                 $call =~ s/-\d+$//o;        # remove ssid for users
281                                 my $user = DXUser->get_current($call);
282                                 $user = DXUser->new($call) if !$user;
283                                 $user->homenode($node->call) if !$user->homenode;
284                                 $user->node($node->call);
285                                 $user->lastin($main::systime);
286                                 $user->put;
287                         }
288                         
289                         # queue up any messages (look for privates only)
290                         DXMsg::queue_msg(1) if $self->state eq 'normal';     
291                         last SWITCH;
292                 }
293                 
294                 if ($pcno == 17) {              # remove a user
295                         
296                         my $ref = DXCluster->get_exact($field[1]);
297                         $ref->del() if $ref;
298                         last SWITCH;
299                 }
300                 
301                 if ($pcno == 18) {              # link request
302                         $self->send_local_config();
303                         $self->send(pc20());
304                         $self->state('init');   
305                         return;             # we don't pass these on
306                 }
307                 
308                 if ($pcno == 19) {              # incoming cluster list
309                         my $i;
310                         for ($i = 1; $i < $#field-1; $i += 4) {
311                                 my $here = $field[$i];
312                                 my $call = uc $field[$i+1];
313                                 my $confmode = $field[$i+2] eq '*';
314                                 my $ver = $field[$i+3];
315                                 
316                                 # now check the call over
317                                 next if DXCluster->get_exact($call); # we already have this
318                                 
319                                 # check for sane parameters
320                                 next if $ver < 5000; # only works with version 5 software
321                                 next if length $call < 3; # min 3 letter callsigns
322                                 DXNode->new($self, $call, $confmode, $here, $ver);
323                                 
324                                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
325                                 my $mref = DXMsg::get_busy($call);
326                                 $mref->stop_msg($self) if $mref;
327                                 
328                                 # add this station to the user database, if required (don't remove SSID from nodes)
329                                 my $user = DXUser->get_current($call);
330                                 if (!$user) {
331                                         $user = DXUser->new($call);
332                                         $user->sort('A');
333                                         $user->priv(1);                   # I have relented and defaulted nodes
334                                         $self->{priv} = 1;                # to user RCMDs allowed
335                                         $user->homenode($call);
336                                         $user->node($call);
337                                 }
338                                 $user->lastin($main::systime);
339                                 $user->put;
340                         }
341                         
342                         # queue up any messages
343                         DXMsg::queue_msg(0) if $self->state eq 'normal';
344                         last SWITCH;
345                 }
346                 
347                 if ($pcno == 20) {              # send local configuration
348                         $self->send_local_config();
349                         $self->send(pc22());
350                         $self->state('normal');
351                         
352                         # queue mail
353                         DXMsg::queue_msg(0);
354                         return;
355                 }
356                 
357                 if ($pcno == 21) {              # delete a cluster from the list
358                         my $call = uc $field[1];
359                         if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
360                                 my $ref = DXCluster->get_exact($call);
361                                 $ref->del() if $ref;
362                         }
363                         last SWITCH;
364                 }
365                 
366                 if ($pcno == 22) {
367                         $self->state('normal');
368                         
369                         # queue mail
370                         DXMsg::queue_msg(0);
371                         return;
372                 }
373                 
374                 if ($pcno == 23 || $pcno == 27) { # WWV info
375                         # do some de-duping
376                         my $d = cltounix($field[1], sprintf("%02d18Z", $field[2]));
377                         my $sfi = unpad($field[3]);
378                         my $k = unpad($field[4]);
379                         my $i = unpad($field[5]);
380                         my $dupkey = "$d.$sfi$k$i";
381                         if ($wwvdup{$dupkey}) {
382                                 dbg('chan', "Dup WWV Spot ignored\n");
383                                 return;
384                         }
385                         
386                         $wwvdup{$dupkey} = $d;
387                         Geomag::update($field[1], $field[2], $sfi, $k, $i, @field[6..$#field]);
388
389                         my $r;
390                         eval {
391                                 $r = Local::wwv2($self, $field[1], $field[2], $sfi, $k, $i, @field[6..$#field]);
392                         };
393                         dbg('local', "Local::wwv2 error $@") if $@;
394                         return if $r;
395
396                         # DON'T be silly and send on PC27s!
397                         return if $pcno == 27;
398                         
399                         last SWITCH;
400                 }
401                 
402                 if ($pcno == 24) {              # set here status
403                         my $call = uc $field[1];
404                         my $ref = DXCluster->get_exact($call);
405                         $ref->here($field[2]) if $ref;
406                         last SWITCH;
407                 }
408                 
409                 if ($pcno == 25) {      # merge request
410                         unless ($field[1] eq $main::mycall) {
411                                 dbg('chan', "merge request to $field[1] from $field[2] ignored");
412                                 return;
413                         }
414
415                         Log('DXProt', "Merge request for $field[3] spots and $field[4] WWV from $field[1]");
416                         
417                         # spots
418                         if ($field[3] > 0) {
419                                 my @in = reverse Spot::search(1, undef, undef, 0, $field[3]-1);
420                                 my $in;
421                                 foreach $in (@in) {
422                                         $self->send(pc26(@{$in}[0..4], $in->[7]));
423                                 }
424                         }
425
426                         # wwv
427                         if ($field[4] > 0) {
428                                 my @in = reverse Geomag::search(0, $field[4], time, 1);
429                                 my $in;
430                                 foreach $in (@in) {
431                                         $self->send(pc27(@{$in}));
432                                 }
433                         }
434                         return;
435                 }
436                 
437                 if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42 || $pcno == 49) { # mail/file handling
438                         DXMsg::process($self, $line);
439                         return;
440                 }
441                 
442                 if ($pcno == 34 || $pcno == 36) { # remote commands (incoming)
443                         if ($field[1] eq $main::mycall) {
444                                 my $ref = DXUser->get_current($field[2]);
445                                 Log('rcmd', 'in', $ref->{priv}, $field[2], $field[3]);
446                                 unless ($field[3] =~ /rcmd/i) {    # not allowed to relay RCMDS!
447                                         if ($ref->{priv}) {     # you have to have SOME privilege, the commands have further filtering
448                                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
449                                                 my @in = (DXCommandmode::run_cmd($self, $field[3]));
450                                                 for (@in) {
451                                                         s/\s*$//og;
452                                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:$_"));
453                                                         Log('rcmd', 'out', $field[2], $_);
454                                                 }
455                                                 delete $self->{remotecmd};
456                                         }
457                                 } else {
458                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:Tut tut tut...!"));
459                                 }
460                         } else {
461                                 route($field[1], $line);
462                         }
463                         return;
464                 }
465                 
466                 if ($pcno == 35) {              # remote command replies
467                         if ($field[1] eq $main::mycall) {
468                                 my $s = $rcmds{$field[2]};
469                                 if ($s) {
470                                         my $dxchan = DXChannel->get($s->{call});
471                                         $dxchan->send($field[3]) if $dxchan;
472                                         delete $rcmds{$field[2]} if !$dxchan;
473                                 }
474                         } else {
475                                 route($field[1], $line);
476                         }
477                         return;
478                 }
479                 
480                 if ($pcno == 37) {
481                         last SWITCH;
482                 }
483                 
484                 if ($pcno == 38) {              # node connected list from neighbour
485                         return;
486                 }
487                 
488                 if ($pcno == 39) {              # incoming disconnect
489                         $self->disconnect();
490                         return;
491                 }
492                 
493                 if ($pcno == 41) {              # user info
494                         # add this station to the user database, if required
495                         my $user = DXUser->get_current($field[1]);
496                         if (!$user) {
497                                 # then try without an SSID
498                                 $field[1] =~ s/-\d+$//o;
499                                 $user = DXUser->get_current($field[1]);
500                         }
501                         $user = DXUser->new($field[1]) if !$user;
502                         
503                         if ($field[2] == 1) {
504                                 $user->name($field[3]);
505                         } elsif ($field[2] == 2) {
506                                 $user->qth($field[3]);
507                         } elsif ($field[2] == 3) {
508                                 my ($lat, $long) = DXBearing::stoll($field[3]);
509                                 $user->lat($lat);
510                                 $user->long($long);
511                         } elsif ($field[2] == 4) {
512                                 $user->homenode($field[3]);
513                         }
514                         $user->put;
515                         last SWITCH;
516                 }
517                 if ($pcno == 43) {
518                         last SWITCH;
519                 }
520                 if ($pcno == 44) {
521                         last SWITCH;
522                 }
523                 if ($pcno == 45) {
524                         last SWITCH;
525                 }
526                 if ($pcno == 46) {
527                         last SWITCH;
528                 }
529                 if ($pcno == 47) {
530                         last SWITCH;
531                 }
532                 if ($pcno == 48) {
533                         last SWITCH;
534                 }
535                 
536                 if ($pcno == 50) {              # keep alive/user list
537                         my $ref = DXCluster->get_exact($field[1]);
538                         $ref->update_users($field[2]) if $ref;
539                         last SWITCH;
540                 }
541                 
542                 if ($pcno == 51) {              # incoming ping requests/answers
543                         
544                         # is it for us?
545                         if ($field[1] eq $main::mycall) {
546                                 my $flag = $field[3];
547                                 if ($flag == 1) {
548                                         $self->send(pc51($field[2], $field[1], '0'));
549                                 } else {
550                                         # it's a reply, look in the ping list for this one
551                                         my $ref = $pings{$field[2]};
552                                         if ($ref) {
553                                                 my $r = shift @$ref;
554                                                 my $dxchan = DXChannel->get($r->{call});
555                                                 $dxchan->send($dxchan->msg('pingi', $field[2], atime($main::systime), $main::systime - $r->{t})) if $dxchan;
556                                         }
557                                 }
558                                 
559                         } else {
560                                 # route down an appropriate thingy
561                                 route($field[1], $line);
562                         }
563                         return;
564                 }
565         }
566          
567          # if get here then rebroadcast the thing with its Hop count decremented (if
568          # there is one). If it has a hop count and it decrements to zero then don't
569          # rebroadcast it.
570          #
571          # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
572          #        REBROADCAST!!!!
573          #
574          
575         if (!$self->{isolate}) {
576                 broadcast_ak1a($line, $self); # send it to everyone but me
577         }
578 }
579
580 #
581 # This is called from inside the main cluster processing loop and is used
582 # for despatching commands that are doing some long processing job
583 #
584 sub process
585 {
586         my $t = time;
587         my @dxchan = DXChannel->get_all();
588         my $dxchan;
589         
590         foreach $dxchan (@dxchan) {
591                 next unless $dxchan->is_ak1a();
592                 next if $dxchan == $me;
593                 
594                 # send a pc50 out on this channel
595                 if ($t >= $dxchan->pc50_t + $DXProt::pc50_interval) {
596                         $dxchan->send(pc50());
597                         $dxchan->pc50_t($t);
598                 }
599         }
600         
601         my $key;
602         my $val;
603         my $cutoff;
604         if ($main::systime - 3600 > $last_hour) {
605                 $cutoff  = $main::systime - $pc11_dup_age;
606                 while (($key, $val) = each %spotdup) {
607                         delete $spotdup{$key} if $val < $cutoff;
608                 }
609                 $cutoff = $main::systime - $pc23_dup_age;
610                 while (($key, $val) = each %wwvdup) {
611                         delete $wwvdup{$key} if $val < $cutoff;
612                 }
613                 $last_hour = $main::systime;
614         }
615 }
616
617 #
618 # finish up a pc context
619 #
620 sub finish
621 {
622         my $self = shift;
623         my $call = $self->call;
624         my $ref = DXCluster->get_exact($call);
625         
626         # unbusy and stop and outgoing mail
627         my $mref = DXMsg::get_busy($call);
628         $mref->stop_msg($self) if $mref;
629         
630         # broadcast to all other nodes that all the nodes connected to via me are gone
631         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
632         my $node;
633         
634         foreach $node (@gonenodes) {
635                 next if $node->call eq $call;
636                 broadcast_ak1a(pc21($node->call, 'Gone') , $self) unless $self->{isolate}; 
637                 $node->del();
638         }
639
640         # remove outstanding pings
641         delete $pings{$call};
642         
643         # now broadcast to all other ak1a nodes that I have gone
644         broadcast_ak1a(pc21($call, 'Gone.'), $self);
645         
646         Log('DXProt', $call . " Disconnected");
647         $ref->del() if $ref;
648 }
649
650 #
651 # some active measures
652 #
653
654 sub send_local_config
655 {
656         my $self = shift;
657         my $n;
658         my @nodes;
659         
660         # send our nodes
661         if ($self->{isolate}) {
662                 @nodes = (DXCluster->get_exact($main::mycall));
663         } else {
664                 # create a list of all the nodes that are not connected to this connection
665                 @nodes = DXNode::get_all();
666                 @nodes = grep { $_->dxchan != $self } @nodes;
667         }
668
669         my @s = $me->pc19(@nodes);
670         for (@s) {
671                 my $routeit = adjust_hops($self, $_);
672                 $self->send($routeit) if $routeit;
673         }
674         
675         # get all the users connected on the above nodes and send them out
676         foreach $n (@nodes) {
677                 my @users = values %{$n->list};
678                 my @s = pc16($n, @users);
679                 for (@s) {
680                         my $routeit = adjust_hops($self, $_);
681                         $self->send($routeit) if $routeit;
682                 }
683         }
684 }
685
686 #
687 # route a message down an appropriate interface for a callsign
688 #
689 # is called route(to, pcline);
690 #
691 sub route
692 {
693         my ($call, $line) = @_;
694         my $cl = DXCluster->get_exact($call);
695         if ($cl) {
696                 my $hops;
697                 my $dxchan = $cl->{dxchan};
698                 if ($dxchan) {
699                         my $routeit = adjust_hops($dxchan, $line);   # adjust its hop count by node name
700                         if ($routeit) {
701                                 $dxchan->send($routeit) if $dxchan;
702                         }
703                 }
704         }
705 }
706
707 # broadcast a message to all clusters [except those mentioned after buffer]
708 sub broadcast_ak1a
709 {
710         my $s = shift;                          # the line to be rebroadcast
711         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
712         my @dxchan = get_all_ak1a();
713         my $dxchan;
714         
715         # send it if it isn't the except list and isn't isolated and still has a hop count
716         foreach $dxchan (@dxchan) {
717                 next if grep $dxchan == $_, @except;
718                 my $routeit = adjust_hops($dxchan, $s);      # adjust its hop count by node name
719                 $dxchan->send($routeit) unless $dxchan->{isolate} || !$routeit;
720         }
721 }
722
723 # broadcast to all users
724 sub broadcast_users
725 {
726         my $s = shift;                          # the line to be rebroadcast
727         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
728         my @dxchan = get_all_users();
729         my $dxchan;
730         
731         foreach $dxchan (@dxchan) {
732                 next if grep $dxchan == $_, @except;
733                 $s =~ s/\a//og if !$dxchan->{beep};
734                 $dxchan->send($s);              # send it if it isn't the except list or hasn't a passout flag
735         }
736 }
737
738 # broadcast to a list of users
739 sub broadcast_list
740 {
741         my $s = shift;
742         my $dxchan;
743         
744         foreach $dxchan (@_) {
745                 $dxchan->send($s);              # send it 
746         }
747 }
748
749 #
750 # gimme all the ak1a nodes
751 #
752 sub get_all_ak1a
753 {
754         my @list = DXChannel->get_all();
755         my $ref;
756         my @out;
757         foreach $ref (@list) {
758                 push @out, $ref if $ref->is_ak1a;
759         }
760         return @out;
761 }
762
763 # return a list of all users
764 sub get_all_users
765 {
766         my @list = DXChannel->get_all();
767         my $ref;
768         my @out;
769         foreach $ref (@list) {
770                 push @out, $ref if $ref->is_user;
771         }
772         return @out;
773 }
774
775 # return a list of all user callsigns
776 sub get_all_user_calls
777 {
778         my @list = DXChannel->get_all();
779         my $ref;
780         my @out;
781         foreach $ref (@list) {
782                 push @out, $ref->call if $ref->is_user;
783         }
784         return @out;
785 }
786
787 #
788 # obtain the hops from the list for this callsign and pc no 
789 #
790
791 sub get_hops
792 {
793         my ($pcno) = @_;
794         my $hops = $DXProt::hopcount{$pcno};
795         $hops = $DXProt::def_hopcount if !$hops;
796         return "H$hops";       
797 }
798
799
800 # adjust the hop count on a per node basis using the user loadable 
801 # hop table if available or else decrement an existing one
802 #
803
804 sub adjust_hops
805 {
806         my $self = shift;
807         my $s = shift;
808         my $call = $self->{call};
809         my $hops;
810         
811         if (($hops) = $s =~ /\^H(\d+)\^~?$/o) {
812                 my ($pcno) = $s =~ /^PC(\d\d)/o;
813                 confess "$call called adjust_hops with '$s'" unless $pcno;
814                 my $ref = $nodehops{$call} if %nodehops;
815                 if ($ref) {
816                         my $newhops = $ref->{$pcno};
817                         return "" if defined $newhops && $newhops == 0;
818                         $newhops = $ref->{default} unless $newhops;
819                         return "" if defined $newhops && $newhops == 0;
820                         $newhops = $hops if !$newhops;
821                         $s =~ s/\^H(\d+)(\^~?)$/\^H$newhops$2/ if $newhops;
822                 } else {
823                         # simply decrement it
824                         $hops--;
825                         return "" if !$hops;
826                         $s =~ s/\^H(\d+)(\^~?)$/\^H$hops$2/ if $hops;
827                 }
828         }
829         return $s;
830 }
831
832
833 # load hop tables
834 #
835 sub load_hops
836 {
837         my $self = shift;
838         return $self->msg('lh1') unless -e "$main::data/hop_table.pl";
839         do "$main::data/hop_table.pl";
840         return $@ if $@;
841         return 0;
842 }
843
844 # remove leading and trailing spaces from an input string
845 sub unpad
846 {
847         my $s = shift;
848         $s =~ s/^\s+|\s+$//;
849         return $s;
850 }
851
852 # add a ping request to the ping queues
853 sub addping
854 {
855         my ($from, $to) = @_;
856         my $ref = $pings{$to};
857         $ref = $pings{$to} = [] if !$ref;
858         my $r = {};
859         $r->{call} = $from;
860         $r->{t} = $main::systime;
861         route($to, pc51($to, $main::mycall, 1));
862         push @$ref, $r;
863 }
864
865 # add a rcmd request to the rcmd queues
866 sub addrcmd
867 {
868         my ($from, $to, $cmd) = @_;
869         my $r = {};
870         $r->{call} = $from;
871         $r->{t} = $main::systime;
872         $r->{cmd} = $cmd;
873         route($to, pc34($main::mycall, $to, $cmd));
874         $rcmds{$to} = $r;
875 }
876 1;
877 __END__