File Coverage

blib/lib/WebService/RTMAgent.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1 4     4   67240 use strict;
  4         8  
  4         154  
2 4     4   18 use warnings;
  4         4  
  4         251  
3             package WebService::RTMAgent;
4             # ABSTRACT: a user agent for the Remember The Milk API
5             $WebService::RTMAgent::VERSION = '0.601';
6             #pod =head1 SYNOPSIS
7             #pod
8             #pod $ua = WebService::RTMAgent->new;
9             #pod $ua->api_key($key_provided_by_rtm);
10             #pod $ua->api_secret($secret_provided_by_rtm);
11             #pod $ua->init;
12             #pod $url = $ua->get_auth_url; # then do something with the URL
13             #pod $res = $ua->tasks_getList('filter=status:incomplete');
14             #pod
15             #pod ...
16             #pod
17             #pod =head1 DESCRIPTION
18             #pod
19             #pod WebService::RTMAgent is a Perl implementation of the rememberthemilk.com API.
20             #pod
21             #pod =head2 Calling API methods
22             #pod
23             #pod All API methods documented at L
24             #pod can be called as methods, changing dots for underscores and optionnaly taking
25             #pod off the leading 'rtm': C<< $ua->auth_checkToken >>, C<< $ua->tasks_add >>, etc.
26             #pod
27             #pod Parameters should be given as a list of strings, e.g.:
28             #pod
29             #pod $ua->tasks_complete(
30             #pod "list_id=4231233",
31             #pod "taskseries_id=124233",
32             #pod "task_id=1234",
33             #pod );
34             #pod
35             #pod Refer to the API documentation for each method's parameters.
36             #pod
37             #pod Return values are the XML response, parsed through L. Please refer
38             #pod to XML::Simple for more information (and Data::Dumper, to see what the values
39             #pod look like) and the sample B script for examples.
40             #pod
41             #pod If the method call was not successful, C is returned, and an error
42             #pod message is set which can be accessed with the B method:
43             #pod
44             #pod $res = $ua->tasks_getList;
45             #pod die $ua->error unless defined $res;
46             #pod
47             #pod Please note that at this stage, I am not very sure that this is the best way to implement the API. "It works for me," but:
48             #pod
49             #pod =for :list
50             #pod * Parameters may turn to hashes at some point
51             #pod * Output values may turn to something more abstract and useful,
52             #pod as I gain experience with API usage.
53             #pod
54             #pod =head2 Authentication and authorisation
55             #pod
56             #pod Before using the API, you need to authenticate it. If you are going to be
57             #pod building a desktop application, you should get an API key and shared secret
58             #pod from the people at rememberthemilk.com (see
59             #pod L
60             #pod for rationale) and provide them to RTMAgent.pm with the C and
61             #pod C methods.
62             #pod
63             #pod You then need to proceed through the authentication cycle: create a useragent,
64             #pod call the get_auth_url method and direct a Web browser to the URL it returns.
65             #pod There RememberTheMilk will present you with an authorisation page: you can
66             #pod authorise the API to access your account.
67             #pod
68             #pod At that stage, the API will get a token which identifies the API/user
69             #pod authorisation. B saves the token in a file, so you should never need
70             #pod to do the authentication again.
71             #pod
72             #pod =head2 Proxy and other strange things
73             #pod
74             #pod The object returned by B is also a LWP::UserAgent. This means you can
75             #pod configure it the same way, in particular to cross proxy servers:
76             #pod
77             #pod $ua = new WebService::RTMAgent;
78             #pod $ua->api_key($key);
79             #pod $ua->api_secret($secret);
80             #pod $ua->proxy('http', 'https://proxy:8080');
81             #pod $ua->init;
82             #pod $list = $ua->tasks_getList;
83             #pod
84             #pod Incidentally, this is the reason why the C method exists: C needs
85             #pod to access the network, so its work cannot be done in C as that would leave
86             #pod no opportunity to configure the LWP::UserAgent.
87             #pod
88             #pod =cut
89              
90 4     4   18 use Carp;
  4         6  
  4         319  
91 4     4   21 use Digest::MD5 qw(md5_hex);
  4         5  
  4         250  
92 4     4   2521 use LWP::UserAgent;
  4         155008  
  4         134  
93 4     4   4515 use XML::Simple;
  0            
  0            
