package Mojo::Facebook; =head1 NAME Mojo::Facebook - Talk with Facebook =head1 VERSION 0.03 =head1 DESCRIPTION This module implements basic actions to the Facebook graph protocol. =head1 SYNOPSIS use Mojo::Facebook; my $fb = Mojo::Facebook->new(access_token => $some_secret); # fetch facebook name Mojo::IOLoop->delay( sub { my($delay) = @_; $fb->fetch({ from => '1234567890', fields => 'name', }, $delay->begin); }, sub { my($delay, $res) = @_; warn $res->{error} || $res->{name}; }, ) # fetch cover photo url $fb->fetch({ from => '1234567890', fields => ['cover'] }, sub { my($fb, $res) = @_; return $res->{errors} if $res->{error}; warn $res->{cover}{source}; # URL }); =head1 ERROR HANDLING Facebook JSON errors will be set in the C<$res> hash returned to the callback: =head2 Error messages =over 4 =item * Could not decode JSON from Facebook =item * $fb_json->{error}{message} =item * HTTP status message =item * Unknown error from JSON structure =back =cut use Mojo::Base -base; use Mojo::UserAgent; use Mojo::Util qw/ url_unescape /; use constant TEST => $INC{'Test/Mojo.pm'}; our $VERSION = '0.03'; =head1 ATTRIBUTES =head2 access_token This attribute need to be set when doing L</fetch> on private objects or when issuing L</post>. This is not "code" query param from the Facebook authentication process, something which need to be fetched from Facebook later on. See the source code forL<Mojolicious::Plugin::OAuth2> for details. $oauth2->get_token(facebook => sub { my($oauth2, $access_token) = @_; $fb = Mojo::Facebook->new(access_token => $access_token); $fb->post({ to => $fb_uid, message => "Mojo::Facebook works!", }, sub { # ... }); }); =head2 app_namespace This attribute is used by L</publish> as prefix to the publish URL: https://graph.facebook.com/$id/$app_namespace:$action =head2 scheme Used to either run requests over "http" or "https". Default to "https". =cut has access_token => ''; has app_namespace => ''; has scheme => 'https'; has _ua => sub { Mojo::UserAgent->new }; =head1 METHODS =head2 fetch $self->fetch({ from => $id, fields => [...] ids => [...], limit => $Int, offset => $Int, }, $callback); Will fetch information from Facebook about a user. C<$id> can be ommitted and will then default to "me". C<$callback> will be called like this: $callback->($self, $res); C<$res> will be a hash-ref containing the result. Look for the "error" key to check for errors. =cut sub fetch { my($self, $args, $cb) = @_; my $tx = $self->_tx('GET'); my $url = $tx->req->url; Scalar::Util::weaken($self); if($self->access_token) { $url->query([ access_token => url_unescape $self->access_token ]); } for my $key (qw/ fields ids /) { my $value = $args->{$key} or next; $url->query([ $key => ref $value eq 'ARRAY' ? join ',', @$value : $value ]); } for my $key (qw/ date_format limit metadata offset since until /) { defined $args->{$key} or next; $url->query([ $key => $args->{$key} ]); } push @{ $url->path->parts }, $args->{from} || 'me'; $self->_ua->start($tx, sub { $self->$cb(__check_response(@_)) }); } =head2 post $self->post({ to => $id, message => $str, link => $url, name => $str, caption => $str, description => $str, picture => $url, }, $callback); Creates a post at C<$who>'s wall, looking like this: .------------------------------------. | $message ... | | | | .----------. | | | $picture | [$link]($name) | | | | $caption ... | | | | $description ... | | '----------' | '------------------------------------' C<$callback> will be called like this: $callback->($self, $res); C<$res> will be a hash-ref containing the result. Look for the "error" key to check for errors. TODO: Tags are not supported yet. Getting { "error":{ "message":"(#100) Array does not resolve to a valid user ID", "type":"OAuthException", "code":100 } } =cut sub post { my($self, $args, $cb) = @_; my($message, $tags) = $self->_message_to_tags($args->{message}); my $tx = $self->_tx('POST'); my $p = Mojo::Parameters->new; my $path = $tx->req->url->path; Scalar::Util::weaken($self); $p->append(access_token => $self->access_token); $p->append(message => $message); for my $key (qw/ picture link name caption description source place /) { $args->{$key} or next; $p->append($key => $args->{$key}); } #if(@$tags) { # $p->append(tags => Mojo::JSON->new->encode($tags)); #} if($args->{action} and $args->{object}) { push @{ $path->parts }, $args->{to}, join ':', @$args{qw/ object action /}; } else { push @{ $path->parts }, $args->{to}, 'feed'; } $tx->req->body($p->to_string); $self->_ua->start($tx, sub { $self->$cb(__check_response(@_)) }); } sub _message_to_tags { my($self, $message) = @_; my @tags; while(1) { $message =~ s/\@\[ (\w+) : ([^\]]+) \]/$2/ox or last; push @tags, { id => int $1, name => $2, offset => $-[0], length => length $2, }; } return $message, \@tags; } =head2 comment $self->comment({ on => $id, message => $str }, $callback); Will add a comment to a graph element with the given C<$id>. C<$callback> will be called like this: $callback->($self, $res); C<$res> will be a hash-ref containing the result. Look for the "error" key to check for errors. =cut sub comment { my($self, $args, $cb) = @_; my $tx = $self->_tx('POST'); my $p = Mojo::Parameters->new; Scalar::Util::weaken($self); $p->append(access_token => $self->access_token); $p->append(message => $args->{message}); $tx->req->body($p->to_string); push @{ $tx->req->url->path->parts }, $args->{on}, 'comments'; $self->_ua->start($tx, sub { $self->$cb(__check_response(@_)) }); } =head2 publish $self->publish({ to => $id, action => $str, $object_name => $object_url, # optional start_time => $DateTime, end_time => $DateTime, expires_in => $int, message => $str, place => $facebook_id, ref => String, tags => "$facebook_id,...", # any other key/value is considered to be custom $custom_attribute => $any, }); Publish a story at C<$who>'s wall, looking like this: .--------------------------------------. | $who $action a $object_name ... $app | | | | .----------. | | | $image | [$url]($title) | | | | $descripton ... | | '----------' | '--------------------------------------' Required HTML: <meta property="fb:app_id" content="$app_id" /> <meta property="og:image" content="$url" /> <meta property="og:title" content="$str" /> <meta property="og:url" content="$url_to_self" /> <meta property="og:description" content="$str"> <meta property="og:type" content="$app_namespace:$action" /> C<$callback> will be called like this: $callback->($self, $res); C<$res> will be a hash-ref containing the result. Look for the "error" key to check for errors. =cut sub publish { my($self, $args, $cb) = @_; my $tx = $self->_tx('POST'); my $p = Mojo::Parameters->new; my $tags = []; Scalar::Util::weaken($self); if($args->{message}) { ($args->{message}, $tags) = $self->_message_to_tags($args->{message}); } while(my($name, $value) = each %$args) { next if $name eq 'to' or $name eq 'action'; $p->append($name => $value); } if(@$tags) { $p->append(tags => join ',', map { $_->{id} } @$tags); } $p->append(access_token => $self->access_token); push @{ $tx->req->url->path }, $args->{to}, join ':', $self->app_namespace, $args->{action}; $tx->req->body($p->to_string); $self->_ua->start($tx, sub { $self->$cb(__check_response(@_)) }); } =head2 delete_object $self->delete_object($id, $callback); Will try to remove an object from Facebook. C<&callback> will be called like this: $callback->($self, $res); C<$res> will be a hash-ref containing the result. Look for the "error" key to check for errors. =cut sub delete_object { my($self, $id, $cb) = @_; my $tx = $self->_tx('DELETE'); Scalar::Util::weaken($self); $tx->req->url->query->param(access_token => $self->access_token); push @{ $tx->req->url->path->parts }, $id; $self->_ua->start($tx, sub { $self->$cb(__check_response(@_)) }); } =head2 picture $url = $self->picture; $url = $self->picture($who, $type); Returns a L<Mojo::URL> object with the URL to a Facebook image. C<$who> defaults to "me". C<$type> can be "square", "small" or "large". Default to "square". =cut sub picture { my $self = shift; my $who = shift || 'me'; my $type = shift || 'square'; my $url = Mojo::URL->new($ENV{FAKE_FACEBOOK_URL} || 'https://graph.facebook.com'); push @{ $url->path->parts }, $who, 'picture'; $url->query(type => $type); $url; } sub __check_response { my($ua, $tx) = @_; my $res = $tx->res; my $json = $res->json; if(ref $json eq 'HASH' and $json->{error}) { $json->{error} = $json->{error}{message} if $json->{error}{message}; $json->{code} = $res->code; } elsif($res->error) { $json = { error => ($res->error)[0], code => $res->code }; } elsif(!$json) { $json = { error => 'Could not decode JSON from Facebook', code => $res->code }; } $json->{__tx} = $tx if TEST; $json; } sub _tx { my($self, $method) = @_; my $url = Mojo::URL->new($ENV{FAKE_FACEBOOK_URL} || 'https://graph.facebook.com'); $url->scheme($self->scheme) if $url->host; $self->_ua->build_tx($method => $url); } =head1 COPYRIGHT & LICENSE This library is free software. You can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR Jan Henning Thorsen - jhthorsen@cpan.org =cut 1; =cut 1;