627356823274c7fe1d5aee98935b3693d7858d3e
[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 Spot;
22 use DXProtout;
23 use Carp;
24
25 use strict;
26 use vars qw($me $pc11_max_age $pc11_dup_age %dup $last_hour);
27
28 $me = undef;                # the channel id for this cluster
29 $pc11_max_age = 1*3600;     # the maximum age for an incoming 'real-time' pc11
30 $pc11_dup_age = 24*3600;    # the maximum time to keep the dup list for
31 %dup = ();                  # the pc11 and 26 dup hash 
32 $last_hour = time;          # last time I did an hourly periodic update
33
34 sub init
35 {
36   my $user = DXUser->get($main::mycall);
37   $me = DXProt->new($main::mycall, undef, $user); 
38 #  $me->{sort} = 'M';    # M for me
39 }
40
41 #
42 # obtain a new connection this is derived from dxchannel
43 #
44
45 sub new 
46 {
47   my $self = DXChannel::alloc(@_);
48   $self->{sort} = 'A';   # in absence of how to find out what sort of an object I am
49   return $self;
50 }
51
52 # this is how a pc connection starts (for an incoming connection)
53 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
54 # all the crap that comes between).
55 sub start
56 {
57   my ($self, $line) = shift;
58   my $call = $self->call;
59   
60   # remember type of connection
61   $self->{consort} = $line;
62
63   # set unbuffered
64   $self->send_now('B',"0");
65   
66   # send initialisation string
67   $self->send(pc38()) if DXNode->get_all();
68   $self->send(pc18());
69   $self->state('init');
70   $self->pc50_t(time);
71 }
72
73 #
74 # This is the normal pcxx despatcher
75 #
76 sub normal
77 {
78   my ($self, $line) = @_;
79   my @field = split /[\^\~]/, $line;
80   
81   # ignore any lines that don't start with PC
82   return if !$field[0] =~ /^PC/;
83
84   # process PC frames
85   my ($pcno) = $field[0] =~ /^PC(\d\d)/;          # just get the number
86   return if $pcno < 10 || $pcno > 51;
87   
88   SWITCH: {
89     if ($pcno == 10) {             # incoming talk
90
91       # is it for me or one of mine?
92           my $call = ($field[5] gt ' ') ? $field[5] : $field[2];
93           if ($call eq $main::mycall || grep $_ eq $call, get_all_user_calls()) {
94             
95                 # yes, it is
96                 my $text = unpad($field[3]);
97                 my $ref = DXChannel->get($call);
98                 $ref->send("$call de $field[1]: $text") if $ref;
99           } else {
100             route($field[2], $line);       # relay it on its way
101           }
102           return;
103         }
104         
105     if ($pcno == 11 || $pcno == 26) {             # dx spot
106
107       # if this is a 'nodx' node then ignore it
108           last SWITCH if grep $field[7] =~ /^$_/,  @DXProt::nodx_node;
109           
110       # convert the date to a unix date
111           my $d = cltounix($field[3], $field[4]);
112           return if !$d || ($pcno == 11 && $d < $main::systime - $pc11_max_age);  # bang out (and don't pass on) if date is invalid or the spot is too old
113           
114           # strip off the leading & trailing spaces from the comment
115           my $text = unpad($field[5]);
116           
117           # store it away
118           my $spotter = $field[6];
119           $spotter =~ s/-\d+$//o;         # strip off the ssid from the spotter
120
121       # do some de-duping
122           my $dupkey = "$field[1]$field[2]$d$text$field[6]";
123           return if $dup{$dupkey};
124           $dup{$dupkey} = $d;
125           
126           my $spot = Spot::add($field[1], $field[2], $d, $text, $spotter);
127           
128           # send orf to the users
129       if ($spot && $pcno == 11) {
130             my $buf = Spot::formatb($field[1], $field[2], $d, $text, $spotter);
131         broadcast_users("$buf\a\a");
132           }
133           
134           last SWITCH;
135         }
136         
137     if ($pcno == 12) {             # announces
138         
139           if ($field[2] eq '*' || $field[2] eq $main::mycall) {
140
141         # strip leading and trailing stuff
142             my $text = unpad($field[3]);
143                 my $target;
144                 my @list;
145                 
146             if ($field[4] eq '*') {          # sysops
147                   $target = "To Sysops";
148                   @list = map { $_->priv >= 5 ? $_ : () } get_all_users();
149                 } elsif ($field[4] gt ' ') {     # speciality list handling
150                   my ($name) = split /\./, $field[4]; 
151           $target = "To $name";          # put the rest in later (if bothered) 
152         } 
153                 
154         $target = "WX" if $field[6] eq '1';
155                 $target = "To All" if !$target;
156                 
157                 if (@list > 0) {
158                   broadcast_list("$target de $field[1]: $text", @list);
159                 } else {
160                   broadcast_users("$target de $field[1]: $text");
161                 }
162                 
163                 return if $field[2] eq $main::mycall;   # it's routed to me
164           } else {
165             route($field[2], $line);
166                 return;                     # only on a routed one
167           }
168           
169           last SWITCH;
170         }
171         
172     if ($pcno == 13) {last SWITCH;}
173     if ($pcno == 14) {last SWITCH;}
174     if ($pcno == 15) {last SWITCH;}
175         
176     if ($pcno == 16) {              # add a user
177           my $node = DXCluster->get($field[1]);
178           last SWITCH if !$node;        # ignore if havn't seen a PC19 for this one yet
179           my $i;
180           
181           for ($i = 2; $i < $#field; $i++) {
182             my ($call, $confmode, $here) = $field[$i] =~ /^(\w+) (-) (\d)/o;
183                 next if length $call < 3;
184                 next if !$confmode;
185                 $call = uc $call;
186         $call =~ s/-\d+$//o;        # remove ssid
187                 next if DXCluster->get($call);    # we already have this (loop?)
188                 
189                 $confmode = $confmode eq '*';
190                 DXNodeuser->new($self, $node, $call, $confmode, $here);
191                 
192                 # add this station to the user database, if required
193                 my $user = DXUser->get_current($call);
194                 $user = DXUser->new($call) if !$user;
195                 $user->node($node->call) if !$user->node;
196                 $user->put;
197           }
198           
199           # queue up any messages (look for privates only)
200           DXMsg::queue_msg(1) if $self->state eq 'normal';     
201           last SWITCH;
202         }
203         
204     if ($pcno == 17) {              # remove a user
205           my $ref = DXCluster->get($field[1]);
206           $ref->del() if $ref;
207           last SWITCH;
208         }
209         
210     if ($pcno == 18) {              # link request
211           $self->send_local_config();
212           $self->send(pc20());
213       $self->state('init');     
214           last SWITCH;
215         }
216         
217     if ($pcno == 19) {               # incoming cluster list
218       my $i;
219           for ($i = 1; $i < $#field-1; $i += 4) {
220             my $here = $field[$i];
221             my $call = uc $field[$i+1];
222                 my $confmode = $field[$i+2] eq '*';
223                 my $ver = $field[$i+3];
224
225                 # now check the call over
226                 next if DXCluster->get($call);   # we already have this
227                 
228                 # check for sane parameters
229                 next if $ver < 5000;             # only works with version 5 software
230                 next if length $call < 3;        # min 3 letter callsigns
231         DXNode->new($self, $call, $confmode, $here, $ver);
232
233         # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
234                 my $mref = DXMsg::get_busy($call);
235                 $mref->stop_msg($self) if $mref;
236           }
237           
238           # queue up any messages
239           DXMsg::queue_msg() if $self->state eq 'normal';     
240           last SWITCH;
241         }
242         
243     if ($pcno == 20) {              # send local configuration
244           $self->send_local_config();
245           $self->send(pc22());
246           $self->state('normal');
247           
248           # queue mail
249           DXMsg::queue_msg();
250           return;
251         }
252         
253     if ($pcno == 21) {             # delete a cluster from the list
254           my $call = uc $field[1];
255           if ($call ne $main::mycall) {              # don't allow malicious buggers to disconnect me!
256             my $ref = DXCluster->get($call);
257             $ref->del() if $ref;
258           }
259           last SWITCH;
260         }
261         
262     if ($pcno == 22) {last SWITCH;}
263
264     if ($pcno == 23 || $pcno == 27) {  # WWV info
265       last SWITCH;
266         }
267
268     if ($pcno == 24) {             # set here status
269           my $call = uc $field[1];
270           $call =~ s/-\d+//o;
271           my $ref = DXCluster->get($call);
272           $ref->here($field[2]) if $ref;
273           last SWITCH;
274         }
275         
276     if ($pcno == 25) {last SWITCH;}
277
278     if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42) {   # mail/file handling
279           DXMsg::process($self, $line);
280           return;
281         }
282         
283     if ($pcno == 34 || $pcno == 36) {   # remote commands (incoming)
284           last SWITCH;
285         }
286         
287     if ($pcno == 35) {                  # remote command replies
288           last SWITCH;
289         }
290         
291     if ($pcno == 37) {last SWITCH;}
292     
293         if ($pcno == 38) {                  # node connected list from neighbour
294           return;
295         }
296
297     if ($pcno == 39) {              # incoming disconnect
298       $self->disconnect();
299           return;
300         }
301         
302     if ($pcno == 41) {              # user info
303       # add this station to the user database, if required
304           my $user = DXUser->get_current($field[1]);
305           $user = DXUser->new($field[1]) if !$user;
306           
307           if ($field[2] == 1) {
308             $user->name($field[3]);
309           } elsif ($field[2] == 2) {
310             $user->qth($field[3]);
311           } elsif ($field[2] == 3) {
312         my ($latd, $latm, $latl, $longd, $longm, $longl) = split /\s+/, $field[3];
313                 $longd += ($longm/60);
314                 $longd = 0-$longd if (uc $longl) eq 'W'; 
315                 $user->long($longd);
316                 $latd += ($latm/60);
317                 $latd = 0-$latd if (uc $latl) eq 'S';
318                 $user->lat($latd);
319           } elsif ($field[2] == 4) {
320             $user->node($field[3]);
321           }
322           $user->put;
323           last SWITCH;
324         }
325     if ($pcno == 43) {last SWITCH;}
326     if ($pcno == 44) {last SWITCH;}
327     if ($pcno == 45) {last SWITCH;}
328     if ($pcno == 46) {last SWITCH;}
329     if ($pcno == 47) {last SWITCH;}
330     if ($pcno == 48) {last SWITCH;}
331     if ($pcno == 49) {last SWITCH;}
332         
333     if ($pcno == 50) {              # keep alive/user list
334           my $ref = DXCluster->get($field[1]);
335           $ref->update_users($field[2]) if $ref;
336           last SWITCH;
337         }
338         
339     if ($pcno == 51) {              # incoming ping requests/answers
340           
341           # is it for us?
342           if ($field[1] eq $main::mycall) {
343             my $flag = $field[3];
344             $flag ^= 1;
345             $self->send($self->pc51($field[2], $field[1], $flag));
346           } else {
347             # route down an appropriate thingy
348                 route($field[1], $line);
349           }
350           return;
351         }
352   }
353   
354   # if get here then rebroadcast the thing with its Hop count decremented (if
355   # there is one). If it has a hop count and it decrements to zero then don't
356   # rebroadcast it.
357   #
358   # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
359   #        REBROADCAST!!!!
360   #
361   
362   my $hops;
363   if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
364         my $newhops = $hops - 1;
365         if ($newhops > 0) {
366           $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;       # change the hop count
367           broadcast_ak1a($line, $self);             # send it to everyone but me
368         }
369   }
370 }
371
372 #
373 # This is called from inside the main cluster processing loop and is used
374 # for despatching commands that are doing some long processing job
375 #
376 sub process
377 {
378   my $t = time;
379   my @chan = DXChannel->get_all();
380   my $chan;
381   
382   foreach $chan (@chan) {
383     next if !$chan->is_ak1a();
384
385     # send a pc50 out on this channel
386     if ($t >= $chan->pc50_t + $DXProt::pc50_interval) {
387       $chan->send(pc50());
388           $chan->pc50_t($t);
389         }
390   }
391   
392   my $key;
393   my $val;
394   my $cutoff;
395   if ($main::systime - 3600 > $last_hour) {
396     $cutoff  = $main::systime - $pc11_dup_age;
397         while (($key, $val) = each %dup) {
398           delete $dup{$key} if $val < $cutoff;
399         }
400         $last_hour = $main::systime;
401   }
402 }
403
404 #
405 # finish up a pc context
406 #
407 sub finish
408 {
409   my $self = shift;
410   my $ref = DXCluster->get($self->call);
411
412   # unbusy and stop and outgoing mail
413   my $mref = DXMsg::get_busy($self->call);
414   $mref->stop_msg($self) if $mref;
415   
416   # broadcast to all other nodes that all the nodes connected to via me are gone
417   my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
418   my $node;
419
420   foreach $node (@gonenodes) {
421     next if $node->call eq $self->call; 
422     broadcast_ak1a(pc21($node->call, 'Gone'), $self);    # done like this 'cos DXNodes don't have a pc21 method
423         $node->del();
424   }
425
426   # now broadcast to all other ak1a nodes that I have gone
427   broadcast_ak1a(pc21($self->call, 'Gone.'), $self);
428   $ref->del() if $ref;
429 }
430
431 #
432 # some active measures
433 #
434
435 sub send_local_config
436 {
437   my $self = shift;
438   my $n;
439
440   # send our nodes
441   my @nodes = DXNode::get_all();
442   
443   # create a list of all the nodes that are not connected to this connection
444   @nodes = map { $_->dxchan != $self ? $_ : () } @nodes;
445   $self->send($me->pc19(@nodes));
446           
447   # get all the users connected on the above nodes and send them out
448   foreach $n (@nodes) {
449     my @users = values %{$n->list};
450     $self->send(DXProt::pc16($n, @users));
451   }
452 }
453
454 #
455 # route a message down an appropriate interface for a callsign
456 #
457 # is called route(to, pcline);
458 #
459 sub route
460 {
461   my ($call, $line) = @_;
462   my $cl = DXCluster->get($call);
463   if ($cl) {
464     my $hops;
465         my $dxchan = $cl->{dxchan};
466         if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
467           my $newhops = $hops - 1;
468           if ($newhops > 0) {
469             $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;       # change the hop count
470                 $dxchan->send($line) if $dxchan;
471           }
472         } else {
473           $dxchan->send($line) if $dxchan;                    # for them wot don't have Hops
474         }
475   }
476 }
477
478 # broadcast a message to all clusters [except those mentioned after buffer]
479 sub broadcast_ak1a
480 {
481   my $s = shift;                  # the line to be rebroadcast
482   my @except = @_;                # to all channels EXCEPT these (dxchannel refs)
483   my @chan = get_all_ak1a();
484   my $chan;
485   
486   foreach $chan (@chan) {
487      next if grep $chan == $_, @except;
488          $chan->send($s);              # send it if it isn't the except list
489   }
490 }
491
492 # broadcast to all users
493 sub broadcast_users
494 {
495   my $s = shift;                  # the line to be rebroadcast
496   my @except = @_;                # to all channels EXCEPT these (dxchannel refs)
497   my @chan = get_all_users();
498   my $chan;
499   
500   foreach $chan (@chan) {
501     next if grep $chan == $_, @except;
502         $chan->send($s);              # send it if it isn't the except list
503   }
504 }
505
506 # broadcast to a list of users
507 sub broadcast_list
508 {
509   my $s = shift;
510   my $chan;
511   
512   foreach $chan (@_) {
513         $chan->send($s);              # send it 
514   }
515 }
516
517 #
518 # gimme all the ak1a nodes
519 #
520 sub get_all_ak1a
521 {
522   my @list = DXChannel->get_all();
523   my $ref;
524   my @out;
525   foreach $ref (@list) {
526     push @out, $ref if $ref->is_ak1a;
527   }
528   return @out;
529 }
530
531 # return a list of all users
532 sub get_all_users
533 {
534   my @list = DXChannel->get_all();
535   my $ref;
536   my @out;
537   foreach $ref (@list) {
538     push @out, $ref if $ref->is_user;
539   }
540   return @out;
541 }
542
543 # return a list of all user callsigns
544 sub get_all_user_calls
545 {
546   my @list = DXChannel->get_all();
547   my $ref;
548   my @out;
549   foreach $ref (@list) {
550     push @out, $ref->call if $ref->is_user;
551   }
552   return @out;
553 }
554
555 #
556 # obtain the hops from the list for this callsign and pc no 
557 #
558
559 sub get_hops
560 {
561   my ($pcno) = @_;
562   my $hops = $DXProt::hopcount{$pcno};
563   $hops = $DXProt::def_hopcount if !$hops;
564   return "H$hops";       
565 }
566
567 # remove leading and trailing spaces from an input string
568 sub unpad
569 {
570   my $s = shift;
571   $s =~ s/^\s+|\s+$//;
572   return $s;
573 }
574 1;
575 __END__