94              
95             use parent 'LWP::UserAgent';
96              
97             my $REST_endpoint = "https://api.rememberthemilk.com/services/rest/";
98             my $auth_endpoint = "https://api.rememberthemilk.com/services/auth/";
99              
100             our $config_file = "$ENV{HOME}/.rtmagent";
101             our $config; # reference to config hash
102              
103             #pod =head1 PUBLIC METHODS
104             #pod
105             #pod =head2 $ua = WebService::RTMAgent->new;
106             #pod
107             #pod Creates a new agent.
108             #pod
109             #pod =cut
110              
111             sub new {
112             my ($class) = @_;
113             my $self = bless {}, $class;
114             $self->verbose('');
115             return $self;
116             }
117              
118             #pod =head2 $ua->api_key($key);
119             #pod
120             #pod =head2 $ua->api_secret($secret);
121             #pod
122             #pod Set the API key and secret. These are obtained from the people are
123             #pod RememberTheMilk.com.
124             #pod
125             #pod =head2 $ua->verbose('netin netout');
126             #pod
127             #pod Sets what type of traces the module should print. You can use 'netout' to print
128             #pod all the outgoing messages, 'netin' to print all the incoming messages.
129             #pod
130             #pod =head2 $err = $ua->error;
131             #pod
132             #pod Get a message describing the last error that happened.
133             #pod
134             #pod =cut
135              
136             # Create accessors
137             BEGIN {
138             my $subs;
139             foreach my $data ( qw/error verbose api_secret api_key/ ) {
140             $subs .= qq{
141             sub $data {
142             \$_[0]->{rtma_$data} =
143             defined \$_[1] ? \$_[1] : \$_[0]->{rtma_$data};
144             }
145             }
146             }
147             eval $subs;
148             }
149              
150             #pod =head2 $ua->init;
151             #pod
152             #pod Performs authentication with RTM and various other book-keeping
153             #pod initialisations.
154             #pod
155             #pod =cut
156              
157             sub init {
158             my ($self) = @_;
159              
160             if (-e $config_file) {
161             die "$config_file: can't read or write\n"
162             unless -r $config_file and -w $config_file;
163              
164             my $ok = eval {
165             $config = XMLin($config_file, KeyAttr=>'', ForceArray => ['undo']);
166             1;
167             };
168             croak "$config_file: Invalid XML file" unless $ok;
169             }
170              
171             # Check Token
172             if ($config->{token}) {
173             my $res = $self->auth_checkToken;
174             if (not defined $res) {
175             delete $config->{frob};
176             delete $config->{token};
177             croak $self->error;
178             }
179             }
180              
181             # If we have a frob and no token, we're half-way through
182             # authentication -- finish it
183             if ($config->{frob} and not $config->{token}) {
184             warn "frobbed -- getting token\n";
185             my $res = $self->auth_getToken("frob=$config->{frob}");
186             die $self->error."(Maybe you need to erase $config_file)\n"
187             unless defined $res;
188             $config->{token} = $res->{auth}->[0]->{token}->[0];
189             warn "token $config->{token}\n";
190             }
191              
192             # If we have no timeline, get one
193             unless ($config->{timeline}) {
194             my $res = $self->timelines_create();
195             $config->{timeline} = $res->{timeline}->[0];
196             $config->{undo} = [];
197             }
198             }
199              
200             #pod =head2 $ua->get_auth_url;
201             #pod
202             #pod Performs the beginning of the authentication: this returns a URL to which
203             #pod the user must then go to allow RTMAgent to access his or her account.
204             #pod
205             #pod This mecanism is slightly contrieved and designed so that users do not have
206             #pod to give their username and password to third party software (like this one).
207             #pod
208             #pod =cut
209              
210             sub get_auth_url {
211             my ($self) = @_;
212              
213             my $res = $self->auth_getFrob();
214              
215             my $frob = $res->{'frob'}->[0];
216              
217             my @params;
218             push @params, "api_key=".$self->api_key, "perms=delete", "frob=$frob";
219             push @params, "api_sig=".($self->sign(@params));
220              
221             my $url = "$auth_endpoint?". (join '&', @params);
222              
223             # save frob for later
224             $config->{'frob'} = $frob;
225              
226             return $url;
227             }
228              
229             #pod =head2 @undo = $ua->get_undoable;
230             #pod
231             #pod Returns the transactions which we know how to undo (unless data has been lost,
232             #pod that's all the undo-able transaction that go with the timeline that is saved in
233             #pod the state file).
234             #pod
235             #pod The value returned is a list of { id, op, [ params ] } with id the transaction
236             #pod id, op the API method that was called, and params the API parameters that were
237             #pod called.
238             #pod
239             #pod =cut
240              
241             sub get_undoable {
242             my ($self) = @_;
243              
244             return $config->{undo};
245             }
246              
247             #pod =head2 $ua->clear_undo(3);
248             #pod
249             #pod Removes an undo entry.
250             #pod
251             #pod =cut
252              
253             sub clear_undo {
254             my ($self, $index) = @_;
255              
256             splice @{$config->{undo}}, $index, 1;
257             }
258              
259             #pod =head1 PRIVATE METHODS
260             #pod
261             #pod Don't use those and we'll stay friends.
262             #pod
263             #pod =head2 $ua->sign(@params);
264             #pod
265             #pod Returns the md5 signature for signing parameters. See RTM Web site for details.
266             #pod This should only be useful for the module, don't use it.
267             #pod
268             #pod =cut
269              
270             sub sign {
271             my ($self, @params) = @_;
272              
273             my $sign_str = join '', sort @params;
274             $sign_str =~ s/=//g;
275              
276             return md5_hex($self->api_secret."$sign_str");
277             }
278              
279             #pod =head2 $ua->rtm_request("rtm.tasks.getList", "list_id=234", "taskseries_id=2"..)
280             #pod
281             #pod Signs the parameters, performs the request, returns a parsed XML::Simple
282             #pod object.
283             #pod
284             #pod =cut
285              
286             sub rtm_request {
287             my ($self, $request, @params) = @_;
288              
289             unshift @params, "method=$request";
290             push @params, "api_key=".$self->api_key;
291             push @params, "auth_token=$config->{token}" if exists $config->{token};
292             push @params, "timeline=$config->{timeline}" if exists $config->{timeline};
293             my $sig = $self->sign(@params);
294             my $param = join '&', @params;
295              
296             my $req = HTTP::Request->new( POST => $REST_endpoint);
297             $req->content_type('application/x-www-form-urlencoded');
298             $req->content("$param&api_sig=$sig");
299             warn("request:\n".$req->as_string."\n\n") if $self->verbose =~ /netout/;
300              
301             my $res = $self->request($req);
302             die $res->status_line unless $res->is_success;
303              
304             warn("response:\n".$res->as_string."\n\n") if $self->verbose =~ /netin/;
305             return XMLin($res->content, KeyAttr=>'', ForceArray=>1);
306             }
307              
308             # AUTOLOAD gets calls to undefined functions
309             # we add 'rtm' and change underscores to dots, to change perl function
310             # names to RTM API: tasks_getList => rtm.tasks.getList
311             # arguments are as strings:
312             # $useragent->tasks_complete("list_id=$a", "taskseries_id=$b" ...);
313             our $AUTOLOAD;
314             sub AUTOLOAD {
315             my $function = $AUTOLOAD;
316              
317             my $self = shift;
318              
319             $function =~ s/^.*:://; # Remove class name
320             $function =~ s/_/./g; # Change underscores to dots (auth_getFrob => auth.getFrob)
321             $function =~ s/^/rtm./ unless $function =~ /^rtm./; # prepends rtm if needed
322             my $res = $self->rtm_request($function, @_);
323              
324             # Treat errors
325             if (exists $res->{'err'}) {
326             croak ("$function does not exist\n") if $res->{'err'}->[0]->{'code'} == 112;
327             $self->error("$res->{'err'}->[0]->{'code'}: $res->{'err'}->[0]->{'msg'}\n");
328             return undef;
329             }
330              
331             # If action is undo-able, store transaction ID
332             if (exists $res->{transaction} and
333             exists $res->{transaction}->[0]->{undoable}) {
334             push @{$config->{undo}}, {
335             'id' => $res->{transaction}->[0]->{id},
336             'op' => $function,
337             'params' => \@_,
338             };
339             }
340             return $res;
341             }
342              
343              
344             # When destroying the object, save the config file
345             # (careful, this all means we can only have one instance running...)
346             sub DESTROY {
347             return unless defined $config;
348             open my $f, "> $config_file";
349             print $f XMLout($config, NoAttr=>1, RootName=>'RTMAgent');
350             }
351              
352             #pod =head1 FILES
353             #pod
354             #pod =for :list
355             #pod = F<~/.rtmagent>
356             #pod XML file containing runtime data: frob, timeline, authentication token. This
357             #pod file is overwritten on exit, which means you should only have one instance of
358             #pod RTMAgent (this should be corrected in a future version).
359             #pod
360             #pod =head1 SEE ALSO
361             #pod
362             #pod =for :list
363             #pod * C<< L >>, example command-line script.
364             #pod * L
365             #pod * L
366             #pod
367             #pod =cut
368              
369             1;
370              
371             __END__