File Coverage

blib/lib/Net/Posterous.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             package Net::Posterous;
2              
3 1     1   45295 use warnings;
  1         2  
  1         35  
4 1     1   6 use strict;
  1         1  
  1         35  
5 1     1   1066 use LWP::UserAgent;
  1         54520  
  1         30  
6 1     1   810 use LWP::Simple;
  1         22156  
  1         10  
7 1     1   1292 use MIME::Base64;
  1         731  
  1         61  
8 1     1   448 use XML::Simple;
  0            
  0            
9             use Data::Dumper;
10              
11             use Net::Posterous::Site;
12             use Net::Posterous::Post;
13             use Net::Posterous::Media;
14             use Net::Posterous::Tag;
15             use Net::Posterous::Comment;
16              
17              
18             use HTTP::Request::Common;
19              
20             our $VERSION = "0.8";
21             our $DOMAIN = "http://posterous.com";
22              
23              
24              
25             =head1 NAME
26              
27             Net::Posterous - read and post from Posterous blogs
28              
29             =head1 SYNOPSIS
30              
31             my $api = Net::Posterous->new($user, $pass);
32            
33             # Get a list of sites the user owns as Net::Posterous::Site objects
34             my @sites = $api->get_sites;
35            
36             # Set the site to use
37             $api->site($sites[0]);
38              
39             # Create a post
40             my $post = Net::Posterous::Post->new(%opts);
41             my $res = $api->post($post);
42            
43             # Update the post
44             $post->title("New Title");
45             my $res = $api->post($post);
46            
47             # Get a list of posts
48             my @posts = $api->get_posts;
49            
50             # Get an individual post using the http://post.ly shortcode
51             # i.e 123abc in http://post.ly/123abc
52             my $post = $api->get_post($post->short_code);
53            
54            
55             # Get a list of tags
56             my @tags = $api->get_tags;
57            
58             # Create a post with an video attached
59             my $video = Net::Posterous::Media::Local->new(file => $path_to_movie);
60             my $vpost = Net::Posterous::Media::Post->new( title => "My movie", media => $video );
61             my $res = $api->post($vpost);
62            
63             # Create a post with a flipbook of pictures
64             my @images = map { Net::Posterous::Media::Local->new(file => $_) } qw(1.jpg 2.jpg 3.jpg);
65             my $ipost = Net::Posterous::Media::Post->new( title => "My flipbook", media => \@images );
66             my $res = $api->post($ipost);
67            
68             # Add a comment
69             my $comment = Net::Posterous::Comment->new( body => "Nice flipbook!" );
70             my $res = $api->comment($ipost, $comment);
71            
72            
73            
74             =head1 DESCRIPTION
75              
76             This allows reading a writing from Posterous sites.
77              
78             It's very similar to the C module but:
79              
80             =over 4
81              
82             =item It doesn't require Perl 5.10
83              
84             =item It's slightly more user friendly
85              
86             =item It's more CPAN namespace friendly
87              
88             =back
89              
90             =head1 METHODS
91              
92             =head2 new [site]
93              
94             Create a new client object.
95              
96             Requires a username and a password.
97              
98             Optionally you can pass in either a site id or a C object to
99             specify which site to read/write from.
100              
101             If it's not passed in then it can be passed in later or your default site will be used.
102              
103             =cut
104             sub new {
105             my $class = shift;
106             # TODO none authenticated
107             my $user = shift || die "You must pass a username";
108             my $pass = shift || die "You must pass a password";
109             my $self = bless { user => $user, pass => $pass, ua => _get_ua(), auth_key => encode_base64($user.":".$pass) };
110              
111             $self->site(@_);
112              
113             return $self;
114            
115             }
116              
117             sub _get_ua {
118             return LWP::UserAgent->new( agent => __PACKAGE__."-".$VERSION );
119             }
120              
121             =head2 site [site]
122              
123             Get or set the current site being used.
124              
125             May return undef.
126              
127             =cut
128             sub site {
129             my $self = shift;
130              
131             if (@_) {
132             ($self->{site_id}, $self->{site}) = $self->_load_site(shift);
133             }
134             return $self->{site};
135             }
136              
137             sub _load_site {
138             my $self = shift;
139             my $site = shift || return;
140            
141             if (ref($site)) {
142             return ($site->id, $site);
143             } else {
144             return ($site, $self->site_from_id($site));
145             }
146             }
147              
148             =head2 site_from_id
149              
150             Return a C object based on a site id.
151              
152             Returns undef if the site can't be found.
153              
154             =cut
155              
156              
157             sub site_from_id {
158             my $self = shift;
159             my $id = shift;
160             $self->get_sites; # Force loading of sites
161             return $self->{sites}->{$id};
162             }
163              
164             =head2 get_sites
165              
166             Get a list of all the user's sites.
167              
168             =cut
169             sub get_sites {
170             my $self = shift;
171             my @sites = $self->_load("GET", "${DOMAIN}/api/getsites", 'site', 'name', 'Net::Posterous::Site');
172             $self->{sites}->{$_->id} = $_ for @sites;
173             return @sites;
174             }
175              
176             =head2 get_posts [opt[s]]
177              
178             Get the posts from a site.
179              
180             Uses, in order - the site passed in using the option key C, the site set by the user, the default site.
181              
182             The options are
183              
184             =over 4
185              
186             =item site_id
187              
188             The id of the site to read from
189              
190             =item hostname
191              
192             Subdomain of the site to read from
193              
194             =item num_posts
195              
196             How many posts you want. Default is 10, max is 50.
197              
198             =item page
199              
200             What 'page' you want (based on num_posts). Default is 1
201              
202             =item tag
203              
204             Only get items with this tag.
205              
206             =back
207              
208              
209             =cut
210             sub get_posts {
211             my $self = shift;
212             my %opts = @_;
213             my $site = $self->_load_site(delete $opts{site} || $self->site); # normalise the site to an object
214             my %params = $site ? ( %opts, site_id => $site->id ) : %opts;
215             return $self->_load("GET", "${DOMAIN}/api/readposts", 'post', 'id', 'Net::Posterous::Post', %params);
216             }
217              
218             =head2 get_post
219              
220             Get an id via the http://post.ly short code i.e 123abc in http://post.ly/123abc
221              
222             =cut
223              
224             sub get_post {
225             my $self = shift;
226             my $id = shift;
227            
228             $id =~ s!^http://post\.ly/!!; # be liberal in what you accept etc etc
229            
230             my @posts = $self->_load("GET", "${DOMAIN}/api/getpost", 'post', 'id', 'Net::Posterous::Post', id => $id);
231             return shift @posts;
232             }
233              
234             =head2 get_tags [opt[s]]
235              
236             Get a list of tags for a site.
237              
238             Uses, in order - the site passed in using the option key C, the site set by the user, the default site.
239              
240             The options are
241              
242             =over 4
243              
244             =item site_id
245              
246             Optional. Id of the site to read from
247              
248             =item hostname
249              
250             Optional. Subdomain of the site to read from
251              
252             =back
253              
254             =cut
255             sub get_tags {
256             my $self = shift;
257             my %opts = @_;
258             my $site = $self->_load_site(delete $opts{site} || $self->site); # normalise the site to an object
259             my %params = $site ? ( %opts, site_id => $site->id ) : %opts;
260             return $self->_load("GET", "${DOMAIN}/api/gettags", 'tag', 'id', 'Net::Posterous::Tag', %params);
261             }
262              
263             =head2 post [opt[s]]
264              
265             Post or update a C or object.
266              
267             Uses, in order - the site passed in using the option key C, the site set by the user, the default site.
268              
269             =cut
270             sub post {
271             my $self = shift;
272             my $post = shift;
273             my %opts = @_;
274             my $site = $self->_load_site(delete $opts{site} || $self->site); # normalise the site to an object
275            
276             my %params = $post->_to_params();
277            
278             my $url = $DOMAIN;
279             # Update or Create depending on whether the Post already has an id
280             if (my $id = delete $params{id}) {
281             $url .= "/api/updatepost";
282             $params{post_id} = $id;
283             } else {
284             $url .= "/api/newpost";
285             $params{site_id} = $site->id if $site;
286             }
287             return $self->_post($post, $url , "post", %params);
288             }
289              
290             =head2 comment
291              
292             Add a comment to the post.
293              
294             =cut
295             sub comment {
296             my $self = shift;
297             my $post = shift;
298             my $comment = shift;
299            
300             my $post_id = ref($post) ? $post->id : $post;
301             my %params = ($comment->_to_params, post_id => $post_id);
302             return $self->_post($comment, "${DOMAIN}/api/newcomment" , "comment", %params);
303             }
304              
305             sub _post {
306             my $self = shift;
307             my $obj = shift;
308             my $url = shift;
309             my $key = shift;
310             my %params = @_;
311            
312             my $res = $self->_request("POST", $url, %params) || return undef;
313            
314             my $data = eval { XMLin($res->content) };
315             if ($@) {
316             $self->error("Couldn't parse XML response: $@");
317             return undef;
318             }
319            
320             # Check to see if we got an error, despite getting a 200 ok
321             if (my $error = $self->_xml_nok($data)) {
322             $self->error("Couldn't POST $url - $error");
323             return undef;
324             }
325            
326             # Merge the new data with object
327             # It would be awesome if Posterous returned a full representation of the new object after a POST
328             my $tmp = $data->{$key} || {};
329             foreach my $key (keys %$tmp) {
330             $obj->$key($tmp->{$key});
331             }
332             return $obj;
333             }
334              
335              
336              
337             sub _load {
338             my $self = shift;
339             my $meth = shift;
340             my $url = shift;
341             my $key = shift;
342             my $id_name = shift;
343             my $class = shift;
344             my %params = @_;
345            
346             # TODO paging
347            
348             my $res = $self->_request($meth, $url, %params) || return ();
349             my $data = eval { XMLin($res->content, ForceArray => [$key, 'media'], KeyAttr => $id_name) };
350             if ($@) {
351             $self->error("Couldn't parse XML response: $@");
352             return undef;
353             }
354            
355             # Check to see if we got an error, despite getting a 200 ok
356             if (my $error = $self->_xml_nok($data)) {
357             $self->error("Couldn't $meth $url - $error");
358             return undef;
359             }
360            
361             # Create new objects from the results
362             my $items = $data->{$key} || {};
363             my @results;
364             foreach my $id (keys %$items) {
365             my $obj = $items->{$id};
366             $obj->{$id_name} = $id;
367             push @results, $class->new(%$obj);
368             }
369             return @results;
370             }
371              
372             # Check the XML for errors
373             sub _xml_nok {
374             my $self = shift;
375             my $data = shift;
376             return 0 if "ok" eq $data->{stat};
377             return $data->{err}->{code}.": ".$data->{err}->{msg};
378             }
379              
380             sub _request {
381             my $self = shift;
382             my $meth = shift;
383             my $url = shift;
384             my %opts = @_;
385             my $ua = $self->{ua};
386              
387             $self->{error} = undef;
388            
389            
390             my $req;
391             my $req_url = URI->new($url);
392             $req_url->query_form(%opts);
393             if ('GET' eq $meth || 'PUT' eq $meth) {
394             $req = HTTP::Request->new( $meth => $req_url);
395             } else {
396             my @content;
397             # This little shennanigans is to allow us to post multiple file uploads
398             # by getting round HTTP::Request::Common's helper feature
399             foreach my $key (keys %opts) {
400             my $value = $opts{$key};
401             $value = [$value] unless ref($value);
402             push @content, ($key, $_) for @$value;
403             }
404             $req = POST($url, Content_Type => 'form-data', Content => [ @content ]);
405             }
406             $req->header(Authorization => "Basic ".$self->{auth_key});
407              
408             my $res = $ua->request($req);
409             unless ($res->is_success) {
410             $self->error("Couldn't $meth $url: ".$res->content);
411             return undef;
412             }
413             return $res;
414             }
415              
416             =head2 error
417              
418             Get or set the last error
419              
420             =cut
421             sub error {
422             my $self = shift;
423             $self->{error} = shift if @_;
424             return $self->{error};
425             }
426              
427             =head1 ADDING MEDIA
428              
429             The way to add media is to create a new C file and then add that to the
430             C object that's going to be created or updated.
431              
432             It will then be turned into a proper C object when the Post is retrieved.
433              
434             =head1 BUGS
435              
436             The Posterous API docs mention being able to post "common document formats" but there's no docs for it.
437              
438             I'll add it when I come across it.
439              
440             =head1 DEVELOPERS
441              
442             The latest code for this module can be found at
443              
444             https://svn.unixbeard.net/simon/Net-Posterous
445              
446             =head1 AUTHOR
447              
448             Simon Wistow, C<>
449              
450             =head1 BUGS
451              
452             Please report any bugs or feature requests to C, or through
453             the web interface at L. I will be notified, and then you'll
454             automatically be notified of progress on your bug as I make changes.
455              
456             =head1 SUPPORT
457              
458             You can find documentation for this module with the perldoc command.
459              
460             perldoc Net::Posterous
461              
462             You can also look for information at:
463              
464             =over 4
465              
466             =item * RT: CPAN's request tracker
467              
468             L
469              
470             =item * AnnoCPAN: Annotated CPAN documentation
471              
472             L
473              
474             =item * CPAN Ratings
475              
476             L
477              
478             =item * Search CPAN
479              
480             L
481              
482             =back
483              
484             =head1 COPYRIGHT & LICENSE
485              
486             Copyright 2010 Simon Wistow, all rights reserved.
487              
488             This program is free software; you can redistribute it and/or modify it
489             under the same terms as Perl itself.
490              
491             =cut
492              
493             1;