File Coverage

blib/lib/Mastodon/Role/UserAgent.pm
Criterion Covered Total %
statement 103 134 76.8
branch 17 36 47.2
condition 6 15 40.0
subroutine 22 28 78.5
pod 0 5 0.0
total 148 218 67.8


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