File Coverage

blib/lib/Games/Lacuna/Task/Client.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Games::Lacuna::Task::Client;
2              
3 1     1   1793 use 5.010;
  1         5  
  1         63  
4             our $VERSION = $Games::Lacuna::Task::VERSION;
5              
6 1     1   552 use Moose;
  0            
  0            
7             with qw(Games::Lacuna::Task::Role::Logger
8             Games::Lacuna::Task::Role::Captcha);
9              
10             use Try::Tiny;
11             use IO::Interactive qw(is_interactive);
12             use YAML::Any qw(LoadFile);
13             use Games::Lacuna::Client;
14             use Games::Lacuna::Task::Utils qw(name_to_class);
15             use Games::Lacuna::Task::Storage;
16              
17             our $API_KEY = '6ca1d525-bd4d-4bbb-ae85-b925ed3ea7b7';
18             our $URI = 'https://us1.lacunaexpanse.com/';
19             our @CONFIG_FILES = qw(lacuna config default);
20              
21             has 'client' => (
22             is => 'rw',
23             isa => 'Games::Lacuna::Client',
24             lazy_build => 1,
25             predicate => 'has_client',
26             clearer => 'reset_client',
27             );
28              
29             has 'configdir' => (
30             is => 'ro',
31             isa => 'Path::Class::Dir',
32             coerce => 1,
33             required => 1,
34             );
35              
36             has 'storage' => (
37             is => 'ro',
38             isa => 'Games::Lacuna::Task::Storage',
39             lazy_build => 1,
40             handles => {
41             storage_do => 'do',
42             storage_prepare => 'prepare',
43             storage_selectrow_array => 'selectrow_array',
44             storage_selectrow_hashref => 'selectrow_hashref',
45             get_cache => 'get_cache',
46             set_cache => 'set_cache',
47             clear_cache => 'clear_cache',
48             },
49             );
50              
51             has 'config' => (
52             is => 'ro',
53             isa => 'HashRef',
54             lazy_build => 1,
55             );
56              
57             has 'stash' => (
58             is => 'rw',
59             isa => 'HashRef',
60             predicate => 'has_stash',
61             );
62              
63             sub _build_config {
64             my ($self) = @_;
65            
66             # Get global config
67             my $global_config = {};
68            
69             # Search all possible files
70             foreach my $file (@CONFIG_FILES) {
71             my $global_config_file = Path::Class::File->new($self->configdir,$file.'.yml');
72             if (-e $global_config_file) {
73             $self->log('debug',"Loading config from %s",$global_config_file->stringify);
74             $global_config = LoadFile($global_config_file->stringify);
75             last;
76             }
77             }
78            
79             unless (scalar keys %{$global_config}) {
80             $self->abort('Config missing. Please create a config file in %s',$self->configdir)
81             unless is_interactive();
82            
83             $self->log('info',"Could not find config. Initializing new config");
84             require Games::Lacuna::Task::Setup;
85             my $setup = Games::Lacuna::Task::Setup->new(
86             configfile => Path::Class::File->new($self->configdir,$CONFIG_FILES[0].'.yml'),
87             );
88             $global_config = $setup->run;
89             }
90            
91             my $connect_config = $global_config->{connect};
92            
93             # Aliases
94             $connect_config->{name} ||= delete $connect_config->{empire}
95             if defined $connect_config->{empire};
96             $connect_config->{uri} ||= delete $connect_config->{server}
97             if defined $connect_config->{server};
98            
99             # Defaults
100             $connect_config->{api_key} ||= $API_KEY;
101             $connect_config->{uri} ||= $URI;
102            
103             # Check required configs
104             $self->abort('Empire name missing in config')
105             unless defined $connect_config->{name};
106             $self->abort('Empire password missing in config')
107             unless defined $connect_config->{password};
108            
109             return $global_config;
110             }
111              
112             sub _build_client {
113             my ($self) = @_;
114            
115             my $connect_config = $self->config->{connect};
116             my $session = $self->get_cache('session') || {};
117              
118             # Check session
119             if (defined $session
120             && defined $session->{session_start}
121             && $session->{session_start} + $session->{session_timeout} < time()) {
122             $self->log('debug','Session %s has expired',$session->{session_id});
123             $session = {};
124             }
125            
126             my $client = Games::Lacuna::Client->new(
127             %{$connect_config},
128             %{$session},
129             session_persistent => 1,
130             );
131              
132             return $client;
133             }
134              
135             sub _build_storage {
136             my ($self) = @_;
137            
138             # Get lacuna database
139             my $storage_file = Path::Class::File->new($self->configdir,'lacuna.db');
140            
141             # Upgrade storage
142             my $storage = Games::Lacuna::Task::Storage->new(
143             file => $storage_file,
144             loglevel => $self->loglevel,
145             debug => $self->debug,
146             );
147            
148            
149             return $storage;
150             }
151              
152             sub get_stash {
153             my ($self,$key) = @_;
154            
155             # Get empire status to build stash
156             $self->request(
157             object => $self->build_object('Empire'),
158             method => 'get_status',
159             ) unless $self->has_stash;
160            
161             # Return stash
162             return $self->stash->{$key};
163             }
164              
165             sub task_config {
166             my ($self,$task_name) = @_;
167            
168             # Convert name tp class
169             my $task_class = name_to_class($task_name);
170             my $config_task = $self->config->{$task_name} || $self->config->{lc($task_name)} || {};
171             my $config_global = $self->config->{global} || {};
172            
173             my $config_final = {};
174            
175             # Set all global attributes from task config, global config or $self
176             foreach my $attribute ($task_class->meta->get_all_attributes) {
177             my $attribute_name = $attribute->name;
178             if ($attribute_name eq 'client') {
179             $config_final->{'client'} //= $self;
180             } else {
181             $config_final->{$attribute_name} = $config_task->{$attribute_name}
182             if defined $config_task->{$attribute_name};
183             $config_final->{$attribute_name} //= $config_global->{$attribute_name}
184             if defined $config_global->{$attribute_name};
185             $config_final->{$attribute_name} //= $self->$attribute_name
186             if $self->can($attribute_name);
187             }
188             }
189            
190             return $config_final;
191             }
192              
193             sub login {
194             my ($self) = @_;
195            
196             my $connect_config = $self->config->{connect};
197             $self->client->empire->login($connect_config->{name}, $connect_config->{password}, $connect_config->{api_key});
198             $self->_update_session;
199             }
200              
201             sub _update_session {
202             my ($self) = @_;
203            
204             my $client = $self->meta->get_attribute('client')->get_raw_value($self);
205              
206             return
207             unless defined $client && $client->session_id;
208              
209             my $session = $self->get_cache('session') || {};
210            
211             return $client
212             if defined $session
213             && defined $session->{session_id}
214             && $session->{session_id} eq $client->session_id;
215              
216             $self->log('debug','New session %s',$client->session_id);
217              
218             $session->{session_id} = $client->session_id;
219             $session->{session_start} = $client->session_start;
220             $session->{session_timeout} = $client->session_timeout;
221            
222            
223             $self->set_cache(
224             key => 'session',
225             value => $session,
226             valid_until => $session->{session_timeout} + $session->{session_start},
227             );
228            
229             return $client;
230             }
231              
232             after 'request' => sub {
233             my ($self) = @_;
234             return $self->_update_session();
235             };
236              
237             sub empire_name {
238             my ($self) = @_;
239             return $self->client->name;
240             }
241              
242             sub request {
243             my ($self,%args) = @_;
244              
245             $args{catch} ||= [];
246              
247             push (
248             @{$args{catch}},
249             [
250             1006,
251             sub {
252             $self->log('debug','Session expired unexpectedly');
253             $self->client->reset_client();
254             $self->clear_cache('session');
255             $self->login;
256             return 1;
257             }
258             ],
259             [
260             1016,
261             sub {
262             my ($error,$error_count) = @_;
263             $self->log('warn','Need to solve captcha');
264             my $solved = $self->get_captcha();
265             if ($solved) {
266             return 1;
267             } else {
268             $error->rethrow;
269             }
270             },
271             ],
272             [
273             1010,
274             qr/^Slow down/,
275             sub {
276             my ($error,$error_count) = @_;
277             if ($error =~ m/Slow\sdown!/) {
278             if ($error_count < 3) {
279             $self->log('warn',$error);
280             $self->log('warn','Too many requests (wait a while)');
281             sleep 50;
282             return 1;
283             } else {
284             $self->log('error','Too many requests (abort)');
285             return 0;
286             }
287             } else {
288             $error->rethrow;
289             }
290             },
291             ]
292             );
293              
294             $self->_raw_request(%args);
295             }
296              
297             sub _raw_request {
298             my ($self,%args) = @_;
299            
300             my $method = delete $args{method};
301             my $object = delete $args{object};
302             my $params = delete $args{params} || [];
303             my $catch = delete $args{catch} || [];
304              
305             my $debug_params = join(',', map { ref($_) || $_ } @$params);
306            
307             $self->log('debug',"Run external request %s->%s(%s)",ref($object),$method,$debug_params);
308            
309             my $response;
310             my $retry = 1;
311             my $error_count = 0;
312            
313             while ($retry) {
314             $retry = 0;
315             my $handled = 0;
316             try {
317             $response = $object->$method(@$params);
318             } catch {
319             my $error = $_;
320             if (blessed($error)
321             && $error->isa('LacunaRPCException')) {
322            
323             foreach my $element (@{$catch}) {
324             next
325             unless $error->code == $element->[0];
326            
327             my $catch_sub;
328             if (ref $element->[1] eq 'CODE') {
329             $catch_sub = $element->[1];
330             } elsif (ref $element->[1] eq 'Regexp') {
331             next
332             unless $error->text =~ $element->[1];
333             $catch_sub = $element->[2];
334             } else {
335             next
336             unless $error->text eq $element->[1];
337             $catch_sub = $element->[2];
338             }
339            
340             $handled = 1;
341             $retry = ($catch_sub->($error,$error_count) ? 1:0);
342             last;
343             }
344            
345             unless ($handled) {
346             $self->abort($error);
347             }
348            
349             $error_count ++;
350             } else {
351             $self->abort($error);
352             }
353             };
354             }
355            
356             return
357             unless defined $response;
358            
359             my $status = $response->{status} || $response;
360            
361             if ($status->{body}) {
362             $self->set_cache(
363             key => 'body/'.$status->{body}{id},
364             value => $status->{body},
365             max_age => 60*70, # One hour+
366             );
367             }
368             if ($response->{buildings}) {
369             $self->set_cache(
370             key => 'body/'.$status->{body}{id}.'/buildings',
371             value => $response->{buildings},
372             max_age => 60*70, # One hour+
373             );
374             }
375            
376             # Set stash
377             unless ($self->has_stash) {
378             $self->stash({
379             star_map_size => $status->{server}{star_map_size},
380             rpc_limit => $status->{server}{rpc_limit},
381             server_version => $status->{server}{version},
382             #empire_name => $response->{empire}{name},
383             empire_id => $status->{empire}{id},
384             home_planet_id => $status->{empire}{home_planet_id},
385             });
386             }
387            
388             my $stash = $self->stash;
389            
390             # Update stash
391             $stash->{rpc_count} = $status->{empire}{rpc_count};
392             $stash->{essentia} = $status->{empire}{essentia};
393             $stash->{planets} = $status->{empire}{planets};
394             $stash->{has_new_messages} = $status->{empire}{has_new_messages};
395            
396             return $response;
397             }
398              
399             sub paged_request {
400             my ($self,%params) = @_;
401            
402             $params{params} ||= [];
403            
404             my $total = delete $params{total};
405             my $data = delete $params{data};
406             my $page = 1;
407             my @result;
408            
409             PAGES:
410             while (1) {
411             push(@{$params{params}},$page);
412             my $response = $self->request(%params);
413             pop(@{$params{params}});
414            
415             foreach my $element (@{$response->{$data}}) {
416             push(@result,$element);
417             }
418            
419             if ($response->{$total} > (25 * $page)) {
420             $page ++;
421             } else {
422             $response->{$data} = \@result;
423             return $response;
424             }
425             }
426             }
427              
428             sub build_object {
429             my ($self,$class,@params) = @_;
430            
431             # Get class and id from status hash
432             if (ref $class eq 'HASH') {
433             push(@params,'id',$class->{id});
434             $class = $class->{url};
435             }
436            
437             # Get class from url
438             if ($class =~ m/^\//) {
439             $class = 'Buildings::'.Games::Lacuna::Client::Buildings::type_from_url($class);
440             }
441            
442             # Build class name
443             $class = 'Games::Lacuna::Client::'.ucfirst($class)
444             unless $class =~ m/^Games::Lacuna::Client::(.+)$/;
445            
446             return $class->new(
447             client => $self->client,
448             @params
449             );
450             }
451              
452             __PACKAGE__->meta->make_immutable;
453             no Moose;
454             1;
455             =encoding utf8
456              
457             =head1 NAME
458              
459             Games::Lacuna::Task::Client - Client class
460              
461             =head1 DESCRIPTION
462              
463             Implements basic caching and the connection to the lacuna API.
464              
465             =head1 ACCESSORS
466              
467             =head2 client
468              
469             L<Games::Lacuna::Client> object
470              
471             =head2 configdir
472              
473             L<Games::Lacuna::Task> config directory
474              
475             =head3 storage
476              
477             Access to the caching database via L<Games::Lacuna::Storage>
478              
479             =head3 config
480              
481             Current config hash as read from the config file in configdir
482              
483             =head3 stash
484              
485             Simple Stash for storing various temporary values.
486              
487             =head1 METHODS
488              
489             =head2 task_config
490              
491             my $config = $client->task_config($task_name);
492              
493             Calculates the config for a given task
494              
495             =head2 empire_name
496              
497             Returns the current empire name
498              
499             =head2 get_cache
500              
501             my $value = $self->get_cache('key1');
502              
503             Fetches a value from the cache. Returns undef if cache is not available
504             or if it has expired.
505              
506             =head2 clear_cache
507              
508             $self->clear_cache('key1');
509              
510             Remove an entry from the cache.
511              
512             =head2 set_cache
513              
514             $self->set_cache(
515             max_age => $valid_seconds, # optional
516             valid_until => $timestamp, # optional, either max_age or valid_until
517             key => 'key1', # required
518             value => $some_data # required
519             );
520              
521             Stores an arbitrary data structure (no objects) in a persistent cache
522              
523             =head3 request
524              
525             Runs a request, caches the response and returns the response.
526              
527             my $response = $self->request(
528             object => Games::Lacuna::Client::* object,
529             method => Method name,
530             params => [ Params ],
531             );
532            
533             =head3 paged_request
534              
535             Fetches all response elements from a paged method
536              
537             my $response = $self->paged_request(
538             object => Games::Lacuna::Client::* object,
539             method => Method name,
540             params => [ Params ],
541             total => 'field storing the total number of items',
542             data => 'field storing the items',
543             );
544              
545             =head3 build_object
546              
547             my $glc_object = $self->build_object('/university', id => $building_id);
548             OR
549             my $glc_object = $self->build_object($building_status_response);
550             OR
551             my $glc_object = $self->build_object('Spaceport', id => $building_id);
552             OR
553             my $glc_object = $self->build_object('Map');
554              
555             Builds an <Games::Lacuna::Client::*> object
556              
557             =head3 storage_do
558              
559             $self->storage_do('UPDATE .... WHERE id = ?',$id);
560              
561             Runs a command in the cache database
562              
563             =head3 storage_prepare
564              
565             my $sth = $self->storage_prepare('SELECT .... WHERE id = ?');
566              
567             Prepares a SQL-query for the cache database and retuns the statement handle.
568              
569             =cut