d9c3702085b759d59533344292c53d5c96290981
[spider.git] / perl / DXUser.pm
1 #
2 # DX cluster user routines
3 #
4 # Copyright (c) 1998-2020 - Dirk Koopman G1TLH
5 #
6 # The new internal structure of the users system looks like this:
7 #
8 # The users.v4 file formatted as a file of lines containing: <callsign>\t{json serialised version of user record}\n
9 #
10 # You can look at it with any text tools or your favourite editor :-)
11 #
12 # In terms of internal structure, the main user hash remains as %u, keyed on callsign as before.
13 #
14 # The value is a one or two element array [position] or [position, ref], depending on whether the record has been "get()ed"
15 # [i.e. got from disk] or not. The 'position' is simply the start of each line in the file. The function "get()" simply returns
16 # the stored reference in array[1], if present, or seeks to the  position from array[0], reads a line, json_decodes it,
17 # stores that reference into array[1] and returns that. That reference will be used from that time onwards.
18 #
19 # The routine writeoutjson() will (very) lazily write out a copy of %u WITHOUT STORING ANY EXTRA CURRENTLY UNREFERENCED CALLSIGN
20 # records to users.v4.n. It, in effect, does a sort of random accessed merge of the current user file and any "in memory"
21 # versions of any user record. This can be done with a spawned command because it will just be reading %u and merging
22 # loaded records, not altering the current users.v4 file in any way. 
23 #
24 # %u -> $u{call} -> [position of json line in users.v4 (, reference -> {call=>'G1TLH', ...} if this record is in use)].
25 #
26 # On my machine, it takes about 250mS to read the entire users.v4 file of 190,000 records and to create a
27 # $u{callsign}->[record position in users.v4] for every callsign in the users.v4 file. Loading ~19,000 records
28 # (read from disk, decode json, store reference) takes about 110mS (or 580nS/record).
29 #
30 # A periodic dump of users.v4.n, with said ~19,000 records in memory takes about 750mS to write (this can be speeded up,
31 # by at least a half, if it becomes a problem!). As this periodic dump will be spawned off, it will not interrupt the data
32 # stream.
33 #
34 # This is the first rewrite of DXUsers since inception. In the mojo branch we will no longer use Storable but use JSON instead.
35 # We will now be storing all the keys in memory and will use opportunistic loading of actual records in "get()". So out of
36 # say 200,000 known users it is unlikely that we will have more than 10% (more likely less) of the user records in memory.
37 # This will mean that there will be a increase in memory requirement, but it is modest. I estimate it's unlikely be more
38 # than 30 or so MB.
39 #
40 # At the moment that means that the working users.v4 is "immutable". 
41 #
42 # In normal operation, when first calling 'init()', the keys and positions will be read from the newer of users.v4.n and
43 # users.v4. If there is no users.v4.n, then users.v4 will be used. As time wears on, %u will then accrete active user records.
44 # Once an hour the current %u will be saved to users.v4.n.
45 #
46 # If it becomes too much of a problem then we are likely to chuck off "close()d" users onto the end of the current users.v4
47 # leaving existing users intact, but updating the pointer to the (now cleared out) user ref to the new location. This will
48 # be a sort of write behind log file. The users.v4 file is still immutable for the starting positions, but any chucked off
49 # records (or even "updates") will be written to the end of that file. If this has to be reread at any time, then the last
50 # entry for any callsign "wins". But this will only happen if I think the memory requirements over time become too much. 
51 #
52 # As there is no functional difference between the users.v4 and export_user generated "user_json" file(s), other than the latter
53 # will be in sorted order with the record elements in "canonical" order. There will now longer be any code to execute to
54 # "restore the users file". Simply copy one of the "user_json" files to users.v4, remove users.v4.n and restart. 
55 #
56 # Hopefully though, this will put to rest the need to do all that messing about ever again... Pigs may well be seen flying over
57 # your node as well :-)
58 #
59
60 package DXUser;
61
62 use DXLog;
63 use DB_File;
64 use Data::Dumper;
65 use Fcntl;
66 use IO::File;
67 use DXUtil;
68 use LRU;
69 use File::Copy;
70 use JSON;
71 use DXDebug;
72 use Data::Structure::Util qw(unbless);
73 use Time::HiRes qw(gettimeofday tv_interval);
74 use IO::File;
75
76 use strict;
77
78 use vars qw(%u  $filename %valid $lastoperinterval $lasttime $lru $lrusize $tooold $v3 $v4);
79
80 %u = ();
81 $filename = undef;
82 $lastoperinterval = 60*24*60*60;
83 $lasttime = 0;
84 $lrusize = 2000;
85 $tooold = 86400 * 365 + 31;             # this marks an old user who hasn't given enough info to be useful
86 $v3 = 0;
87 $v4 = 0;
88 my $json;
89
90 our $maxconnlist = 3;                   # remember this many connection time (duration) [start, end] pairs
91
92 our $newusers = 0;                                      # per execution stats
93 our $modusers = 0;
94 our $totusers = 0;
95 our $delusers = 0;
96 our $cachedusers = 0;
97
98 my $ifh;                                                # the input file, initialised by readinjson()
99
100
101 # hash of valid elements and a simple prompt
102 %valid = (
103                   call => '0,Callsign',
104                   alias => '0,Real Callsign',
105                   name => '0,Name',
106                   qth => '0,Home QTH',
107                   lat => '0,Latitude,slat',
108                   long => '0,Longitude,slong',
109                   qra => '0,Locator',
110                   email => '0,E-mail Address,parray',
111                   priv => '9,Privilege Level',
112                   lastin => '0,Last Time in,cldatetime',
113                   passwd => '9,Password,yesno',
114                   passphrase => '9,Pass Phrase,yesno',
115                   addr => '0,Full Address',
116                   'sort' => '0,Type of User', # A - ak1a, U - User, S - spider cluster, B - BBS
117                   xpert => '0,Expert Status,yesno',
118                   bbs => '0,Home BBS',
119                   node => '0,Last Node',
120                   homenode => '0,Home Node',
121                   lockout => '9,Locked out?,yesno',     # won't let them in at all
122                   dxok => '9,Accept DX Spots?,yesno', # accept his dx spots?
123                   annok => '9,Accept Announces?,yesno', # accept his announces?
124                   lang => '0,Language',
125                   hmsgno => '0,Highest Msgno',
126                   group => '0,Group,parray',    # used to create a group of users/nodes for some purpose or other
127                   buddies => '0,Buddies,parray',
128                   isolate => '9,Isolate network,yesno',
129                   wantbeep => '0,Req Beep,yesno',
130                   wantann => '0,Req Announce,yesno',
131                   wantwwv => '0,Req WWV,yesno',
132                   wantwcy => '0,Req WCY,yesno',
133                   wantecho => '0,Req Echo,yesno',
134                   wanttalk => '0,Req Talk,yesno',
135                   wantwx => '0,Req WX,yesno',
136                   wantdx => '0,Req DX Spots,yesno',
137                   wantemail => '0,Req Msgs as Email,yesno',
138                   pagelth => '0,Current Pagelth',
139                   pingint => '9,Node Ping interval',
140                   nopings => '9,Ping Obs Count',
141                   wantlogininfo => '0,Login Info Req,yesno',
142           wantgrid => '0,Show DX Grid,yesno',
143                   wantann_talk => '0,Talklike Anns,yesno',
144                   wantpc16 => '9,Want Users from node,yesno',
145                   wantsendpc16 => '9,Send PC16,yesno',
146                   wantroutepc19 => '9,Route PC19,yesno',
147                   wantusstate => '0,Show US State,yesno',
148                   wantdxcq => '0,Show CQ Zone,yesno',
149                   wantdxitu => '0,Show ITU Zone,yesno',
150                   wantgtk => '0,Want GTK interface,yesno',
151                   wantpc9x => '0,Want PC9X interface,yesno',
152                   lastoper => '9,Last for/oper,cldatetime',
153                   nothere => '0,Not Here Text',
154                   registered => '9,Registered?,yesno',
155                   prompt => '0,Required Prompt',
156                   version => '1,Version',
157                   build => '1,Build',
158                   believe => '1,Believable nodes,parray',
159                   lastping => '1,Last Ping at,ptimelist',
160                   maxconnect => '1,Max Connections',
161                   startt => '0,Start Time,cldatetime',
162                   connlist => '1,Connections,parraydifft',
163                  );
164
165 #no strict;
166 sub AUTOLOAD
167 {
168         no strict;
169         my $name = $AUTOLOAD;
170   
171         return if $name =~ /::DESTROY$/;
172         $name =~ s/^.*:://o;
173   
174         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
175         # this clever line of code creates a subroutine which takes over from autoload
176         # from OO Perl - Conway
177         *$AUTOLOAD = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}};
178        goto &$AUTOLOAD;
179 }
180
181 #use strict;
182
183 #
184 # initialise the system
185 #
186 sub init
187 {
188         my $mode = shift;
189   
190         my $ufn;
191         my $convert;
192         
193         my $fn = "users";
194
195         $json = JSON->new()->canonical(1);
196         $filename = $ufn = localdata("$fn.v4");
197         
198         if (-e localdata("$fn.v4")) {
199                 $v4 = 1;
200         } else {
201                 eval {
202                         require Storable;
203                 };
204
205                 if ($@) {
206                         if ( ! -e localdata("users.v3") && -e localdata("users.v2") ) {
207                                 $convert = 2;
208                         }
209                         LogDbg('',"the module Storable appears to be missing!!");
210                         LogDbg('',"trying to continue in compatibility mode (this may fail)");
211                         LogDbg('',"please install Storable from CPAN as soon as possible");
212                 } else {
213                         import Storable qw(nfreeze thaw);
214                         $convert = 3 if -e localdata("users.v3") && !-e $ufn;
215                 }
216         }
217
218         # do a conversion if required
219         if ($convert) {
220                 my ($key, $val, $action, $count, $err) = ('','',0,0,0);
221                 my $ta = [gettimeofday];
222                 
223                 my %oldu;
224                 LogDbg('',"Converting the User File from V$convert to $fn.v4 ");
225                 LogDbg('',"This will take a while, I suggest you go and have cup of strong tea");
226                 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?]";
227         for ($action = R_FIRST; !$odbm->seq($key, $val, $action); $action = R_NEXT) {
228                         my $ref;
229                         if ($convert == 3) {
230                                 eval { $ref = storable_decode($val) };
231                         } else {
232                                 eval { $ref = asc_decode($val) };
233                         }
234                         unless ($@) {
235                                 if ($ref) {
236                                         $u{$key} = $ref;
237                                         $count++;
238                                 } else {
239                                         $err++
240                                 }
241                         } else {
242                                 Log('err', "DXUser: error decoding $@");
243                         }
244                 } 
245                 undef $odbm;
246                 untie %oldu;
247                 my $t = _diffms($ta);
248                 LogDbg('',"Conversion from users.v$convert to users.v4 completed $count records $err errors $t mS");
249
250                 # now write it away for future use
251                 $ta = [gettimeofday];
252                 $err = 0;
253                 $count = writeoutjson();
254                 $t = _diffms($ta);
255                 LogDbg('',"New Userfile users.v4 write completed $count records $err errors $t mS");
256                 LogDbg('',"Now restarting..");
257                 $main::ending = 10;
258         } else {
259                 # otherwise (i.e normally) slurp it in
260                 readinjson();
261         }
262         $filename = $ufn;
263 }
264
265 sub del_file
266 {
267         # with extreme prejudice
268         if ($v3) {
269                 unlink "$main::data/users.v3";
270                 unlink "$main::local_data/users.v3";
271         }
272         if ($v4) {
273                 unlink "$main::data/users.v4";
274                 unlink "$main::local_data/users.v4";
275         }
276 }
277
278 #
279 # periodic processing
280 #
281 sub process
282 {
283 #       if ($main::systime > $lasttime + 15) {
284 #               #$dbm->sync if $dbm;
285 #               $lasttime = $main::systime;
286 #       }
287 }
288
289 #
290 # close the system
291 #
292
293 sub finish
294 {
295         
296         writeoutjson();
297 }
298
299 #
300 # new - create a new user
301 #
302
303 sub alloc
304 {
305         my $pkg = shift;
306         my $call = uc shift;
307         my $self = bless {call => $call, 'sort'=>'U'}, $pkg;
308         return $self;
309 }
310
311 sub new
312 {
313         my $pkg = shift;
314         my $call = shift;
315         #  $call =~ s/-\d+$//o;
316   
317         confess "can't create existing call $call in User\n!" if $u{$call};
318
319         my $self = $pkg->alloc($call);
320         $u{$call} = [0, $self];
321         $self->put;
322         ++$newusers;
323         ++$totusers;
324         return $self;
325 }
326
327 #
328 # get - get an existing user - this seems to return a different reference everytime it is
329 #       called - see below
330 #
331
332 sub get
333 {
334         my $call = uc shift;
335         my $nodecode = shift;
336         my $ref = $u{$call};
337         return undef unless $ref;
338         
339         unless ($ref->[1]) {
340                 $ifh->seek($ref->[0], 0);
341                 my $l = $ifh->getline;
342                 if ($l) {
343                         my ($k,$s) = split /\t/, $l;
344                         return $s if $nodecode;
345                         my $j = json_decode($s);
346                         if ($j) {
347                                 $ref->[1] = $j;
348                                 ++$cachedusers;
349                         }
350                 }
351         } elsif ($nodecode) {
352                 return json_encode($ref->[1]);
353         }
354         return $ref->[1];
355 }
356
357 #
358 # get an "ephemeral" reference - i.e. this will give you new temporary copy of
359 # the call's user record, but without storing it (if it isn't already there)
360 #
361 # This is not as quick as get()! But it will allow safe querying of the
362 # user file. Probably in conjunction with get_some_calls feeding it.
363 #
364 # NOTE: for cached records this, in effect, is a faster version of Storable's
365 # dclone - only about 3.5 times as fast!
366 #
367
368 sub get_tmp
369 {
370         my $call = uc shift;
371         my $ref = $u{$call};
372         if ($ref) {
373                 if ($ref->[1]) {
374                         return json_decode(json_encode($ref->[1]));
375                 }
376                 $ifh->seek($ref->[0], 0);
377                 my $l = $ifh->getline;
378                 if ($l) {
379                         my ($k,$s) = split /\t/, $l;
380                         my $j = json_decode($s);
381                         return $j;
382                 }
383         }
384         return undef;
385 }
386
387 #
388 # Master branch:
389 # get an existing record either from the channel (if there is one) or from the database
390 #
391 # It is important to note that if you have done a get (for the channel say) and you
392 # want access or modify that you must use this call (and you must NOT use get's all
393 # over the place willy nilly!)
394 #
395 # NOTE: mojo branch with newusers system:
396 # There is no longer any function difference between get_current()
397 # and get() as they will always reference the same record as held in %u. This is because
398 # there is no more (repeated) thawing of stored records from the underlying "database".
399 #
400 # BUT: notice the difference between this and the get_tmp() function. A get() will online an
401 # othewise unused record, so for poking around looking for that locked out user:
402 # MAKE SURE you use get_tmp(). It will likely still be quicker than DB_File and Storable!
403 #
404
405 sub get_current
406 {
407         goto &get;
408         
409 #       my $call = uc shift;
410 #  
411 #       my $dxchan = DXChannel::get($call);
412 #       if ($dxchan) {
413 #               my $ref = $dxchan->user;
414 #               return $ref if $ref && UNIVERSAL::isa($ref, 'DXUser');
415 #
416 #               dbg("DXUser::get_current: got invalid user ref for $call from dxchan $dxchan->{call} ". ref $ref. " ignoring");
417 #       }
418 #       return get($call);
419 }
420
421 #
422 # get all callsigns in the database 
423 #
424
425 sub get_all_calls
426 {
427         return (sort keys %u);
428 }
429
430 #
431 # get some calls - provide a qr// style selector string as a partial key
432 #
433
434 sub get_some_calls
435 {
436         my $pattern = shift || qr/.*/;
437         return sort grep {$pattern} keys %u;
438 }
439
440 #
441 # if I understand the term correctly, this is a sort of monad.
442 #
443 # Scan through the whole user file and select records that you want
444 # to process further. This routine returns lines of json, yu
445 #
446 # the CODE ref should look like:
447 # sub {
448 #   my $key = shift;
449 #       my $line = shift;
450 #   # maybe do a rough check to see if this is a likely candidate
451 #   return unless $line =~ /something/;
452 #   my $r = json_decode($l);
453 #       return (condition ? wanted thing : ());
454 # }
455 #
456         
457 sub scan
458 {
459         my $c = shift;
460         my @out;
461         
462         if (ref($c) eq 'CODE') {
463                 foreach my $k (get_all_calls()) {
464                         my $l = get($k, 1);     # get the offline json line or the jsoned online version
465                         push @out, $c->($k, $l) if $l;
466                 }
467         } else {
468                 dbg("DXUser::scan supplied argument is not a code ref");
469         }
470         return @out;
471 }
472
473 #
474 # put - put a user
475 #
476
477 sub put
478 {
479         my $self = shift;
480         confess "Trying to put nothing!" unless $self && ref $self;
481         $self->{lastin} = $main::systime;
482         ++$modusers;                            # new or existing, it's still been modified
483 }
484
485 # freeze the user
486 sub encode
487 {
488         goto &json_encode if $v4;
489         goto &asc_encode unless $v3;
490         my $self = shift;
491         return nfreeze($self);
492 }
493
494 # thaw the user
495 sub decode
496 {
497         goto &json_decode if $v4;
498         goto &storable_decode if $v3;
499         goto &asc_decode;
500 }
501
502 # should now be obsolete for mojo branch build 238 and above
503 sub storable_decode
504 {
505         my $ref;
506         $ref = thaw(shift);
507         return $ref;
508 }
509
510
511 #
512 # create a hash from a string (in ascii)
513 #
514 sub asc_decode
515 {
516         my $s = shift;
517         my $ref;
518         $s =~ s/\%([0-9A-F][0-9A-F])/chr(hex($1))/eg;
519         eval '$ref = ' . $s;
520         if ($@) {
521                 LogDbg('err', "DXUser::asc_decode: on '$s' $@");
522                 $ref = undef;
523         }
524         return $ref;
525 }
526
527 sub json_decode
528 {
529         my $s = shift;
530     my $ref;
531         eval { $ref = $json->decode($s) };
532         if ($ref && !$@) {
533         return bless $ref, 'DXUser';
534         } else {
535                 LogDbg('err', "DXUser::json_decode: on '$s' $@");
536         }
537         return undef;
538 }
539
540 sub json_encode
541 {
542         my $ref = shift;
543         unbless($ref);
544     my $s = $json->encode($ref);
545         bless $ref, 'DXUser';
546         return $s;
547 }
548         
549 #
550 # del - delete a user
551 #
552
553 sub del
554 {
555         my $self = shift;
556         my $call = $self->{call};
557         ++$delusers;
558         --$totusers;
559         --$cachedusers if $u{$call}->[1];
560         delete $u{$call};
561 }
562
563 #
564 # close - close down a user
565 #
566
567 sub close
568 {
569         my $self = shift;
570         my $startt = shift;
571         my $ip = shift;
572         $self->{lastin} = $main::systime;
573         # add a record to the connect list
574 #       $self->put();
575 }
576
577 #
578 # sync the database
579 #
580
581 sub sync
582 {
583 #       $dbm->sync;
584 }
585
586 #
587 # return a list of valid elements 
588
589
590 sub fields
591 {
592         return keys(%valid);
593 }
594
595
596 #
597 # export the database to an ascii file
598 #
599
600 sub export
601 {
602         my $name = shift;
603
604         my $fn = $name || localdata("user_json"); # force use of local_data
605         
606         # save old ones
607         move "$fn.oooo", "$fn.ooooo" if -e "$fn.oooo";
608         move "$fn.ooo", "$fn.oooo" if -e "$fn.ooo";
609         move "$fn.oo", "$fn.ooo" if -e "$fn.oo";
610         move "$fn.o", "$fn.oo" if -e "$fn.o";
611         move "$fn", "$fn.o" if -e "$fn";
612
613         my $json = JSON->new;
614         $json->canonical(1);
615         
616         my $count = 0;
617         my $err = 0;
618         my $del = 0;
619         my $fh = new IO::File ">$fn" or return "cannot open $fn ($!)";
620         if ($fh) {
621                 my $key = 0;
622                 my $val = undef;
623                 foreach my $k (sort keys %u) {
624                         my $r = $u{$k};
625                         if ($r->{sort} eq 'U' && !$r->{priv} && $main::systime > $r->{lastin}+$tooold ) {
626                                 unless ($r->{lat} || $r->{long} || $r->{qra} || $r->{qth} || $r->{name}) {
627                                         LogDbg('err', "DXUser::export deleting $k - too old, last in " . cldatetime($r->lastin) . " " . difft([$r->lastin, $main::systime]));
628                                         delete $u{$k};
629                                         ++$del;
630                                         next;
631                                 }
632                         }
633                         eval {$val = json_encode($r);};
634                         if ($@) {
635                                 LogDbg('err', "DXUser::export error encoding call: $k $@");
636                                 ++$err;
637                                 next;
638                         } 
639                         $fh->print("$k\t$val\n");
640                         ++$count;
641                 }
642         $fh->close;
643     }
644         my $s = qq{Exported users to $fn - $count Users $del Deleted $err Errors ('sh/log Export' for details)};
645         LogDbg('command', $s);
646         return $s;
647 }
648
649 #
650 # group handling
651 #
652
653 # add one or more groups
654 sub add_group
655 {
656         my $self = shift;
657         my $ref = $self->{group} || [ 'local' ];
658         $self->{group} = $ref if !$self->{group};
659         push @$ref, @_ if @_;
660 }
661
662 # remove one or more groups
663 sub del_group
664 {
665         my $self = shift;
666         my $ref = $self->{group} || [ 'local' ];
667         my @in = @_;
668         
669         $self->{group} = $ref if !$self->{group};
670         
671         @$ref = map { my $a = $_; return (grep { $_ eq $a } @in) ? () : $a } @$ref;
672 }
673
674 # does this thing contain all the groups listed?
675 sub union
676 {
677         my $self = shift;
678         my $ref = $self->{group};
679         my $n;
680         
681         return 0 if !$ref || @_ == 0;
682         return 1 if @$ref == 0 && @_ == 0;
683         for ($n = 0; $n < @_; ) {
684                 for (@$ref) {
685                         my $a = $_;
686                         $n++ if grep $_ eq $a, @_; 
687                 }
688         }
689         return $n >= @_;
690 }
691
692 # simplified group test just for one group
693 sub in_group
694 {
695         my $self = shift;
696         my $s = shift;
697         my $ref = $self->{group};
698         
699         return 0 if !$ref;
700         return grep $_ eq $s, $ref;
701 }
702
703 # set up a default group (only happens for them's that connect direct)
704 sub new_group
705 {
706         my $self = shift;
707         $self->{group} = [ 'local' ];
708 }
709
710 # set up empty buddies (only happens for them's that connect direct)
711 sub new_buddies
712 {
713         my $self = shift;
714         $self->{buddies} = [  ];
715 }
716
717 #
718 # return a prompt for a field
719 #
720
721 sub field_prompt
722
723         my ($self, $ele) = @_;
724         return $valid{$ele};
725 }
726
727 # some variable accessors
728 sub sort
729 {
730         my $self = shift;
731         @_ ? $self->{'sort'} = shift : $self->{'sort'} ;
732 }
733
734 # some accessors
735
736 # want is default = 1
737 sub _want
738 {
739         my $n = shift;
740         my $self = shift;
741         my $val = shift;
742         my $s = "want$n";
743         $self->{$s} = $val if defined $val;
744         return exists $self->{$s} ? $self->{$s} : 1;
745 }
746
747 # wantnot is default = 0
748 sub _wantnot
749 {
750         my $n = shift;
751         my $self = shift;
752         my $val = shift;
753         my $s = "want$n";
754         $self->{$s} = $val if defined $val;
755         return exists $self->{$s} ? $self->{$s} : 0;
756 }
757
758 sub wantbeep
759 {
760         return _want('beep', @_);
761 }
762
763 sub wantann
764 {
765         return _want('ann', @_);
766 }
767
768 sub wantwwv
769 {
770         return _want('wwv', @_);
771 }
772
773 sub wantwcy
774 {
775         return _want('wcy', @_);
776 }
777
778 sub wantecho
779 {
780         return _want('echo', @_);
781 }
782
783 sub wantwx
784 {
785         return _want('wx', @_);
786 }
787
788 sub wantdx
789 {
790         return _want('dx', @_);
791 }
792
793 sub wanttalk
794 {
795         return _want('talk', @_);
796 }
797
798 sub wantgrid
799 {
800         return _want('grid', @_);
801 }
802
803 sub wantemail
804 {
805         return _want('email', @_);
806 }
807
808 sub wantann_talk
809 {
810         return _want('ann_talk', @_);
811 }
812
813 sub wantpc16
814 {
815         return _want('pc16', @_);
816 }
817
818 sub wantsendpc16
819 {
820         return _want('sendpc16', @_);
821 }
822
823 sub wantroutepc16
824 {
825         return _want('routepc16', @_);
826 }
827
828 sub wantusstate
829 {
830         return _want('usstate', @_);
831 }
832
833 sub wantdxcq
834 {
835         return _want('dxcq', @_);
836 }
837
838 sub wantdxitu
839 {
840         return _want('dxitu', @_);
841 }
842
843 sub wantgtk
844 {
845         return _want('gtk', @_);
846 }
847
848 sub wantpc9x
849 {
850         return _want('pc9x', @_);
851 }
852
853 sub wantlogininfo
854 {
855         my $self = shift;
856         my $val = shift;
857         $self->{wantlogininfo} = $val if defined $val;
858         return $self->{wantlogininfo};
859 }
860
861 sub is_node
862 {
863         my $self = shift;
864         return $self->{sort} =~ /^[ACRSX]$/;
865 }
866
867 sub is_local_node
868 {
869         my $self = shift;
870         return grep $_ eq 'local_node', @{$self->{group}};
871 }
872
873 sub is_user
874 {
875         my $self = shift;
876         return $self->{sort} =~ /^[UW]$/;
877 }
878
879 sub is_web
880 {
881         my $self = shift;
882         return $self->{sort} eq 'W';
883 }
884
885 sub is_bbs
886 {
887         my $self = shift;
888         return $self->{sort} eq 'B';
889 }
890
891 sub is_spider
892 {
893         my $self = shift;
894         return $self->{sort} eq 'S';
895 }
896
897 sub is_clx
898 {
899         my $self = shift;
900         return $self->{sort} eq 'C';
901 }
902
903 sub is_dxnet
904 {
905         my $self = shift;
906         return $self->{sort} eq 'X';
907 }
908
909 sub is_arcluster
910 {
911         my $self = shift;
912         return $self->{sort} eq 'R';
913 }
914
915 sub is_ak1a
916 {
917         my $self = shift;
918         return $self->{sort} eq 'A';
919 }
920
921 sub unset_passwd
922 {
923         my $self = shift;
924         delete $self->{passwd};
925         $self->put;
926 }
927
928 sub unset_passphrase
929 {
930         my $self = shift;
931         delete $self->{passphrase};
932         $self->put;
933 }
934
935 sub set_believe
936 {
937         my $self = shift;
938         my $call = uc shift;
939         $self->{believe} ||= [];
940         unless (grep $_ eq $call, @{$self->{believe}}) {
941                 push @{$self->{believe}}, $call;
942                 $self->put;
943         };
944 }
945
946 sub unset_believe
947 {
948         my $self = shift;
949         my $call = uc shift;
950         if (exists $self->{believe}) {
951                 $self->{believe} = [grep {$_ ne $call} @{$self->{believe}}];
952                 delete $self->{believe} unless @{$self->{believe}};
953                 $self->put;
954         }
955 }
956
957 sub believe
958 {
959         my $self = shift;
960         return exists $self->{believe} ? @{$self->{believe}} : ();
961 }
962
963 sub lastping
964 {
965         my $self = shift;
966         my $call = shift;
967         $self->{lastping} ||= {};
968         $self->{lastping} = {} unless ref $self->{lastping};
969         my $b = $self->{lastping};
970         $b->{$call} = shift if @_;
971         return $b->{$call};     
972 }
973
974 #
975 # read in the latest version of the user file. As this file is immutable, the file one really wants is
976 # a later (generated) copy. But, if the plain users.v4 file is all we have, we'll use that.
977 #
978
979 sub readinjson
980 {
981         my $fn = $filename;
982         my $nfn = "$fn.n";
983         my $ofn = "$fn.o";
984
985         my $ta = [gettimeofday];
986         my $count = 0;
987         my $s;
988         my $err = 0;
989
990         unless (-r $fn) {
991                 dbg("DXUser $fn not found - probably about to convert");
992                 return;
993         }
994
995         if (-e $nfn && -e $fn && (stat($nfn))[9] > (stat($fn))[9]) {
996                 # move the old file to .o
997                 unlink $ofn;
998                 move($fn, $ofn);
999                 move($nfn, $fn);
1000         };
1001
1002         if ($ifh) {
1003                 $ifh->seek(0, 0);
1004         } else {
1005                 $ifh = IO::File->new("+<$fn") or die "$fn read error $!";
1006         }
1007         my $pos = $ifh->tell;
1008         while (<$ifh>) {
1009                 chomp;
1010                 my @f = split /\t/;
1011                 $u{$f[0]} = [$pos];
1012                 $count++;
1013                 $pos = $ifh->tell;
1014         }
1015         $ifh->seek(0, 0);
1016
1017         # $ifh is "global" and should not be closed
1018         
1019         dbg("DXUser::readinjson $count record headers read from $fn in ". _diffms($ta) . " mS");
1020         return $totusers = $count;
1021 }
1022
1023 #
1024 # Write a newer copy of the users.v4 file to users.v4.n, which is what will be read in.
1025 # This means that the existing users.v4 is not touched during a run of dxspider, or at least
1026 # not yet.
1027
1028 sub writeoutjson
1029 {
1030         my $ofn = shift || "$filename.n";
1031         my $ta = [gettimeofday];
1032         
1033         my $ofh = IO::File->new(">$ofn") or die "$ofn write error $!";
1034         my $count = 0;
1035         $ifh->seek(0, 0);
1036         for my $k (sort keys %u) {
1037                 my $l = get($k, 1);
1038                 if ($l) {
1039                         chomp $l;
1040                         print $ofh "$k\t$l\n";
1041                         ++$count;
1042                 } else {
1043                         LogDbg('DXCommand', "DXUser::writeoutjson callsign $k not found")
1044                 }
1045         }
1046         
1047         $ofh->close;
1048         dbg("DXUser::writeoutjson $count records written to $ofn in ". _diffms($ta) . " mS");
1049         return $count;
1050 }
1051 1;
1052 __END__
1053
1054
1055
1056
1057