| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Mastodon::Role::UserAgent; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 5 |  |  | 5 |  | 2432 | use strict; | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 127 |  | 
| 4 | 5 |  |  | 5 |  | 23 | use warnings; | 
|  | 5 |  |  |  |  | 9 |  | 
|  | 5 |  |  |  |  | 158 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 |  |  |  |  |  |  | our $VERSION = '0.016'; | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 5 |  |  | 5 |  | 50 | use v5.10.0; | 
|  | 5 |  |  |  |  | 14 |  | 
| 9 | 5 |  |  | 5 |  | 23 | use Moo::Role; | 
|  | 5 |  |  |  |  | 6 |  | 
|  | 5 |  |  |  |  | 32 |  | 
| 10 |  |  |  |  |  |  |  | 
| 11 | 5 |  |  | 5 |  | 1568 | use Log::Any; | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 29 |  | 
| 12 |  |  |  |  |  |  | my $log = Log::Any->get_logger( category => 'Mastodon' ); | 
| 13 |  |  |  |  |  |  |  | 
| 14 | 5 |  |  | 5 |  | 2217 | use URI::QueryParam; | 
|  | 5 |  |  |  |  | 3291 |  | 
|  | 5 |  |  |  |  | 151 |  | 
| 15 | 5 |  |  | 5 |  | 28 | use List::Util qw( any ); | 
|  | 5 |  |  |  |  | 10 |  | 
|  | 5 |  |  |  |  | 341 |  | 
| 16 | 5 |  |  |  |  | 47 | use Types::Standard qw( | 
| 17 |  |  |  |  |  |  | ArrayRef | 
| 18 |  |  |  |  |  |  | Dict | 
| 19 |  |  |  |  |  |  | HashRef | 
| 20 |  |  |  |  |  |  | Maybe | 
| 21 |  |  |  |  |  |  | Num | 
| 22 |  |  |  |  |  |  | Optional | 
| 23 |  |  |  |  |  |  | Str | 
| 24 |  |  |  |  |  |  | Undef | 
| 25 |  |  |  |  |  |  | slurpy | 
| 26 | 5 |  |  | 5 |  | 28 | ); | 
|  | 5 |  |  |  |  | 8 |  | 
| 27 | 5 |  |  |  |  | 27 | use Mastodon::Types qw( | 
| 28 |  |  |  |  |  |  | HTTPResponse | 
| 29 |  |  |  |  |  |  | Instance | 
| 30 |  |  |  |  |  |  | URI | 
| 31 |  |  |  |  |  |  | UserAgent | 
| 32 |  |  |  |  |  |  | to_Entity | 
| 33 | 5 |  |  | 5 |  | 7430 | ); | 
|  | 5 |  |  |  |  | 10 |  | 
| 34 | 5 |  |  | 5 |  | 19943 | use Type::Params qw( compile ); | 
|  | 5 |  |  |  |  | 11 |  | 
|  | 5 |  |  |  |  | 29 |  | 
| 35 | 5 |  |  | 5 |  | 929 | use Carp; | 
|  | 5 |  |  |  |  | 12 |  | 
|  | 5 |  |  |  |  | 2210 |  | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  | has instance => ( | 
| 38 |  |  |  |  |  |  | is => 'rw', | 
| 39 |  |  |  |  |  |  | isa => Instance, | 
| 40 |  |  |  |  |  |  | default => 'https://mastodon.social', | 
| 41 |  |  |  |  |  |  | coerce => 1, | 
| 42 |  |  |  |  |  |  | ); | 
| 43 |  |  |  |  |  |  |  | 
| 44 |  |  |  |  |  |  | has api_version => ( | 
| 45 |  |  |  |  |  |  | is => 'ro', | 
| 46 |  |  |  |  |  |  | isa => Num, | 
| 47 |  |  |  |  |  |  | default => 1, | 
| 48 |  |  |  |  |  |  | ); | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | has redirect_uri => ( | 
| 51 |  |  |  |  |  |  | is => 'ro', | 
| 52 |  |  |  |  |  |  | isa => Str, | 
| 53 |  |  |  |  |  |  | lazy => 1, | 
| 54 |  |  |  |  |  |  | default => 'urn:ietf:wg:oauth:2.0:oob', | 
| 55 |  |  |  |  |  |  | ); | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | has user_agent => ( | 
| 58 |  |  |  |  |  |  | is => 'ro', | 
| 59 |  |  |  |  |  |  | isa => UserAgent, | 
| 60 |  |  |  |  |  |  | default => sub { | 
| 61 |  |  |  |  |  |  | require HTTP::Thin; | 
| 62 |  |  |  |  |  |  | HTTP::Thin->new; | 
| 63 |  |  |  |  |  |  | }, | 
| 64 |  |  |  |  |  |  | ); | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | has latest_response => ( | 
| 67 |  |  |  |  |  |  | is => 'ro', | 
| 68 |  |  |  |  |  |  | isa => Maybe[HTTPResponse], | 
| 69 |  |  |  |  |  |  | init_args => undef, | 
| 70 |  |  |  |  |  |  | ); | 
| 71 |  |  |  |  |  |  |  | 
| 72 |  |  |  |  |  |  | sub authorization_url { | 
| 73 | 2 |  |  | 2 | 0 | 994 | my $self = shift; | 
| 74 |  |  |  |  |  |  |  | 
| 75 | 2 | 50 | 33 |  |  | 37 | unless ($self->client_id and $self->client_secret) { | 
| 76 | 0 |  |  |  |  | 0 | croak $log->fatal( | 
| 77 |  |  |  |  |  |  | 'Cannot get authorization URL without client_id and client_secret' | 
| 78 |  |  |  |  |  |  | ); | 
| 79 |  |  |  |  |  |  | } | 
| 80 |  |  |  |  |  |  |  | 
| 81 | 2 |  |  |  |  | 58 | state $check = compile( slurpy Dict [ access_code => Optional [Instance] ] ); | 
| 82 | 2 |  |  |  |  | 7753 | my ($params) = $check->(@_); | 
| 83 |  |  |  |  |  |  |  | 
| 84 | 2 |  | 33 |  |  | 143 | $params->{instance} //= $self->instance; | 
| 85 |  |  |  |  |  |  |  | 
| 86 | 2 |  |  |  |  | 18 | my $uri = URI->new('/oauth/authorize')->abs($params->{instance}->uri); | 
| 87 | 2 |  |  |  |  | 591 | $uri->query_param(redirect_uri => $self->redirect_uri); | 
| 88 | 2 |  |  |  |  | 378 | $uri->query_param(response_type => 'code'); | 
| 89 | 2 |  |  |  |  | 389 | $uri->query_param(client_id => $self->client_id); | 
| 90 | 2 |  |  |  |  | 404 | $uri->query_param(scope => join q{ }, sort(@{$self->scopes})); | 
|  | 2 |  |  |  |  | 35 |  | 
| 91 |  |  |  |  |  |  |  | 
| 92 | 2 |  |  |  |  | 1260 | return $uri; | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  |  | 
| 95 | 0 |  |  | 0 | 0 | 0 | sub post   { shift->_request( post   => shift, data   => shift, @_ ) } | 
| 96 | 0 |  |  | 0 | 0 | 0 | sub patch  { shift->_request( patch  => shift, data   => shift, @_ ) } | 
| 97 | 22 |  |  | 22 | 0 | 73 | sub get    { shift->_request( get    => shift, params => shift, @_ ) } | 
| 98 | 0 |  |  | 0 | 0 | 0 | sub delete { shift->_request( delete => shift, params => shift, @_ ) } | 
| 99 |  |  |  |  |  |  |  | 
| 100 |  |  |  |  |  |  | sub _request { | 
| 101 | 22 |  |  | 22 |  | 39 | my $self   = shift; | 
| 102 | 22 |  |  |  |  | 39 | my $method = shift; | 
| 103 | 22 |  |  |  |  | 48 | my $url    = shift; | 
| 104 | 22 |  |  |  |  | 62 | my $args   = { @_ }; | 
| 105 |  |  |  |  |  |  |  | 
| 106 | 22 |  | 50 |  |  | 106 | my $headers = $args->{headers} // {}; | 
| 107 | 22 |  |  |  |  | 87 | my $data    = $self->_prepare_data($args->{data}); | 
| 108 |  |  |  |  |  |  |  | 
| 109 | 22 |  |  |  |  | 78 | $url = $self->_prepare_params($url, $args->{params}); | 
| 110 |  |  |  |  |  |  |  | 
| 111 | 22 |  |  |  |  | 53 | $method = uc($method); | 
| 112 |  |  |  |  |  |  |  | 
| 113 | 22 | 50 | 33 |  |  | 578 | if ($self->can('access_token') and $self->access_token) { | 
| 114 |  |  |  |  |  |  | $headers = { | 
| 115 |  |  |  |  |  |  | Authorization => 'Bearer ' . $self->access_token, | 
| 116 | 0 |  |  |  |  | 0 | %{$headers}, | 
|  | 0 |  |  |  |  | 0 |  | 
| 117 |  |  |  |  |  |  | }; | 
| 118 |  |  |  |  |  |  | } | 
| 119 |  |  |  |  |  |  |  | 
| 120 | 22 | 50 |  |  |  | 2864 | if ($log->is_trace) { | 
| 121 | 0 |  |  |  |  | 0 | require Data::Dumper; | 
| 122 | 0 |  |  |  |  | 0 | $log->debugf('Method:  %s', $method); | 
| 123 | 0 |  |  |  |  | 0 | $log->debugf('URL: %s', $url); | 
| 124 | 0 |  |  |  |  | 0 | $log->debugf('Headers: %s', Data::Dumper::Dumper( $headers )); | 
| 125 | 0 |  |  |  |  | 0 | $log->debugf('Data:    %s', Data::Dumper::Dumper( $data )); | 
| 126 |  |  |  |  |  |  | } | 
| 127 |  |  |  |  |  |  |  | 
| 128 | 5 |  |  | 5 |  | 36 | use Try::Tiny; | 
|  | 5 |  |  |  |  | 11 |  | 
|  | 5 |  |  |  |  | 674 |  | 
| 129 |  |  |  |  |  |  | return try { | 
| 130 | 22 |  |  | 22 |  | 993 | my @args = $url; | 
| 131 | 22 | 50 |  |  |  | 58 | push @args, [%{$data}] unless $method eq 'GET'; | 
|  | 0 |  |  |  |  | 0 |  | 
| 132 | 22 |  |  |  |  | 31 | @args = (@args, %{$headers}); | 
|  | 22 |  |  |  |  | 66 |  | 
| 133 |  |  |  |  |  |  |  | 
| 134 | 22 |  |  |  |  | 121 | require HTTP::Request::Common; | 
| 135 | 22 | 50 |  |  |  | 58 | my $type = ($method eq 'PATCH') ? 'POST' : $method; | 
| 136 | 22 |  |  |  |  | 230 | my $request = HTTP::Request::Common->can($type)->( @args ); | 
| 137 | 22 |  |  |  |  | 2021 | $request->method($method); | 
| 138 |  |  |  |  |  |  |  | 
| 139 | 22 |  |  |  |  | 772 | my $response = $self->user_agent->request( $request ); | 
| 140 |  |  |  |  |  |  |  | 
| 141 | 5 |  |  | 5 |  | 32 | use JSON::MaybeXS qw( decode_json ); | 
|  | 5 |  |  |  |  | 106 |  | 
|  | 5 |  |  |  |  | 290 |  | 
| 142 | 5 |  |  | 5 |  | 36 | use Encode qw( encode ); | 
|  | 5 |  |  |  |  | 10 |  | 
|  | 5 |  |  |  |  | 2891 |  | 
| 143 |  |  |  |  |  |  |  | 
| 144 |  |  |  |  |  |  | # We want to be able to set it, but do not want the user to do so | 
| 145 | 22 |  |  |  |  | 77856 | $self->{latest_response} = $response; | 
| 146 |  |  |  |  |  |  |  | 
| 147 | 22 | 50 |  |  |  | 91 | die $response->status_line unless $response->is_success; | 
| 148 |  |  |  |  |  |  |  | 
| 149 | 22 |  |  |  |  | 288 | my $payload = decode_json encode('utf8', $response->decoded_content); | 
| 150 |  |  |  |  |  |  |  | 
| 151 |  |  |  |  |  |  | # Some API calls return empty objects, which cannot be coerced | 
| 152 | 22 | 50 |  |  |  | 4534 | if ($response->decoded_content ne '{}') { | 
| 153 | 22 | 50 | 33 |  |  | 2530 | if ($url !~ /(?:apps|oauth)/ and $self->coerce_entities) { | 
| 154 |  |  |  |  |  |  | $payload = (ref $payload eq 'ARRAY') | 
| 155 | 18 |  |  |  |  | 25 | ? [ map { to_Entity({ %{$_}, _client => $self }) } @{$payload} ] | 
|  | 18 |  |  |  |  | 202 |  | 
|  | 17 |  |  |  |  | 85 |  | 
| 156 | 22 | 100 |  |  |  | 846 | : to_Entity({ %{$payload}, _client => $self }); | 
|  | 5 |  |  |  |  | 60 |  | 
| 157 |  |  |  |  |  |  | } | 
| 158 |  |  |  |  |  |  | } | 
| 159 |  |  |  |  |  |  |  | 
| 160 | 22 | 100 |  |  |  | 170 | if (ref $payload eq 'ARRAY') { | 
|  |  | 50 |  |  |  |  |  | 
| 161 | 17 | 50 |  |  |  | 177 | die $payload->{error} if any { defined $_->{error} } @{$payload}; | 
|  | 18 |  |  |  |  | 53 |  | 
|  | 17 |  |  |  |  | 72 |  | 
| 162 |  |  |  |  |  |  | } | 
| 163 |  |  |  |  |  |  | elsif (ref $payload eq 'HASH') { | 
| 164 | 0 | 0 |  |  |  | 0 | die $payload->{error} if defined $payload->{error}; | 
| 165 |  |  |  |  |  |  | } | 
| 166 |  |  |  |  |  |  |  | 
| 167 | 22 |  |  |  |  | 234 | return $payload; | 
| 168 |  |  |  |  |  |  | } | 
| 169 |  |  |  |  |  |  | catch { | 
| 170 | 0 |  |  | 0 |  | 0 | my $msg = sprintf 'Could not complete request: %s', $_; | 
| 171 | 0 |  |  |  |  | 0 | $log->fatal($msg); | 
| 172 | 0 |  |  |  |  | 0 | croak $msg; | 
| 173 | 22 |  |  |  |  | 510 | }; | 
| 174 |  |  |  |  |  |  | } | 
| 175 |  |  |  |  |  |  |  | 
| 176 |  |  |  |  |  |  | sub _prepare_data { | 
| 177 | 22 |  |  | 22 |  | 67 | my ($self, $data) = @_; | 
| 178 | 22 |  | 50 |  |  | 105 | $data //= {}; | 
| 179 |  |  |  |  |  |  |  | 
| 180 | 22 |  |  |  |  | 28 | foreach my $key (keys %{$data}) { | 
|  | 22 |  |  |  |  | 76 |  | 
| 181 |  |  |  |  |  |  | # Array parameters to the API need keys that are marked with [] | 
| 182 |  |  |  |  |  |  | # However, HTTP::Request::Common expects an arrayref to encode files | 
| 183 |  |  |  |  |  |  | # for transfer, even though the API does not expect that to be an array | 
| 184 |  |  |  |  |  |  | # So we need to manually skip it, unless we come up with another solution. | 
| 185 | 0 | 0 |  |  |  | 0 | next if $key eq 'file'; | 
| 186 |  |  |  |  |  |  |  | 
| 187 | 0 |  |  |  |  | 0 | my $val = $data->{$key}; | 
| 188 | 0 | 0 |  |  |  | 0 | $data->{$key . '[]'} = delete($data->{$key}) if ref $val eq 'ARRAY'; | 
| 189 |  |  |  |  |  |  | } | 
| 190 |  |  |  |  |  |  |  | 
| 191 | 22 |  |  |  |  | 50 | return $data; | 
| 192 |  |  |  |  |  |  | } | 
| 193 |  |  |  |  |  |  |  | 
| 194 |  |  |  |  |  |  | sub _prepare_params { | 
| 195 | 22 |  |  | 22 |  | 47 | my ($self, $url, $params) = @_; | 
| 196 | 22 |  | 100 |  |  | 56 | $params //= {}; | 
| 197 |  |  |  |  |  |  |  | 
| 198 | 22 | 50 |  |  |  | 48 | croak 'Cannot make a request without a URL' unless $url; | 
| 199 |  |  |  |  |  |  |  | 
| 200 | 22 | 50 |  |  |  | 52 | unless (ref $url eq 'URI') { | 
| 201 | 22 | 50 |  |  |  | 148 | my $base = $url =~ m{^/oauth/} ? '/' : '/api/v' . $self->api_version . '/'; | 
| 202 | 22 |  |  |  |  | 84 | $url = URI->new( $self->instance->uri . $base . $url ); | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | # Adjust query param format to be Ruby-compliant | 
| 206 | 22 |  |  |  |  | 2841 | foreach my $key (keys %{$params}) { | 
|  | 22 |  |  |  |  | 76 |  | 
| 207 | 5 |  |  |  |  | 9 | my $val = $params->{$key}; | 
| 208 | 5 | 100 |  |  |  | 20 | if (ref $val eq 'ARRAY') { $url->query_param($key . '[]' => @{$val}) } | 
|  | 3 |  |  |  |  | 8 |  | 
|  | 3 |  |  |  |  | 28 |  | 
| 209 | 2 |  |  |  |  | 12 | else                     { $url->query_param($key => $val) } | 
| 210 |  |  |  |  |  |  | } | 
| 211 |  |  |  |  |  |  |  | 
| 212 | 22 |  |  |  |  | 795 | return $url; | 
| 213 |  |  |  |  |  |  | } | 
| 214 |  |  |  |  |  |  |  | 
| 215 |  |  |  |  |  |  | 1; |