*** empty log message ***
[spider.git] / perl / Thingy / PCProt.pm
1 #
2 # This module is the PC Protocol Thingy Handler 
3 # It will route transforming them on the way as required.
4 #
5 # Copyright (c) 2001 Dirk Koopman G1TLH
6 #
7 # $Id$
8
9
10 use strict;
11
12 use DXDebug;
13
14 use vars qw($VERSION $BRANCH);
15 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /(\d+)\.(\d+)/ );
16 $BRANCH = sprintf( "%d.%03d", q$Revision$ =~ /\d+\.\d+\.(\d+)\.(\d+)/  || (0,0));
17 $main::build += $VERSION;
18 $main::branch += $BRANCH;
19
20
21 package Thingy::PC10;
22 use vars qw(@ISA);
23 @ISA = qw(Thingy);
24         
25 # incoming talk commands
26 sub handle
27 {
28         my $self = shift;
29         my $dxchan = shift;
30
31         # rsfp check
32         return if $rspfcheck and !$self->rspfcheck(0, $_[6], $_[1]);
33                         
34         # will we allow it at all?
35         if ($censorpc) {
36                 my @bad;
37                 if (@bad = BadWords::check($_[3])) {
38                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
39                         return;
40                 }
41         }
42
43         # is it for me or one of mine?
44         my ($from, $to, $via, $call, $dxchan);
45         $from = $_[1];
46         if ($_[5] gt ' ') {
47                 $via = $_[2];
48                 $to = $_[5];
49         } else {
50                 $to = $_[2];
51         }
52
53         # if this is a 'nodx' node then ignore it
54         if ($badnode->in($_[6]) || ($via && $badnode->in($via))) {
55                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
56                 return;
57         }
58
59         # if this is a 'bad spotter' user then ignore it
60         my $nossid = $from;
61         $nossid =~ s/-\d+$//;
62         if ($badspotter->in($nossid)) {
63                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
64                 return;
65         }
66
67         # if we are converting announces to talk is it a dup?
68         if ($ann_to_talk) {
69                 if (AnnTalk::is_talk_candidate($from, $_[3]) && AnnTalk::dup($from, $to, $_[3])) {
70                         dbg("DXPROT: Dupe talk from announce, dropped") if isdbg('chanerr');
71                         return;
72                 }
73         }
74
75         # it is here and logged on
76         $dxchan = DXChannel->get($main::myalias) if $to eq $main::mycall;
77         $dxchan = DXChannel->get($to) unless $dxchan;
78         if ($dxchan && $dxchan->is_user) {
79                 $_[3] =~ s/\%5E/^/g;
80                 $dxchan->talk($from, $to, $via, $_[3]);
81                 return;
82         }
83
84         # is it elsewhere, visible on the cluster via the to address?
85         # note: this discards the via unless the to address is on
86         # the via address
87         my ($ref, $vref);
88         if ($ref = Route::get($to)) {
89                 $vref = Route::Node::get($via) if $via;
90                 $vref = undef unless $vref && grep $to eq $_, $vref->users;
91                 $ref->dxchan->talk($from, $to, $vref ? $via : undef, $_[3], $_[6]);
92                 return;
93         }
94
95         # not visible here, send a message of condolence
96         $vref = undef;
97         $ref = Route::get($from);
98         $vref = $ref = Route::Node::get($_[6]) unless $ref; 
99         if ($ref) {
100                 $dxchan = $ref->dxchan;
101                 $dxchan->talk($main::mycall, $from, $vref ? $vref->call : undef, $dxchan->msg('talknh', $to) );
102         }
103 }
104
105 # DX Spot handling
106 package Thingy::PC11;
107 use vars qw(@ISA);
108 @ISA = qw(Thingy);
109
110 sub handle
111 {
112         my $self = shift;
113         my $dxchan = shift;
114
115         # route 'foreign' pc26s 
116         if ($pcno == 26) {
117                 if ($_[7] ne $main::mycall) {
118                         $self->route($_[7], $line);
119                         return;
120                 }
121         }
122                         
123         # rsfp check
124         #                       return if $rspfcheck and !$self->rspfcheck(1, $_[7], $_[6]);
125
126         # if this is a 'nodx' node then ignore it
127         if ($badnode->in($_[7])) {
128                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
129                 return;
130         }
131                         
132         # if this is a 'bad spotter' user then ignore it
133         my $nossid = $_[6];
134         $nossid =~ s/-\d+$//;
135         if ($badspotter->in($nossid)) {
136                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
137                 return;
138         }
139                         
140         # convert the date to a unix date
141         my $d = cltounix($_[3], $_[4]);
142         # bang out (and don't pass on) if date is invalid or the spot is too old (or too young)
143         if (!$d || ($pcno == 11 && ($d < $main::systime - $pc11_max_age || $d > $main::systime + 900))) {
144                 dbg("PCPROT: Spot ignored, invalid date or out of range ($_[3] $_[4])\n") if isdbg('chanerr');
145                 return;
146         }
147
148         # is it 'baddx'
149         if ($baddx->in($_[2]) || BadWords::check($_[2]) || $_[2] =~ /COCK/) {
150                 dbg("PCPROT: Bad DX spot, ignored") if isdbg('chanerr');
151                 return;
152         }
153                         
154         # do some de-duping
155         $_[5] =~ s/^\s+//;                      # take any leading blanks off
156         $_[2] = unpad($_[2]);           # take off leading and trailing blanks from spotted callsign
157         if ($_[2] =~ /BUST\w*$/) {
158                 dbg("PCPROT: useless 'BUSTED' spot") if isdbg('chanerr');
159                 return;
160         }
161         if ($censorpc) {
162                 my @bad;
163                 if (@bad = BadWords::check($_[5])) {
164                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
165                         return;
166                 }
167         }
168
169
170         my @spot = Spot::prepare($_[1], $_[2], $d, $_[5], $_[6], $_[7]);
171         # global spot filtering on INPUT
172         if ($self->{inspotsfilter}) {
173                 my ($filter, $hops) = $self->{inspotsfilter}->it(@spot);
174                 unless ($filter) {
175                         dbg("PCPROT: Rejected by input spot filter") if isdbg('chanerr');
176                         return;
177                 }
178         }
179
180         # this goes after the input filtering, but before the add
181         # so that if it is input filtered, it isn't added to the dup
182         # list. This allows it to come in from a "legitimate" source
183         if (Spot::dup($_[1], $_[2], $d, $_[5], $_[6])) {
184                 dbg("PCPROT: Duplicate Spot ignored\n") if isdbg('chanerr');
185                 return;
186         }
187
188         # add it 
189         Spot::add(@spot);
190
191         #
192         # @spot at this point contains:-
193         # freq, spotted call, time, text, spotter, spotted cc, spotters cc, orig node
194         # then  spotted itu, spotted cq, spotters itu, spotters cq
195         # you should be able to route on any of these
196         #
197                         
198         # fix up qra locators of known users 
199         my $user = DXUser->get_current($spot[4]);
200         if ($user) {
201                 my $qra = $user->qra;
202                 unless ($qra && is_qra($qra)) {
203                         my $lat = $user->lat;
204                         my $long = $user->long;
205                         if (defined $lat && defined $long) {
206                                 $user->qra(DXBearing::lltoqra($lat, $long)); 
207                                 $user->put;
208                         }
209                 }
210
211                 # send a remote command to a distant cluster if it is visible and there is no
212                 # qra locator and we havn't done it for a month.
213
214                 unless ($user->qra) {
215                         my $node;
216                         my $to = $user->homenode;
217                         my $last = $user->lastoper || 0;
218                         if ($send_opernam && $to && $to ne $main::mycall && $main::systime > $last + $DXUser::lastoperinterval && ($node = Route::Node::get($to)) ) {
219                                 my $cmd = "forward/opernam $spot[4]";
220                                 # send the rcmd but we aren't interested in the replies...
221                                 my $dxchan = $node->dxchan;
222                                 if ($dxchan && $dxchan->is_clx) {
223                                         route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
224                                 } else {
225                                         route(undef, $to, pc34($main::mycall, $to, $cmd));
226                                 }
227                                 if ($to ne $_[7]) {
228                                         $to = $_[7];
229                                         $node = Route::Node::get($to);
230                                         if ($node) {
231                                                 $dxchan = $node->dxchan;
232                                                 if ($dxchan && $dxchan->is_clx) {
233                                                         route(undef, $to, pc84($main::mycall, $to, $main::mycall, $cmd));
234                                                 } else {
235                                                         route(undef, $to, pc34($main::mycall, $to, $cmd));
236                                                 }
237                                         }
238                                 }
239                                 $user->lastoper($main::systime);
240                                 $user->put;
241                         }
242                 }
243         }
244                                 
245         # local processing 
246         my $r;
247         eval {
248                 $r = Local::spot($self, @spot);
249         };
250         #                       dbg("Local::spot1 error $@") if isdbg('local') if $@;
251         return if $r;
252
253         # DON'T be silly and send on PC26s!
254         return if $pcno == 26;
255
256         # send out the filtered spots
257         send_dx_spot($self, $line, @spot) if @spot;
258 }
259                 
260 # announces
261 package Thingy::PC12;
262 use vars qw(@ISA);
263 @ISA = qw(Thingy);
264
265 sub handle
266 {
267         my $self = shift;
268         my $dxchan = shift;
269
270         #                       return if $rspfcheck and !$self->rspfcheck(1, $_[5], $_[1]);
271
272         # announce duplicate checking
273         $_[3] =~ s/^\s+//;                      # remove leading blanks
274
275         if ($censorpc) {
276                 my @bad;
277                 if (@bad = BadWords::check($_[3])) {
278                         dbg("PCPROT: Bad words: @bad, dropped") if isdbg('chanerr');
279                         return;
280                 }
281         }
282
283         # if this is a 'nodx' node then ignore it
284         if ($badnode->in($_[5])) {
285                 dbg("PCPROT: Bad Node, dropped") if isdbg('chanerr');
286                 return;
287         }
288
289         # if this is a 'bad spotter' user then ignore it
290         my $nossid = $_[1];
291         $nossid =~ s/-\d+$//;
292         if ($badspotter->in($nossid)) {
293                 dbg("PCPROT: Bad Spotter, dropped") if isdbg('chanerr');
294                 return;
295         }
296
297         if ($_[2] eq '*' || $_[2] eq $main::mycall) {
298
299
300                 # here's a bit of fun, convert incoming ann with a callsign in the first word
301                 # or one saying 'to <call>' to a talk if we can route to the recipient
302                 if ($ann_to_talk) {
303                         my $call = AnnTalk::is_talk_candidate($_[1], $_[3]);
304                         if ($call) {
305                                 my $ref = Route::get($call);
306                                 if ($ref) {
307                                         my $dxchan = $ref->dxchan;
308                                         $dxchan->talk($_[1], $call, undef, $_[3], $_[5]) if $dxchan != $self;
309                                         return;
310                                 }
311                         }
312                 }
313         
314                 # send it
315                 $self->send_announce($line, @_[1..6]);
316         } else {
317                 $self->route($_[2], $line);
318         }
319 }
320                 
321 # incoming user         
322 package Thingy::PC16;
323 use vars qw(@ISA);
324 @ISA = qw(Thingy);
325
326 sub handle
327 {
328         my $self = shift;
329         my $dxchan = shift;
330
331
332         if (eph_dup($line)) {
333                 dbg("PCPROT: dup PC16 detected") if isdbg('chanerr');
334                 return;
335         }
336
337         # general checks
338         my $dxchan;
339         my $ncall = $_[1];
340         my $newline = "PC16^";
341                         
342         # do I want users from this channel?
343         unless ($self->user->wantpc16) {
344                 dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
345                 return;
346         }
347         # is it me?
348         if ($ncall eq $main::mycall) {
349                 dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
350                 return;
351         }
352         my $parent = Route::Node::get($ncall); 
353
354         # if there is a parent, proceed, otherwise if there is a latent PC19 in the PC19list, 
355         # fix it up in the routing tables and issue it forth before the PC16
356         unless ($parent) {
357                 my $nl = $pc19list{$ncall};
358
359                 if ($nl && @_ > 3) { # 3 because of the hop count!
360
361                         # this is a new (remembered) node, now attach it to me if it isn't in filtered
362                         # and we haven't disallowed it
363                         my $user = DXUser->get_current($ncall);
364                         if (!$user) {
365                                 $user = DXUser->new($ncall);
366                                 $user->sort('A');
367                                 $user->priv(1); # I have relented and defaulted nodes
368                                 $user->lockout(1);
369                                 $user->homenode($ncall);
370                                 $user->node($ncall);
371                         }
372
373                         my $wantpc19 = $user->wantroutepc19;
374                         if ($wantpc19 || !defined $wantpc19) {
375                                 my $new = Route->new($ncall); # throw away
376                                 if ($self->in_filter_route($new)) {
377                                         my @nrout;
378                                         for (@$nl) {
379                                                 $parent = Route::Node::get($_->[0]);
380                                                 $dxchan = $parent->dxchan if $parent;
381                                                 if ($dxchan && $dxchan ne $self) {
382                                                         dbg("PCPROT: PC19 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
383                                                         $parent = undef;
384                                                 }
385                                                 if ($parent) {
386                                                         my $r = $parent->add($ncall, $_->[1], $_->[2]);
387                                                         push @nrout, $r unless @nrout;
388                                                 }
389                                         }
390                                         $user->wantroutepc19(1) unless defined $wantpc19; # for now we work on the basis that pc16 = real route 
391                                         $user->lastin($main::systime) unless DXChannel->get($ncall);
392                                         $user->put;
393                                                 
394                                         # route the pc19 - this will cause 'stuttering PC19s' for a while
395                                         $self->route_pc19(@nrout) if @nrout ;
396                                         $parent = Route::Node::get($ncall);
397                                         unless ($parent) {
398                                                 dbg("PCPROT: lost $ncall after sending PC19 for it?");
399                                                 return;
400                                         }
401                                 } else {
402                                         return;
403                                 }
404                                 delete $pc19list{$ncall};
405                         }
406                 } else {
407                         dbg("PCPROT: Node $ncall not in config") if isdbg('chanerr');
408                         return;
409                 }
410         } else {
411                                 
412                 $dxchan = $parent->dxchan;
413                 if ($dxchan && $dxchan ne $self) {
414                         dbg("PCPROT: PC16 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
415                         return;
416                 }
417
418                 # input filter if required
419                 return unless $self->in_filter_route($parent);
420         }
421
422         my $i;
423         my @rout;
424         for ($i = 2; $i < $#_; $i++) {
425                 my ($call, $conf, $here) = $_[$i] =~ /^(\S+) (\S) (\d)/o;
426                 next unless $call && $conf && defined $here && is_callsign($call);
427                 next if $call eq $main::mycall;
428
429                 eph_del_regex("^PC17\\^$call\\^$ncall");
430                                 
431                 $conf = $conf eq '*';
432
433                 # reject this if we think it is a node already
434                 my $r = Route::Node::get($call);
435                 my $u = DXUser->get_current($call) unless $r;
436                 if ($r || ($u && $u->is_node)) {
437                         dbg("PCPROT: $call is a node") if isdbg('chanerr');
438                         next;
439                 }
440                                 
441                 $r = Route::User::get($call);
442                 my $flags = Route::here($here)|Route::conf($conf);
443                                 
444                 if ($r) {
445                         my $au = $r->addparent($parent);                                        
446                         if ($r->flags != $flags) {
447                                 $r->flags($flags);
448                                 $au = $r;
449                         }
450                         push @rout, $r if $au;
451                 } else {
452                         push @rout, $parent->add_user($call, $flags);
453                 }
454                 
455                                 
456                 # add this station to the user database, if required
457                 $call =~ s/-\d+$//o;    # remove ssid for users
458                 my $user = DXUser->get_current($call);
459                 $user = DXUser->new($call) if !$user;
460                 $user->homenode($parent->call) if !$user->homenode;
461                 $user->node($parent->call);
462                 $user->lastin($main::systime) unless DXChannel->get($call);
463                 $user->put;
464         }
465                         
466         $self->route_pc16($parent, @rout) if @rout;
467 }
468                 
469 # remove a user
470 package Thingy::PC17;
471 use vars qw(@ISA);
472 @ISA = qw(Thingy);
473
474 sub handle
475 {
476         my $self = shift;
477         my $dxchan = shift;
478         my $ncall = $_[2];
479         my $ucall = $_[1];
480
481         eph_del_regex("^PC16\\^$ncall.*$ucall");
482                         
483         # do I want users from this channel?
484         unless ($self->user->wantpc16) {
485                 dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
486                 return;
487         }
488         if ($ncall eq $main::mycall) {
489                 dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
490                 return;
491         }
492
493         my $uref = Route::User::get($ucall);
494         unless ($uref) {
495                 dbg("PCPROT: Route::User $ucall not in config") if isdbg('chanerr');
496                 return;
497         }
498         my $parent = Route::Node::get($ncall);
499         unless ($parent) {
500                 dbg("PCPROT: Route::Node $ncall not in config") if isdbg('chanerr');
501                 return;
502         }                       
503
504         $dxchan = $parent->dxchan;
505         if ($dxchan && $dxchan ne $self) {
506                 dbg("PCPROT: PC17 from $self->{call} trying to alter locally connected $ncall, ignored!") if isdbg('chanerr');
507                 return;
508         }
509
510         # input filter if required
511         return unless $self->in_filter_route($parent);
512                         
513         $parent->del_user($uref);
514
515         if (eph_dup($line)) {
516                 dbg("PCPROT: dup PC17 detected") if isdbg('chanerr');
517                 return;
518         }
519
520         $self->route_pc17($parent, $uref);
521 }
522                 
523 # link request
524 package Thingy::PC18;
525 use vars qw(@ISA);
526 @ISA = qw(Thingy);
527
528 sub handle
529 {
530         my $self = shift;
531         my $dxchan = shift;
532         $self->state('init');   
533
534         # record the type and version offered
535         if ($_[1] =~ /DXSpider Version: (\d+\.\d+) Build: (\d+\.\d+)/) {
536                 $self->version(53 + $1);
537                 $self->user->version(53 + $1);
538                 $self->build(0 + $2);
539                 $self->user->build(0 + $2);
540                 unless ($self->is_spider) {
541                         $self->user->sort('S');
542                         $self->user->put;
543                         $self->sort('S');
544                 }
545         } else {
546                 $self->version(50.0);
547                 $self->version($_[2] / 100) if $_[2] && $_[2] =~ /^\d+$/;
548                 $self->user->version($self->version);
549         }
550
551         # first clear out any nodes on this dxchannel
552         my $parent = Route::Node::get($self->{call});
553         my @rout = $parent->del_nodes;
554         $self->route_pc21(@rout, $parent) if @rout;
555         $self->send_local_config();
556         $self->send(pc20());
557 }
558                 
559 # incoming cluster list
560 package Thingy::PC19;
561 use vars qw(@ISA);
562 @ISA = qw(Thingy);
563
564 sub handle
565 {
566         my $self = shift;
567         my $dxchan = shift;
568
569         my $i;
570         my $newline = "PC19^";
571
572         if (eph_dup($line)) {
573                 dbg("PCPROT: dup PC19 detected") if isdbg('chanerr');
574                 return;
575         }
576
577         # new routing list
578         my @rout;
579         my $parent = Route::Node::get($self->{call});
580         unless ($parent) {
581                 dbg("DXPROT: my parent $self->{call} has disappeared");
582                 $self->disconnect;
583                 return;
584         }
585
586         # parse the PC19
587         for ($i = 1; $i < $#_-1; $i += 4) {
588                 my $here = $_[$i];
589                 my $call = uc $_[$i+1];
590                 my $conf = $_[$i+2];
591                 my $ver = $_[$i+3];
592                 next unless defined $here && defined $conf && is_callsign($call);
593
594                 eph_del_regex("^PC(?:21\\^$call|17\\^[^\\^]+\\^$call)");
595                                 
596                 # check for sane parameters
597                 #                               $ver = 5000 if $ver eq '0000';
598                 next if $ver < 5000;    # only works with version 5 software
599                 next if length $call < 3; # min 3 letter callsigns
600                 next if $call eq $main::mycall;
601
602                 # check that this PC19 isn't trying to alter the wrong dxchan
603                 my $dxchan = DXChannel->get($call);
604                 if ($dxchan && $dxchan != $self) {
605                         dbg("PCPROT: PC19 from $self->{call} trying to alter wrong locally connected $call, ignored!") if isdbg('chanerr');
606                         next;
607                 }
608
609                 # add this station to the user database, if required (don't remove SSID from nodes)
610                 my $user = DXUser->get_current($call);
611                 if (!$user) {
612                         $user = DXUser->new($call);
613                         $user->sort('A');
614                         $user->priv(1);         # I have relented and defaulted nodes
615                         $user->lockout(1);
616                         $user->homenode($call);
617                         $user->node($call);
618                 }
619
620                 my $r = Route::Node::get($call);
621                 my $flags = Route::here($here)|Route::conf($conf);
622
623                 # modify the routing table if it is in it, otherwise store it in the pc19list for now
624                 if ($r) {
625                         my $ar;
626                         if ($call ne $parent->call) {
627                                 if ($self->in_filter_route($r)) {
628                                         $ar = $parent->add($call, $ver, $flags);
629                                         push @rout, $ar if $ar;
630                                 } else {
631                                         next;
632                                 }
633                         }
634                         if ($r->version ne $ver || $r->flags != $flags) {
635                                 $r->version($ver);
636                                 $r->flags($flags);
637                                 push @rout, $r unless $ar;
638                         }
639                 } else {
640
641                         # if he is directly connected or allowed then add him, otherwise store him up for later
642                         if ($call eq $self->{call} || $user->wantroutepc19) {
643                                 my $new = Route->new($call); # throw away
644                                 if ($self->in_filter_route($new)) {
645                                         my $ar = $parent->add($call, $ver, $flags);
646                                         $user->wantroutepc19(1) unless defined $user->wantroutepc19;
647                                         push @rout, $ar if $ar;
648                                 } else {
649                                         next;
650                                 }
651                         } else {
652                                 $pc19list{$call} = [] unless exists $pc19list{$call};
653                                 my $nl = $pc19list{$call};
654                                 push @{$pc19list{$call}}, [$self->{call}, $ver, $flags] unless grep $_->[0] eq $self->{call}, @$nl;
655                         }
656                 }
657
658                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
659                 my $mref = DXMsg::get_busy($call);
660                 $mref->stop_msg($call) if $mref;
661                                 
662                 $user->lastin($main::systime) unless DXChannel->get($call);
663                 $user->put;
664         }
665
666
667         $self->route_pc19(@rout) if @rout;
668 }
669                 
670 # send local configuration
671 package Thingy::PC20;
672 use vars qw(@ISA);
673 @ISA = qw(Thingy);
674
675 sub handle
676 {
677         my $self = shift;
678         my $dxchan = shift;
679         $self->send_local_config();
680         $self->send(pc22());
681         $self->state('normal');
682         $self->{lastping} = 0;
683 }
684                 
685 # delete a cluster from the list
686 package Thingy::PC21;
687 use vars qw(@ISA);
688 @ISA = qw(Thingy);
689
690 sub handle
691 {
692         my $self = shift;
693         my $dxchan = shift;
694         my $call = uc $_[1];
695
696         eph_del_regex("^PC1[679].*$call");
697                         
698         # if I get a PC21 from the same callsign as self then treat it
699         # as a PC39: I have gone away
700         if ($call eq $self->call) {
701                 $self->disconnect(1);
702                 return;
703         }
704
705         # check to see if we are in the pc19list, if we are then don't bother with any of
706         # this routing table manipulation, just remove it from the list and dump it
707         my @rout;
708         if (my $nl = $pc19list{$call}) {
709                 $pc19list{$call} = [ grep {$_->[0] ne $self->{call}} @$nl ];
710                 delete $pc19list{$call} unless @{$pc19list{$call}};
711         } else {
712                                 
713                 my $parent = Route::Node::get($self->{call});
714                 unless ($parent) {
715                         dbg("DXPROT: my parent $self->{call} has disappeared");
716                         $self->disconnect;
717                         return;
718                 }
719                 if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
720                         my $node = Route::Node::get($call);
721                         if ($node) {
722                                                 
723                                 my $dxchan = DXChannel->get($call);
724                                 if ($dxchan && $dxchan != $self) {
725                                         dbg("PCPROT: PC21 from $self->{call} trying to alter locally connected $call, ignored!") if isdbg('chanerr');
726                                         return;
727                                 }
728                                                 
729                                 # input filter it
730                                 return unless $self->in_filter_route($node);
731                                                 
732                                 # routing objects
733                                 push @rout, $node->del($parent);
734                         }
735                 } else {
736                         dbg("PCPROT: I WILL _NOT_ be disconnected!") if isdbg('chanerr');
737                         return;
738                 }
739         }
740
741         $self->route_pc21(@rout) if @rout;
742 }
743                 
744
745 package Thingy::PC22;
746 use vars qw(@ISA);
747 @ISA = qw(Thingy);
748
749 sub handle
750 {
751         my $self = shift;
752         my $dxchan = shift;
753         $self->state('normal');
754         $self->{lastping} = 0;
755 }
756                                 
757 # WWV info
758 package Thingy::PC23;
759 use vars qw(@ISA);
760 @ISA = qw(Thingy);
761
762 sub handle
763 {
764         my $self = shift;
765         my $dxchan = shift;
766                         
767         # route 'foreign' pc27s 
768         if ($pcno == 27) {
769                 if ($_[8] ne $main::mycall) {
770                         $self->route($_[8], $line);
771                         return;
772                 }
773         }
774
775         return if $rspfcheck and !$self->rspfcheck(1, $_[8], $_[7]);
776
777         # do some de-duping
778         my $d = cltounix($_[1], sprintf("%02d18Z", $_[2]));
779         my $sfi = unpad($_[3]);
780         my $k = unpad($_[4]);
781         my $i = unpad($_[5]);
782         my ($r) = $_[6] =~ /R=(\d+)/;
783         $r = 0 unless $r;
784         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $_[2] < 0 || $_[2] > 23) {
785                 dbg("PCPROT: WWV Date ($_[1] $_[2]) out of range") if isdbg('chanerr');
786                 return;
787         }
788         if (Geomag::dup($d,$sfi,$k,$i,$_[6])) {
789                 dbg("PCPROT: Dup WWV Spot ignored\n") if isdbg('chanerr');
790                 return;
791         }
792         $_[7] =~ s/-\d+$//o;            # remove spotter's ssid
793                 
794         my $wwv = Geomag::update($d, $_[2], $sfi, $k, $i, @_[6..8], $r);
795
796         my $rep;
797         eval {
798                 $rep = Local::wwv($self, $_[1], $_[2], $sfi, $k, $i, @_[6..8], $r);
799         };
800         #                       dbg("Local::wwv2 error $@") if isdbg('local') if $@;
801         return if $rep;
802
803         # DON'T be silly and send on PC27s!
804         return if $pcno == 27;
805
806         # broadcast to the eager world
807         send_wwv_spot($self, $line, $d, $_[2], $sfi, $k, $i, @_[6..8]);
808 }
809                 
810 # set here status
811 package Thingy::PC24;
812 use vars qw(@ISA);
813 @ISA = qw(Thingy);
814
815 sub handle
816 {
817         my $self = shift;
818         my $dxchan = shift;
819         my $call = uc $_[1];
820         my ($nref, $uref);
821         $nref = Route::Node::get($call);
822         $uref = Route::User::get($call);
823         return unless $nref || $uref; # if we don't know where they are, it's pointless sending it on
824                         
825         if (eph_dup($line)) {
826                 dbg("PCPROT: Dup PC24 ignored\n") if isdbg('chanerr');
827                 return;
828         }
829         
830         $nref->here($_[2]) if $nref;
831         $uref->here($_[2]) if $uref;
832         my $ref = $nref || $uref;
833         return unless $self->in_filter_route($ref);
834
835         $self->route_pc24($ref, $_[3]);
836 }
837                 
838 # merge request
839 package Thingy::PC25;
840 use vars qw(@ISA);
841 @ISA = qw(Thingy);
842
843 sub handle
844 {
845         my $self = shift;
846         my $dxchan = shift;
847         if ($_[1] ne $main::mycall) {
848                 $self->route($_[1], $line);
849                 return;
850         }
851         if ($_[2] eq $main::mycall) {
852                 dbg("PCPROT: Trying to merge to myself, ignored") if isdbg('chanerr');
853                 return;
854         }
855
856         Log('DXProt', "Merge request for $_[3] spots and $_[4] WWV from $_[2]");
857                         
858         # spots
859         if ($_[3] > 0) {
860                 my @in = reverse Spot::search(1, undef, undef, 0, $_[3]);
861                 my $in;
862                 foreach $in (@in) {
863                         $self->send_frame($main::me, pc26(@{$in}[0..4], $_[2]));
864                 }
865         }
866
867         # wwv
868         if ($_[4] > 0) {
869                 my @in = reverse Geomag::search(0, $_[4], time, 1);
870                 my $in;
871                 foreach $in (@in) {
872                         $self->send_frame($main::me, pc27(@{$in}[0..5], $_[2]));
873                 }
874         }
875 }
876
877 sub handle_26 {goto &handle_11}
878 sub handle_27 {goto &handle_23}
879
880 # mail/file handling
881 package Thingy::PC28;
882 use vars qw(@ISA);
883 @ISA = qw(Thingy);
884
885 sub handle
886 {
887         my $self = shift;
888         my $dxchan = shift;
889         if ($_[1] eq $main::mycall) {
890                 no strict 'refs';
891                 my $sub = "DXMsg::handle_$pcno";
892                 &$sub($self, @_);
893         } else {
894                 $self->route($_[1], $line) unless $self->is_clx;
895         }
896 }
897
898 sub handle_29 {goto &handle_28}
899 sub handle_30 {goto &handle_28}
900 sub handle_31 {goto &handle_28}
901 sub handle_32 {goto &handle_28}
902 sub handle_33 {goto &handle_28}
903                 
904 package Thingy::PC34;
905 use vars qw(@ISA);
906 @ISA = qw(Thingy);
907
908 sub handle
909 {
910         my $self = shift;
911         my $dxchan = shift;
912         if (eph_dup($line, $eph_pc34_restime)) {
913                 dbg("PCPROT: dupe PC34, ignored") if isdbg('chanerr');
914         } else {
915                 $self->process_rcmd($_[1], $_[2], $_[2], $_[3]);
916         }
917 }
918                 
919 # remote command replies
920 package Thingy::PC35;
921 use vars qw(@ISA);
922 @ISA = qw(Thingy);
923
924 sub handle
925 {
926         my $self = shift;
927         my $dxchan = shift;
928         eph_del_regex("^PC35\\^$_[2]\\^$_[1]\\^");
929         $self->process_rcmd_reply($_[1], $_[2], $_[1], $_[3]);
930 }
931                 
932 sub handle_36 {goto &handle_34}
933
934 # database stuff
935 package Thingy::PC37;
936 use vars qw(@ISA);
937 @ISA = qw(Thingy);
938
939 sub handle
940 {
941         my $self = shift;
942         my $dxchan = shift;
943         DXDb::process($self, $line);
944 }
945
946 # node connected list from neighbour
947 package Thingy::PC38;
948 use vars qw(@ISA);
949 @ISA = qw(Thingy);
950
951 sub handle
952 {
953         my $self = shift;
954         my $dxchan = shift;
955 }
956                 
957 # incoming disconnect
958 package Thingy::PC39;
959 use vars qw(@ISA);
960 @ISA = qw(Thingy);
961
962 sub handle
963 {
964         my $self = shift;
965         my $dxchan = shift;
966         if ($_[1] eq $self->{call}) {
967                 $self->disconnect(1);
968         } else {
969                 dbg("PCPROT: came in on wrong channel") if isdbg('chanerr');
970         }
971 }
972
973 sub handle_40 {goto &handle_28}
974                 
975 # user info
976 package Thingy::PC41;
977 use vars qw(@ISA);
978 @ISA = qw(Thingy);
979
980 sub handle
981 {
982         my $self = shift;
983         my $dxchan = shift;
984         my $call = $_[1];
985
986         my $l = $line;
987         $l =~ s/[\x00-\x20\x7f-\xff]+//g; # remove all funny characters and spaces for dup checking
988         if (eph_dup($l, $eph_info_restime)) {
989                 dbg("PCPROT: dup PC41, ignored") if isdbg('chanerr');
990                 return;
991         }
992                         
993         # input filter if required
994         #                       my $ref = Route::get($call) || Route->new($call);
995         #                       return unless $self->in_filter_route($ref);
996
997         if ($_[3] eq $_[2] || $_[3] =~ /^\s*$/) {
998                 dbg('PCPROT: invalid value') if isdbg('chanerr');
999                 return;
1000         }
1001
1002         # add this station to the user database, if required
1003         my $user = DXUser->get_current($call);
1004         $user = DXUser->new($call) unless $user;
1005                         
1006         if ($_[2] == 1) {
1007                 $user->name($_[3]);
1008         } elsif ($_[2] == 2) {
1009                 $user->qth($_[3]);
1010         } elsif ($_[2] == 3) {
1011                 if (is_latlong($_[3])) {
1012                         my ($lat, $long) = DXBearing::stoll($_[3]);
1013                         $user->lat($lat);
1014                         $user->long($long);
1015                         $user->qra(DXBearing::lltoqra($lat, $long));
1016                 } else {
1017                         dbg('PCPROT: not a valid lat/long') if isdbg('chanerr');
1018                         return;
1019                 }
1020         } elsif ($_[2] == 4) {
1021                 $user->homenode($_[3]);
1022         } elsif ($_[2] == 5) {
1023                 if (is_qra(uc $_[3])) {
1024                         my ($lat, $long) = DXBearing::qratoll(uc $_[3]);
1025                         $user->lat($lat);
1026                         $user->long($long);
1027                         $user->qra(uc $_[3]);
1028                 } else {
1029                         dbg('PCPROT: not a valid QRA locator') if isdbg('chanerr');
1030                         return;
1031                 }
1032         }
1033         $user->lastoper($main::systime); # to cut down on excessive for/opers being generated
1034         $user->put;
1035
1036         unless ($self->{isolate}) {
1037                 DXChannel::broadcast_nodes($line, $self); # send it to everyone but me
1038         }
1039
1040         #  perhaps this IS what we want after all
1041         #                       $self->route_pc41($ref, $call, $_[2], $_[3], $_[4]);
1042 }
1043
1044 sub handle_42 {goto &handle_28}
1045
1046
1047 # database
1048 sub handle_44 {goto &handle_37}
1049 sub handle_45 {goto &handle_37}
1050 sub handle_46 {goto &handle_37}
1051 sub handle_47 {goto &handle_37}
1052 sub handle_48 {goto &handle_37}
1053                 
1054 # message and database
1055 package Thingy::PC49;
1056 use vars qw(@ISA);
1057 @ISA = qw(Thingy);
1058
1059 sub handle
1060 {
1061         my $self = shift;
1062         my $dxchan = shift;
1063
1064         if (eph_dup($line)) {
1065                 dbg("PCPROT: Dup PC49 ignored\n") if isdbg('chanerr');
1066                 return;
1067         }
1068         
1069         if ($_[1] eq $main::mycall) {
1070                 DXMsg::handle_49($self, @_);
1071         } else {
1072                 $self->route($_[1], $line) unless $self->is_clx;
1073         }
1074 }
1075
1076 # keep alive/user list
1077 package Thingy::PC50;
1078 use vars qw(@ISA);
1079 @ISA = qw(Thingy);
1080
1081 sub handle
1082 {
1083         my $self = shift;
1084         my $dxchan = shift;
1085
1086         my $call = $_[1];
1087         my $node = Route::Node::get($call);
1088         if ($node) {
1089                 return unless $node->call eq $self->{call};
1090                 $node->usercount($_[2]);
1091
1092                 # input filter if required
1093                 return unless $self->in_filter_route($node);
1094
1095                 $self->route_pc50($node, $_[2], $_[3]) unless eph_dup($line);
1096         }
1097 }
1098                 
1099 # incoming ping requests/answers
1100 package Thingy::PC51;
1101 use vars qw(@ISA);
1102 @ISA = qw(Thingy);
1103
1104 sub handle
1105 {
1106         my $self = shift;
1107         my $dxchan = shift;
1108         my $to = $_[1];
1109         my $from = $_[2];
1110         my $flag = $_[3];
1111
1112                         
1113         # is it for us?
1114         if ($to eq $main::mycall) {
1115                 if ($flag == 1) {
1116                         $self->send_frame($main::me, pc51($from, $to, '0'));
1117                 } else {
1118                         # it's a reply, look in the ping list for this one
1119                         my $ref = $pings{$from};
1120                         if ($ref) {
1121                                 my $tochan =  DXChannel->get($from);
1122                                 while (@$ref) {
1123                                         my $r = shift @$ref;
1124                                         my $dxchan = DXChannel->get($r->{call});
1125                                         next unless $dxchan;
1126                                         my $t = tv_interval($r->{t}, [ gettimeofday ]);
1127                                         if ($dxchan->is_user) {
1128                                                 my $s = sprintf "%.2f", $t; 
1129                                                 my $ave = sprintf "%.2f", $tochan ? ($tochan->{pingave} || $t) : $t;
1130                                                 $dxchan->send($dxchan->msg('pingi', $from, $s, $ave))
1131                                         } elsif ($dxchan->is_node) {
1132                                                 if ($tochan) {
1133                                                         my $nopings = $tochan->user->nopings || 2;
1134                                                         push @{$tochan->{pingtime}}, $t;
1135                                                         shift @{$tochan->{pingtime}} if @{$tochan->{pingtime}} > 6;
1136
1137                                                                 # cope with a missed ping, this means you must set the pingint large enough
1138                                                         if ($t > $tochan->{pingint}  && $t < 2 * $tochan->{pingint} ) {
1139                                                                 $t -= $tochan->{pingint};
1140                                                         }
1141
1142                                                                 # calc smoothed RTT a la TCP
1143                                                         if (@{$tochan->{pingtime}} == 1) {
1144                                                                 $tochan->{pingave} = $t;
1145                                                         } else {
1146                                                                 $tochan->{pingave} = $tochan->{pingave} + (($t - $tochan->{pingave}) / 6);
1147                                                         }
1148                                                         $tochan->{nopings} = $nopings; # pump up the timer
1149                                                 }
1150                                         } 
1151                                 }
1152                         }
1153                 }
1154         } else {
1155                 if (eph_dup($line)) {
1156                         dbg("PCPROT: dup PC51 detected") if isdbg('chanerr');
1157                         return;
1158                 }
1159                 # route down an appropriate thingy
1160                 $self->route($to, $line);
1161         }
1162 }
1163
1164 # dunno but route it
1165 package Thingy::PC75;
1166 use vars qw(@ISA);
1167 @ISA = qw(Thingy);
1168
1169 sub handle
1170 {
1171         my $self = shift;
1172         my $dxchan = shift;
1173         my $call = $_[1];
1174         if ($call ne $main::mycall) {
1175                 $self->route($call, $line);
1176         }
1177 }
1178
1179 # WCY broadcasts
1180 package Thingy::PC73;
1181 use vars qw(@ISA);
1182 @ISA = qw(Thingy);
1183
1184 sub handle
1185 {
1186         my $self = shift;
1187         my $dxchan = shift;
1188         my $call = $_[1];
1189                         
1190         # do some de-duping
1191         my $d = cltounix($call, sprintf("%02d18Z", $_[2]));
1192         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $_[2] < 0 || $_[2] > 23) {
1193                 dbg("PCPROT: WCY Date ($call $_[2]) out of range") if isdbg('chanerr');
1194                 return;
1195         }
1196         @_ = map { unpad($_) } @_;
1197         if (WCY::dup($d)) {
1198                 dbg("PCPROT: Dup WCY Spot ignored\n") if isdbg('chanerr');
1199                 return;
1200         }
1201                 
1202         my $wcy = WCY::update($d, @_[2..12]);
1203
1204         my $rep;
1205         eval {
1206                 $rep = Local::wcy($self, @_[1..12]);
1207         };
1208         # dbg("Local::wcy error $@") if isdbg('local') if $@;
1209         return if $rep;
1210
1211         # broadcast to the eager world
1212         send_wcy_spot($self, $line, $d, @_[2..12]);
1213 }
1214
1215 # remote commands (incoming)
1216 package Thingy::PC84;
1217 use vars qw(@ISA);
1218 @ISA = qw(Thingy);
1219
1220 sub handle
1221 {
1222         my $self = shift;
1223         my $dxchan = shift;
1224         $self->process_rcmd($_[1], $_[2], $_[3], $_[4]);
1225 }
1226
1227 # remote command replies
1228 package Thingy::PC85;
1229 use vars qw(@ISA);
1230 @ISA = qw(Thingy);
1231
1232 sub handle
1233 {
1234         my $self = shift;
1235         my $dxchan = shift;
1236         $self->process_rcmd_reply($_[1], $_[2], $_[3], $_[4]);
1237 }
1238          
1239 # if get here then rebroadcast the thing with its Hop count decremented (if
1240 # there is one). If it has a hop count and it decrements to zero then don't
1241 # rebroadcast it.
1242 #
1243 # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
1244 #        REBROADCAST!!!!
1245 #
1246
1247 package Thingy::PCdefault;
1248 use vars qw(@ISA);
1249 @ISA = qw(Thingy);
1250
1251 sub handle
1252 {
1253         my $self = shift;
1254         my $dxchan = shift;
1255
1256         if (eph_dup($line)) {
1257                 dbg("PCPROT: Ephemeral dup, dropped") if isdbg('chanerr');
1258         } else {
1259                 unless ($self->{isolate}) {
1260                         DXChannel::broadcast_nodes($line, $self); # send it to everyone but me
1261                 }
1262         }
1263 }
1264
1265 1;