File Coverage

blib/lib/Mastodon/Client.pm
Criterion Covered Total %
statement 138 237 58.2
branch 25 42 59.5
condition 25 53 47.1
subroutine 24 44 54.5
pod 21 22 95.4
total 233 398 58.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Talk to a Mastodon server
2             package Mastodon::Client;
3              
4 5     5   1423848 use strict;
  5         42  
  5         170  
5 5     5   30 use warnings;
  5         14  
  5         122  
6 5     5   64 use v5.10.0;
  5         34  
7              
8             our $VERSION = '0.017';
9              
10 5     5   29 use Carp;
  5         14  
  5         305  
11 5     5   2246 use Mastodon::Types qw( Acct Account DateTime Image URI Instance );
  5         66  
  5         61  
12 5     5   9927 use Moo;
  5         37053  
  5         28  
13 5     5   10464 use Types::Common::String qw( NonEmptyStr );
  5         164178  
  5         53  
14 5         43 use Types::Standard qw(
15             Any Int Str Bool Undef HashRef ArrayRef Dict Tuple StrictNum
16             slurpy Maybe Optional
17 5     5   2805 );
  5         11  
18 5     5   11643 use Types::Path::Tiny qw( File );
  5         12  
  5         48  
19 5     5   4917 use Type::Params qw( compile validate );
  5         80186  
  5         75  
20              
21 5     5   3951 use Log::Any;
  5         35046  
  5         36  
