File Coverage

blib/lib/Games/Lacuna/Client.pm
Criterion Covered Total %
statement 48 137 35.0
branch 5 64 7.8
condition 4 15 26.6
subroutine 14 25 56.0
pod n/a
total 71 241 29.4


line stmt bran cond sub pod time code
1             package Games::Lacuna::Client;
2             {
3             $Games::Lacuna::Client::VERSION = '0.003';
4             }
5 1     1   25169 use 5.0080000;
  1         4  
  1         43  
6 1     1   5 use strict;
  1         2  
  1         43  
7 1     1   4 use warnings;
  1         12  
  1         29  
8 1     1   5 use Carp 'croak';
  1         1  
  1         78  
9 1     1   1331 use File::Temp qw( tempfile );
  1         44648  
  1         106  
10 1     1   13 use Cwd qw( abs_path );
  1         5  
  1         192  
11              
12 1     1   9 use constant DEBUG => 1;
  1         2  
  1         92  
13              
14 1     1   972 use Games::Lacuna::Client::Module; # base module class
  1         4  
  1         34  
15 1     1   1263 use Data::Dumper ();
  1         8767  
  1         80  
16 1     1   2620 use YAML::Any ();
  1         1005  
  1         46  
17              
18             #our @ISA = qw(JSON::RPC::Client);
19             use Class::XSAccessor {
20 1         15 getters => [qw(
21             rpc
22             uri name password api_key
23             cache_dir
24             )],
25             accessors => [qw(
26             debug
27             session_id
28             session_start
29             session_timeout
30             session_persistent
31             cfg_file
32             rpc_sleep
33             prompt_captcha
34             open_captcha
35             )],
36 1     1   6 };
  1         3  
