| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Facebook::OpenGraph; | 
| 2 | 33 |  |  | 33 |  | 2395800 | use strict; | 
|  | 33 |  |  |  |  | 217 |  | 
|  | 33 |  |  |  |  | 843 |  | 
| 3 | 33 |  |  | 33 |  | 160 | use warnings; | 
|  | 33 |  |  |  |  | 55 |  | 
|  | 33 |  |  |  |  | 988 |  | 
| 4 | 33 |  |  | 33 |  | 707 | use 5.008001; | 
|  | 33 |  |  |  |  | 103 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 | 33 |  |  | 33 |  | 12012 | use Facebook::OpenGraph::Response; | 
|  | 33 |  |  |  |  | 86 |  | 
|  | 33 |  |  |  |  | 942 |  | 
| 7 | 33 |  |  | 33 |  | 14291 | use HTTP::Request::Common; | 
|  | 33 |  |  |  |  | 673049 |  | 
|  | 33 |  |  |  |  | 2096 |  | 
| 8 | 33 |  |  | 33 |  | 239 | use URI; | 
|  | 33 |  |  |  |  | 70 |  | 
|  | 33 |  |  |  |  | 722 |  | 
| 9 | 33 |  |  | 33 |  | 5444 | use Furl::HTTP; | 
|  | 33 |  |  |  |  | 165806 |  | 
|  | 33 |  |  |  |  | 1017 |  | 
| 10 | 33 |  |  | 33 |  | 13652 | use Data::Recursive::Encode; | 
|  | 33 |  |  |  |  | 350453 |  | 
|  | 33 |  |  |  |  | 1173 |  | 
| 11 | 33 |  |  | 33 |  | 220 | use JSON 2 (); | 
|  | 33 |  |  |  |  | 714 |  | 
|  | 33 |  |  |  |  | 720 |  | 
| 12 | 33 |  |  | 33 |  | 156 | use Carp qw(croak); | 
|  | 33 |  |  |  |  | 61 |  | 
|  | 33 |  |  |  |  | 1507 |  | 
| 13 | 33 |  |  | 33 |  | 14839 | use Digest::SHA qw(hmac_sha256 hmac_sha256_hex); | 
|  | 33 |  |  |  |  | 86648 |  | 
|  | 33 |  |  |  |  | 2518 |  | 
| 14 | 33 |  |  | 33 |  | 12760 | use MIME::Base64::URLSafe qw(urlsafe_b64decode); | 
|  | 33 |  |  |  |  | 39046 |  | 
|  | 33 |  |  |  |  | 1800 |  | 
| 15 | 33 |  |  | 33 |  | 208 | use Scalar::Util qw(blessed); | 
|  | 33 |  |  |  |  | 66 |  | 
|  | 33 |  |  |  |  | 98472 |  | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | our $VERSION = '1.25'; | 
| 18 |  |  |  |  |  |  |  | 
| 19 |  |  |  |  |  |  | sub new { | 
| 20 | 64 |  |  | 64 | 1 | 141726 | my $class = shift; | 
| 21 | 64 |  | 100 |  |  | 278 | my $args  = shift || +{}; | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | return bless +{ | 
| 24 |  |  |  |  |  |  | app_id              => $args->{app_id}, | 
| 25 |  |  |  |  |  |  | secret              => $args->{secret}, | 
| 26 |  |  |  |  |  |  | namespace           => $args->{namespace}, | 
| 27 |  |  |  |  |  |  | access_token        => $args->{access_token}, | 
| 28 |  |  |  |  |  |  | redirect_uri        => $args->{redirect_uri}, | 
| 29 |  |  |  |  |  |  | batch_limit         => $args->{batch_limit} || 50, | 
| 30 |  |  |  |  |  |  | is_beta             => $args->{is_beta} || 0, | 
| 31 |  |  |  |  |  |  | json                => $args->{json} || JSON->new->utf8, | 
| 32 |  |  |  |  |  |  | use_appsecret_proof => $args->{use_appsecret_proof} || 0, | 
| 33 |  |  |  |  |  |  | use_post_method     => $args->{use_post_method} || 0, | 
| 34 |  |  |  |  |  |  | version             => $args->{version} || undef, | 
| 35 | 64 |  | 100 |  |  | 2315 | ua                  => $args->{ua} || Furl::HTTP->new( | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
| 36 |  |  |  |  |  |  | capture_request     => 1, | 
| 37 |  |  |  |  |  |  | agent               => __PACKAGE__ . '/' . $VERSION, | 
| 38 |  |  |  |  |  |  | ), | 
| 39 |  |  |  |  |  |  | }, $class; | 
| 40 |  |  |  |  |  |  | } | 
| 41 |  |  |  |  |  |  |  | 
| 42 |  |  |  |  |  |  | # accessors | 
| 43 | 33 |  |  | 33 | 1 | 220 | sub app_id              { shift->{app_id}              } | 
| 44 | 29 |  |  | 29 | 1 | 208 | sub secret              { shift->{secret}              } | 
| 45 | 40 |  |  | 40 | 1 | 772 | sub ua                  { shift->{ua}                  } | 
| 46 | 4 |  |  | 4 | 1 | 21 | sub namespace           { shift->{namespace}           } | 
| 47 | 82 |  |  | 82 | 1 | 2553 | sub access_token        { shift->{access_token}        } | 
| 48 | 16 |  |  | 16 | 1 | 79 | sub redirect_uri        { shift->{redirect_uri}        } | 
| 49 | 17 |  |  | 17 | 1 | 349 | sub batch_limit         { shift->{batch_limit}         } | 
| 50 | 57 |  |  | 57 | 1 | 223 | sub is_beta             { shift->{is_beta}             } | 
| 51 | 66 |  |  | 66 | 1 | 1283 | sub json                { shift->{json}                } | 
| 52 | 40 |  |  | 40 | 1 | 136 | sub use_appsecret_proof { shift->{use_appsecret_proof} } | 
| 53 | 2 |  |  | 2 | 1 | 8 | sub use_post_method     { shift->{use_post_method}     } | 
| 54 | 61 |  |  | 61 | 1 | 248 | sub version             { shift->{version}             } | 
| 55 |  |  |  |  |  |  |  | 
| 56 |  |  |  |  |  |  | sub uri { | 
| 57 | 44 |  |  | 44 | 1 | 15248 | my $self = shift; | 
| 58 |  |  |  |  |  |  |  | 
| 59 | 44 | 100 |  |  |  | 131 | my $base = $self->is_beta ? 'https://graph.beta.facebook.com/' | 
| 60 |  |  |  |  |  |  | :                  'https://graph.facebook.com/' | 
| 61 |  |  |  |  |  |  | ; | 
| 62 |  |  |  |  |  |  |  | 
| 63 | 44 |  |  |  |  | 169 | return $self->_uri($base, @_); | 
| 64 |  |  |  |  |  |  | } | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | sub video_uri { | 
| 67 | 7 |  |  | 7 | 1 | 12279 | my $self = shift; | 
| 68 |  |  |  |  |  |  |  | 
| 69 | 7 | 100 |  |  |  | 17 | my $base = $self->is_beta ? 'https://graph-video.beta.facebook.com/' | 
| 70 |  |  |  |  |  |  | :                  'https://graph-video.facebook.com/' | 
| 71 |  |  |  |  |  |  | ; | 
| 72 |  |  |  |  |  |  |  | 
| 73 | 7 |  |  |  |  | 19 | return $self->_uri($base, @_); | 
| 74 |  |  |  |  |  |  | } | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | sub site_uri { | 
| 77 | 4 |  |  | 4 | 1 | 12 | my $self = shift; | 
| 78 |  |  |  |  |  |  |  | 
| 79 | 4 | 100 |  |  |  | 9 | my $base = $self->is_beta ? 'https://www.beta.facebook.com/' | 
| 80 |  |  |  |  |  |  | :                  'https://www.facebook.com/' | 
| 81 |  |  |  |  |  |  | ; | 
| 82 |  |  |  |  |  |  |  | 
| 83 | 4 |  |  |  |  | 11 | return $self->_uri($base, @_); | 
| 84 |  |  |  |  |  |  | } | 
| 85 |  |  |  |  |  |  |  | 
| 86 |  |  |  |  |  |  | sub _uri { | 
| 87 | 55 |  |  | 55 |  | 153 | my ($self, $base, $path, $param_ref) = @_; | 
| 88 | 55 |  | 100 |  |  | 341 | my $uri = URI->new_abs($path || '/', $base); | 
| 89 |  |  |  |  |  |  | $uri->query_form(+{ | 
| 90 |  |  |  |  |  |  | $uri->query_form,       # when given $path is like /foo?bar=bazz | 
| 91 | 55 | 100 |  |  |  | 186491 | %{ $param_ref || +{} }, # additional query parameter | 
|  | 55 |  |  |  |  | 1232 |  | 
| 92 |  |  |  |  |  |  | }); | 
| 93 |  |  |  |  |  |  |  | 
| 94 | 55 |  |  |  |  | 3094 | return $uri; | 
| 95 |  |  |  |  |  |  | } | 
| 96 |  |  |  |  |  |  |  | 
| 97 |  |  |  |  |  |  | # Login for Games on Facebook > Checking Login Status > Parsing the Signed Request | 
| 98 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/using-login-with-games | 
| 99 |  |  |  |  |  |  | sub parse_signed_request { | 
| 100 | 5 |  |  | 5 | 1 | 125 | my ($self, $signed_request) = @_; | 
| 101 | 5 | 50 |  |  |  | 10 | croak 'signed_request is not given' unless $signed_request; | 
| 102 | 5 | 100 |  |  |  | 10 | croak 'secret key must be set'      unless $self->secret; | 
| 103 |  |  |  |  |  |  |  | 
| 104 |  |  |  |  |  |  | # "1. Split the signed request into two parts delineated by a '.' character | 
| 105 |  |  |  |  |  |  | # (eg. 238fsdfsd.oijdoifjsidf899)" | 
| 106 | 4 |  |  |  |  | 17 | my ($enc_sig, $payload) = split(m{ \. }xms, $signed_request); | 
| 107 |  |  |  |  |  |  |  | 
| 108 |  |  |  |  |  |  | # "2. Decode the first part - the encoded signature - from base64url" | 
| 109 | 4 |  |  |  |  | 11 | my $sig = urlsafe_b64decode($enc_sig); | 
| 110 |  |  |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | # "3. Decode the second part - the 'payload' - from base64url and then | 
| 112 |  |  |  |  |  |  | # decode the resultant JSON object" | 
| 113 | 4 |  |  |  |  | 67 | my $val = $self->json->decode(urlsafe_b64decode($payload)); | 
| 114 |  |  |  |  |  |  |  | 
| 115 |  |  |  |  |  |  | # "It specifically uses HMAC-SHA256 encoding, which you can again use with | 
| 116 |  |  |  |  |  |  | # most programming languages." | 
| 117 |  |  |  |  |  |  | croak 'algorithm must be HMAC-SHA256' | 
| 118 | 4 | 50 |  |  |  | 63 | unless uc( $val->{algorithm} ) eq 'HMAC-SHA256'; | 
| 119 |  |  |  |  |  |  |  | 
| 120 |  |  |  |  |  |  | # "You can compare this encoded signature with an expected signature using | 
| 121 |  |  |  |  |  |  | # the payload you received as well as the app secret which is known only to | 
| 122 |  |  |  |  |  |  | # your and ensure that they match." | 
| 123 | 4 |  |  |  |  | 11 | my $expected_sig = hmac_sha256($payload, $self->secret); | 
| 124 | 4 | 50 |  |  |  | 19 | croak 'Signature does not match' unless $sig eq $expected_sig; | 
| 125 |  |  |  |  |  |  |  | 
| 126 | 4 |  |  |  |  | 9 | return $val; | 
| 127 |  |  |  |  |  |  | } | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | # Detailed flow is described here. | 
| 130 |  |  |  |  |  |  | # Manually Build a Login Flow > Logging people in > Invoking the login dialog | 
| 131 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/ | 
| 132 |  |  |  |  |  |  | # | 
| 133 |  |  |  |  |  |  | # Parameters for login dialog are shown here. | 
| 134 |  |  |  |  |  |  | # Login Dialog > Parameters | 
| 135 |  |  |  |  |  |  | # https://developers.facebook.com/docs/reference/dialogs/oauth/ | 
| 136 |  |  |  |  |  |  | sub auth_uri { | 
| 137 | 6 |  |  | 6 | 1 | 289 | my ($self, $param_ref) = @_; | 
| 138 | 6 |  | 100 |  |  | 17 | $param_ref ||= +{}; | 
| 139 | 6 | 100 | 66 |  |  | 12 | croak 'redirect_uri and app_id must be set' | 
| 140 |  |  |  |  |  |  | unless $self->redirect_uri && $self->app_id; | 
| 141 |  |  |  |  |  |  |  | 
| 142 |  |  |  |  |  |  | # "A comma separated list of permission names which you would like people | 
| 143 |  |  |  |  |  |  | # to grant your app." | 
| 144 | 5 | 100 |  |  |  | 14 | if (my $scope_ref = ref $param_ref->{scope}) { | 
| 145 | 4 | 100 |  |  |  | 16 | croak 'scope must be string or array ref' unless $scope_ref eq 'ARRAY'; | 
| 146 | 3 |  |  |  |  | 5 | $param_ref->{scope} = join q{,}, @{ $param_ref->{scope} }; | 
|  | 3 |  |  |  |  | 11 |  | 
| 147 |  |  |  |  |  |  | } | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  | # "The URL to redirect to after a button is clicked or tapped in the | 
| 150 |  |  |  |  |  |  | # dialog." | 
| 151 | 4 |  |  |  |  | 8 | $param_ref->{redirect_uri} = $self->redirect_uri; | 
| 152 |  |  |  |  |  |  |  | 
| 153 |  |  |  |  |  |  | # "Your App ID. This is called client_id instead of app_id for this | 
| 154 |  |  |  |  |  |  | # particular method in order to be compliant with the OAuth 2.0 | 
| 155 |  |  |  |  |  |  | # specification." | 
| 156 | 4 |  |  |  |  | 6 | $param_ref->{client_id} = $self->app_id; | 
| 157 |  |  |  |  |  |  |  | 
| 158 |  |  |  |  |  |  | # "If you are using the URL redirect dialog implementation, then this will | 
| 159 |  |  |  |  |  |  | # be a full page display, shown within Facebook.com. This display type is | 
| 160 |  |  |  |  |  |  | # called page." | 
| 161 | 4 |  | 50 |  |  | 15 | $param_ref->{display} ||= 'page'; | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | # "Response data is included as URL parameters and contains code parameter | 
| 164 |  |  |  |  |  |  | # (an encrypted string unique to each login request). This is the default | 
| 165 |  |  |  |  |  |  | # behaviour if this parameter is not specified." | 
| 166 | 4 |  | 50 |  |  | 15 | $param_ref->{response_type} ||= 'code'; | 
| 167 |  |  |  |  |  |  |  | 
| 168 | 4 |  |  |  |  | 11 | my $uri = $self->site_uri('/dialog/oauth', $param_ref); | 
| 169 |  |  |  |  |  |  |  | 
| 170 |  |  |  |  |  |  | # Platform Versioning > Making Versioned Requests > Dialogs. | 
| 171 |  |  |  |  |  |  | # https://developers.facebook.com/docs/apps/versions#dialogs | 
| 172 | 4 |  |  |  |  | 11 | $uri->path( $self->gen_versioned_path($uri->path) ); | 
| 173 |  |  |  |  |  |  |  | 
| 174 | 4 |  |  |  |  | 116 | return $uri->as_string; | 
| 175 |  |  |  |  |  |  | } | 
| 176 |  |  |  |  |  |  |  | 
| 177 |  |  |  |  |  |  | sub set_access_token { | 
| 178 | 4 |  |  | 4 | 1 | 15 | my ($self, $token) = @_; | 
| 179 | 4 |  |  |  |  | 10 | $self->{access_token} = $token; | 
| 180 |  |  |  |  |  |  | } | 
| 181 |  |  |  |  |  |  |  | 
| 182 |  |  |  |  |  |  | # Access Tokens > App Tokens | 
| 183 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/access-tokens/#apptokens | 
| 184 |  |  |  |  |  |  | sub get_app_token { | 
| 185 | 3 |  |  | 3 | 1 | 153 | my $self = shift; | 
| 186 |  |  |  |  |  |  |  | 
| 187 |  |  |  |  |  |  | # Document does not mention what grant_type is all about or what values can | 
| 188 |  |  |  |  |  |  | # be set, but RFC 6749 covers the basic idea of grant types and its Section | 
| 189 |  |  |  |  |  |  | # 4.4 describes Client Credentials Grant. | 
| 190 |  |  |  |  |  |  | # http://tools.ietf.org/html/rfc6749#section-4.4 | 
| 191 | 3 |  |  |  |  | 13 | return $self->_get_token(+{grant_type => 'client_credentials'}); | 
| 192 |  |  |  |  |  |  | } | 
| 193 |  |  |  |  |  |  |  | 
| 194 |  |  |  |  |  |  | # Manually Build a Login Flow > Confirming identity > Exchanging code for an access token | 
| 195 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow | 
| 196 |  |  |  |  |  |  | sub get_user_token_by_code { | 
| 197 | 2 |  |  | 2 | 1 | 49 | my ($self, $code) = @_; | 
| 198 |  |  |  |  |  |  |  | 
| 199 | 2 | 50 |  |  |  | 6 | croak 'code is not given'        unless $code; | 
| 200 | 2 | 50 |  |  |  | 7 | croak 'redirect_uri must be set' unless $self->redirect_uri; | 
| 201 |  |  |  |  |  |  |  | 
| 202 | 2 |  |  |  |  | 7 | my $query_ref = +{ | 
| 203 |  |  |  |  |  |  | redirect_uri => $self->redirect_uri, | 
| 204 |  |  |  |  |  |  | code         => $code, | 
| 205 |  |  |  |  |  |  | }; | 
| 206 | 2 |  |  |  |  | 7 | return $self->_get_token($query_ref); | 
| 207 |  |  |  |  |  |  | } | 
| 208 |  |  |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | sub get_user_token_by_cookie { | 
| 210 | 4 |  |  | 4 | 1 | 179 | my ($self, $cookie_value) = @_; | 
| 211 |  |  |  |  |  |  |  | 
| 212 | 4 | 100 |  |  |  | 21 | croak 'cookie value is not given' unless $cookie_value; | 
| 213 |  |  |  |  |  |  |  | 
| 214 | 3 |  |  |  |  | 6 | my $parsed_signed_request = $self->parse_signed_request($cookie_value); | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | # https://github.com/oklahomer/p5-Facebook-OpenGraph/issues/1#issuecomment-41065480 | 
| 217 |  |  |  |  |  |  | # parsed content should be something like below. | 
| 218 |  |  |  |  |  |  | # { | 
| 219 |  |  |  |  |  |  | #     algorithm => "HMAC-SHA256", | 
| 220 |  |  |  |  |  |  | #     issued_at => 1398180151, | 
| 221 |  |  |  |  |  |  | #     code      => "SOME_OPAQUE_STRING", | 
| 222 |  |  |  |  |  |  | #     user_id   => 44007581, | 
| 223 |  |  |  |  |  |  | # }; | 
| 224 |  |  |  |  |  |  | croak q{"code" is not contained in cookie value: } . $cookie_value | 
| 225 | 3 | 100 |  |  |  | 18 | unless $parsed_signed_request->{code}; | 
| 226 |  |  |  |  |  |  |  | 
| 227 |  |  |  |  |  |  | # Redirect_uri MUST be empty string in this case. | 
| 228 |  |  |  |  |  |  | # That's why I didn't use get_user_token_by_code(). | 
| 229 |  |  |  |  |  |  | my $query_ref = +{ | 
| 230 |  |  |  |  |  |  | code         => $parsed_signed_request->{code}, | 
| 231 | 2 |  |  |  |  | 5 | redirect_uri => '', | 
| 232 |  |  |  |  |  |  | }; | 
| 233 | 2 |  |  |  |  | 5 | return $self->_get_token($query_ref); | 
| 234 |  |  |  |  |  |  | } | 
| 235 |  |  |  |  |  |  |  | 
| 236 |  |  |  |  |  |  | # Access Tokens > Expiration and Extending Tokens | 
| 237 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/access-tokens/ | 
| 238 |  |  |  |  |  |  | sub exchange_token { | 
| 239 | 3 |  |  | 3 | 1 | 119 | my ($self, $short_term_token) = @_; | 
| 240 |  |  |  |  |  |  |  | 
| 241 | 3 | 50 |  |  |  | 6 | croak 'short term token is not given' unless $short_term_token; | 
| 242 |  |  |  |  |  |  |  | 
| 243 | 3 |  |  |  |  | 11 | my $query_ref = +{ | 
| 244 |  |  |  |  |  |  | grant_type        => 'fb_exchange_token', | 
| 245 |  |  |  |  |  |  | fb_exchange_token => $short_term_token, | 
| 246 |  |  |  |  |  |  | }; | 
| 247 | 3 |  |  |  |  | 7 | return $self->_get_token($query_ref); | 
| 248 |  |  |  |  |  |  | } | 
| 249 |  |  |  |  |  |  |  | 
| 250 |  |  |  |  |  |  | sub _get_token { | 
| 251 | 10 |  |  | 10 |  | 23 | my ($self, $param_ref) = @_; | 
| 252 |  |  |  |  |  |  |  | 
| 253 | 10 | 100 | 100 |  |  | 24 | croak 'app_id and secret must be set' unless $self->app_id && $self->secret; | 
| 254 |  |  |  |  |  |  |  | 
| 255 | 6 |  |  |  |  | 25 | $param_ref = +{ | 
| 256 |  |  |  |  |  |  | %$param_ref, | 
| 257 |  |  |  |  |  |  | client_id     => $self->app_id, | 
| 258 |  |  |  |  |  |  | client_secret => $self->secret, | 
| 259 |  |  |  |  |  |  | }; | 
| 260 |  |  |  |  |  |  |  | 
| 261 | 6 |  |  |  |  | 23 | my $response = $self->request('GET', '/oauth/access_token', $param_ref); | 
| 262 |  |  |  |  |  |  | # Document describes as follows: | 
| 263 |  |  |  |  |  |  | # "The response you will receive from this endpoint, if successful, is | 
| 264 |  |  |  |  |  |  | # access_token={access-token}&expires={seconds-til-expiration} | 
| 265 |  |  |  |  |  |  | # If it is not successful, you'll receive an explanatory error message." | 
| 266 |  |  |  |  |  |  | # | 
| 267 |  |  |  |  |  |  | # It, however, returnes no "expires" parameter on some edge cases. | 
| 268 |  |  |  |  |  |  | # e.g. Your app requests manage_pages permission. | 
| 269 |  |  |  |  |  |  | # https://developers.facebook.com/bugs/597779113651383/ | 
| 270 | 6 | 100 |  |  |  | 25 | if ($response->is_api_version_eq_or_later_than('v2.3')) { | 
| 271 |  |  |  |  |  |  | # As of v2.3, to be compliant with RFC 6749, response is JSON formatted | 
| 272 |  |  |  |  |  |  | # as described below. | 
| 273 |  |  |  |  |  |  | # {"access_token": , "token_type":, "expires_in": | 
| 274 |  |  |  |  |  |  | # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.3#confirm | 
| 275 | 1 |  |  |  |  | 5 | return $response->as_hashref; | 
| 276 |  |  |  |  |  |  | } | 
| 277 |  |  |  |  |  |  |  | 
| 278 | 5 |  |  |  |  | 22 | my $res_content = $response->content; | 
| 279 | 5 |  |  |  |  | 30 | my $token_ref = +{ URI->new("?$res_content")->query_form }; | 
| 280 |  |  |  |  |  |  | croak "can't get access_token properly: $res_content" | 
| 281 | 5 | 50 |  |  |  | 681 | unless $token_ref->{access_token}; | 
| 282 |  |  |  |  |  |  |  | 
| 283 | 5 |  |  |  |  | 58 | return $token_ref; | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  | sub get { | 
| 287 | 10 |  |  | 10 | 1 | 281 | return shift->request('GET', @_)->as_hashref; | 
| 288 |  |  |  |  |  |  | } | 
| 289 |  |  |  |  |  |  |  | 
| 290 |  |  |  |  |  |  | sub post { | 
| 291 | 19 |  |  | 19 | 1 | 131 | return shift->request('POST', @_)->as_hashref; | 
| 292 |  |  |  |  |  |  | } | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | # Deleting > Objects | 
| 295 |  |  |  |  |  |  | # https://developers.facebook.com/docs/reference/api/deleting/ | 
| 296 |  |  |  |  |  |  | sub delete { | 
| 297 | 1 |  |  | 1 | 1 | 16 | return shift->request('DELETE', @_)->as_hashref; | 
| 298 |  |  |  |  |  |  | } | 
| 299 |  |  |  |  |  |  |  | 
| 300 |  |  |  |  |  |  | # For those who got used to Facebook::Graph | 
| 301 |  |  |  |  |  |  | *fetch   = \&get; | 
| 302 |  |  |  |  |  |  | *publish = \&post; | 
| 303 |  |  |  |  |  |  |  | 
| 304 |  |  |  |  |  |  | # Using ETags | 
| 305 |  |  |  |  |  |  | # https://developers.facebook.com/docs/reference/ads-api/etags-reference/ | 
| 306 |  |  |  |  |  |  | sub fetch_with_etag { | 
| 307 | 2 |  |  | 2 | 1 | 44 | my ($self, $uri, $param_ref, $etag) = @_; | 
| 308 |  |  |  |  |  |  |  | 
| 309 |  |  |  |  |  |  | # Attach ETag value to header | 
| 310 |  |  |  |  |  |  | # Returns status 304 without contnet, or status 200 with modified content | 
| 311 | 2 |  |  |  |  | 5 | my $header   = ['IF-None-Match' => $etag]; | 
| 312 | 2 |  |  |  |  | 8 | my $response = $self->request('GET', $uri, $param_ref, $header); | 
| 313 |  |  |  |  |  |  |  | 
| 314 | 2 | 100 |  |  |  | 7 | return $response->is_modified ? $response->as_hashref : undef; | 
| 315 |  |  |  |  |  |  | } | 
| 316 |  |  |  |  |  |  |  | 
| 317 |  |  |  |  |  |  | sub bulk_fetch { | 
| 318 | 1 |  |  | 1 | 1 | 22 | my ($self, $paths_ref) = @_; | 
| 319 |  |  |  |  |  |  |  | 
| 320 |  |  |  |  |  |  | my @queries = map { | 
| 321 | 1 |  |  |  |  | 2 | +{ | 
| 322 | 2 |  |  |  |  | 8 | method       => 'GET', | 
| 323 |  |  |  |  |  |  | relative_url => $_, | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  | } @$paths_ref; | 
| 326 |  |  |  |  |  |  |  | 
| 327 | 1 |  |  |  |  | 3 | return $self->batch(\@queries); | 
| 328 |  |  |  |  |  |  | } | 
| 329 |  |  |  |  |  |  |  | 
| 330 |  |  |  |  |  |  | # Making Multiple API Requests > Making a simple batched request | 
| 331 |  |  |  |  |  |  | # https://developers.facebook.com/docs/graph-api/making-multiple-requests | 
| 332 |  |  |  |  |  |  | sub batch { | 
| 333 | 6 |  |  | 6 | 1 | 254 | my $self  = shift; | 
| 334 |  |  |  |  |  |  |  | 
| 335 | 6 |  |  |  |  | 21 | my $responses_ref = $self->batch_fast(@_); | 
| 336 |  |  |  |  |  |  |  | 
| 337 |  |  |  |  |  |  | # Devide response content and create response objects that correspond to | 
| 338 |  |  |  |  |  |  | # each request | 
| 339 | 5 |  |  |  |  | 12 | my @data = (); | 
| 340 | 5 |  |  |  |  | 13 | for my $r (@$responses_ref) { | 
| 341 | 7 |  |  |  |  | 14 | for my $res_ref (@$r) { | 
| 342 |  |  |  |  |  |  | my $response = $self->create_response( | 
| 343 |  |  |  |  |  |  | $res_ref->{code}, | 
| 344 |  |  |  |  |  |  | $res_ref->{message}, | 
| 345 | 58 |  |  |  |  | 124 | [ map { $_->{name} => $_->{value} } @{ $res_ref->{headers} } ], | 
|  | 13 |  |  |  |  | 31 |  | 
| 346 |  |  |  |  |  |  | $res_ref->{body}, | 
| 347 | 13 |  |  |  |  | 38 | ); | 
| 348 | 13 | 50 |  |  |  | 53 | croak $response->error_string unless $response->is_success; | 
| 349 |  |  |  |  |  |  |  | 
| 350 | 13 |  |  |  |  | 61 | push @data, $response->as_hashref; | 
| 351 |  |  |  |  |  |  | } | 
| 352 |  |  |  |  |  |  | } | 
| 353 |  |  |  |  |  |  |  | 
| 354 | 5 |  |  |  |  | 40 | return \@data; | 
| 355 |  |  |  |  |  |  | } | 
| 356 |  |  |  |  |  |  |  | 
| 357 |  |  |  |  |  |  | # doesn't create F::OG::Response object for each response | 
| 358 |  |  |  |  |  |  | sub batch_fast { | 
| 359 | 6 |  |  | 6 | 1 | 12 | my $self  = shift; | 
| 360 | 6 |  |  |  |  | 16 | my $batch = shift; | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  | # Other than HTTP header, you need to set access_token as top level | 
| 363 |  |  |  |  |  |  | # parameter. You can specify individual token for each request so you can | 
| 364 |  |  |  |  |  |  | # act as several other users and pages. | 
| 365 | 6 | 100 |  |  |  | 30 | croak 'Top level access_token must be set' unless $self->access_token; | 
| 366 |  |  |  |  |  |  |  | 
| 367 |  |  |  |  |  |  | # "We currently limit the number of requests which can be in a batch to 50" | 
| 368 | 5 |  |  |  |  | 13 | my @responses = (); | 
| 369 | 5 |  |  |  |  | 19 | while(my @queries = splice @$batch, 0, $self->batch_limit) { | 
| 370 |  |  |  |  |  |  |  | 
| 371 | 7 |  |  |  |  | 18 | for my $q (@queries) { | 
| 372 | 13 | 50 | 66 |  |  | 56 | if ($q->{method} eq 'POST' && $q->{body}) { | 
| 373 | 3 |  |  |  |  | 8 | my $body_ref = $self->prep_param($q->{body}); | 
| 374 | 3 |  |  |  |  | 13 | my $uri = URI->new; | 
| 375 | 3 |  |  |  |  | 4300 | $uri->query_form(%$body_ref); | 
| 376 | 3 |  |  |  |  | 375 | $q->{body} = $uri->query; | 
| 377 |  |  |  |  |  |  | } | 
| 378 |  |  |  |  |  |  | } | 
| 379 |  |  |  |  |  |  |  | 
| 380 | 7 |  |  |  |  | 49 | my @req = ( | 
| 381 |  |  |  |  |  |  | '/', | 
| 382 |  |  |  |  |  |  | +{ | 
| 383 |  |  |  |  |  |  | access_token => $self->access_token, | 
| 384 |  |  |  |  |  |  | batch        => $self->json->encode(\@queries), | 
| 385 |  |  |  |  |  |  | }, | 
| 386 |  |  |  |  |  |  | @_, | 
| 387 |  |  |  |  |  |  | ); | 
| 388 |  |  |  |  |  |  |  | 
| 389 | 7 |  |  |  |  | 26 | push @responses, $self->post(@req); | 
| 390 |  |  |  |  |  |  | } | 
| 391 |  |  |  |  |  |  |  | 
| 392 | 5 |  |  |  |  | 16 | return \@responses; | 
| 393 |  |  |  |  |  |  | } | 
| 394 |  |  |  |  |  |  |  | 
| 395 |  |  |  |  |  |  | # Facebook Query Language (FQL) Overview | 
| 396 |  |  |  |  |  |  | # https://developers.facebook.com/docs/technical-guides/fql/ | 
| 397 |  |  |  |  |  |  | sub fql { | 
| 398 | 2 |  |  | 2 | 1 | 24 | my $self  = shift; | 
| 399 | 2 |  |  |  |  | 4 | my $query = shift; | 
| 400 | 2 |  |  |  |  | 9 | return $self->get('/fql', +{q => $query}, @_); | 
| 401 |  |  |  |  |  |  | } | 
| 402 |  |  |  |  |  |  |  | 
| 403 |  |  |  |  |  |  | # Facebook Query Language (FQL) Overview: Multi-query | 
| 404 |  |  |  |  |  |  | # https://developers.facebook.com/docs/technical-guides/fql/#multi | 
| 405 |  |  |  |  |  |  | sub bulk_fql { | 
| 406 | 1 |  |  | 1 | 1 | 21 | my $self  = shift; | 
| 407 | 1 |  |  |  |  | 2 | my $batch = shift; | 
| 408 | 1 |  |  |  |  | 3 | return $self->fql($self->json->encode($batch), @_); | 
| 409 |  |  |  |  |  |  | } | 
| 410 |  |  |  |  |  |  |  | 
| 411 |  |  |  |  |  |  | sub request { | 
| 412 | 38 |  |  | 38 | 1 | 128 | my ($self, $method, $uri, $param_ref, $headers) = @_; | 
| 413 |  |  |  |  |  |  |  | 
| 414 | 38 |  |  |  |  | 112 | $method = uc $method; | 
| 415 | 38 | 50 | 33 |  |  | 249 | $uri    = $self->uri($uri) unless blessed($uri) && $uri->isa('URI'); | 
| 416 | 38 |  |  |  |  | 129 | $uri->path( $self->gen_versioned_path($uri->path) ); | 
| 417 |  |  |  |  |  |  | $param_ref = $self->prep_param(+{ | 
| 418 |  |  |  |  |  |  | $uri->query_form(+{}), | 
| 419 | 38 | 100 |  |  |  | 1319 | %{$param_ref || +{}}, | 
|  | 38 |  |  |  |  | 1702 |  | 
| 420 |  |  |  |  |  |  | }); | 
| 421 |  |  |  |  |  |  |  | 
| 422 |  |  |  |  |  |  | # Securing Graph API Requests > Verifying Graph API Calls with appsecret_proof | 
| 423 |  |  |  |  |  |  | # https://developers.facebook.com/docs/graph-api/securing-requests/ | 
| 424 | 38 | 100 |  |  |  | 154 | if ($self->use_appsecret_proof) { | 
| 425 | 1 |  |  |  |  | 3 | $param_ref->{appsecret_proof} = $self->gen_appsecret_proof; | 
| 426 |  |  |  |  |  |  | } | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | # Use POST as default HTTP method and add method=(POST|GET|DELETE) to query | 
| 429 |  |  |  |  |  |  | # parameter. Document only says we can send HTTP DELETE method or, instead, | 
| 430 |  |  |  |  |  |  | # HTTP POST method with ?method=delete to delete object. It does not say | 
| 431 |  |  |  |  |  |  | # POST with method=(get|post) parameter works, but PHP SDK always sends POST | 
| 432 |  |  |  |  |  |  | # with method parameter so... I just give you this option. | 
| 433 |  |  |  |  |  |  | # Check PHP SDK's base_facebook.php for detail. | 
| 434 | 38 | 100 |  |  |  | 122 | if ($self->{use_post_method}) { | 
| 435 | 2 |  |  |  |  | 4 | $param_ref->{method} = $method; | 
| 436 | 2 |  |  |  |  | 3 | $method              = 'POST'; | 
| 437 |  |  |  |  |  |  | } | 
| 438 |  |  |  |  |  |  |  | 
| 439 | 38 |  | 100 |  |  | 197 | $headers ||= []; | 
| 440 |  |  |  |  |  |  |  | 
| 441 |  |  |  |  |  |  | # Document says we can pass access_token as a part of query parameter, | 
| 442 |  |  |  |  |  |  | # but it actually supports Authorization header to be compliant with the | 
| 443 |  |  |  |  |  |  | # OAuth 2.0 spec. | 
| 444 |  |  |  |  |  |  | # http://tools.ietf.org/html/rfc6749#section-7 | 
| 445 | 38 | 100 |  |  |  | 124 | if ($self->access_token) { | 
| 446 | 17 |  |  |  |  | 47 | push @$headers, ( | 
| 447 |  |  |  |  |  |  | 'Authorization', | 
| 448 |  |  |  |  |  |  | sprintf('OAuth %s', $self->access_token), | 
| 449 |  |  |  |  |  |  | ); | 
| 450 |  |  |  |  |  |  | } | 
| 451 |  |  |  |  |  |  |  | 
| 452 | 38 |  |  |  |  | 80 | my $content = q{}; | 
| 453 | 38 | 100 |  |  |  | 122 | if ($method eq 'POST') { | 
| 454 | 21 | 100 | 100 |  |  | 103 | if ($param_ref->{source} || $param_ref->{file}) { | 
| 455 |  |  |  |  |  |  | # post image or video file | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | # https://developers.facebook.com/docs/reference/api/video/ | 
| 458 |  |  |  |  |  |  | # When posting a video, use graph-video.facebook.com . | 
| 459 |  |  |  |  |  |  | # base_facebook.php has an equivalent part in isVideoPost(). | 
| 460 |  |  |  |  |  |  | # ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) | 
| 461 |  |  |  |  |  |  | # For other actions, use graph.facebook.com/VIDEO_ID/CONNECTION_TYPE | 
| 462 | 3 | 100 |  |  |  | 11 | if ($uri->path =~ m{\A /.+/videos \z}xms) { | 
| 463 | 1 |  |  |  |  | 15 | $uri->host($self->video_uri->host); | 
| 464 |  |  |  |  |  |  | } | 
| 465 |  |  |  |  |  |  |  | 
| 466 |  |  |  |  |  |  | # Content-Type should be multipart/form-data | 
| 467 |  |  |  |  |  |  | # https://developers.facebook.com/docs/reference/api/publishing/ | 
| 468 | 3 |  |  |  |  | 115 | push @$headers, (Content_Type => 'form-data'); | 
| 469 |  |  |  |  |  |  |  | 
| 470 |  |  |  |  |  |  | # Furl::HTTP document says we can use multipart/form-data with | 
| 471 |  |  |  |  |  |  | # HTTP::Request::Common. | 
| 472 | 3 |  |  |  |  | 19 | my $req = POST $uri, @$headers, Content => [%$param_ref]; | 
| 473 | 3 |  |  |  |  | 53382 | $content = $req->content; | 
| 474 | 3 |  |  |  |  | 98 | my $req_header = $req->headers; | 
| 475 |  |  |  |  |  |  | $headers = +[ | 
| 476 |  |  |  |  |  |  | map { | 
| 477 | 3 |  |  |  |  | 29 | my $k = $_; | 
|  | 9 |  |  |  |  | 135 |  | 
| 478 | 9 |  |  |  |  | 22 | map { ( $k => $_ ) } $req_header->header($k); | 
|  | 9 |  |  |  |  | 269 |  | 
| 479 |  |  |  |  |  |  | } $req_header->header_field_names | 
| 480 |  |  |  |  |  |  | ]; | 
| 481 |  |  |  |  |  |  | } | 
| 482 |  |  |  |  |  |  | else { | 
| 483 |  |  |  |  |  |  | # Post simple parameters such as message, link, description, etc... | 
| 484 |  |  |  |  |  |  | # Content-Type: application/x-www-form-urlencoded will be set in | 
| 485 |  |  |  |  |  |  | # Furl::HTTP, and $content will be treated properly. | 
| 486 | 18 |  |  |  |  | 32 | $content = $param_ref; | 
| 487 |  |  |  |  |  |  | } | 
| 488 |  |  |  |  |  |  | } | 
| 489 |  |  |  |  |  |  | else { | 
| 490 | 17 |  |  |  |  | 61 | $uri->query_form($param_ref); | 
| 491 |  |  |  |  |  |  | } | 
| 492 |  |  |  |  |  |  |  | 
| 493 | 38 |  |  |  |  | 2061 | my ($res_minor_version, @res_elms) = $self->ua->request( | 
| 494 |  |  |  |  |  |  | method  => $method, | 
| 495 |  |  |  |  |  |  | url     => $uri, | 
| 496 |  |  |  |  |  |  | headers => $headers, | 
| 497 |  |  |  |  |  |  | content => $content, | 
| 498 |  |  |  |  |  |  | ); | 
| 499 |  |  |  |  |  |  |  | 
| 500 | 38 |  |  |  |  | 72391 | my $res = $self->create_response(@res_elms); | 
| 501 |  |  |  |  |  |  |  | 
| 502 |  |  |  |  |  |  | # return F::OG::Response object on success | 
| 503 | 38 | 100 |  |  |  | 195 | return $res if $res->is_success; | 
| 504 |  |  |  |  |  |  |  | 
| 505 |  |  |  |  |  |  | # Use later version of Furl::HTTP to utilize req_headers and req_content. | 
| 506 |  |  |  |  |  |  | # This Should be helpful when debugging. | 
| 507 | 4 |  |  |  |  | 17 | my $msg = $res->error_string; | 
| 508 | 4 | 50 |  |  |  | 14 | if ($res->req_headers) { | 
| 509 | 0 |  |  |  |  | 0 | $msg .= "\n" . $res->req_headers . $res->req_content; | 
| 510 |  |  |  |  |  |  | } | 
| 511 | 4 |  |  |  |  | 86 | croak $msg; | 
| 512 |  |  |  |  |  |  | } | 
| 513 |  |  |  |  |  |  |  | 
| 514 |  |  |  |  |  |  | # Securing Graph API Requests > Verifying Graph API Calls with appsecret_proof > Generating the proof | 
| 515 |  |  |  |  |  |  | # https://developers.facebook.com/docs/graph-api/securing-requests/ | 
| 516 |  |  |  |  |  |  | sub gen_appsecret_proof { | 
| 517 | 2 |  |  | 2 | 1 | 51 | my $self = shift; | 
| 518 | 2 | 50 |  |  |  | 5 | croak 'app secret must be set'   unless $self->secret; | 
| 519 | 2 | 50 |  |  |  | 5 | croak 'access_token must be set' unless $self->access_token; | 
| 520 | 2 |  |  |  |  | 7 | return hmac_sha256_hex($self->access_token, $self->secret); | 
| 521 |  |  |  |  |  |  | } | 
| 522 |  |  |  |  |  |  |  | 
| 523 |  |  |  |  |  |  | # Platform Versioning > Making Versioned Requests | 
| 524 |  |  |  |  |  |  | # https://developers.facebook.com/docs/apps/versions | 
| 525 |  |  |  |  |  |  | sub gen_versioned_path { | 
| 526 | 54 |  |  | 54 | 0 | 630 | my ($self, $path) = @_; | 
| 527 |  |  |  |  |  |  |  | 
| 528 | 54 | 100 |  |  |  | 157 | $path = '/' unless $path; | 
| 529 |  |  |  |  |  |  |  | 
| 530 | 54 | 100 | 100 |  |  | 154 | if ($self->version && $path !~ m{\A /v(?:\d+)\.(?:\d+)/ }x) { | 
| 531 |  |  |  |  |  |  | # If default platform version is set on initialisation | 
| 532 |  |  |  |  |  |  | # and given path doesn't contain version, | 
| 533 |  |  |  |  |  |  | # then prepend the default version. | 
| 534 | 5 |  |  |  |  | 12 | $path = sprintf('/%s%s', $self->version, $path); | 
| 535 |  |  |  |  |  |  | } | 
| 536 |  |  |  |  |  |  |  | 
| 537 | 54 |  |  |  |  | 202 | return $path; | 
| 538 |  |  |  |  |  |  | } | 
| 539 |  |  |  |  |  |  |  | 
| 540 |  |  |  |  |  |  | sub js_cookie_name { | 
| 541 | 2 |  |  | 2 | 1 | 75 | my $self = shift; | 
| 542 | 2 | 100 |  |  |  | 5 | croak 'app_id must be set' unless $self->app_id; | 
| 543 |  |  |  |  |  |  |  | 
| 544 |  |  |  |  |  |  | # Cookie is set by JS SDK with a name of fbsr_{app_id}. Official document | 
| 545 |  |  |  |  |  |  | # is not provided for more than 3 yaers so I quote from PHP SDK's code. | 
| 546 |  |  |  |  |  |  | # "Constructs and returns the name of the cookie that potentially houses | 
| 547 |  |  |  |  |  |  | # the signed request for the app user. The cookie is not set by the | 
| 548 |  |  |  |  |  |  | # BaseFacebook class, but it may be set by the JavaScript SDK." | 
| 549 |  |  |  |  |  |  | # The cookie value can be parsed as signed request and it contains 'code' | 
| 550 |  |  |  |  |  |  | # to exchange for access toekn. | 
| 551 | 1 |  |  |  |  | 2 | return sprintf('fbsr_%d', $self->app_id); | 
| 552 |  |  |  |  |  |  | } | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  | sub create_response { | 
| 555 | 51 |  |  | 51 | 1 | 112 | my $self = shift; | 
| 556 |  |  |  |  |  |  | return Facebook::OpenGraph::Response->new(+{ | 
| 557 |  |  |  |  |  |  | json => $self->json, | 
| 558 |  |  |  |  |  |  | map { | 
| 559 | 51 |  |  |  |  | 199 | $_ => shift | 
|  | 306 |  |  |  |  | 827 |  | 
| 560 |  |  |  |  |  |  | } qw/code message headers content req_headers req_content/, | 
| 561 |  |  |  |  |  |  | }); | 
| 562 |  |  |  |  |  |  | } | 
| 563 |  |  |  |  |  |  |  | 
| 564 |  |  |  |  |  |  | sub prep_param { | 
| 565 | 42 |  |  | 42 | 1 | 179 | my ($self, $param_ref) = @_; | 
| 566 |  |  |  |  |  |  |  | 
| 567 | 42 |  | 50 |  |  | 336 | $param_ref = Data::Recursive::Encode->encode_utf8($param_ref || +{}); | 
| 568 |  |  |  |  |  |  |  | 
| 569 |  |  |  |  |  |  | # /?ids=4,http://facebook-docs.oklahome.net | 
| 570 | 42 | 100 |  |  |  | 3832 | if (my $ids = $param_ref->{ids}) { | 
| 571 | 1 | 50 |  |  |  | 6 | $param_ref->{ids} = ref $ids ? join q{,}, @$ids : $ids; | 
| 572 |  |  |  |  |  |  | } | 
| 573 |  |  |  |  |  |  |  | 
| 574 |  |  |  |  |  |  | # mostly for /APP_ID/accounts/test-users | 
| 575 | 42 | 100 |  |  |  | 133 | if (my $perms = $param_ref->{permissions}) { | 
| 576 | 5 | 100 |  |  |  | 23 | $param_ref->{permissions} = ref $perms ? join q{,}, @$perms : $perms; | 
| 577 |  |  |  |  |  |  | } | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | # Source and file parameter contains file path. | 
| 580 |  |  |  |  |  |  | # It must be an array ref to work with HTTP::Request::Common. | 
| 581 | 42 | 100 |  |  |  | 130 | if (my $path = $param_ref->{source}) { | 
| 582 | 3 | 50 |  |  |  | 14 | $param_ref->{source} = ref $path ? $path : [$path]; | 
| 583 |  |  |  |  |  |  | } | 
| 584 | 42 | 100 |  |  |  | 120 | if (my $path = $param_ref->{file}) { | 
| 585 | 1 | 50 |  |  |  | 7 | $param_ref->{file} = ref $path ? $path : [$path]; | 
| 586 |  |  |  |  |  |  | } | 
| 587 |  |  |  |  |  |  |  | 
| 588 |  |  |  |  |  |  | # use Field Expansion | 
| 589 | 42 | 100 |  |  |  | 107 | if (my $field_ref = $param_ref->{fields}) { | 
| 590 | 2 |  |  |  |  | 7 | $param_ref->{fields} = $self->prep_fields_recursive($field_ref); | 
| 591 |  |  |  |  |  |  | } | 
| 592 |  |  |  |  |  |  |  | 
| 593 |  |  |  |  |  |  | # Using Objects: Using the Object API | 
| 594 |  |  |  |  |  |  | # https://developers.facebook.com/docs/opengraph/using-objects/#objectapi | 
| 595 | 42 |  |  |  |  | 86 | my $object = $param_ref->{object}; | 
| 596 | 42 | 100 | 66 |  |  | 155 | if ($object && ref $object eq 'HASH') { | 
| 597 | 1 |  |  |  |  | 3 | $param_ref->{object} = $self->json->encode($object); | 
| 598 |  |  |  |  |  |  | } | 
| 599 |  |  |  |  |  |  |  | 
| 600 | 42 |  |  |  |  | 91 | return $param_ref; | 
| 601 |  |  |  |  |  |  | } | 
| 602 |  |  |  |  |  |  |  | 
| 603 |  |  |  |  |  |  | # Using the Graph API: Reading > Choosing Fields > Making Nested Requests | 
| 604 |  |  |  |  |  |  | # https://developers.facebook.com/docs/graph-api/using-graph-api/ | 
| 605 |  |  |  |  |  |  | sub prep_fields_recursive { | 
| 606 | 4 |  |  | 4 | 1 | 10 | my ($self, $val) = @_; | 
| 607 |  |  |  |  |  |  |  | 
| 608 | 4 |  |  |  |  | 10 | my $ref = ref $val; | 
| 609 | 4 | 100 |  |  |  | 9 | if (!$ref) { | 
|  |  | 50 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 610 | 3 |  |  |  |  | 10 | return $val; | 
| 611 |  |  |  |  |  |  | } | 
| 612 |  |  |  |  |  |  | elsif ($ref eq 'ARRAY') { | 
| 613 | 1 |  |  |  |  | 3 | return join q{,}, map { $self->prep_fields_recursive($_) } @$val; | 
|  | 2 |  |  |  |  | 5 |  | 
| 614 |  |  |  |  |  |  | } | 
| 615 |  |  |  |  |  |  | elsif ($ref eq 'HASH') { | 
| 616 | 0 |  |  |  |  | 0 | my @strs = (); | 
| 617 | 0 |  |  |  |  | 0 | while (my ($k, $v) = each %$val) { | 
| 618 | 0 |  |  |  |  | 0 | my $r = ref $v; | 
| 619 | 0 | 0 | 0 |  |  | 0 | my $pattern = $r && $r eq 'HASH' ? '%s.%s' : '%s(%s)'; | 
| 620 | 0 |  |  |  |  | 0 | push @strs, sprintf($pattern, $k, $self->prep_fields_recursive($v)); | 
| 621 |  |  |  |  |  |  | } | 
| 622 | 0 |  |  |  |  | 0 | return join q{.}, @strs; | 
| 623 |  |  |  |  |  |  | } | 
| 624 |  |  |  |  |  |  | } | 
| 625 |  |  |  |  |  |  |  | 
| 626 |  |  |  |  |  |  | # Using Actions > Publishing Actions | 
| 627 |  |  |  |  |  |  | # https://developers.facebook.com/docs/opengraph/using-actions/#publish | 
| 628 |  |  |  |  |  |  | sub publish_action { | 
| 629 | 1 |  |  | 1 | 1 | 23 | my $self   = shift; | 
| 630 | 1 |  |  |  |  | 2 | my $action = shift; | 
| 631 | 1 | 50 |  |  |  | 4 | croak 'namespace is not set' unless $self->namespace; | 
| 632 | 1 |  |  |  |  | 4 | return $self->post(sprintf('/me/%s:%s', $self->namespace, $action), @_); | 
| 633 |  |  |  |  |  |  | } | 
| 634 |  |  |  |  |  |  |  | 
| 635 |  |  |  |  |  |  | # Using Objects > Using the Object API > Images with the Object API | 
| 636 |  |  |  |  |  |  | # https://developers.facebook.com/docs/opengraph/using-objects/ | 
| 637 |  |  |  |  |  |  | sub publish_staging_resource { | 
| 638 | 1 |  |  | 1 | 1 | 24 | my $self = shift; | 
| 639 | 1 |  |  |  |  | 1 | my $file = shift; | 
| 640 | 1 |  |  |  |  | 5 | return $self->post('/me/staging_resources', +{file => $file}, @_); | 
| 641 |  |  |  |  |  |  | } | 
| 642 |  |  |  |  |  |  |  | 
| 643 |  |  |  |  |  |  | # Test Users: Creating | 
| 644 |  |  |  |  |  |  | # https://developers.facebook.com/docs/test_users/ | 
| 645 |  |  |  |  |  |  | sub create_test_users { | 
| 646 | 2 |  |  | 2 | 1 | 49 | my $self         = shift; | 
| 647 | 2 |  |  |  |  | 3 | my $settings_ref = shift; | 
| 648 |  |  |  |  |  |  |  | 
| 649 | 2 | 100 |  |  |  | 7 | if (ref $settings_ref ne 'ARRAY') { | 
| 650 | 1 |  |  |  |  | 3 | $settings_ref = [$settings_ref]; | 
| 651 |  |  |  |  |  |  | } | 
| 652 |  |  |  |  |  |  |  | 
| 653 | 2 |  |  |  |  | 4 | my @settings = (); | 
| 654 | 2 |  |  |  |  | 6 | my $relative_url = sprintf('/%s/accounts/test-users', $self->app_id); | 
| 655 | 2 |  |  |  |  | 6 | for my $setting (@$settings_ref) { | 
| 656 | 3 |  |  |  |  | 11 | push @settings, +{ | 
| 657 |  |  |  |  |  |  | method       => 'POST', | 
| 658 |  |  |  |  |  |  | relative_url => $relative_url, | 
| 659 |  |  |  |  |  |  | body         => $setting, | 
| 660 |  |  |  |  |  |  | }; | 
| 661 |  |  |  |  |  |  | } | 
| 662 |  |  |  |  |  |  |  | 
| 663 | 2 |  |  |  |  | 7 | return $self->batch(\@settings); | 
| 664 |  |  |  |  |  |  | } | 
| 665 |  |  |  |  |  |  |  | 
| 666 |  |  |  |  |  |  | # Using Objects > Using Self-Hosted Objects > Updating Objects | 
| 667 |  |  |  |  |  |  | # https://developers.facebook.com/docs/opengraph/using-objects/ | 
| 668 |  |  |  |  |  |  | sub check_object { | 
| 669 | 6 |  |  | 6 | 1 | 250 | my ($self, $target) = @_; | 
| 670 | 6 |  |  |  |  | 15 | my $param_ref = +{ | 
| 671 |  |  |  |  |  |  | id     => $target, # $target is object url or open graph object id | 
| 672 |  |  |  |  |  |  | scrape => 'true', | 
| 673 |  |  |  |  |  |  | }; | 
| 674 | 6 |  |  |  |  | 16 | return $self->post(q{}, $param_ref); | 
| 675 |  |  |  |  |  |  | } | 
| 676 |  |  |  |  |  |  |  | 
| 677 |  |  |  |  |  |  | 1; | 
| 678 |  |  |  |  |  |  | __END__ |