added US State information on DX Spots
[spider.git] / perl / DXUser.pm
1 #
2 # DX cluster user routines
3 #
4 # Copyright (c) 1998 - Dirk Koopman G1TLH
5 #
6 # $Id$
7 #
8
9 package DXUser;
10
11 use DXLog;
12 use DB_File;
13 use Data::Dumper;
14 use Fcntl;
15 use IO::File;
16 use DXDebug;
17 use DXUtil;
18 use LRU;
19
20 use strict;
21
22 use vars qw($VERSION $BRANCH);
23 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /(\d+)\.(\d+)/ );
24 $BRANCH = sprintf( "%d.%03d", q$Revision$ =~ /\d+\.\d+\.(\d+)\.(\d+)/  || (0,0));
25 $main::build += $VERSION;
26 $main::branch += $BRANCH;
27
28 use vars qw(%u $dbm $filename %valid $lastoperinterval $lasttime $lru $lrusize $tooold $v3);
29
30 %u = ();
31 $dbm = undef;
32 $filename = undef;
33 $lastoperinterval = 60*24*60*60;
34 $lasttime = 0;
35 $lrusize = 2000;
36 $tooold = 86400 * 365;          # this marks an old user who hasn't given enough info to be useful
37 $v3 = 0;
38
39 # hash of valid elements and a simple prompt
40 %valid = (
41                   call => '0,Callsign',
42                   alias => '0,Real Callsign',
43                   name => '0,Name',
44                   qth => '0,Home QTH',
45                   lat => '0,Latitude,slat',
46                   long => '0,Longitude,slong',
47                   qra => '0,Locator',
48                   email => '0,E-mail Address,parray',
49                   priv => '9,Privilege Level',
50                   lastin => '0,Last Time in,cldatetime',
51                   passwd => '9,Password,yesno',
52                   passphrase => '9,Pass Phrase,yesno',
53                   addr => '0,Full Address',
54                   'sort' => '0,Type of User', # A - ak1a, U - User, S - spider cluster, B - BBS
55                   xpert => '0,Expert Status,yesno',
56                   bbs => '0,Home BBS',
57                   node => '0,Last Node',
58                   homenode => '0,Home Node',
59                   lockout => '9,Locked out?,yesno',     # won't let them in at all
60                   dxok => '9,Accept DX Spots?,yesno', # accept his dx spots?
61                   annok => '9,Accept Announces?,yesno', # accept his announces?
62                   lang => '0,Language',
63                   hmsgno => '0,Highest Msgno',
64                   group => '0,Access Group,parray',     # used to create a group of users/nodes for some purpose or other
65                   isolate => '9,Isolate network,yesno',
66                   wantbeep => '0,Req Beep,yesno',
67                   wantann => '0,Req Announce,yesno',
68                   wantwwv => '0,Req WWV,yesno',
69                   wantwcy => '0,Req WCY,yesno',
70                   wantecho => '0,Req Echo,yesno',
71                   wanttalk => '0,Req Talk,yesno',
72                   wantwx => '0,Req WX,yesno',
73                   wantdx => '0,Req DX Spots,yesno',
74                   wantemail => '0,Req Msgs as Email,yesno',
75                   pagelth => '0,Current Pagelth',
76                   pingint => '9,Node Ping interval',
77                   nopings => '9,Ping Obs Count',
78                   wantlogininfo => '9,Login info req,yesno',
79           wantgrid => '0,DX Grid Info,yesno',
80                   wantann_talk => '0,Talklike Anns,yesno',
81                   wantpc90 => '1,Req PC90,yesno',
82                   wantnp => '1,Req New Protocol,yesno',
83                   wantpc16 => '9,Want Users from node,yesno',
84                   wantsendpc16 => '9,Send PC16,yesno',
85                   wantroutepc19 => '9,Route PC19,yesno',
86                   wantusstate => '9,Show US State,yesno',
87                   lastoper => '9,Last for/oper,cldatetime',
88                   nothere => '0,Not Here Text',
89                   registered => '9,Registered?,yesno',
90                   prompt => '0,Required Prompt',
91                   version => '1,Version',
92                   build => '1,Build',
93                  );
94
95 #no strict;
96 sub AUTOLOAD
97 {
98         no strict;
99         my $name = $AUTOLOAD;
100   
101         return if $name =~ /::DESTROY$/;
102         $name =~ s/^.*:://o;
103   
104         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
105         # this clever line of code creates a subroutine which takes over from autoload
106         # from OO Perl - Conway
107         *$AUTOLOAD = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}};
108        goto &$AUTOLOAD;
109 }
110
111 #use strict;
112
113 #
114 # initialise the system
115 #
116 sub init
117 {
118         my ($pkg, $fn, $mode) = @_;
119   
120         confess "need a filename in User" if !$fn;
121
122         my $ufn;
123         my $convert;
124         
125         eval {
126                 require Storable;
127         };
128
129 #       eval "use Storable qw(nfreeze thaw)";
130         
131         if ($@) {
132                 $ufn = "$fn.v2";
133                 $v3 = $convert = 0;
134                 dbg("the module Storable appears to be missing!!");
135                 dbg("trying to continue in compatibility mode (this may fail)");
136                 dbg("please install Storable from CPAN as soon as possible");
137         } else {
138                 import Storable qw(nfreeze thaw);
139
140                 $ufn = "$fn.v3";
141                 $v3 = 1;
142                 $convert++ unless -e $ufn;
143         }
144         
145         if ($mode) {
146                 $dbm = tie (%u, 'DB_File', $ufn, O_CREAT|O_RDWR, 0666, $DB_BTREE) or confess "can't open user file: $fn ($!) [rebuild it from user_asc?]";
147         } else {
148                 $dbm = tie (%u, 'DB_File', $ufn, O_RDONLY, 0666, $DB_BTREE) or confess "can't open user file: $fn ($!) [rebuild it from user_asc?]";
149         }
150
151         $lru = LRU->newbase("DXUser", $lrusize);
152         
153         # do a conversion if required
154         if ($convert) {
155                 my ($key, $val, $action, $count, $err) = ('','',0,0,0);
156                 
157                 my %oldu;
158                 dbg("Converting the User File to V3 ");
159                 dbg("This will take a while, I suggest you go and have cup of strong tea");
160                 my $odbm = tie (%oldu, 'DB_File', "$fn.v2", O_RDONLY, 0666, $DB_BTREE) or confess "can't open user file: $fn.v2 ($!) [rebuild it from user_asc?]";
161         for ($action = R_FIRST; !$odbm->seq($key, $val, $action); $action = R_NEXT) {
162                         my $ref = asc_decode($val);
163                         if ($ref) {
164                                 $ref->put;
165                                 $count++;
166                         } else {
167                                 $err++
168                         }
169                 } 
170                 undef $odbm;
171                 untie %oldu;
172                 dbg("Conversion completed $count records $err errors");
173         }
174         $filename = $ufn;
175 }
176
177 sub del_file
178 {
179         my ($pkg, $fn) = @_;
180   
181         confess "need a filename in User" if !$fn;
182         $fn .= $v3 ? ".v3" : ".v2";
183         unlink $fn;
184 }
185
186 #
187 # periodic processing
188 #
189 sub process
190 {
191         if ($main::systime > $lasttime + 15) {
192                 $dbm->sync;
193                 $lasttime = $main::systime;
194         }
195 }
196
197 #
198 # close the system
199 #
200
201 sub finish
202 {
203         undef $dbm;
204         untie %u;
205 }
206
207 #
208 # new - create a new user
209 #
210
211 sub new
212 {
213         my $pkg = shift;
214         my $call = uc shift;
215         #  $call =~ s/-\d+$//o;
216   
217 #       confess "can't create existing call $call in User\n!" if $u{$call};
218
219         my $self = bless {}, $pkg;
220         $self->{call} = $call;
221         $self->{'sort'} = 'U';
222         $self->put;
223         return $self;
224 }
225
226 #
227 # get - get an existing user - this seems to return a different reference everytime it is
228 #       called - see below
229 #
230
231 sub get
232 {
233         my $pkg = shift;
234         my $call = uc shift;
235         my $data;
236         
237         # is it in the LRU cache?
238         my $ref = $lru->get($call);
239         return $ref if $ref;
240         
241         # search for it
242         unless ($dbm->get($call, $data)) {
243                 $ref = decode($data);
244                 $lru->put($call, $ref);
245                 return $ref;
246         }
247         return undef;
248 }
249
250 #
251 # get an existing either from the channel (if there is one) or from the database
252 #
253 # It is important to note that if you have done a get (for the channel say) and you
254 # want access or modify that you must use this call (and you must NOT use get's all
255 # over the place willy nilly!)
256 #
257
258 sub get_current
259 {
260         my $pkg = shift;
261         my $call = uc shift;
262   
263         my $dxchan = DXChannel->get($call);
264         return $dxchan->user if $dxchan;
265         my $rref = Route::get($call);
266         return $rref->user if $rref && exists $rref->{user};
267         return $pkg->get($call);
268 }
269
270 #
271 # get all callsigns in the database 
272 #
273
274 sub get_all_calls
275 {
276         return (sort keys %u);
277 }
278
279 #
280 # put - put a user
281 #
282
283 sub put
284 {
285         my $self = shift;
286         confess "Trying to put nothing!" unless $self && ref $self;
287         my $call = $self->{call};
288         # delete all instances of this 
289 #       for ($dbm->get_dup($call)) {
290 #               $dbm->del_dup($call, $_);
291 #       }
292         $dbm->del($call);
293         delete $self->{annok} if $self->{annok};
294         delete $self->{dxok} if $self->{dxok};
295         $lru->put($call, $self);
296         my $ref = $self->encode;
297         $dbm->put($call, $ref);
298 }
299
300 # freeze the user
301 sub encode
302 {
303         goto &asc_encode unless $v3;
304         my $self = shift;
305         return nfreeze($self);
306 }
307
308 # thaw the user
309 sub decode
310 {
311         goto &asc_decode unless $v3;
312         return thaw(shift);
313 }
314
315
316 # create a string from a user reference (in_ascii)
317 #
318 sub asc_encode
319 {
320         my $self = shift;
321         my $dd = new Data::Dumper([$self]);
322         $dd->Indent(0);
323         $dd->Terse(1);
324     $dd->Quotekeys($] < 5.005 ? 1 : 0);
325         return $dd->Dumpxs;
326 }
327
328 #
329 # create a hash from a string (in ascii)
330 #
331 sub asc_decode
332 {
333         my $s = shift;
334         my $ref;
335         eval '$ref = ' . $s;
336         if ($@) {
337                 dbg($@);
338                 Log('err', $@);
339                 $ref = undef;
340         }
341         return $ref;
342 }
343
344 #
345 # del - delete a user
346 #
347
348 sub del
349 {
350         my $self = shift;
351         my $call = $self->{call};
352         # delete all instances of this 
353 #       for ($dbm->get_dup($call)) {
354 #               $dbm->del_dup($call, $_);
355 #       }
356         $lru->remove($call);
357         $dbm->del($call);
358 }
359
360 #
361 # close - close down a user
362 #
363
364 sub close
365 {
366         my $self = shift;
367         $self->{lastin} = time;
368         $self->put();
369 }
370
371 #
372 # sync the database
373 #
374
375 sub sync
376 {
377         $dbm->sync;
378 }
379
380 #
381 # return a list of valid elements 
382
383
384 sub fields
385 {
386         return keys(%valid);
387 }
388
389
390 #
391 # export the database to an ascii file
392 #
393
394 sub export
395 {
396         my $fn = shift;
397         
398         # save old ones
399         rename "$fn.oooo", "$fn.ooooo" if -e "$fn.oooo";
400         rename "$fn.ooo", "$fn.oooo" if -e "$fn.ooo";
401         rename "$fn.oo", "$fn.ooo" if -e "$fn.oo";
402         rename "$fn.o", "$fn.oo" if -e "$fn.o";
403         rename "$fn", "$fn.o" if -e "$fn";
404
405         my $count = 0;
406         my $err = 0;
407         my $del = 0;
408         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
409         if ($fh) {
410                 my $key = 0;
411                 my $val = undef;
412                 my $action;
413                 my $t = scalar localtime;
414                 print $fh q{#!/usr/bin/perl
415 #
416 # The exported userfile for a DXSpider System
417 #
418 # Input file: $filename
419 #       Time: $t
420 #
421                         
422 package main;
423                         
424 # search local then perl directories
425 BEGIN {
426         umask 002;
427                                 
428         # root of directory tree for this system
429         $root = "/spider"; 
430         $root = $ENV{'DXSPIDER_ROOT'} if $ENV{'DXSPIDER_ROOT'};
431         
432         unshift @INC, "$root/perl";     # this IS the right way round!
433         unshift @INC, "$root/local";
434         
435         # try to detect a lockfile (this isn't atomic but 
436         # should do for now
437         $lockfn = "$root/local/cluster.lck";       # lock file name
438         if (-e $lockfn) {
439                 open(CLLOCK, "$lockfn") or die "Can't open Lockfile ($lockfn) $!";
440                 my $pid = <CLLOCK>;
441                 chomp $pid;
442                 die "Lockfile ($lockfn) and process $pid exists - cluster must be stopped first\n" if kill 0, $pid;
443                 close CLLOCK;
444         }
445 }
446
447 package DXUser;
448
449 use DXVars;
450 use DXUser;
451
452 if (@ARGV) {
453         $main::userfn = shift @ARGV;
454         print "user filename now $userfn\n";
455 }
456
457 DXUser->del_file($main::userfn);
458 DXUser->init($main::userfn, 1);
459 %u = ();
460 my $count = 0;
461 my $err = 0;
462 while (<DATA>) {
463         chomp;
464         my @f = split /\t/;
465         my $ref = asc_decode($f[1]);
466         if ($ref) {
467                 $ref->put();
468                 $count++;
469         } else {
470                 print "# Error: $f[0]\t$f[1]\n";
471                 $err++
472         }
473 }
474 DXUser->sync; DXUser->finish;
475 print "There are $count user records and $err errors\n";
476 };
477                 print $fh "__DATA__\n";
478
479         for ($action = R_FIRST; !$dbm->seq($key, $val, $action); $action = R_NEXT) {
480                         if (!is_callsign($key) || $key =~ /^0/) {
481                                 Log('DXCommand', "Export Error1: $key\t$val");
482                                 eval {$dbm->del($key)};
483                                 dbg(carp("Export Error1: $key\t$val\n$@")) if $@;
484                                 ++$err;
485                                 next;
486                         }
487                         my $ref = decode($val);
488                         if ($ref) {
489                                 my $t = $ref->{lastin} || 0;
490                                 if ($main::systime > $t + $tooold) {
491                                         unless ($ref->{lat} && $ref->{long} || $ref->{qth} || $ref->{qra}) {
492                                                 eval {$dbm->del($key)};
493                                                 dbg(carp("Export Error2: $key\t$val\n$@")) if $@;
494                                                 Log('DXCommand', "$ref->{call} deleted, too old");
495                                                 $del++;
496                                                 next;
497                                         }
498                                 }
499                                 # only store users that are reasonably active or have useful information
500                                 print $fh "$key\t" . $ref->asc_encode . "\n";
501                                 ++$count;
502                         } else {
503                                 Log('DXCommand', "Export Error3: $key\t$val");
504                                 eval {$dbm->del($key)};
505                                 dbg(carp("Export Error3: $key\t$val\n$@")) if $@;
506                                 ++$err;
507                         }
508                 } 
509         $fh->close;
510     } 
511         return "$count Users $del Deleted $err Errors ('sh/log Export' for details)";
512 }
513
514 #
515 # group handling
516 #
517
518 # add one or more groups
519 sub add_group
520 {
521         my $self = shift;
522         my $ref = $self->{group} || [ 'local' ];
523         $self->{group} = $ref if !$self->{group};
524         push @$ref, @_ if @_;
525 }
526
527 # remove one or more groups
528 sub del_group
529 {
530         my $self = shift;
531         my $ref = $self->{group} || [ 'local' ];
532         my @in = @_;
533         
534         $self->{group} = $ref if !$self->{group};
535         
536         @$ref = map { my $a = $_; return (grep { $_ eq $a } @in) ? () : $a } @$ref;
537 }
538
539 # does this thing contain all the groups listed?
540 sub union
541 {
542         my $self = shift;
543         my $ref = $self->{group};
544         my $n;
545         
546         return 0 if !$ref || @_ == 0;
547         return 1 if @$ref == 0 && @_ == 0;
548         for ($n = 0; $n < @_; ) {
549                 for (@$ref) {
550                         my $a = $_;
551                         $n++ if grep $_ eq $a, @_; 
552                 }
553         }
554         return $n >= @_;
555 }
556
557 # simplified group test just for one group
558 sub in_group
559 {
560         my $self = shift;
561         my $s = shift;
562         my $ref = $self->{group};
563         
564         return 0 if !$ref;
565         return grep $_ eq $s, $ref;
566 }
567
568 # set up a default group (only happens for them's that connect direct)
569 sub new_group
570 {
571         my $self = shift;
572         $self->{group} = [ 'local' ];
573 }
574
575 #
576 # return a prompt for a field
577 #
578
579 sub field_prompt
580
581         my ($self, $ele) = @_;
582         return $valid{$ele};
583 }
584
585 # some variable accessors
586 sub sort
587 {
588         my $self = shift;
589         @_ ? $self->{'sort'} = shift : $self->{'sort'} ;
590 }
591
592 # some accessors
593 sub _want
594 {
595         my $n = shift;
596         my $self = shift;
597         my $val = shift;
598         my $s = "want$n";
599         $self->{$s} = $val if defined $val;
600         return exists $self->{$s} ? $self->{$s} : 1;
601 }
602
603 sub wantbeep
604 {
605         return _want('beep', @_);
606 }
607
608 sub wantann
609 {
610         return _want('ann', @_);
611 }
612
613 sub wantwwv
614 {
615         return _want('wwv', @_);
616 }
617
618 sub wantwcy
619 {
620         return _want('wcy', @_);
621 }
622
623 sub wantecho
624 {
625         return _want('echo', @_);
626 }
627
628 sub wantwx
629 {
630         return _want('wx', @_);
631 }
632
633 sub wantdx
634 {
635         return _want('dx', @_);
636 }
637
638 sub wanttalk
639 {
640         return _want('talk', @_);
641 }
642
643 sub wantgrid
644 {
645         return _want('grid', @_);
646 }
647
648 sub wantemail
649 {
650         return _want('email', @_);
651 }
652
653 sub wantann_talk
654 {
655         return _want('ann_talk', @_);
656 }
657
658 sub wantpc16
659 {
660         return _want('pc16', @_);
661 }
662
663 sub wantsendpc16
664 {
665         return _want('sendpc16', @_);
666 }
667
668 sub wantroutepc16
669 {
670         return _want('routepc16', @_);
671 }
672
673 sub wantusstate
674 {
675         return _want('usstate', @_);
676 }
677
678 sub wantlogininfo
679 {
680         my $self = shift;
681         my $val = shift;
682         $self->{wantlogininfo} = $val if defined $val;
683         return $self->{wantlogininfo};
684 }
685
686 sub is_node
687 {
688         my $self = shift;
689         return $self->{sort} =~ /[ACRSX]/;
690 }
691
692 sub is_user
693 {
694         my $self = shift;
695         return $self->{sort} eq 'U';
696 }
697
698 sub is_bbs
699 {
700         my $self = shift;
701         return $self->{sort} eq 'B';
702 }
703
704 sub is_spider
705 {
706         my $self = shift;
707         return $self->{sort} eq 'S';
708 }
709
710 sub is_clx
711 {
712         my $self = shift;
713         return $self->{sort} eq 'C';
714 }
715
716 sub is_dxnet
717 {
718         my $self = shift;
719         return $self->{sort} eq 'X';
720 }
721
722 sub is_arcluster
723 {
724         my $self = shift;
725         return $self->{sort} eq 'R';
726 }
727
728 sub is_ak1a
729 {
730         my $self = shift;
731         return $self->{sort} eq 'A';
732 }
733
734 sub unset_passwd
735 {
736         my $self = shift;
737         delete $self->{passwd};
738 }
739
740 sub unset_passphrase
741 {
742         my $self = shift;
743         delete $self->{passphrase};
744 }
745 1;
746 __END__
747
748
749
750
751