37              
38             require Games::Lacuna::Client::RPC;
39              
40             require Games::Lacuna::Client::Alliance;
41             require Games::Lacuna::Client::Body;
42             require Games::Lacuna::Client::Buildings;
43             require Games::Lacuna::Client::Captcha;
44             require Games::Lacuna::Client::Empire;
45             require Games::Lacuna::Client::Inbox;
46             require Games::Lacuna::Client::Map;
47             require Games::Lacuna::Client::Stats;
48              
49              
50             sub new {
51 2     2   956 my $class = shift;
52 2         11 my %opt = @_;
53 2 50       10 if ($opt{cfg_file}) {
54 0 0       0 open my $fh, '<', $opt{cfg_file}
55             or croak("Could not open config file for reading: $!");
56 0         0 my $yml = YAML::Any::Load(do { local $/; <$fh> });
  0         0  
  0         0  
57 0         0 close $fh;
58 0 0       0 $opt{name} = defined $opt{name} ? $opt{name} : $yml->{empire_name};
59 0 0       0 $opt{password} = defined $opt{password} ? $opt{password} : $yml->{empire_password};
60 0 0       0 $opt{uri} = defined $opt{uri} ? $opt{uri} : $yml->{server_uri};
61 0 0       0 $opt{open_captcha} = defined $opt{open_captcha} ? $opt{open_captcha} : $yml->{open_captcha};
62 0 0       0 $opt{prompt_captcha} = defined $opt{prompt_captcha} ? $opt{prompt_captcha} : $yml->{prompt_captcha};
63 0         0 for (qw(uri api_key session_start session_id session_persistent cache_dir)) {
64 0 0       0 if (exists $yml->{$_}) {
65 0 0       0 $opt{$_} = defined $opt{$_} ? $opt{$_} : $yml->{$_};
66             }
67             }
68             }
69 2         7 my @req = qw(uri name password api_key);
70 2 50 66     236 croak("Need the following parameters: @req")
      33        
      33        
71             if not exists $opt{uri}
72             or not exists $opt{name}
73             or not exists $opt{password}
74             or not exists $opt{api_key};
75 1         8 $opt{uri} =~ s/\/+$//;
76              
77 1 50       7 my $debug = exists $ENV{GLC_DEBUG} ? $ENV{GLC_DEBUG}
78             : 0;
79              
80 1         13 my $self = bless {
81             session_start => 0,
82             session_id => 0,
83             session_timeout => 3600*1.8, # server says it's 2h, but let's play it safe.
84             session_persistent => 0,
85             cfg_file => undef,
86             debug => $debug,
87             %opt
88             } => $class;
89              
90             # the actual RPC client
91 1         127 $self->{rpc} = Games::Lacuna::Client::RPC->new(client => $self);
92              
93 0         0 return $self,
94             }
95              
96             sub empire {
97 1     1   1 my $self = shift;
98 1         201 return Games::Lacuna::Client::Empire->new(client => $self, @_);
99             }
100              
101             sub alliance {
102 0     0   0 my $self = shift;
103 0         0 return Games::Lacuna::Client::Alliance->new(client => $self, @_);
104             }
105              
106             sub body {
107 0     0   0 my $self = shift;
108 0         0 return Games::Lacuna::Client::Body->new(client => $self, @_);
109             }
110              
111             sub building {
112 0     0   0 my $self = shift;
113 0         0 return Games::Lacuna::Client::Buildings->new(client => $self, @_);
114             }
115              
116             sub captcha {
117 0     0   0 my $self = shift;
118 0         0 return Games::Lacuna::Client::Captcha->new(client => $self, @_);
119             }
120              
121             sub inbox {
122 0     0   0 my $self = shift;
123 0         0 return Games::Lacuna::Client::Inbox->new(client => $self, @_);
124             }
125              
126             sub map {
127 0     0   0 my $self = shift;
128 0         0 return Games::Lacuna::Client::Map->new(client => $self, @_);
129             }
130              
131             sub stats {
132 0     0   0 my $self = shift;
133 0         0 return Games::Lacuna::Client::Stats->new(client => $self, @_);
134             }
135              
136              
137             sub register_destroy_hook {
138 0     0   0 my $self = shift;
139 0         0 my $hook = shift;
140 0         0 push @{$self->{destroy_hooks}}, $hook;
  0         0  
141             }
142              
143             sub DESTROY {
144 1     1   3 my $self = shift;
145 1 50       10 if ($self->{destroy_hooks}) {
146 0         0 $_->($self) for @{$self->{destroy_hooks}};
  0         0  
147             }
148              
149 1 50       16 if (not $self->session_persistent) {
    0          
150 1         7 $self->empire->logout;
151             }
152             elsif (defined $self->cfg_file) {
153 0           $self->write_cfg;
154             }
155             }
156              
157             sub write_cfg {
158 0     0     my $self = shift;
159 0 0         if ($self->debug) {
160 0           print STDERR "DEBUG: Writing configuration to disk";
161             }
162 0 0         croak("No config file")
163             if not defined $self->cfg_file;
164 0           my %cfg = map { ($_ => $self->{$_}) } qw(session_start
  0            
165             session_id
166             session_timeout
167             session_persistent
168             cache_dir
169             api_key);
170 0           $cfg{server_uri} = $self->{uri};
171 0           $cfg{empire_name} = $self->{name};
172 0           $cfg{empire_password} = $self->{password};
173 0           my $yml = YAML::Any::Dump(\%cfg);
174              
175             eval {
176 0           my $target = $self->cfg_file();
177              
178             # preserve symlinks: operate directly at destination
179 0           $target = abs_path $target;
180              
181             # save data to a temporary, so we don't risk trashing the target
182 0           my ($tfh, $tempfile) = tempfile("$target.XXXXXXX"); # croaks on err
183 0 0         print {$tfh} $yml or die $!;
  0            
184 0 0         close $tfh or die $!;
185              
186             # preserve mode in temporary file
187 0 0         my (undef, undef, $mode) = stat $target or die $!;
188 0 0         chmod $mode, $tempfile or die $!;
189              
190             # rename should be atomic, so there should be no need for flock
191 0 0         rename $tempfile, $target or die $!;
192              
193 0           1;
194 0 0         } or do {
195 0           warn("Can not save Lacuna client configuration: $@");
196 0           return;
197             };
198              
199 0           return 1;
200             }
201              
202             sub assert_session {
203 0     0     my $self = shift;
204              
205 0           my $now = time();
206 0 0 0       if (!$self->session_id || $now - $self->session_start > $self->session_timeout) {
    0          
207 0 0         if ($self->debug) {
208 0           print STDERR "DEBUG: Logging in since there is no session id or it timed out.\n";
209             }
210 0           my $res = $self->empire->login($self->{name}, $self->{password}, $self->{api_key});
211 0           $self->{session_id} = $res->{session_id};
212 0 0         if ($self->debug) {
213 0           print STDERR "DEBUG: Set session id to $self->{session_id} and updated session start time.\n";
214             }
215             }
216             elsif ($self->debug) {
217 0           print STDERR "DEBUG: Using existing session.\n";
218             }
219 0           $self->{session_start} = $now; # update timeout
220 0           return $self->session_id;
221             }
222              
223             sub get_config_file {
224 0     0     my ($class, $files, $optional) = @_;
225 0 0         $files = ref $files eq 'ARRAY' ? $files : [ $files ];
226 0           $files = [map {
227 0           my @values = ($_);
228 0           my $dist_file = eval {
229 0           require File::HomeDir;
230 0           File::HomeDir->VERSION(0.93);
231 0           require File::Spec;
232 0           my $dist = File::HomeDir->my_dist_config('Games-Lacuna-Client');
233 0 0         File::Spec->catfile(
234             $dist,
235             $_
236             ) if $dist;
237             };
238 0 0         warn $@ if $@;
239 0 0         push @values, $dist_file if $dist_file;
240 0           @values;
241 0           } grep { $_ } @$files];
242              
243 0           foreach my $file (@$files) {
244 0 0 0       return $file if ( $file and -e $file );
245             }
246              
247 0 0         die "Did not provide a config file (" . join(',', @$files) . ")" unless $optional;
248 0           return;
249             }
250              
251              
252             1;
253             __END__
254              
255             =head1 NAME
256              
257             Games::Lacuna::Client - An RPC client for the Lacuna Expanse
258              
259             =head1 SYNOPSIS
260              
261             use Games::Lacuna::Client;
262             my $client = Games::Lacuna::Client->new(cfg_file => 'path/to/myempire.yml');
263              
264             # or manually:
265             my $client = Games::Lacuna::Client->new(
266             uri => 'https://path/to/server',
267             api_key => 'your api key here',
268             name => 'empire name',
269             password => 'sekrit',
270             #session_peristent => 1, # only makes sense with cfg_file set!
271             #debug => 1,
272             );
273              
274             my $res = $client->alliance->find("The Understanding");
275             my $id = $res->{alliances}->[0]->{id};
276              
277             use Data::Dumper;
278             print Dumper $client->alliance->view_profile( $res->{alliances}->[0]->{id} );
279              
280             =head1 DESCRIPTION
281              
282             This module implements the Lacuna Expanse API as of 10.10.2010.
283              
284             You will need to have a basic familiarity with the Lacuna RPC API
285             itself, so check out L<http://gameserver.lacunaexpanse.com/api/>
286             where C<gameserver> is the server you intend to use it on. As of this
287             writing, the only server is C<us1>.
288              
289             The different API I<modules> are available by calling the respective
290             module name as a method on the client object. The returned object then
291             implements the various methods.
292              
293             The return values of the methods are (currently) just exactly C<result> portion
294             of the deflated JSON responses. This is subject to change!
295              
296             On failure, the methods C<croak> with a simple to parse message.
297             Example:
298              
299             RPC Error (1002): Empire does not exist. at ...
300              
301             The number is the error code number (see API docs). The text after the colon
302             is the human-readable error message from the server.
303              
304             You do not need to login explicitly. The client will do this on demand. It will
305             also handle session-timeouts and logging out for you. (Log out happens in the
306             destructor.)
307              
308             All methods that take a session id as first argument in the
309             JSON-RPC API B<DO NOT REQUIRE> that you pass the session_id
310             manually. This is handled internally and the client will
311             automatically log in for you as necessary.
312              
313             =head1 Methods
314              
315             =head2 new
316              
317             Games::Lacuna::Client->new(
318             name => 'My empire', # empire_name in config file
319             password => 'password of the empire', # empire_password in config file
320             uri => 'https://us1.lacunaexpanse.com/', # server_uri in config file
321             api_key => 'public api key',
322             );
323              
324             =head1 CONFIGURATION FILE
325              
326             Some of the parameters of the constructor can also be supplied in a
327             configuration file in YAML format. You can find a template in the
328             F<examples> subdirectory.
329              
330             empire_name: The name of my Empire
331             empire_password: The password
332             server_uri: https://us1.lacunaexpanse.com/
333              
334             uri: will overwrite the server_uri key (might be a bug)
335             api_key:
336              
337             session_start:
338             session_id:
339             session_persistent:
340            
341             open_captcha: 1 # Will attempt to open the captcha URL in a browser,
342             # and prompts for the answer. If the browser-open fails,
343             # falls back to prompt_captcha behaviour if that setting
344             # is also true
345            
346             prompt_captcha: 1 # Will print an image URL, and prompts for the answer
347              
348             =head1 SEE ALSO
349              
350             API docs at L<http://us1.lacunaexpanse.com/api/>.
351              
352             A few ready-to-use tools of varying quality live
353             in the F<examples> subdirectory.
354              
355             =head1 AUTHOR
356              
357             Steffen Mueller, E<lt>smueller@cpan.orgE<gt>
358              
359             =head1 COPYRIGHT AND LICENSE
360              
361             Copyright (C) 2010 by Steffen Mueller
362              
363             This library is free software; you can redistribute it and/or modify
364             it under the same terms as Perl itself, either Perl version 5.10.0 or,
365             at your option, any later version of Perl 5 you may have available.
366              
367             =cut