File Coverage

blib/lib/WebService/HabitRPG.pm
Criterion Covered Total %
statement 64 236 27.1
branch 1 124 0.8
condition 2 5 40.0
subroutine 31 46 67.3
pod 0 1 0.0
total 98 412 23.7


line stmt bran cond sub pod time code
1             package WebService::HabitRPG;
2 3     3   51435 use v5.010;
  3         11  
3 3     3   16 use strict;
  3         6  
  3         74  
4 3     3   16 use warnings;
  3         13  
  3         94  
5 3     3   1554 use autodie;
  3         36313  
  3         19  
6 3     3   21220 use Moo;
  3         45606  
  3         19  
7 3     3   8948 use WWW::Mechanize;
  3         566405  
  3         162  
8 3     3   3379 use Method::Signatures 20121201;
  3         237619  
  3         22  
9 3     3   3218 use WebService::HabitRPG::Task;
  3         11  
  3         92  
10 3     3   2659 use JSON::Any;
  3         11113  
  3         13  
11 3     3   14144 use Data::Dumper;
  3         7  
  3         196  
12 3     3   17 use Carp qw(croak);
  3         8  
  3         1058  
13              
14             our $DEBUG = $ENV{HRPG_DEBUG} || 0;
15             our $TAG_PREFIX_CHARACTER = '^';
16              
17             # ABSTRACT: Perl interface to the HabitRPG API
18              
19             our $VERSION = '0.23'; # VERSION: Generated by DZP::OurPkg:Version
20              
21              
22             has 'api_token' => (is => 'ro'); # aka x-api-key
23             has 'user_id' => (is => 'ro'); # aka x-api-user
24             has 'agent' => (is => 'rw');
25             has 'api_base' => (is => 'ro', default => sub { 'https://habitrpg.com/api/v1' });
26             has '_last_json' => (is => 'rw'); # For debugging
27             has 'tags' => (is => 'rw');
28             has 'tag_prefix' => (is => 'rw', default => sub { '^' });
29              
30             # use constant URL_BASE => 'https://habitrpg.com/api/v1';
31              
32             sub BUILD {
33 3     3 0 31 my ($self, $args) = @_;
34            
35 3   100     18 my $keep_alive = $args->{keep_alive} // 1;
36              
37             # Set a default agent if we don't already have one.
38              
39 3 50       22 if (not $self->agent) {
40 3         50 $self->agent(
41             WWW::Mechanize->new(
42             agent => "Perl/$], WebService::HabitRPG/" . $self->VERSION,
43             keep_alive => $keep_alive,
44             )
45             );
46             }
47              
48 3         46806 return;
49             }
50              
51              
52 3 0   3   3213 method user() { return $self->_get_request( '/user' ); }
  0     0      
  0            
  0            
53              
54              
55 3 0   3   440955 method tasks($type where qr{^(?: habit | daily | todo | reward | )$}x = "") {
  3 0   3   3368  
  3 0   0   74  
  3         21  
  0            
  0            
  0            
  0            
  0            
  0            
56 0 0         if ($type) {
57 0           return $self->_get_tasks( "/user/tasks?type=$type" );
58             }
59 0           return $self->_get_tasks( "/user/tasks" );
60             }
61              
62              
63 3 0   3   7626 method get_task($task_id) {
  0 0   0      
  0            
  0            
  0            
64              
65             # _get_tasks() always returns an array ref, so we unpack that here.
66              
67 0           return $self->_get_tasks("/user/task/$task_id")->[0];
68             }
69              
70              
71 3     3   74626 method new_task(
  0     0      
  0            
  0            
72 3 0   3   439 :$type! where qr{^(?: habit | daily | todo | reward )$}x,
  3 0       9  
  3         18  
  0            
  0            
  0            
  0            
  0            
  0            
  0            
73 0 0         :$text!,
  0            
  0            
  0            
74 0           :$completed,
  0            
  0            
75 0 0         :$value = 0,
  0            
  0            
76 0 0         :$note = '',
  0            
  0            
77 0 0         :$up = 1,
  0            
  0            
78 0 0         :$down = 1,
  0            
  0            
79 0 0         :$extend = {},
  0 0          
  0 0          
  0            
  0            
80             ) {
81              
82             # Magical boolification for JSONification.
83             # TODO: These work with JSON::XS. Do they work with other backends?
84              
85 0 0         $up = $up ? \1 : \0;
86 0 0         $down = $down ? \1 : \0;
87              
88             # TODO : The API spec doesn't allow the submission of up/down
89             # values, but I feel that *should* be allowed, otherwise
90             # creating goals isn't full-featured.
91              
92 0           my $payload = $self->_encode_json({
93             type => $type,
94             text => $text,
95             completed => $completed,
96             value => $value,
97             note => $note,
98             up => $up,
99             down => $down,
100             %$extend,
101             });
102              
103 0           my $req = $self->_build_request('POST', '/user/task');
104              
105 0           $req->content( $payload );
106              
107 0           return $self->_request( $req );
108              
109             }
110              
111              
112 3     3   21497 method updown(
  0     0      
  0            
113 0 0         $task!,
  0            
  0            
114 3 0   3   424 $direction! where qr{up|down}
  3 0       7  
  3 0       17  
  0            
  0            
  0            
  0            
  0            
  0            
  0            
115             ) {
116              
117 0           my $req = $self->_build_request('POST', "/user/tasks/$task/$direction");
118              
119 0           return $self->_request( $req );
120             }
121              
122              
123             # Convenience methods
124 3 0   3   6887 method up ($task) { return $self->updown($task, 'up' ); }
  0 0   0      
  0            
  0            
  0            
  0            
