a mostly working send message implementation
[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           my $ref = DXCluster->get($call);
256           $ref->del() if $ref;
257           last SWITCH;
258         }
259         
260     if ($pcno == 22) {last SWITCH;}
261
262     if ($pcno == 23 || $pcno == 27) {  # WWV info
263       last SWITCH;
264         }
265
266     if ($pcno == 24) {             # set here status
267           my $call = uc $field[1];
268           $call =~ s/-\d+//o;
269           my $ref = DXCluster->get($call);
270           $ref->here($field[2]) if $ref;
271           last SWITCH;
272         }
273         
274     if ($pcno == 25) {last SWITCH;}
275
276     if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42) {   # mail/file handling
277           DXMsg::process($self, $line);
278           return;
279         }
280         
281     if ($pcno == 34 || $pcno == 36) {   # remote commands (incoming)
282           last SWITCH;
283         }
284         
285     if ($pcno == 35) {                  # remote command replies
286           last SWITCH;
287         }
288         
289     if ($pcno == 37) {last SWITCH;}
290     
291         if ($pcno == 38) {                  # node connected list from neighbour
292           return;
293         }
294
295     if ($pcno == 39) {              # incoming disconnect
296       $self->disconnect();
297           return;
298         }
299         
300     if ($pcno == 41) {              # user info
301       # add this station to the user database, if required
302           my $user = DXUser->get_current($field[1]);
303           $user = DXUser->new($field[1]) if !$user;
304           
305           if ($field[2] == 1) {
306             $user->name($field[3]);
307           } elsif ($field[2] == 2) {
308             $user->qth($field[3]);
309           } elsif ($field[2] == 3) {
310         my ($latd, $latm, $latl, $longd, $longm, $longl) = split /\s+/, $field[3];
311                 $longd += ($longm/60);
312                 $longd = 0-$longd if (uc $longl) eq 'W'; 
313                 $user->long($longd);
314                 $latd += ($latm/60);
315                 $latd = 0-$latd if (uc $latl) eq 'S';
316                 $user->lat($latd);
317           } elsif ($field[2] == 4) {
318             $user->node($field[3]);
319           }
320           $user->put;
321           last SWITCH;
322         }
323     if ($pcno == 43) {last SWITCH;}
324     if ($pcno == 44) {last SWITCH;}
325     if ($pcno == 45) {last SWITCH;}
326     if ($pcno == 46) {last SWITCH;}
327     if ($pcno == 47) {last SWITCH;}
328     if ($pcno == 48) {last SWITCH;}
329     if ($pcno == 49) {last SWITCH;}
330         
331     if ($pcno == 50) {              # keep alive/user list
332           my $ref = DXCluster->get($field[1]);
333           $ref->update_users($field[2]) if $ref;
334           last SWITCH;
335         }
336         
337     if ($pcno == 51) {              # incoming ping requests/answers
338           
339           # is it for us?
340           if ($field[1] eq $main::mycall) {
341             my $flag = $field[3];
342             $flag ^= 1;
343             $self->send($self->pc51($field[2], $field[1], $flag));
344           } else {
345             # route down an appropriate thingy
346                 route($field[1], $line);
347           }
348           return;
349         }
350   }
351   
352   # if get here then rebroadcast the thing with its Hop count decremented (if
353   # there is one). If it has a hop count and it decrements to zero then don't
354   # rebroadcast it.
355   #
356   # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
357   #        REBROADCAST!!!!
358   #
359   
360   my $hops;
361   if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
362         my $newhops = $hops - 1;
363         if ($newhops > 0) {
364           $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;       # change the hop count
365           broadcast_ak1a($line, $self);             # send it to everyone but me
366         }
367   }
368 }
369
370 #
371 # This is called from inside the main cluster processing loop and is used
372 # for despatching commands that are doing some long processing job
373 #
374 sub process
375 {
376   my $t = time;
377   my @chan = DXChannel->get_all();
378   my $chan;
379   
380   foreach $chan (@chan) {
381     next if !$chan->is_ak1a();
382
383     # send a pc50 out on this channel
384     if ($t >= $chan->pc50_t + $DXProt::pc50_interval) {
385       $chan->send(pc50());
386           $chan->pc50_t($t);
387         }
388   }
389   
390   my $key;
391   my $val;
392   my $cutoff;
393   if ($main::systime - 3600 > $last_hour) {
394     $cutoff  = $main::systime - $pc11_dup_age;
395         while (($key, $val) = each %dup) {
396           delete $dup{$key} if $val < $cutoff;
397         }
398         $last_hour = $main::systime;
399   }
400 }
401
402 #
403 # finish up a pc context
404 #
405 sub finish
406 {
407   my $self = shift;
408   my $ref = DXCluster->get($self->call);
409
410   # unbusy and stop and outgoing mail
411   my $mref = DXMsg::get_busy($self->call);
412   $mref->stop_msg($self) if $mref;
413   
414   # broadcast to all other nodes that all the nodes connected to via me are gone
415   my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
416   my $node;
417
418   foreach $node (@gonenodes) {
419     next if $node->call eq $self->call; 
420     broadcast_ak1a(pc21($node->call, 'Gone'), $self);    # done like this 'cos DXNodes don't have a pc21 method
421         $node->del();
422   }
423
424   # now broadcast to all other ak1a nodes that I have gone
425   broadcast_ak1a(pc21($self->call, 'Gone.'), $self);
426   $ref->del() if $ref;
427 }
428
429 #
430 # some active measures
431 #
432
433 sub send_local_config
434 {
435   my $self = shift;
436   my $n;
437
438   # send our nodes
439   my @nodes = DXNode::get_all();
440   
441   # create a list of all the nodes that are not connected to this connection
442   @nodes = map { $_->dxchan != $self ? $_ : () } @nodes;
443   $self->send($me->pc19(@nodes));
444           
445   # get all the users connected on the above nodes and send them out
446   foreach $n (@nodes) {
447     my @users = values %{$n->list};
448     $self->send(DXProt::pc16($n, @users));
449   }
450 }
451
452 #
453 # route a message down an appropriate interface for a callsign
454 #
455 # is called route(to, pcline);
456 #
457 sub route
458 {
459   my ($call, $line) = @_;
460   my $cl = DXCluster->get($call);
461   if ($cl) {
462     my $hops;
463         my $dxchan = $cl->{dxchan};
464         if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
465           my $newhops = $hops - 1;
466           if ($newhops > 0) {
467             $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;       # change the hop count
468                 $dxchan->send($line) if $dxchan;
469           }
470         } else {
471           $dxchan->send($line) if $dxchan;                    # for them wot don't have Hops
472         }
473   }
474 }
475
476 # broadcast a message to all clusters [except those mentioned after buffer]
477 sub broadcast_ak1a
478 {
479   my $s = shift;                  # the line to be rebroadcast
480   my @except = @_;                # to all channels EXCEPT these (dxchannel refs)
481   my @chan = get_all_ak1a();
482   my $chan;
483   
484   foreach $chan (@chan) {
485      next if grep $chan == $_, @except;
486          $chan->send($s);              # send it if it isn't the except list
487   }
488 }
489
490 # broadcast to all users
491 sub broadcast_users
492 {
493   my $s = shift;                  # the line to be rebroadcast
494   my @except = @_;                # to all channels EXCEPT these (dxchannel refs)
495   my @chan = get_all_users();
496   my $chan;
497   
498   foreach $chan (@chan) {
499     next if grep $chan == $_, @except;
500         $chan->send($s);              # send it if it isn't the except list
501   }
502 }
503
504 # broadcast to a list of users
505 sub broadcast_list
506 {
507   my $s = shift;
508   my $chan;
509   
510   foreach $chan (@_) {
511         $chan->send($s);              # send it 
512   }
513 }
514
515 #
516 # gimme all the ak1a nodes
517 #
518 sub get_all_ak1a
519 {
520   my @list = DXChannel->get_all();
521   my $ref;
522   my @out;
523   foreach $ref (@list) {
524     push @out, $ref if $ref->is_ak1a;
525   }
526   return @out;
527 }
528
529 # return a list of all users
530 sub get_all_users
531 {
532   my @list = DXChannel->get_all();
533   my $ref;
534   my @out;
535   foreach $ref (@list) {
536     push @out, $ref if $ref->is_user;
537   }
538   return @out;
539 }
540
541 # return a list of all user callsigns
542 sub get_all_user_calls
543 {
544   my @list = DXChannel->get_all();
545   my $ref;
546   my @out;
547   foreach $ref (@list) {
548     push @out, $ref->call if $ref->is_user;
549   }
550   return @out;
551 }
552
553 #
554 # obtain the hops from the list for this callsign and pc no 
555 #
556
557 sub get_hops
558 {
559   my ($pcno) = @_;
560   my $hops = $DXProt::hopcount{$pcno};
561   $hops = $DXProt::def_hopcount if !$hops;
562   return "H$hops";       
563 }
564
565 # remove leading and trailing spaces from an input string
566 sub unpad
567 {
568   my $s = shift;
569   $s =~ s/^\s+|\s+$//;
570   return $s;
571 }
572 1;
573 __END__