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