125 3 0   3   6045 method down($task) { return $self->updown($task, 'down'); }
  0 0   0      
  0            
  0            
  0            
  0            
126              
127              
128 3     3   11370 method _update(
  0     0      
  0            
129 0 0         $task!,
  0            
  0            
130 0 0         $updates!
  0 0          
  0            
  0            
131             ) {
132 0           my $payload = $self->_encode_json({
133             %$updates,
134             });
135              
136 0           my $req = $self->_build_request('PUT', "/user/task/$task");
137              
138 0           $req->content( $payload );
139              
140 0           return $self->_request( $req );
141             }
142              
143              
144             # NOTE: We exclude rewards
145             # NOTE: This returns a list of data structures.
146             # NOTE: Case insensitive search
147              
148 3 0   3   14991 method search_tasks($search_term, :$all = 0) {
  0 0   0      
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
149 0           my $tasks = $self->tasks;
150 0           my @matches;
151             my $tag_uuid;
152              
153 0           my $tag_prefix = $self->tag_prefix;
154              
155             # Check to see if we're doing a tag search.
156              
157 0 0         if ($search_term =~ /^\Q$tag_prefix\E(?<tag>.*)/ms) {
158 0 0         if (not $self->tags) { croak "No tags defined on " . ref($self) . " object!"; }
  0            
159 3     3   3600 $tag_uuid = $self->tags->{ $+{tag} };
  3         1611  
  3         662  
  0            
160 0 0         $tag_uuid or croak "Search for unknown tag: $+{tag}";
161             }
162              
163 0           foreach my $task (@$tasks) {
164              
165 0 0         next if $task->type eq 'reward';
166 0 0 0       if ($task->completed and not $all) { next; }
  0            
167              
168             # If we're doing a tag search...
169 0 0         if ($tag_uuid) {
    0          
    0          
170 0 0         next if not $task->tags; # Skip tagless tasks
171 0 0         push(@matches, $task) if $task->tags->{$tag_uuid};
172             }
173              
174             # If our search term exactly matches a task ID, then use that.
175             elsif ($task->id eq $search_term) {
176 0           return $task;
177             }
178              
179             # Otherwise, if it contains our search term.
180             elsif ($task->text =~ /\Q$search_term\E/i) {
181 0           push(@matches, $task);
182             }
183             }
184              
185 0           return @matches;
186             }
187              
188             #### Internal use only code beyond this point ####
189              
190 3 0   3   6646 method _get_tasks($url) {
  0 0   0      
  0            
  0            
  0            
191 0           my $results = $self->_get_request($url);
192              
193             # If we're fetching a single task, it can come back as
194             # an un-wrapped hash. We re-wrap it here if that's the case.
195              
196 0 0         if (ref($results) ne 'ARRAY') {
197 0           $results = [$results];
198             }
199              
200 0           my @tasks;
201              
202             # Somehow we can get back completely undefined results,
203             # hence the grep to only look at defined ones.
204              
205 0           foreach my $raw (grep { defined } @$results) {
  0            
206 0           push @tasks, WebService::HabitRPG::Task->new(
207             $raw,
208             );
209             }
210              
211             # Sort based on task type. The old API used to do this for us.
212 0           @tasks = sort { $a->type cmp $b->type } @tasks;
  0            
213              
214 0           return \@tasks;
215             }
216              
217 3 0   3   6691 method _get_request($url) {
  0 0   0      
  0            
  0            
  0            
218 0           my $req = $self->_build_request('GET', $url);
219 0           return $self->_request( $req );
220             }
221              
222             # I don't like the name here, but this makes our request, and decodes
223             # the JSON-filled result
224              
225 3 0   3   6256 method _request($req) {
  0 0   0      
  0            
  0            
  0            
226 0           return $self->_decode_json($self->agent->request( $req )->decoded_content);
227             }
228              
229 3 0   3   8571 method _build_request($type, $url) {
  0 0   0      
  0 0          
  0            
  0            
  0            
  0            
230              
231 0 0         warn "Making $type request to $url" if $DEBUG;
232              
233 0           my $req = HTTP::Request->new( $type, $self->api_base . $url );
234 0           $req->header( 'Content-Type' => 'application/json');
235 0           $req->header( 'x-api-user' => $self->user_id );
236 0           $req->header( 'x-api-key' => $self->api_token );
237              
238 0           return $req;
239             }
240              
241             my $json = JSON::Any->new;
242              
243 3 0   3   6491 method _decode_json($string) {
  0 0   0      
  0            
  0            
  0            
244              
245 0 0         warn "Decoding JSON: $string" if $DEBUG;
246              
247 0           $self->_last_json($string); # For debugging
248 0           my $result = $json->decode( $string );
249              
250 0 0         if ($DEBUG) {
251 0           warn "JSON decoded to: ", Dumper($result), "\n";
252             }
253              
254 0           return $result;
255             }
256              
257 3 0   3   6226 method _encode_json($string) {
  0 0   0      
  0            
  0            
  0            
258 0           return $json->encode( $string );
259             }
260              
261              
262             1;
263              
264             __END__
265              
266             =pod
267              
268             =encoding UTF-8
269              
270             =head1 NAME
271              
272             WebService::HabitRPG - Perl interface to the HabitRPG API
273              
274             =head1 VERSION
275              
276             version 0.23
277              
278             =head1 SYNOPSIS
279              
280             use WebService::HabitRPG;
281              
282             # The API Token and User ID are obained through the
283             # Setting -> API link on http://habitrpg.com/
284              
285             my $hrpg = WebService::HabitRPG->new(
286             api_token => 'your-token-goes-here',
287             user_id => 'your-user-id-goes-here',
288             tags => { work => $uuid, home => $uuid2, ... }, # optional
289             );
290              
291             # Get everyting about the user
292             my $user = $hrpg->user;
293              
294             # Get all tasks.
295             my $tasks = $hrpg->tasks;
296              
297             # Get all tasks of a particular type (eg: 'daily')
298             my $daily = $hrpg->tasks('daily');
299              
300             # Increment/decrement a task
301             $hrpg->up($task_id);
302             $hrpg->down($task_id);
303              
304             # Make a new task
305             $hrpg->new_task(
306             type => 'daily',
307             text => 'floss teeth',
308             up => 1,
309             down => 0,
310             );
311              
312             =head1 DESCRIPTION
313              
314             Interface to API provided by L<HabitRPG|http://habitrpg.com/>.
315              
316             At the time of release, the HabitRPG API is still under construction.
317             This module may change as a result.
318              
319             Note that when data structures are returned, they are almost
320             always straight conversions from the JSON returned by the
321             HabitRPG API.
322              
323             =head1 METHODS
324              
325             =head2 new
326              
327             my $hrpg = WebService::HabitRPG->new(
328             api_token => 'your-token-goes-here',
329             user_id => 'your-user-id-goes-here',
330             tags => { work => $work_uuid, home => $home_uuid, ... },
331             tag_prefix => '^', # Optional, defaults to '^'
332             );
333              
334             Creates a new C<WebService::HabitRPG> object. The C<api_token> and C<user_id>
335             parameters are mandatory. You may also pass your own L<WWW::Mechanize>
336             compatible user-agent with C<agent>, and should you need it your own HabitRPG
337             API base URL with C<api_base> (useful for testing, or if you're running your
338             own server).
339              
340             By default, the official API base of C<https://habitrpg.com/api/v1> is used.
341              
342             The C<tags> field is optional, but if included should consist of C<tag => uuid>
343             pairs. When API support is added for tags, this optional will become obsolete.
344              
345             I<Use of the tags feature should be considered experimental>.
346              
347             =head2 user
348              
349             my $user = $hrpg->user();
350              
351             Returns everything from the C</user> route in the HabitRPG API.
352             This is practically everything about the user, their tasks, scores,
353             and other information.
354              
355             The Perl data structure that is returned is a straight conversion
356             from the JSON provided by the HabitRPG API.
357              
358             =head2 tasks
359              
360             my $tasks = $hrpg->tasks(); # All tasks
361             my $habits = $hrpg->tasks('habit'); # Only habits
362              
363             Return a reference to an array of tasks. With no arguments, all
364             tasks (habits, dailies, todos and rewards) are returned. With
365             an argument, only tasks of the given type are returned. The
366             argument must be one of C<habit>, C<daily>, C<todo> or C<reward>.
367              
368             See L<WebService::HabitRPG::Task> for a complete description of
369             what task objects look like.
370              
371             Not all tasks will have all fields. Using the L<hrpg> command-line
372             tool with C<hrpg dump tasks> is a convenient way to see the
373             data structures returned by this method.
374              
375             =head2 get_task
376              
377             my $task = $hrpg->get_task('6a11dd4d-c2d6-42b7-b9ff-f562d4ccce4e');
378              
379             Given a task ID, returns information on that task in the same format
380             at L</tasks> above.
381              
382             =head2 new_task
383              
384             $hrpg->new_task(
385             type => 'daily', # Required
386             text => 'floss teeth', # Required
387             up => 1, # Suggested, defaults true
388             down => 0, # Suggested, defaults true
389             value => 0,
390             note => "Floss every tooth for great justice",
391             completed => 0,
392             extend => {},
393             );
394              
395             Creates a new task. Only the C<type> and C<text> arguments are
396             required, all other tasks are optional. The C<up> and C<down>
397             options default to true (ie, tasks can be both incremented and
398             decremented).
399              
400             The C<type> parameter must be one of: C<habit>, C<daily>,
401             C<todo> or C<reward>.
402              
403             The C<extend> parameter consists to key/value pairs that will be
404             added to the JSON create packet. This should only be used if you
405             know what you're doing, and wish to take advantage of new or
406             undocumented features in the API.
407              
408             Returns a task data structure of the task created, identical
409             to the L</tasks> method above.
410              
411             Creating tasks that can be neither incremented nor decremented
412             is of dubious usefulness.
413              
414             =head2 updown
415              
416             $hrpg->updown('6a11dd4d-c2d6-42b7-b9ff-f562d4ccce4e', 'up' );
417             $hrpg->updown('6a11dd4d-c2d6-42b7-b9ff-f562d4ccce4e', 'down');
418              
419             Moves the habit in the direction specified. Returns a data structure
420             of character status:
421              
422             {
423             exp => 11,
424             gp => 15.5,
425             hp => 50,
426             lv => 2,
427             delta => 1,
428             }
429              
430             =head2 up
431              
432             $hrpg->up($task);
433              
434             Convenience method. Equivalent to C<$hrpg->updown($task, 'up')>;
435              
436             =head2 down
437              
438             $hrpg->down($task);
439              
440             Convenience method. Equivalent to C<$hrpg->updown($task, 'down')>;
441              
442             =head2 _update
443              
444             $hrpg->_update($task, { attr => value });
445              
446             I<This method should be considered experimental.>
447              
448             Updates the given task on the server (using the underlying C<PUT>
449             functionality in the API). Attributes are not checked for sanity,
450             they're just directly converted into JSON.
451              
452             =head2 search_tasks
453              
454             my @tasks = $hrpg->search_tasks($search_term, all => $bool);
455              
456             # Eg:
457             my @tasks = $hrpg->search_tasks('floss');
458             my @tasks = $hrpg->search_tasks('git', all => 1);
459              
460             Search for tasks which match the provided search term. If the
461             search term C<exactly> matches a task ID, then the task ID
462             is returned. Otherwise, returns a list of tasks which contain
463             the search term in their names (the C<text> field returned by the API).
464             This list is in the same format as the as the L</tasks> method call.
465              
466             If the term begins with the tag prefix character ('^' by default),
467             it is considered to be a tag, and the hashless form is searched for.
468             For example, '^work' will result in returning all tasks which match
469             the tag 'work'.
470              
471             If the term does not begin with a hash, then the search term is
472             treated in a literal, case-insensitive fashion.
473              
474             If the optional C<all> parameter is set, then all tasks are
475             returned. Otherwise only non-completed tasks are returned.
476              
477             This is useful for providing a human-friendly way to refer to
478             tasks. For example:
479              
480             # Search for a user-provided term
481             my @tasks = $hrpg->search_tasks($term);
482            
483             # Increment task if found
484             if (@tasks == 1) {
485             $hrpg->up($tasks[0]->id);
486             }
487             else {
488             say "Too few or too many tasks found.";
489             }
490              
491             =for Pod::Coverage BUILD DEMOLISH api_token user_id agent api_base tags tag_prefix
492              
493             =head1 BUGS
494              
495             I'm sure there are plenty! Please view and/or record them at
496             L<https://github.com/pjf/WebService-HabitRPG/issues> .
497              
498             =head1 SEE ALSO
499              
500             The L<HabitRPG API spec|https://github.com/lefnire/habitrpg/wiki/API>.
501              
502             The L<hrpg> command-line client. It's freakin' awesome.
503              
504             =head1 AUTHOR
505              
506             Paul Fenwick <pjf@cpan.org>
507              
508             =head1 COPYRIGHT AND LICENSE
509              
510             This software is copyright (c) 2013 by Paul Fenwick.
511              
512             This is free software; you can redistribute it and/or modify it under
513             the same terms as the Perl 5 programming language system itself.
514              
515             =cut