File Coverage

blib/lib/WebService/HabitRPG.pm
Criterion Covered Total %
statement 67 245 27.3
branch 1 128 0.7
condition 2 5 40.0
subroutine 32 47 68.0
pod 0 1 0.0
total 102 426 23.9


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