22             my $log = Log::Any->get_logger(category => 'Mastodon');
23              
24             with 'Mastodon::Role::UserAgent';
25              
26             has coerce_entities => (
27             is => 'rw',
28             isa => Bool,
29             lazy => 1,
30             default => 0,
31             );
32              
33             has access_token => (
34             is => 'rw',
35             isa => NonEmptyStr,
36             lazy => 1,
37             predicate => '_has_access_token',
38             );
39              
40             has authorized => (
41             is => 'rw',
42             isa => DateTime|Bool,
43             lazy => 1,
44             default => sub { $_[0]->_has_access_token },
45             coerce => 1,
46             );
47              
48             has client_id => (
49             is => 'rw',
50             isa => NonEmptyStr,
51             lazy => 1,
52             );
53              
54             has client_secret => (
55             is => 'rw',
56             isa => NonEmptyStr,
57             lazy => 1,
58             );
59              
60             has name => (
61             is => 'ro',
62             isa => NonEmptyStr,
63             );
64              
65             has website => (
66             is => 'ro',
67             isa => Str,
68             lazy => 1,
69             default => q{},
70             );
71              
72             has account => (
73             is => 'rw',
74             isa => HashRef|Account,
75             init_arg => undef,
76             lazy => 1,
77             default => sub {
78             $_[0]->get_account;
79             },
80             );
81              
82             has scopes => (
83             is => 'rw',
84             isa => ArrayRef->plus_coercions( Str, sub { [ split / /, $_ ] } ),
85             lazy => 1,
86             default => sub { [ 'read' ] },
87             coerce => 1,
88             );
89              
90             after access_token => sub {
91             $_[0]->authorized(1) unless $_[0]->authorized;
92             };
93              
94             sub authorize {
95 11     11 1 2267 my $self = shift;
96              
97 11 100 100     241 unless ( $self->client_id and $self->client_secret ) {
98 4         93 croak $log->fatal(
99             'Cannot authorize client without client_id and client_secret');
100             }
101              
102 7 100       337 if ( $self->access_token ) {
103 1         96 $log->warn('Client is already authorized');
104 1         89 return $self;
105             }
106              
107 6         2396 state $check = compile(
108             slurpy Dict [
109             access_code => Optional [Str],
110             username => Optional [Str],
111             password => Optional [Str],
112             ],
113             );
114 6         10743 my ($params) = $check->(@_);
115              
116 6         286 my $data = {
117             client_id => $self->client_id,
118             client_secret => $self->client_secret,
119             redirect_uri => $self->redirect_uri,
120             };
121              
122 6 100       404 if ( $params->{access_code} ) {
123 1         3 $data->{grant_type} = 'authorization_code';
124 1         3 $data->{code} = $params->{access_code};
125             }
126             else {
127 5         12 $data->{grant_type} = 'password';
128 5   100     23 $data->{username} = $params->{username} // '';
129 5   100     23 $data->{password} = $params->{password} // '';
130             }
131              
132 6         10 $data->{scope} = join q{ }, sort @{ $self->scopes };
  6         107  
133              
134 6         1129 my $response = $self->post( 'oauth/token' => $data );
135              
136 6 100       47 if ( defined $response->{error} ) {
137 1         4 $log->warn( $response->{error_description} );
138             }
139             else {
140 5         27 my $granted_scopes = join q{ }, sort split( / /, $response->{scope} );
141 5         10 my $requested_scopes = join q{ }, sort @{ $self->scopes };
  5         86  
142              
143 5 100       48 croak $log->fatal('Granted and requested scopes do not match')
144             if $granted_scopes ne $requested_scopes;
145              
146 4         79 $self->access_token( $response->{access_token} );
147 4         161 $self->authorized( $response->{created_at} );
148             }
149              
150 5         3179 return $self;
151             }
152              
153             # Authorize follow requests by account ID
154             sub authorize_follow {
155 0     0 1 0 my $self = shift;
156 0         0 state $check = compile( Int );
157 0         0 my ($id) = $check->(@_);
158 0         0 return $self->post( 'follow_requests/authorize' => { id => $id } );
159             }
160              
161             # Clears notifications
162             sub clear_notifications {
163 0     0 1 0 my $self = shift;
164 0         0 state $check = compile();
165 0         0 $check->(@_);
166              
167 0         0 return $self->post( 'notifications/clear' );
168             }
169              
170             # Delete a status by ID
171             sub delete_status {
172 0     0 1 0 my $self = shift;
173              
174 0         0 state $check = compile( Int );
175 0         0 my ($id) = $check->(@_);
176              
177 0         0 return $self->delete( "statuses/$id" );
178             }
179              
180             sub fetch_instance {
181 1     1 1 186 my $self = shift;
182              
183             # Do not return from the instance attribute, since the user might have
184             # disabled coercions, and the attribute is always coerced
185 1         13 my $instance = $self->get( 'instance' );
186 1         76 $self->instance($instance);
187 1         170 return $instance;
188             }
189              
190             sub get_account {
191 4     4 1 1896 my $self = shift;
192 4         17 my $own = 'verify_credentials';
193              
194 4         14 state $check = compile( Optional [Int|HashRef], Optional [HashRef] );
195 4         4999 my ($id, $params) = $check->(@_);
196              
197 4 100       66 if (ref $id eq 'HASH') {
198 1         3 $params = $id;
199 1         2 $id = undef;
200             }
201              
202 4   66     19 $id //= $own;
203 4   100     32 $params //= {};
204              
205 4         25 my $data = $self->get( "accounts/$id", $params );
206              
207             # We fetched authenticated user account's data
208             # Update local reference
209 4 100       240 $self->account($data) if ($id eq $own);
210 4         167 return $data;
211             }
212              
213             # Get a single notification by ID
214             sub get_notification {
215 0     0 1 0 my $self = shift;
216 0         0 state $check = compile( Int, Optional [HashRef] );
217 0         0 my ($id, $params) = $check->(@_);
218              
219 0         0 return $self->get( "notifications/$id", $params );
220             }
221              
222             # Get a single status by ID
223             sub get_status {
224 0     0 1 0 my $self = shift;
225 0         0 state $check = compile( Int, Optional [HashRef] );
226 0         0 my ($id, $params) = $check->(@_);
227              
228 0         0 return $self->get( "statuses/$id", $params );
229             }
230              
231             # Post a status
232             sub post_status {
233 0     0 1 0 my $self = shift;
234 0         0 state $check = compile( Str|HashRef, Optional[HashRef]);
235 0         0 my ($text, $params) = $check->(@_);
236 0   0     0 $params //= {};
237              
238 0         0 my $payload;
239 0 0       0 if (ref $text eq 'HASH') {
240 0         0 $params = $text;
241             croak $log->fatal('Post must contain a (possibly empty) status text')
242 0 0       0 unless defined $params->{status};
243 0         0 $payload = $params;
244             }
245             else {
246 0         0 $payload = { status => $text, %{$params} };
  0         0  
247             }
248              
249 0         0 return $self->post( 'statuses', $payload);
250             }
251              
252             # Reblog a status by ID
253             sub reblog_status {
254 0     0 0 0 my $self = shift;
255              
256 0         0 state $check = compile( Int );
257 0         0 my ($id) = $check->(@_);
258              
259 0         0 return $self->post( "statuses/$id/reblog" );
260             }
261              
262             sub register {
263 2     2 1 2053 my $self = shift;
264              
265 2 50 33     53 if ( $self->client_id && $self->client_secret ) {
266 0         0 $log->warn('Client is already registered');
267 0         0 return $self;
268             }
269              
270 2         30 state $check = compile(
271             slurpy Dict [
272             redirect_uris => Optional [ Str ],
273             scopes => Optional [ ArrayRef [ Str ] ],
274             website => Optional [ Str ],
275             instance => Optional [ Any ],
276             ]
277             );
278 2         16083 my ($params) = $check->(@_);
279              
280             # We used to accept instance by mistake, we want to turn it off
281             warn 'Deprecation notice: do not pass an instance parameter to register'
282 2 50       46 if $params->{instance};
283              
284 2   33     52 my $website = $params->{website} // $self->website;
285 2   33     107 my $scopes = $params->{scopes} // $self->scopes;
286              
287             my $response = $self->post(
288             'apps' => {
289             client_name => $self->name,
290             redirect_uris => $params->{redirect_uris} // $self->redirect_uri,
291 2 50 33     991 scopes => join( ' ', sort @{$scopes} ),
  2         81  
292             $website ? ( website => $website ) : (),
293             },
294             );
295              
296 2         63 $self->client_id( $response->{client_id} );
297 2         94 $self->client_secret( $response->{client_secret} );
298 2         80 $self->scopes($scopes);
299              
300 2         132 return $self;
301             }
302              
303             sub statuses {
304 4     4 1 10954 my $self = shift;
305 4         13 state $check = compile( Optional [HashRef|Int], Optional [HashRef]);
306 4         3705 my ($id, $params) = $check->(@_);
307 4 100       64 if (ref $id) {
308 1         3 $params = $id;
309 1         2 $id = undef;
310             }
311 4   66     66 $id //= $self->account->{id};
312 4   100     51 $params //= {};
313              
314 4         57 return $self->get( "accounts/$id/statuses", $params );
315             }
316              
317             # Reject follow requests by account ID
318             sub reject_follow {
319 0     0 1 0 my $self = shift;
320 0         0 state $check = compile( Int );
321 0         0 my ($id) = $check->(@_);
322 0         0 return $self->post( 'follow_requests/reject' => { id => $id } );
323             }
324              
325             # Follow a remote user by acct (username@instance)
326             sub remote_follow {
327 0     0 1 0 my $self = shift;
328 0         0 state $check = compile( Acct );
329 0         0 my ($acct) = $check->(@_);
330 0         0 return $self->post( 'follows' => { uri => $acct } );
331             }
332              
333             # Report a user account or list of statuses
334             sub report {
335 0     0 1 0 my $self = shift;
336             state $check = compile( slurpy Dict[
337             account_id => Optional[Int],
338 0     0   0 status_ids => Optional[ArrayRef->plus_coercions( Int, sub { [ $_ ] } ) ],
  0         0  
339             comment => Optional[Str],
340             ]);
341 0         0 my ($data) = $check->(@_);
342              
343             croak $log->fatal('Either account_id or status_ids are required for report')
344 0 0       0 unless join(q{ }, keys(%{$data})) =~ /\b(account_id|status_ids)\b/;
  0         0  
345              
346 0         0 return $self->post( 'reports' => $data );
347             }
348              
349             sub relationships {
350 5     5 1 12065 my $self = shift;
351              
352 5         15 state $check = compile( slurpy ArrayRef [Int|HashRef] );
353 5         6369 my ($ids) = $check->(@_);
354 5 100       101 my $params = (ref $ids->[-1] eq 'HASH') ? pop(@{$ids}) : {};
  2         7  
355              
356             croak $log->fatal('At least one ID number needed in relationships')
357 5 100       11 unless scalar @{$ids};
  5         51  
358              
359             $params = {
360             id => $ids,
361 3         9 %{$params},
  3         13  
362             };
363              
364 3         16 return $self->get( 'accounts/relationships', $params );
365             }
366              
367             sub search {
368 0     0 1 0 my $self = shift;
369              
370 0         0 state $check = compile( Str, Optional [HashRef] );
371 0         0 my ($query, $params) = $check->(@_);
372 0   0     0 $params //= {};
373              
374             $params = {
375             'q' => $query,
376 0         0 %{$params},
  0         0  
377             };
378              
379 0         0 return $self->get( 'search', $params );
380             }
381              
382             sub search_accounts {
383 5     5 1 10635 my $self = shift;
384              
385 5         16 state $check = compile( Str, Optional [HashRef] );
386 5         1589 my ($query, $params) = $check->(@_);
387 2   100     55 $params //= {};
388              
389             $params = {
390             'q' => $query,
391 2         6 %{$params},
  2         7  
392             };
393              
394 2         10 return $self->get( 'accounts/search', $params );
395             }
396              
397             sub stream {
398 0     0 1 0 my $self = shift;
399              
400 0         0 state $check = compile( NonEmptyStr );
401 0         0 my ($query) = $check->(@_);
402              
403 0 0       0 my $endpoint
404             = $self->instance->uri
405             . '/api/v'
406             . $self->api_version
407             . '/streaming/'
408             . (( $query =~ /^#/ )
409             ? ( 'hashtag?' . $query )
410             : $query
411             );
412              
413 5     5   16016 use Mastodon::Listener;
  5         18  
  5         2987  
414 0         0 return Mastodon::Listener->new(
415             url => $endpoint,
416             access_token => $self->access_token,
417             coerce_entities => $self->coerce_entities,
418             );
419             }
420              
421             sub timeline {
422 0     0 1 0 my $self = shift;
423              
424 0         0 state $check = compile( NonEmptyStr, Optional [HashRef] );
425 0         0 my ($query, $params) = $check->(@_);
426              
427 0 0       0 my $endpoint
428             = ( $query =~ /^#/ )
429             ? 'timelines/tag/' . $query
430             : 'timelines/' . $query;
431              
432 0         0 return $self->get($endpoint, $params);
433             }
434              
435             sub update_account {
436 0     0 1 0 my $self = shift;
437              
438 0         0 state $check = compile(
439             slurpy Dict [
440             display_name => Optional [Str],
441             note => Optional [Str],
442             avatar => Optional [Image],
443             header => Optional [Image],
444             ]
445             );
446 0         0 my ($data) = $check->(@_);
447              
448 0         0 return $self->patch( 'accounts/update_credentials' => $data );
449             }
450              
451             sub upload_media {
452 0     0 1 0 my $self = shift;
453              
454             state $check = compile(
455 0     0   0 File->plus_coercions( Str, sub { Path::Tiny::path($_) } ),
  0         0  
456             Optional [ Dict[
457             description => Optional[Str],
458             focus => Optional[Tuple[StrictNum, StrictNum]],
459             ]]
460             );
461 0         0 my ($file, $params) = $check->(@_);
462 0   0     0 $params //= {};
463              
464 0 0       0 if (exists $params->{focus}) {
465 0         0 my ($x,$y) = @{$params->{focus}};
  0         0  
466 0 0 0     0 if ($x >= -1 && $x <= 1 && $y >= -1 && $y <= 1) {
      0        
      0        
467 0         0 $params->{focus} = "$x,$y";
468             } else {
469 0         0 delete $params->{focus};
470             }
471             }
472 0         0 return $self->post( 'media' =>
473             { file => [ $file, undef ], %$params },
474             headers => { Content_Type => 'form-data' },
475             );
476             }
477              
478             # POST requests with no data and a mandatory ID number
479             foreach my $pair ([
480             [ statuses => [qw( reblog unreblog favourite unfavourite )] ],
481             [ accounts => [qw( mute unmute block unblock follow unfollow )] ],
482             ]) {
483              
484             my ($base, $endpoints) = @{$pair};
485              
486             foreach my $endpoint (@{$endpoints}) {
487             my $method = ($base eq 'statuses') ? $endpoint . '_status' : $endpoint;
488              
489 5     5   95 no strict 'refs';
  5         17  
  5         827  
490             *{ __PACKAGE__ . '::' . $method } = sub {
491 0     0   0 my $self = shift;
492 0         0 state $check = compile( Int );
493 0         0 my ($id) = $check->(@_);
494              
495 0         0 return $self->post( "$base/$id/$endpoint" );
496             };
497             }
498             }
499              
500             # GET requests with no parameters but optional parameter hashref
501             for my $action (qw(
502             blocks favourites follow_requests mutes notifications reports
503             )) {
504              
505 5     5   50 no strict 'refs';
  5         20  
  5         709  
506             *{ __PACKAGE__ . '::' . $action } = sub {
507 0     0   0 my $self = shift;
508 0         0 state $check = compile(Optional [HashRef]);
509 0         0 my ($params) = $check->(@_);
510 0   0     0 $params //= {};
511              
512 0         0 return $self->get( $action, $params );
513             };
514             }
515              
516             # GET requests with optional ID and parameter hashref
517             # ID number defaults to authenticated account's ID
518             for my $action (qw( following followers )) {
519 5     5   42 no strict 'refs';
  5         13  
  5         1189  
520             *{ __PACKAGE__ . '::' . $action } = sub {
521 8     8   20037 my $self = shift;
522 8         26 state $check = compile( Optional [Int|HashRef], Optional [HashRef] );
523 8         4752 my ($id, $params) = $check->(@_);
524              
525 8 100       117 if (ref $id eq 'HASH') {
526 2         8 $params = $id;
527 2         3 $id = undef;
528             }
529              
530 8   66     191 $id //= $self->account->{id};
531 8   100     76 $params //= {};
532              
533 8         43 return $self->get( "accounts/$id/$action", $params );
534             };
535             }
536              
537             # GET requests for status details
538             foreach my $pair ([
539             [ get_status_context => 'context' ],
540             [ get_status_card => 'card' ],
541             [ get_status_reblogs => 'reblogged_by' ],
542             [ get_status_favourites => 'favourited_by' ],
543             ]) {
544              
545             my ($method, $endpoint) = @{$pair};
546              
547 5     5   51 no strict 'refs';
  5         17  
  5         970  
548             *{ __PACKAGE__ . '::' . $method } = sub {
549 0     0     my $self = shift;
550 0           state $check = compile( Int, Optional [HashRef] );
551 0           my ($id, $params) = $check->(@_);
552              
553 0           return $self->get( "statuses/$id/$endpoint", $params );
554             };
555             }
556              
557             1;
558              
559             __END__
560              
561             =encoding utf8
562              
563             =head1 NAME
564              
565             Mastodon::Client - Talk to a Mastodon server
566              
567             =head1 SYNOPSIS
568              
569             use Mastodon::Client;
570              
571             my $client = Mastodon::Client->new(
572             instance => 'mastodon.social',
573             name => 'PerlBot',
574             client_id => $client_id,
575             client_secret => $client_secret,
576             access_token => $access_token,
577             coerce_entities => 1,
578             );
579              
580             $client->post_status('Posted to a Mastodon server!');
581             $client->post_status('And now in secret...',
582             { visibility => 'unlisted' }
583             )
584              
585             # Streaming interface might change!
586             my $listener = $client->stream( 'public' );
587             $listener->on( update => sub {
588             my ($listener, $status) = @_;
589             printf "%s said: %s\n",
590             $status->account->display_name,
591             $status->content;
592             });
593             $listener->start;
594              
595             =head1 DESCRIPTION
596              
597             Mastodon::Client lets you talk to a Mastodon server to obtain authentication
598             credentials, read posts from timelines in both static or streaming mode, and
599             perform all the other operations exposed by the Mastodon API.
600              
601             Most of these are available through the convenience methods listed below, which
602             validate input parameters and are likely to provide more meaningful feedback in
603             case of errors.
604              
605             Alternatively, this distribution can be used via the low-level request methods
606             (B<post>, B<get>, etc), which allow direct access to the API endpoints. All
607             other methods call one of these at some point.
608              
609             =head1 VERSION NOTICE
610              
611             This distribution currently B<only supports version 1 of the Mastodon API>.
612              
613             Help implementing support for any missing features in version 1, and for the
614             new features in version 2, would be greatfully appreciated.
615              
616             =head1 ATTRIBUTES
617              
618             =over 4
619              
620             =item B<instance>
621              
622             A Mastodon::Entity::Instance object representing the instance to which this
623             client will speak. Defaults to C<mastodon.social>.
624              
625             =item B<api_version>
626              
627             An integer specifying the version of the API endpoints to use. Defaults to C<1>.
628              
629             =item B<redirect_uri>
630              
631             The URI to which authorization codes should be forwarded as part of the OAuth2
632             flow. Defaults to C<urn:ietf:wg:oauth:2.0:oob> (meaning no redirection).
633              
634             =item B<user_agent>
635              
636             The user agent to use for the requests. Defaults to an instance of
637             L<HTTP::Thin>. It is expected to have a C<request> method that
638             accepts L<HTTP::Request> objects.
639              
640             =item B<coerce_entities>
641              
642             A boolean value. Set to true if you want Mastodon::Client to internally coerce
643             all response entities to objects. This adds a level of validation, and can
644             make the objects easier to use.
645              
646             Although this does require some additional processing, the coercion is done by
647             L<Type::Tiny>, so the impact is negligible.
648              
649             For now, it defaults to B<false> (but this will likely change, so I recommend
650             you use it).
651              
652             =item B<access_token>
653              
654             The access token of your client. This is provided by the Mastodon API and is
655             used for the OAuth2 authentication required for most API calls.
656              
657             You can get this by calling B<authorize> with either an access code or your
658             account's username and password.
659              
660             =item B<authorized>
661              
662             Boolean. False is the client has no defined access_token. When an access token
663             is set, this is set to true or to a L<DateTime> object representing the time of
664             authorization if possible (as received from the server).
665              
666             =item B<client_id>
667              
668             =item B<client_secret>
669              
670             The client ID and secret are provided by the Mastodon API when you register
671             your client using the B<register> method. They are used to identify where your
672             calls are coming from, and are required before you can use the B<authorize>
673             method to get the access token.
674              
675             =item B<name>
676              
677             Your client's name. This is required when registering, but is otherwise seldom
678             used. If you are using the B<authorization_url> to get an access code from your
679             users, then they will see this name when they go to that page.
680              
681             =item B<account>
682              
683             Holds the authenticated account. It is set internally by the B<get_account>
684             method.
685              
686             =item B<scopes>
687              
688             This array reference holds the scopes set by you for the client. These are
689             required when registering your client with the Mastodon instance. Defaults to
690             C<read>.
691              
692             Mastodon::Client will internally make sure that the scopes you were provided
693             when calling B<authorize> match those that you requested. If this is not the
694             case, it will helpfully die.
695              
696             =item B<website>
697              
698             The URL of a human-readable website for the client. If made available, it
699             appears as a link in the "authorized applications" tab of the user preferences
700             in the default Mastodon web GUI. Defaults to the empty string.
701              
702             =back
703              
704             =head1 METHODS
705              
706             =head2 Authorizing an application
707              
708             Although not all of the API methods require authentication to be used, most of
709             them do. The authentication process involves a) registering an application with
710             a Mastodon server to obtain a client secret and ID; b) authorizing the
711             application by either providing a user's credentials, or by using an
712             authentication URL.
713              
714             The methods facilitating this process are detailed below:
715              
716             =over 4
717              
718             =item B<register()>
719              
720             =item B<register(%data)>
721              
722             Obtain a client secret and ID from a given mastodon instance. Takes a single
723             hash as an argument, with the following possible keys:
724              
725             =over 4
726              
727             =item B<redirect_uris>
728              
729             The URL to which authorization codes should be forwarded after authorized by
730             the user. Defaults to the value of the B<redirect_uri> attribute.
731              
732             =item B<scopes>
733              
734             The scopes requested by this client. Defaults to the value of the B<scopes>
735             attribute.
736              
737             =item B<website>
738              
739             The client's website. Defaults to the value of the C<website> attribute.
740              
741             =back
742              
743             When successful, sets the C<client_secret>, C<scopes>, and C<client_id>
744             attributes of the Mastodon::Client object and returns the modified object.
745              
746             This should be called B<once> per client and its contents cached locally.
747              
748             =item B<authorization_url()>
749              
750             Generate an authorization URL for the given application. Accessing this URL
751             via a browser by a logged in user will allow that user to grant this
752             application access to the requested scopes. The scopes used are the ones in the
753             B<scopes> attribute at the time this method is called.
754              
755             =item B<authorize()>
756              
757             =item B<authorize( %data )>
758              
759             Grant the application access to the requested scopes for a given user. This
760             method takes a hash with either an access code or a user's login credentials to
761             grant authorization. Valid keys are:
762              
763             =over 4
764              
765             =item B<access_code>
766              
767             The access code obtained by visiting the URL generated by B<authorization_url>.
768              
769             =item B<username>
770              
771             =item B<password>
772              
773             The user's login credentials.
774              
775             =back
776              
777             When successful, the method automatically sets the client's B<authorized>
778             attribute to a true value and caches the B<access_token> for all future calls.
779              
780             =back
781              
782             =head2 Error handling
783              
784             Methods that make requests to the server will C<die> whenever an error is
785             encountered, or the data that was received from the server is not what is
786             expected. The error string will, when possible, come from the response's
787             status line, but this will likely not be enough to fully diagnose what
788             went wrong.
789              
790             =over 4
791              
792             =item B<latest_response>
793              
794             To make this easier, the client will cache the server's response after each
795             request has been made, and expose it through the C<latest_response> accessor.
796              
797             Note that, as its name implies, I<this will only store the most recent
798             response>.
799              
800             If called before any request has been made, it will return an undefined
801             value.
802              
803             =back
804              
805             The remaining methods listed here follow the order of those in the official API
806             documentation.
807              
808             =head2 Accounts
809              
810             =over 4
811              
812             =item B<get_account()>
813              
814             =item B<get_account($id)>
815              
816             =item B<get_account($params)>
817              
818             =item B<get_account($id, $params)>
819              
820             Fetches an account by ID. If no ID is provided, this defaults to the current
821             authenticated account. Global GET parameters are available for this method.
822              
823             Depending on the value of C<coerce_entities>, it returns a
824             Mastodon::Entity::Account object, or a plain hash reference.
825              
826             =item B<update_account($params)>
827              
828             Make changes to the authenticated account. Takes a hash reference with the
829             following possible keys:
830              
831             =over 4
832              
833             =item B<display_name>
834              
835             =item B<note>
836              
837             Strings
838              
839             =item B<avatar>
840              
841             =item B<header>
842              
843             A base64 encoded image, or the name of a file to be encoded.
844              
845             =back
846              
847             Depending on the value of C<coerce_entities>, returns the modified
848             Mastodon::Entity::Account object, or a plain hash reference.
849              
850             =item B<followers()>
851              
852             =item B<followers($id)>
853              
854             =item B<followers($params)>
855              
856             =item B<followers($id, $params)>
857              
858             Get the list of followers of an account by ID. If no ID is provided, the one
859             for the current authenticated account is used. Global GET parameters are
860             available for this method.
861              
862             Depending on the value of C<coerce_entities>, returns an array reference of
863             Mastodon::Entity::Account objects, or a plain array reference.
864              
865             =item B<following()>
866              
867             =item B<following($id)>
868              
869             =item B<following($params)>
870              
871             =item B<following($id, $params)>
872              
873             Get the list of accounts followed by the account specified by ID. If no ID is
874             provided, the one for the current authenticated account is used. Global GET
875             parameters are available for this method.
876              
877             Depending on the value of C<coerce_entities>, returns an array reference of
878             Mastodon::Entity::Account objects, or a plain array reference.
879              
880             =item B<statuses()>
881              
882             =item B<statuses($id)>
883              
884             =item B<statuses($params)>
885              
886             =item B<statuses($id, $params)>
887              
888             Get a list of statuses from the account specified by ID. If no ID is
889             provided, the one for the current authenticated account is used.
890              
891             In addition to the global GET parameters, this method accepts the following
892             parameters:
893              
894             =over 4
895              
896             =item B<only_media>
897              
898             =item B<exclude_replies>
899              
900             Both boolean.
901              
902             =back
903              
904             Depending on the value of C<coerce_entities>, returns an array reference of
905             Mastodon::Entity::Status objects, or a plain array reference.
906              
907             =item B<follow($id)>
908              
909             =item B<unfollow($id)>
910              
911             Follow or unfollow an account specified by ID. The ID argument is mandatory.
912              
913             Depending on the value of C<coerce_entities>, returns the new
914             Mastodon::Entity::Relationship object, or a plain hash reference.
915              
916             =item B<block($id)>
917              
918             =item B<unblock($id)>
919              
920             Block or unblock an account specified by ID. The ID argument is mandatory.
921              
922             Depending on the value of C<coerce_entities>, returns the new
923             Mastodon::Entity::Relationship object, or a plain hash reference.
924              
925             =item B<mute($id)>
926              
927             =item B<unmute($id)>
928              
929             Mute or unmute an account specified by ID. The ID argument is mandatory.
930              
931             Depending on the value of C<coerce_entities>, returns the new
932             Mastodon::Entity::Relationship object, or a plain hash reference.
933              
934             =item B<relationships(@ids)>
935              
936             =item B<relationships(@ids, $params)>
937              
938             Get the list of relationships of the current authenticated user with the
939             accounts specified by ID. At least one ID is required, but more can be passed
940             at once. Global GET parameters are available for this method, and can be passed
941             as an additional hash reference as a final argument.
942              
943             Depending on the value of C<coerce_entities>, returns an array reference of
944             Mastodon::Entity::Relationship objects, or a plain array reference.
945              
946             =item B<search_accounts($query)>
947              
948             =item B<search_accounts($query, $params)>
949              
950             Search for accounts. Takes a mandatory string argument to use as the search
951             query. If the search query is of the form C<username@domain>, the accounts
952             will be searched remotely.
953              
954             In addition to the global GET parameters, this method accepts the following
955             parameters:
956              
957             =over 4
958              
959             =item B<limit>
960              
961             The maximum number of matches. Defaults to 40.
962              
963             =back
964              
965             Depending on the value of C<coerce_entities>, returns an array reference of
966             Mastodon::Entity::Account objects, or a plain array reference.
967              
968             This method does not require authentication.
969              
970             =back
971              
972             =head2 Blocks
973              
974             =over 4
975              
976             =item B<blocks()>
977              
978             =item B<blocks($params)>
979              
980             Get the list of accounts blocked by the authenticated user. Global GET
981             parameters are available for this method.
982              
983             Depending on the value of C<coerce_entities>, returns an array reference of
984             Mastodon::Entity::Account objects, or a plain array reference.
985              
986             =back
987              
988             =head2 Favourites
989              
990             =over 4
991              
992             =item B<favourites()>
993              
994             =item B<favourites($params)>
995              
996             Get the list of statuses favourited by the authenticated user. Global GET
997             parameters are available for this method.
998              
999             Depending on the value of C<coerce_entities>, returns an array reference of
1000             Mastodon::Entity::Status objects, or a plain array reference.
1001              
1002             =back
1003              
1004             =head2 Follow requests
1005              
1006             =over 4
1007              
1008             =item B<follow_requests()>
1009              
1010             =item B<follow_requests($params)>
1011              
1012             Get the list of accounts requesting to follow the the authenticated user.
1013             Global GET parameters are available for this method.
1014              
1015             Depending on the value of C<coerce_entities>, returns an array reference of
1016             Mastodon::Entity::Account objects, or a plain array reference.
1017              
1018             =item B<authorize_follow($id)>
1019              
1020             =item B<reject_follow($id)>
1021              
1022             Accept or reject the follow request by the account of the specified ID. The ID
1023             argument is mandatory.
1024              
1025             Returns an empty object.
1026              
1027             =back
1028              
1029             =head2 Follows
1030              
1031             =over 4
1032              
1033             =item B<remote_follow($acct)>
1034              
1035             Follow a remote user by account string (ie. C<username@domain>). The argument
1036             is mandatory.
1037              
1038             Depending on the value of C<coerce_entities>, returns an
1039             Mastodon::Entity::Account object, or a plain hash reference with the local
1040             representation of the specified account.
1041              
1042             =back
1043              
1044             =head2 Instances
1045              
1046             =over 4
1047              
1048             =item B<fetch_instance()>
1049              
1050             Fetches the latest information for the current instance the client is talking
1051             to. When successful, this method updates the value of the C<instance>
1052             attribute.
1053              
1054             Depending on the value of C<coerce_entities>, returns an
1055             Mastodon::Entity::Instance object, or a plain hash reference.
1056              
1057             This method does not require authentication.
1058              
1059             =back
1060              
1061             =head2 Media
1062              
1063             =over 4
1064              
1065             =item B<upload_media($file)>
1066              
1067             =item B<upload_media($file, $params)>
1068              
1069             Upload a file as an attachment. Takes a mandatory argument with the name of a
1070             local file to encode and upload, and an optional hash reference with the
1071             following additional parameters:
1072              
1073             =over 4
1074              
1075             =item B<description>
1076              
1077             A plain-text description of the media, for accessibility, as a string.
1078              
1079             =item B<focus>
1080              
1081             An array reference of two floating point values, to be used as
1082             the x and y focus values. These inform clients which point in
1083             the image is the most important one to show in a cropped view.
1084              
1085             The value of a coordinate is a real number between -1 and +1,
1086             where 0 is the center. x:-1 indicates the left edge of the
1087             image, x:1 the right edge. For the y axis, y:1 is the top edge
1088             and y:-1 is the bottom.
1089              
1090             =back
1091              
1092             Depending on the value of C<coerce_entities>, returns an
1093             Mastodon::Entity::Attachment object, or a plain hash reference.
1094              
1095             The returned object's ID can be passed to the B<post_status> to post it to a
1096             timeline.
1097              
1098             =back
1099              
1100             =head2 Mutes
1101              
1102             =over 4
1103              
1104             =item B<mutes()>
1105              
1106             =item B<mutes($params)>
1107              
1108             Get the list of accounts muted by the authenticated user. Global GET
1109             parameters are available for this method.
1110              
1111             Depending on the value of C<coerce_entities>, returns an array reference of
1112             Mastodon::Entity::Account objects, or a plain array reference.
1113              
1114             =back
1115              
1116             =head2 Notifications
1117              
1118             =over 4
1119              
1120             =item B<notifications()>
1121              
1122             =item B<notifications($params)>
1123              
1124             Get the list of notifications for the authenticated user. Global GET
1125             parameters are available for this method.
1126              
1127             Depending on the value of C<coerce_entities>, returns an array reference of
1128             Mastodon::Entity::Notification objects, or a plain array reference.
1129              
1130             =item B<get_notification($id)>
1131              
1132             Get a notification by ID. The argument is mandatory.
1133              
1134             Depending on the value of C<coerce_entities>, returns an
1135             Mastodon::Entity::Notification object, or a plain hash reference.
1136              
1137             =item B<clear_notifications()>
1138              
1139             Clears all notifications for the authenticated user.
1140              
1141             This method takes no arguments and returns an empty object.
1142              
1143             =back
1144              
1145             =head2 Reports
1146              
1147             =over 4
1148              
1149             =item B<reports()>
1150              
1151             =item B<reports($params)>
1152              
1153             Get a list of reports made by the authenticated user. Global GET
1154             parameters are available for this method.
1155              
1156             Depending on the value of C<coerce_entities>, returns an array reference of
1157             Mastodon::Entity::Report objects, or a plain array reference.
1158              
1159             =item B<report($params)>
1160              
1161             Report a user or status. Takes a mandatory hash with the following keys:
1162              
1163             =over 4
1164              
1165             =item B<account_id>
1166              
1167             The ID of a single account to report.
1168              
1169             =item B<status_ids>
1170              
1171             The ID of a single status to report, or an array reference of statuses to
1172             report.
1173              
1174             =item B<comment>
1175              
1176             An optional string.
1177              
1178             =back
1179              
1180             While the comment is always optional, either the B<account_id> or the list of
1181             B<status_ids> must be present.
1182              
1183             Depending on the value of C<coerce_entities>, returns the new
1184             Mastodon::Entity::Report object, or a plain hash reference.
1185              
1186             =back
1187              
1188             =head2 Search
1189              
1190             =over 4
1191              
1192             =item B<search($query)>
1193              
1194             =item B<search($query, $params)>
1195              
1196             Search for content. Takes a mandatory string argument to use as the search
1197             query. If the search query is a URL, Mastodon will attempt to fetch the
1198             provided account or status. Otherwise, it will do a local account and hashtag
1199             search.
1200              
1201             In addition to the global GET parameters, this method accepts the following
1202             parameters:
1203              
1204             =over 4
1205              
1206             =item B<resolve>
1207              
1208             Whether to resolve non-local accounts.
1209              
1210             =back
1211              
1212             =back
1213              
1214             =head2 Statuses
1215              
1216             =over 4
1217              
1218             =item B<get_status($id)>
1219              
1220             =item B<get_status($id, $params)>
1221              
1222             Fetches a status by ID. The ID argument is mandatory. Global GET parameters are available for this method as an additional hash reference.
1223              
1224             Depending on the value of C<coerce_entities>, it returns a
1225             Mastodon::Entity::Status object, or a plain hash reference.
1226              
1227             This method does not require authentication.
1228              
1229             =item B<get_status_context($id)>
1230              
1231             =item B<get_status_context($id, $params)>
1232              
1233             Fetches the context of a status by ID. The ID argument is mandatory. Global GET parameters are available for this method as an additional hash reference.
1234              
1235             Depending on the value of C<coerce_entities>, it returns a
1236             Mastodon::Entity::Context object, or a plain hash reference.
1237              
1238             This method does not require authentication.
1239              
1240             =item B<get_status_card($id)>
1241              
1242             =item B<get_status_card($id, $params)>
1243              
1244             Fetches a card associated to a status by ID. The ID argument is mandatory.
1245             Global GET parameters are available for this method as an additional hash
1246             reference.
1247              
1248             Depending on the value of C<coerce_entities>, it returns a
1249             Mastodon::Entity::Card object, or a plain hash reference.
1250              
1251             This method does not require authentication.
1252              
1253             =item B<get_status_reblogs($id)>
1254              
1255             =item B<get_status_reblogs($id, $params)>
1256              
1257             =item B<get_status_favourites($id)>
1258              
1259             =item B<get_status_favourites($id, $params)>
1260              
1261             Fetches a list of accounts who have reblogged or favourited a status by ID.
1262             The ID argument is mandatory. Global GET parameters are available for this
1263             method as an additional hash reference.
1264              
1265             Depending on the value of C<coerce_entities>, it returns an array reference of
1266             Mastodon::Entity::Account objects, or a plain array reference.
1267              
1268             This method does not require authentication.
1269              
1270             =item B<post_status($text)>
1271              
1272             =item B<post_status($text, $params)>
1273              
1274             Posts a new status. Takes a mandatory string as the content of the status
1275             (which can be the empty string), and an optional hash reference with the
1276             following additional parameters:
1277              
1278             =over 4
1279              
1280             =item B<status>
1281              
1282             The content of the status, as a string. Since this is already provided as the
1283             first argument of the method, this is not necessary. But if provided, this
1284             value will overwrite that of the first argument.
1285              
1286             =item B<in_reply_to_id>
1287              
1288             The optional ID of a status to reply to.
1289              
1290             =item B<media_ids>
1291              
1292             An array reference of up to four media IDs. These can be obtained as the result
1293             of a call to B<upload_media()>.
1294              
1295             =item B<sensitive>
1296              
1297             Boolean, to mark status content as NSFW.
1298              
1299             =item B<spoiler_text>
1300              
1301             A string, to be shown as a warning before the actual content.
1302              
1303             =item B<visibility>
1304              
1305             A string; one of C<direct>, C<private>, C<unlisted>, or C<public>.
1306              
1307             =back
1308              
1309             Depending on the value of C<coerce_entities>, it returns the new
1310             Mastodon::Entity::Status object, or a plain hash reference.
1311              
1312             =item B<delete_status($id)>
1313              
1314             Delete a status by ID. The ID is mandatory. Returns an empty object.
1315              
1316             =item B<reblog($id)>
1317              
1318             =item B<unreblog($id)>
1319              
1320             =item B<favourite($id)>
1321              
1322             =item B<unfavourite($id)>
1323              
1324             Reblog or favourite a status by ID, or revert this action. The ID argument is
1325             mandatory.
1326              
1327             Depending on the value of C<coerce_entities>, it returns the specified
1328             Mastodon::Entity::Status object, or a plain hash reference.
1329              
1330             =back
1331              
1332             =head2 Timelines
1333              
1334             =over 4
1335              
1336             =item B<timeline($query)>
1337              
1338             =item B<timeline($query, $params)>
1339              
1340             Retrieves a timeline. The first argument defines either the name of a timeline
1341             (which can be one of C<home> or C<public>), or a hashtag (if it begins with the
1342             C<#> character). This argument is mandatory.
1343              
1344             In addition to the global GET parameters, this method accepts the following
1345             parameters:
1346              
1347             Accessing the public and tag timelines does not require authentication.
1348              
1349             =over 4
1350              
1351             =item B<local>
1352              
1353             Boolean. If true, limits results only to those originating from the current
1354             instance. Only applies to public and tag timelines.
1355              
1356             =back
1357              
1358             Depending on the value of C<coerce_entities>, it returns an array of
1359             Mastodon::Entity::Status objects, or a plain array reference. The more recent
1360             statuses come first.
1361              
1362             =back
1363              
1364             =head1 STREAMING RESULTS
1365              
1366             Alternatively, it is possible to use the streaming API to get a constant stream
1367             of updates. To do this, there is the B<stream()> method.
1368              
1369             =over 4
1370              
1371             =item B<stream($query)>
1372              
1373             Creates a Mastodon::Listener object which will fetch a stream for the
1374             specified query. Possible values for the query are either C<user>, for events
1375             that are relevant to the authorized user; C<public>, for all public statuses;
1376             or a tag (if it begins with the C<#> character), for all public statuses for
1377             the particular tag.
1378              
1379             For more details on how to use this object, see the documentation for
1380             L<Mastodon::Listener>.
1381              
1382             Accessing streaming public timeline does not require authentication.
1383              
1384             =back
1385              
1386             =head1 REQUEST METHODS
1387              
1388             Mastodon::Client uses four lower-level request methods to contact the API
1389             with GET, POST, PATCH, and DELETE requests. These are left available in case
1390             one of the higher-level convenience methods are unsuitable or undesirable, but
1391             you use them at your own risk.
1392              
1393             They all take a URL as their first parameter, which can be a string with the
1394             API endpoint to contact, or a L<URI> object, which will be used as-is.
1395              
1396             If passed as a string, the methods expect one that contains only the variable
1397             parts of the endpoint (ie. not including the C<HOST/api/v1> part). The
1398             remaining parts will be filled-in appropriately internally.
1399              
1400             =over 4
1401              
1402             =item B<delete($url)>
1403              
1404             =item B<get($url)>
1405              
1406             =item B<get($url, $params)>
1407              
1408             Query parameters can be passed as part of the L<URI> object, but it is not
1409             recommended you do so, since Mastodon has expectations for array parameters
1410             that do not meet those of eg. L<URI::QueryParam>. It will be easier and safer
1411             if any additional parameters are passed as a hash reference, which will be
1412             added to the URL before the request is sent.
1413              
1414             =item B<post($url)>
1415              
1416             =item B<post($url, $data)>
1417              
1418             =item B<patch($url)>
1419              
1420             =item B<patch($url, $data)>
1421              
1422             the C<post> and C<patch> methods work similarly to C<get> and C<delete>, but
1423             the optional hash reference is sent in as form data, instead of processed as
1424             query parameters. The Mastodon API does not use query parameters on POST or
1425             PATCH endpoints.
1426              
1427             =back
1428              
1429             =head1 CONTRIBUTIONS AND BUG REPORTS
1430              
1431             Contributions of any kind are most welcome!
1432              
1433             The main repository for this distribution is on
1434             L<GitLab|https://gitlab.com/jjatria/Mastodon-Client>, which is where patches
1435             and bug reports are mainly tracked. The repository is also mirrored on
1436             L<Github|https://github.com/jjatria/Mastodon-Client>, in case that platform
1437             makes it easier to post contributions.
1438              
1439             If none of the above is acceptable, bug reports can also be sent through the
1440             CPAN RT system, or by mail directly to the developers at the address below,
1441             although these will not be as closely tracked.
1442              
1443             =head1 AUTHOR
1444              
1445             =over 4
1446              
1447             =item *
1448              
1449             José Joaquín Atria <jjatria@cpan.org>
1450              
1451             =back
1452              
1453             =head1 CONTRIBUTORS
1454              
1455             =over 4
1456              
1457             =item *
1458              
1459             Lance Wicks <lancew@cpan.org>
1460              
1461             =back
1462              
1463             =head1 COPYRIGHT AND LICENSE
1464              
1465             This software is copyright (c) 2017 by José Joaquín Atria.
1466              
1467             This is free software; you can redistribute it and/or modify it under
1468             the same terms as the Perl 5 programming language system itself.
1469              
1470             =cut