WIP DXUser no storable, no db_file
[spider.git] / perl / DXUser.pm
1 #
2 # DX cluster user routines
3 #
4 # Copyright (c) 1998 - Dirk Koopman G1TLH
5 #
6 #
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 DXUtil;
17 use LRU;
18 use File::Copy;
19 use JSON;
20 use DXDebug;
21 use Data::Structure::Util qw(unbless);
22 use Time::HiRes qw(gettimeofday tv_interval);
23
24 use strict;
25
26 use vars qw(%u $dbm $filename %valid $lastoperinterval $lasttime $lru $lrusize $tooold $v3 $v4);
27
28 %u = ();
29 $dbm = undef;
30 $filename = undef;
31 $lastoperinterval = 60*24*60*60;
32 $lasttime = 0;
33 $lrusize = 2000;
34 $tooold = 86400 * 365 + 31;             # this marks an old user who hasn't given enough info to be useful
35 $v3 = 0;
36 $v4 = 0;
37 my $json;
38
39 our $maxconnlist = 3;                   # remember this many connection time (duration) [start, end] pairs
40
41 # hash of valid elements and a simple prompt
42 %valid = (
43                   call => '0,Callsign',
44                   alias => '0,Real Callsign',
45                   name => '0,Name',
46                   qth => '0,Home QTH',
47                   lat => '0,Latitude,slat',
48                   long => '0,Longitude,slong',
49                   qra => '0,Locator',
50                   email => '0,E-mail Address,parray',
51                   priv => '9,Privilege Level',
52                   lastin => '0,Last Time in,cldatetime',
53                   passwd => '9,Password,yesno',
54                   passphrase => '9,Pass Phrase,yesno',
55                   addr => '0,Full Address',
56                   'sort' => '0,Type of User', # A - ak1a, U - User, S - spider cluster, B - BBS
57                   xpert => '0,Expert Status,yesno',
58                   bbs => '0,Home BBS',
59                   node => '0,Last Node',
60                   homenode => '0,Home Node',
61                   lockout => '9,Locked out?,yesno',     # won't let them in at all
62                   dxok => '9,Accept DX Spots?,yesno', # accept his dx spots?
63                   annok => '9,Accept Announces?,yesno', # accept his announces?
64                   lang => '0,Language',
65                   hmsgno => '0,Highest Msgno',
66                   group => '0,Group,parray',    # used to create a group of users/nodes for some purpose or other
67                   buddies => '0,Buddies,parray',
68                   isolate => '9,Isolate network,yesno',
69                   wantbeep => '0,Req Beep,yesno',
70                   wantann => '0,Req Announce,yesno',
71                   wantwwv => '0,Req WWV,yesno',
72                   wantwcy => '0,Req WCY,yesno',
73                   wantecho => '0,Req Echo,yesno',
74                   wanttalk => '0,Req Talk,yesno',
75                   wantwx => '0,Req WX,yesno',
76                   wantdx => '0,Req DX Spots,yesno',
77                   wantemail => '0,Req Msgs as Email,yesno',
78                   pagelth => '0,Current Pagelth',
79                   pingint => '9,Node Ping interval',
80                   nopings => '9,Ping Obs Count',
81                   wantlogininfo => '0,Login Info Req,yesno',
82           wantgrid => '0,Show DX Grid,yesno',
83                   wantann_talk => '0,Talklike Anns,yesno',
84                   wantpc16 => '9,Want Users from node,yesno',
85                   wantsendpc16 => '9,Send PC16,yesno',
86                   wantroutepc19 => '9,Route PC19,yesno',
87                   wantusstate => '0,Show US State,yesno',
88                   wantdxcq => '0,Show CQ Zone,yesno',
89                   wantdxitu => '0,Show ITU Zone,yesno',
90                   wantgtk => '0,Want GTK interface,yesno',
91                   wantpc9x => '0,Want PC9X interface,yesno',
92                   lastoper => '9,Last for/oper,cldatetime',
93                   nothere => '0,Not Here Text',
94                   registered => '9,Registered?,yesno',
95                   prompt => '0,Required Prompt',
96                   version => '1,Version',
97                   build => '1,Build',
98                   believe => '1,Believable nodes,parray',
99                   lastping => '1,Last Ping at,ptimelist',
100                   maxconnect => '1,Max Connections',
101                   startt => '0,Start Time,cldatetime',
102                   connlist => '1,Connections,parraydifft',
103                  );
104
105 #no strict;
106 sub AUTOLOAD
107 {
108         no strict;
109         my $name = $AUTOLOAD;
110   
111         return if $name =~ /::DESTROY$/;
112         $name =~ s/^.*:://o;
113   
114         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
115         # this clever line of code creates a subroutine which takes over from autoload
116         # from OO Perl - Conway
117         *$AUTOLOAD = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}};
118        goto &$AUTOLOAD;
119 }
120
121 #use strict;
122
123 #
124 # initialise the system
125 #
126 sub init
127 {
128         my $mode = shift;
129   
130         my $ufn;
131         my $convert;
132         
133         my $fn = "users";
134
135         $json = JSON->new();
136         $filename = $ufn = localdata("$fn.json");
137         
138         if (-e localdata("$fn.json")) {
139                 $v4 = 1;
140         } else {
141                 eval {
142                         require Storable;
143                 };
144
145                 if ($@) {
146                         if ( ! -e localdata("users.v3") && -e localdata("users.v2") ) {
147                                 $convert = 2;
148                         }
149                         LogDbg('',"the module Storable appears to be missing!!");
150                         LogDbg('',"trying to continue in compatibility mode (this may fail)");
151                         LogDbg('',"please install Storable from CPAN as soon as possible");
152                 }
153                 else {
154                         import Storable qw(nfreeze thaw);
155                         $convert = 3 if -e localdata("users.v3") && !-e $ufn;
156                 }
157         }
158
159         # do a conversion if required
160         if ($convert) {
161                 my ($key, $val, $action, $count, $err) = ('','',0,0,0);
162                 my $ta = [gettimeofday];
163                 
164                 my %oldu;
165                 LogDbg('',"Converting the User File from V$convert to $fn.json ");
166                 LogDbg('',"This will take a while, I suggest you go and have cup of strong tea");
167                 my $odbm = tie (%oldu, 'DB_File', localdata("users.v$convert"), O_RDONLY, 0666, $DB_BTREE) or confess "can't open user file: $fn.v$convert ($!) [rebuild it from user_asc?]";
168         for ($action = R_FIRST; !$odbm->seq($key, $val, $action); $action = R_NEXT) {
169                         my $ref;
170                         if ($convert == 3) {
171                                 eval { $ref = storable_decode($val) };
172                         } else {
173                                 eval { $ref = asc_decode($val) };
174                         }
175                         unless ($@) {
176                                 if ($ref) {
177                                         $u{$key} = $ref;
178                                         $count++;
179                                 } else {
180                                         $err++
181                                 }
182                         } else {
183                                 Log('err', "DXUser: error decoding $@");
184                         }
185                 } 
186                 undef $odbm;
187                 untie %oldu;
188                 my $t = _diffms($ta);
189                 LogDbg('',"Conversion from users.v$convert to users.json completed $count records $err errors $t mS");
190
191                 # now write it away for future use
192                 $ta = [gettimeofday];
193                 $err = 0;
194                 $count = writeoutjson();
195                 $t = _diffms($ta);
196                 LogDbg('',"New Userfile users.json write completed $count records $err errors $t mS");
197                 LogDbg('',"Now restarting..");
198                 $main::ending = 10;
199         } else {
200                 # otherwise (i.e normally) slurp it in
201                 readinjson();
202         }
203         $filename = $ufn;
204 }
205
206 sub del_file
207 {
208         # with extreme prejudice
209         if ($v3) {
210                 unlink "$main::data/users.v3";
211                 unlink "$main::local_data/users.v3";
212         }
213         if ($v4) {
214                 unlink "$main::data/users.v4";
215                 unlink "$main::local_data/users.v4";
216         }
217 }
218
219 #
220 # periodic processing
221 #
222 sub process
223 {
224 #       if ($main::systime > $lasttime + 15) {
225 #               #$dbm->sync if $dbm;
226 #               $lasttime = $main::systime;
227 #       }
228 }
229
230 #
231 # close the system
232 #
233
234 sub finish
235 {
236         undef $dbm;
237         untie %u;
238 }
239
240 #
241 # new - create a new user
242 #
243
244 sub alloc
245 {
246         my $pkg = shift;
247         my $call = uc shift;
248         my $self = bless {call => $call, 'sort'=>'U'}, $pkg;
249         return $self;
250 }
251
252 sub new
253 {
254         my $pkg = shift;
255         my $call = shift;
256         #  $call =~ s/-\d+$//o;
257   
258 #       confess "can't create existing call $call in User\n!" if $u{$call};
259
260         my $self = $pkg->alloc($call);
261         $self->put;
262         return $self;
263 }
264
265 #
266 # get - get an existing user - this seems to return a different reference everytime it is
267 #       called - see below
268 #
269
270 sub get
271 {
272         my $call = uc shift;
273         my $data;
274         
275         # is it in the LRU cache?
276         my $ref = $u{$call} if exists $u{$call};
277 #       my $ref = $lru->get($call);
278         return $ref if $ref && ref $ref eq 'DXUser';
279         
280         # search for it
281         # unless ($dbm->get($call, $data)) {
282         #       eval { $ref = decode($data); };
283                 
284         #       if ($ref) {
285         #               if (!UNIVERSAL::isa($ref, 'DXUser')) {
286         #                       dbg("DXUser::get: got strange answer from decode of $call". ref $ref. " ignoring");
287         #                       return undef;
288         #               }
289         #               # we have a reference and it *is* a DXUser
290         #       } else {
291         #               if ($@) {
292         #                       LogDbg('err', "DXUser::get decode error on $call '$@'");
293         #               } else {
294         #                       dbg("DXUser::get: no reference returned from decode of $call $!");
295         #               }
296         #               return undef;
297         #       }
298         #       $lru->put($call, $ref);
299         #       return $ref;
300         # }
301         return undef;
302 }
303
304 #
305 # get an existing either from the channel (if there is one) or from the database
306 #
307 # It is important to note that if you have done a get (for the channel say) and you
308 # want access or modify that you must use this call (and you must NOT use get's all
309 # over the place willy nilly!)
310 #
311
312 sub get_current
313 {
314         my $call = uc shift;
315   
316         my $dxchan = DXChannel::get($call);
317         if ($dxchan) {
318                 my $ref = $dxchan->user;
319                 return $ref if $ref && UNIVERSAL::isa($ref, 'DXUser');
320
321                 dbg("DXUser::get_current: got invalid user ref for $call from dxchan $dxchan->{call} ". ref $ref. " ignoring");
322         }
323         return get($call);
324 }
325
326 #
327 # get all callsigns in the database 
328 #
329
330 sub get_all_calls
331 {
332         return (sort keys %u);
333 }
334
335 #
336 # put - put a user
337 #
338
339 sub put
340 {
341         my $self = shift;
342         confess "Trying to put nothing!" unless $self && ref $self;
343         my $call = $self->{call};
344         $self->{lastin} = $main::systime;
345 }
346
347 # freeze the user
348 sub encode
349 {
350         goto &json_encode if $v4;
351         goto &asc_encode unless $v3;
352         my $self = shift;
353         return nfreeze($self);
354 }
355
356 # thaw the user
357 sub decode
358 {
359         goto &json_decode if $v4;
360         goto &storable_decode if $v3;
361         goto &asc_decode;
362 }
363
364 # should now be obsolete for mojo branch build 238 and above
365 sub storable_decode
366 {
367         my $ref;
368         $ref = thaw(shift);
369         return $ref;
370 }
371
372
373 #
374 # create a hash from a string (in ascii)
375 #
376 sub asc_decode
377 {
378         my $s = shift;
379         my $ref;
380         $s =~ s/\%([0-9A-F][0-9A-F])/chr(hex($1))/eg;
381         eval '$ref = ' . $s;
382         if ($@) {
383                 LogDbg('err', "DXUser::asc_decode: on '$s' $@");
384                 $ref = undef;
385         }
386         return $ref;
387 }
388
389 sub json_decode
390 {
391         my $s = shift;
392     my $ref;
393         eval { $ref = $json->decode($s) };
394         if ($ref && !$@) {
395         return bless $ref, 'DXUser';
396         } else {
397                 LogDbg('err', "DXUser::json_decode: on '$s' $@");
398         }
399         return undef;
400 }
401
402 sub json_encode
403 {
404         my $ref = shift;
405         unbless($ref);
406     my $s = $json->encode($ref);
407         bless $ref, 'DXUser';
408         return $s;
409 }
410         
411 #
412 # del - delete a user
413 #
414
415 sub del
416 {
417         my $self = shift;
418         my $call = $self->{call};
419 #       $lru->remove($call);
420         #       $dbm->del($call);
421         delete $u{$call};
422 }
423
424 #
425 # close - close down a user
426 #
427
428 sub close
429 {
430         my $self = shift;
431         my $startt = shift;
432         my $ip = shift;
433         $self->{lastin} = $main::systime;
434         # add a record to the connect list
435         my $ref = [$startt || $self->{startt}, $main::systime];
436         push @$ref, $ip if $ip;
437         push @{$self->{connlist}}, $ref;
438         shift @{$self->{connlist}} if @{$self->{connlist}} > $maxconnlist;
439 #       $self->put();
440 }
441
442 #
443 # sync the database
444 #
445
446 sub sync
447 {
448 #       $dbm->sync;
449 }
450
451 #
452 # return a list of valid elements 
453
454
455 sub fields
456 {
457         return keys(%valid);
458 }
459
460
461 #
462 # export the database to an ascii file
463 #
464
465 sub export
466 {
467         my $name = shift;
468
469         my $fn = $name || "$main::local_data/user_json"; # force use of local_data
470         
471         # save old ones
472         move "$fn.oooo", "$fn.ooooo" if -e "$fn.oooo";
473         move "$fn.ooo", "$fn.oooo" if -e "$fn.ooo";
474         move "$fn.oo", "$fn.ooo" if -e "$fn.oo";
475         move "$fn.o", "$fn.oo" if -e "$fn.o";
476         move "$fn", "$fn.o" if -e "$fn";
477
478         my $json = JSON->new;
479         $json->canonical(1);
480         
481         my $count = 0;
482         my $err = 0;
483         my $del = 0;
484         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
485         if ($fh) {
486                 my $key = 0;
487                 my $val = undef;
488                 foreach my $k (sort keys %u) {
489                         my $r = $u{$k};
490                         if ($r->{sort} eq 'U' && !$r->{priv} && $main::systime > $r->{lastin}+$tooold ) {
491                                 unless ($r->{lat} || $r->{long} || $r->{qra} || $r->{qth} || $r->{name}) {
492                                         LogDbg('err', "DXUser::export deleting $k - too old, last in " . cldatetime($r->lastin) . " " . difft([$r->lastin, $main::systime]));
493                                         delete $u{$k};
494                                         ++$del;
495                                         next;
496                                 }
497                         }
498                         eval {$val = json_encode($r);};
499                         if ($@) {
500                                 LogDbg('err', "DXUser::export error encoding call: $k $@");
501                                 ++$err;
502                                 next;
503                         } 
504                         $fh->print("$k\t$val\n");
505                         ++$count;
506                 }
507         $fh->close;
508     }
509         my $s = qq{Exported users to $fn - $count Users $del Deleted $err Errors ('sh/log Export' for details)};
510         LogDbg('command', $s);
511         return $s;
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 # set up empty buddies (only happens for them's that connect direct)
576 sub new_buddies
577 {
578         my $self = shift;
579         $self->{buddies} = [  ];
580 }
581
582 #
583 # return a prompt for a field
584 #
585
586 sub field_prompt
587
588         my ($self, $ele) = @_;
589         return $valid{$ele};
590 }
591
592 # some variable accessors
593 sub sort
594 {
595         my $self = shift;
596         @_ ? $self->{'sort'} = shift : $self->{'sort'} ;
597 }
598
599 # some accessors
600
601 # want is default = 1
602 sub _want
603 {
604         my $n = shift;
605         my $self = shift;
606         my $val = shift;
607         my $s = "want$n";
608         $self->{$s} = $val if defined $val;
609         return exists $self->{$s} ? $self->{$s} : 1;
610 }
611
612 # wantnot is default = 0
613 sub _wantnot
614 {
615         my $n = shift;
616         my $self = shift;
617         my $val = shift;
618         my $s = "want$n";
619         $self->{$s} = $val if defined $val;
620         return exists $self->{$s} ? $self->{$s} : 0;
621 }
622
623 sub wantbeep
624 {
625         return _want('beep', @_);
626 }
627
628 sub wantann
629 {
630         return _want('ann', @_);
631 }
632
633 sub wantwwv
634 {
635         return _want('wwv', @_);
636 }
637
638 sub wantwcy
639 {
640         return _want('wcy', @_);
641 }
642
643 sub wantecho
644 {
645         return _want('echo', @_);
646 }
647
648 sub wantwx
649 {
650         return _want('wx', @_);
651 }
652
653 sub wantdx
654 {
655         return _want('dx', @_);
656 }
657
658 sub wanttalk
659 {
660         return _want('talk', @_);
661 }
662
663 sub wantgrid
664 {
665         return _want('grid', @_);
666 }
667
668 sub wantemail
669 {
670         return _want('email', @_);
671 }
672
673 sub wantann_talk
674 {
675         return _want('ann_talk', @_);
676 }
677
678 sub wantpc16
679 {
680         return _want('pc16', @_);
681 }
682
683 sub wantsendpc16
684 {
685         return _want('sendpc16', @_);
686 }
687
688 sub wantroutepc16
689 {
690         return _want('routepc16', @_);
691 }
692
693 sub wantusstate
694 {
695         return _want('usstate', @_);
696 }
697
698 sub wantdxcq
699 {
700         return _want('dxcq', @_);
701 }
702
703 sub wantdxitu
704 {
705         return _want('dxitu', @_);
706 }
707
708 sub wantgtk
709 {
710         return _want('gtk', @_);
711 }
712
713 sub wantpc9x
714 {
715         return _want('pc9x', @_);
716 }
717
718 sub wantlogininfo
719 {
720         my $self = shift;
721         my $val = shift;
722         $self->{wantlogininfo} = $val if defined $val;
723         return $self->{wantlogininfo};
724 }
725
726 sub is_node
727 {
728         my $self = shift;
729         return $self->{sort} =~ /^[ACRSX]$/;
730 }
731
732 sub is_local_node
733 {
734         my $self = shift;
735         return grep $_ eq 'local_node', @{$self->{group}};
736 }
737
738 sub is_user
739 {
740         my $self = shift;
741         return $self->{sort} =~ /^[UW]$/;
742 }
743
744 sub is_web
745 {
746         my $self = shift;
747         return $self->{sort} eq 'W';
748 }
749
750 sub is_bbs
751 {
752         my $self = shift;
753         return $self->{sort} eq 'B';
754 }
755
756 sub is_spider
757 {
758         my $self = shift;
759         return $self->{sort} eq 'S';
760 }
761
762 sub is_clx
763 {
764         my $self = shift;
765         return $self->{sort} eq 'C';
766 }
767
768 sub is_dxnet
769 {
770         my $self = shift;
771         return $self->{sort} eq 'X';
772 }
773
774 sub is_arcluster
775 {
776         my $self = shift;
777         return $self->{sort} eq 'R';
778 }
779
780 sub is_ak1a
781 {
782         my $self = shift;
783         return $self->{sort} eq 'A';
784 }
785
786 sub unset_passwd
787 {
788         my $self = shift;
789         delete $self->{passwd};
790 }
791
792 sub unset_passphrase
793 {
794         my $self = shift;
795         delete $self->{passphrase};
796 }
797
798 sub set_believe
799 {
800         my $self = shift;
801         my $call = uc shift;
802         $self->{believe} ||= [];
803         push @{$self->{believe}}, $call unless grep $_ eq $call, @{$self->{believe}};
804 }
805
806 sub unset_believe
807 {
808         my $self = shift;
809         my $call = uc shift;
810         if (exists $self->{believe}) {
811                 $self->{believe} = [grep {$_ ne $call} @{$self->{believe}}];
812                 delete $self->{believe} unless @{$self->{believe}};
813         }
814 }
815
816 sub believe
817 {
818         my $self = shift;
819         return exists $self->{believe} ? @{$self->{believe}} : ();
820 }
821
822 sub lastping
823 {
824         my $self = shift;
825         my $call = shift;
826         $self->{lastping} ||= {};
827         $self->{lastping} = {} unless ref $self->{lastping};
828         my $b = $self->{lastping};
829         $b->{$call} = shift if @_;
830         return $b->{$call};     
831 }
832
833 sub readinjson
834 {
835         my $fn = shift || $filename;
836         
837         my $ta = [gettimeofday];
838         my $count = 0;
839         my $s;
840         my $err = 0;
841
842         unless (-r $fn) {
843                 dbg("DXUser $fn not found - probably about to convert");
844                 return;
845         }
846         
847         open DATA, "$fn" or die "$fn read error $!";
848         while (<DATA>) {
849                 chomp;
850                 my @f = split /\t/;
851                 my $ref;
852                 eval { $ref = json_decode($f[1]); };
853                 if ($ref) {
854                         $u{$f[0]} = $ref;
855                         $count++;
856                 } else {
857                         LogDbg('DXCommand', "# readinjson Error: '$f[0]\t$f[1]' $@");
858                         $err++
859                 }
860         }
861         close DATA;
862         $s = _diffms($ta);
863         dbg("DXUser::readinjson $count records $s mS");
864 }
865
866 sub writeoutjson()
867 {
868         my $fn = shift || $filename;
869
870         link $fn, "$fn.o";
871         unlink $fn;
872         open DATA, ">$fn" or die "$fn write error $!";
873         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
874         my $count = 0;
875         if ($fh) {
876                 my $key = 0;
877                 my $val = undef;
878                 foreach my $k (keys %u) { # this is to make it as quick as possible (no sort)
879                         my $r = $u{$k};
880                         $val = json_encode($r);
881                         $fh->print("$k\t$val\n");
882                         ++$count;
883                 }
884         $fh->close;
885     }
886         close DATA;
887         return $count;
888 }
889 1;
890 __END__
891
892
893
894
895