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