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