de55f1b6e84d01ffa170d215fa91b759d02d0e50
[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         my $ref = $u{$call} if exists $u{$call};
276         return $ref if $ref && ref $ref eq 'DXUser';
277         
278         return undef;
279 }
280
281 #
282 # get an existing either from the channel (if there is one) or from the database
283 #
284 # It is important to note that if you have done a get (for the channel say) and you
285 # want access or modify that you must use this call (and you must NOT use get's all
286 # over the place willy nilly!)
287 #
288
289 sub get_current
290 {
291         my $call = uc shift;
292   
293         my $dxchan = DXChannel::get($call);
294         if ($dxchan) {
295                 my $ref = $dxchan->user;
296                 return $ref if $ref && UNIVERSAL::isa($ref, 'DXUser');
297
298                 dbg("DXUser::get_current: got invalid user ref for $call from dxchan $dxchan->{call} ". ref $ref. " ignoring");
299         }
300         return get($call);
301 }
302
303 #
304 # get all callsigns in the database 
305 #
306
307 sub get_all_calls
308 {
309         return (sort keys %u);
310 }
311
312 #
313 # put - put a user
314 #
315
316 sub put
317 {
318         my $self = shift;
319         confess "Trying to put nothing!" unless $self && ref $self;
320         my $call = $self->{call};
321         $self->{lastin} = $main::systime;
322 }
323
324 # freeze the user
325 sub encode
326 {
327         goto &json_encode if $v4;
328         goto &asc_encode unless $v3;
329         my $self = shift;
330         return nfreeze($self);
331 }
332
333 # thaw the user
334 sub decode
335 {
336         goto &json_decode if $v4;
337         goto &storable_decode if $v3;
338         goto &asc_decode;
339 }
340
341 # should now be obsolete for mojo branch build 238 and above
342 sub storable_decode
343 {
344         my $ref;
345         $ref = thaw(shift);
346         return $ref;
347 }
348
349
350 #
351 # create a hash from a string (in ascii)
352 #
353 sub asc_decode
354 {
355         my $s = shift;
356         my $ref;
357         $s =~ s/\%([0-9A-F][0-9A-F])/chr(hex($1))/eg;
358         eval '$ref = ' . $s;
359         if ($@) {
360                 LogDbg('err', "DXUser::asc_decode: on '$s' $@");
361                 $ref = undef;
362         }
363         return $ref;
364 }
365
366 sub json_decode
367 {
368         my $s = shift;
369     my $ref;
370         eval { $ref = $json->decode($s) };
371         if ($ref && !$@) {
372         return bless $ref, 'DXUser';
373         } else {
374                 LogDbg('err', "DXUser::json_decode: on '$s' $@");
375         }
376         return undef;
377 }
378
379 sub json_encode
380 {
381         my $ref = shift;
382         unbless($ref);
383     my $s = $json->encode($ref);
384         bless $ref, 'DXUser';
385         return $s;
386 }
387         
388 #
389 # del - delete a user
390 #
391
392 sub del
393 {
394         my $self = shift;
395         my $call = $self->{call};
396 #       $lru->remove($call);
397         #       $dbm->del($call);
398         delete $u{$call};
399 }
400
401 #
402 # close - close down a user
403 #
404
405 sub close
406 {
407         my $self = shift;
408         my $startt = shift;
409         my $ip = shift;
410         $self->{lastin} = $main::systime;
411         # add a record to the connect list
412         my $ref = [$startt || $self->{startt}, $main::systime];
413         push @$ref, $ip if $ip;
414         push @{$self->{connlist}}, $ref;
415         shift @{$self->{connlist}} if @{$self->{connlist}} > $maxconnlist;
416 #       $self->put();
417 }
418
419 #
420 # sync the database
421 #
422
423 sub sync
424 {
425 #       $dbm->sync;
426 }
427
428 #
429 # return a list of valid elements 
430
431
432 sub fields
433 {
434         return keys(%valid);
435 }
436
437
438 #
439 # export the database to an ascii file
440 #
441
442 sub export
443 {
444         my $name = shift;
445
446         my $fn = $name || localdata("user_json"); # force use of local_data
447         
448         # save old ones
449         move "$fn.oooo", "$fn.ooooo" if -e "$fn.oooo";
450         move "$fn.ooo", "$fn.oooo" if -e "$fn.ooo";
451         move "$fn.oo", "$fn.ooo" if -e "$fn.oo";
452         move "$fn.o", "$fn.oo" if -e "$fn.o";
453         move "$fn", "$fn.o" if -e "$fn";
454
455         my $json = JSON->new;
456         $json->canonical(1);
457         
458         my $count = 0;
459         my $err = 0;
460         my $del = 0;
461         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
462         if ($fh) {
463                 my $key = 0;
464                 my $val = undef;
465                 foreach my $k (sort keys %u) {
466                         my $r = $u{$k};
467                         if ($r->{sort} eq 'U' && !$r->{priv} && $main::systime > $r->{lastin}+$tooold ) {
468                                 unless ($r->{lat} || $r->{long} || $r->{qra} || $r->{qth} || $r->{name}) {
469                                         LogDbg('err', "DXUser::export deleting $k - too old, last in " . cldatetime($r->lastin) . " " . difft([$r->lastin, $main::systime]));
470                                         delete $u{$k};
471                                         ++$del;
472                                         next;
473                                 }
474                         }
475                         eval {$val = json_encode($r);};
476                         if ($@) {
477                                 LogDbg('err', "DXUser::export error encoding call: $k $@");
478                                 ++$err;
479                                 next;
480                         } 
481                         $fh->print("$k\t$val\n");
482                         ++$count;
483                 }
484         $fh->close;
485     }
486         my $s = qq{Exported users to $fn - $count Users $del Deleted $err Errors ('sh/log Export' for details)};
487         LogDbg('command', $s);
488         return $s;
489 }
490
491 #
492 # group handling
493 #
494
495 # add one or more groups
496 sub add_group
497 {
498         my $self = shift;
499         my $ref = $self->{group} || [ 'local' ];
500         $self->{group} = $ref if !$self->{group};
501         push @$ref, @_ if @_;
502 }
503
504 # remove one or more groups
505 sub del_group
506 {
507         my $self = shift;
508         my $ref = $self->{group} || [ 'local' ];
509         my @in = @_;
510         
511         $self->{group} = $ref if !$self->{group};
512         
513         @$ref = map { my $a = $_; return (grep { $_ eq $a } @in) ? () : $a } @$ref;
514 }
515
516 # does this thing contain all the groups listed?
517 sub union
518 {
519         my $self = shift;
520         my $ref = $self->{group};
521         my $n;
522         
523         return 0 if !$ref || @_ == 0;
524         return 1 if @$ref == 0 && @_ == 0;
525         for ($n = 0; $n < @_; ) {
526                 for (@$ref) {
527                         my $a = $_;
528                         $n++ if grep $_ eq $a, @_; 
529                 }
530         }
531         return $n >= @_;
532 }
533
534 # simplified group test just for one group
535 sub in_group
536 {
537         my $self = shift;
538         my $s = shift;
539         my $ref = $self->{group};
540         
541         return 0 if !$ref;
542         return grep $_ eq $s, $ref;
543 }
544
545 # set up a default group (only happens for them's that connect direct)
546 sub new_group
547 {
548         my $self = shift;
549         $self->{group} = [ 'local' ];
550 }
551
552 # set up empty buddies (only happens for them's that connect direct)
553 sub new_buddies
554 {
555         my $self = shift;
556         $self->{buddies} = [  ];
557 }
558
559 #
560 # return a prompt for a field
561 #
562
563 sub field_prompt
564
565         my ($self, $ele) = @_;
566         return $valid{$ele};
567 }
568
569 # some variable accessors
570 sub sort
571 {
572         my $self = shift;
573         @_ ? $self->{'sort'} = shift : $self->{'sort'} ;
574 }
575
576 # some accessors
577
578 # want is default = 1
579 sub _want
580 {
581         my $n = shift;
582         my $self = shift;
583         my $val = shift;
584         my $s = "want$n";
585         $self->{$s} = $val if defined $val;
586         return exists $self->{$s} ? $self->{$s} : 1;
587 }
588
589 # wantnot is default = 0
590 sub _wantnot
591 {
592         my $n = shift;
593         my $self = shift;
594         my $val = shift;
595         my $s = "want$n";
596         $self->{$s} = $val if defined $val;
597         return exists $self->{$s} ? $self->{$s} : 0;
598 }
599
600 sub wantbeep
601 {
602         return _want('beep', @_);
603 }
604
605 sub wantann
606 {
607         return _want('ann', @_);
608 }
609
610 sub wantwwv
611 {
612         return _want('wwv', @_);
613 }
614
615 sub wantwcy
616 {
617         return _want('wcy', @_);
618 }
619
620 sub wantecho
621 {
622         return _want('echo', @_);
623 }
624
625 sub wantwx
626 {
627         return _want('wx', @_);
628 }
629
630 sub wantdx
631 {
632         return _want('dx', @_);
633 }
634
635 sub wanttalk
636 {
637         return _want('talk', @_);
638 }
639
640 sub wantgrid
641 {
642         return _want('grid', @_);
643 }
644
645 sub wantemail
646 {
647         return _want('email', @_);
648 }
649
650 sub wantann_talk
651 {
652         return _want('ann_talk', @_);
653 }
654
655 sub wantpc16
656 {
657         return _want('pc16', @_);
658 }
659
660 sub wantsendpc16
661 {
662         return _want('sendpc16', @_);
663 }
664
665 sub wantroutepc16
666 {
667         return _want('routepc16', @_);
668 }
669
670 sub wantusstate
671 {
672         return _want('usstate', @_);
673 }
674
675 sub wantdxcq
676 {
677         return _want('dxcq', @_);
678 }
679
680 sub wantdxitu
681 {
682         return _want('dxitu', @_);
683 }
684
685 sub wantgtk
686 {
687         return _want('gtk', @_);
688 }
689
690 sub wantpc9x
691 {
692         return _want('pc9x', @_);
693 }
694
695 sub wantlogininfo
696 {
697         my $self = shift;
698         my $val = shift;
699         $self->{wantlogininfo} = $val if defined $val;
700         return $self->{wantlogininfo};
701 }
702
703 sub is_node
704 {
705         my $self = shift;
706         return $self->{sort} =~ /^[ACRSX]$/;
707 }
708
709 sub is_local_node
710 {
711         my $self = shift;
712         return grep $_ eq 'local_node', @{$self->{group}};
713 }
714
715 sub is_user
716 {
717         my $self = shift;
718         return $self->{sort} =~ /^[UW]$/;
719 }
720
721 sub is_web
722 {
723         my $self = shift;
724         return $self->{sort} eq 'W';
725 }
726
727 sub is_bbs
728 {
729         my $self = shift;
730         return $self->{sort} eq 'B';
731 }
732
733 sub is_spider
734 {
735         my $self = shift;
736         return $self->{sort} eq 'S';
737 }
738
739 sub is_clx
740 {
741         my $self = shift;
742         return $self->{sort} eq 'C';
743 }
744
745 sub is_dxnet
746 {
747         my $self = shift;
748         return $self->{sort} eq 'X';
749 }
750
751 sub is_arcluster
752 {
753         my $self = shift;
754         return $self->{sort} eq 'R';
755 }
756
757 sub is_ak1a
758 {
759         my $self = shift;
760         return $self->{sort} eq 'A';
761 }
762
763 sub unset_passwd
764 {
765         my $self = shift;
766         delete $self->{passwd};
767 }
768
769 sub unset_passphrase
770 {
771         my $self = shift;
772         delete $self->{passphrase};
773 }
774
775 sub set_believe
776 {
777         my $self = shift;
778         my $call = uc shift;
779         $self->{believe} ||= [];
780         push @{$self->{believe}}, $call unless grep $_ eq $call, @{$self->{believe}};
781 }
782
783 sub unset_believe
784 {
785         my $self = shift;
786         my $call = uc shift;
787         if (exists $self->{believe}) {
788                 $self->{believe} = [grep {$_ ne $call} @{$self->{believe}}];
789                 delete $self->{believe} unless @{$self->{believe}};
790         }
791 }
792
793 sub believe
794 {
795         my $self = shift;
796         return exists $self->{believe} ? @{$self->{believe}} : ();
797 }
798
799 sub lastping
800 {
801         my $self = shift;
802         my $call = shift;
803         $self->{lastping} ||= {};
804         $self->{lastping} = {} unless ref $self->{lastping};
805         my $b = $self->{lastping};
806         $b->{$call} = shift if @_;
807         return $b->{$call};     
808 }
809
810 sub readinjson
811 {
812         my $fn = shift || $filename;
813         
814         my $ta = [gettimeofday];
815         my $count = 0;
816         my $s;
817         my $err = 0;
818
819         unless (-r $fn) {
820                 dbg("DXUser $fn not found - probably about to convert");
821                 return;
822         }
823         
824         open DATA, "$fn" or die "$fn read error $!";
825         while (<DATA>) {
826                 chomp;
827                 my @f = split /\t/;
828                 my $ref;
829                 eval { $ref = json_decode($f[1]); };
830                 if ($ref) {
831                         $u{$f[0]} = $ref;
832                         $count++;
833                 } else {
834                         LogDbg('DXCommand', "# readinjson Error: '$f[0]\t$f[1]' $@");
835                         $err++
836                 }
837         }
838         close DATA;
839         $s = _diffms($ta);
840         dbg("DXUser::readinjson $count records $s mS");
841 }
842
843 sub writeoutjson()
844 {
845         my $fn = shift || $filename;
846
847         link $fn, "$fn.o";
848         unlink $fn;
849         open DATA, ">$fn" or die "$fn write error $!";
850         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
851         my $count = 0;
852         if ($fh) {
853                 my $key = 0;
854                 my $val = undef;
855                 foreach my $k (keys %u) { # this is to make it as quick as possible (no sort)
856                         my $r = $u{$k};
857                         $val = json_encode($r);
858                         $fh->print("$k\t$val\n");
859                         ++$count;
860                 }
861         $fh->close;
862     }
863         close DATA;
864         return $count;
865 }
866 1;
867 __END__
868
869
870
871
872