File Coverage

blib/lib/API/Instagram.pm
Criterion Covered Total %
statement 128 128 100.0
branch 27 34 79.4
condition 19 30 63.3
subroutine 35 35 100.0
pod 9 10 90.0
total 218 237 91.9


line stmt bran cond sub pod time code
1             package API::Instagram;
2              
3             # ABSTRACT: Object Oriented Interface for the Instagram REST and Search APIs
4              
5             our $VERSION = '0.013';
6              
7 15     15   969039 use Moo;
  15         317546  
  15         100  
8             # with 'MooX::Singleton';
9              
10 15     15   31315 use Carp;
  15         35  
  15         1348  
11 15     15   80 use strict;
  15         27  
  15         448  
12 15     15   73 use warnings;
  15         26  
  15         392  
13 15     15   76 use Digest::MD5 'md5_hex';
  15         26  
  15         773  
14              
15 15     15   27961 use URI;
  15         132619  
  15         488  
16 15     15   1887 use JSON;
  15         18640  
  15         138  
17 15     15   15734 use Furl;
  15         617311  
  15         623  
18              
19 15     15   16162 use API::Instagram::User;
  15         58  
  15         686  
20 15     15   11799 use API::Instagram::Location;
  15         52  
  15         573  
21 15     15   11232 use API::Instagram::Tag;
  15         46  
  15         548  
22 15     15   17609 use API::Instagram::Media;
  15         50  
  15         719  
23 15     15   11650 use API::Instagram::Media::Comment;
  15         45  
  15         485  
24 15     15   10611 use API::Instagram::Search;
  15         50  
  15         26820  
25              
26             has client_id => ( is => 'ro', required => 1 );
27             has client_secret => ( is => 'ro', required => 1 );
28             has redirect_uri => ( is => 'ro', required => 1 );
29             has scope => ( is => 'ro', default => sub { 'basic' } );
30             has response_type => ( is => 'ro', default => sub { 'code' } );
31             has grant_type => ( is => 'ro', default => sub { 'authorization_code' } );
32             has code => ( is => 'rw', isa => sub { confess "Code not provided" unless $_[0] } );
33             has access_token => ( is => 'rw', isa => sub { confess "No access token provided" unless $_[0] } );
34             has no_cache => ( is => 'rw', default => sub { 0 } );
35              
36             has _ua => ( is => 'ro', default => sub { Furl->new() } );
37             has _obj_cache => ( is => 'ro', default => sub { { User => {}, Media => {}, Location => {}, Tag => {}, 'Media::Comment' => {} } } );
38             has _endpoint_url => ( is => 'ro', default => sub { 'https://api.instagram.com/v1' } );
39             has _authorize_url => ( is => 'ro', default => sub { 'https://api.instagram.com/oauth/authorize' } );
40             has _access_token_url => ( is => 'ro', default => sub { 'https://api.instagram.com/oauth/access_token' } );
41              
42             has _debug => ( is => 'rw', lazy => 1 );
43              
44             my $instance;
45 14     14 0 361 sub BUILD { $instance = shift }
46              
47 28   33 28 1 719 sub instance { $instance //= shift->new(@_) }
48              
49             sub get_auth_url {
50 2     2 1 869 my $self = shift;
51              
52 2 100       32 carp "User already authorized with code: " . $self->code if $self->code;
53              
54 2         816 my @auth_fields = qw(client_id redirect_uri response_type scope);
55 2         7 for ( @auth_fields ) {
56 8 50 0     37 carp "ERROR: $_ required for generating authorization URL" and return unless defined $self->$_;
57             }
58              
59 2         21 my $uri = URI->new( $self->_authorize_url );
60 2         11302 $uri->query_form( map { $_ => $self->$_ } @auth_fields );
  8         48  
61 2         446 $uri->as_string();
62             }
63              
64              
65             sub get_access_token {
66 2     2 1 398 my $self = shift;
67              
68 2         8 my @access_token_fields = qw(client_id redirect_uri grant_type client_secret code);
69 2         5 for ( @access_token_fields ) {
70 10 100 50     125 carp "ERROR: $_ required for generating access token." and return unless defined $self->$_;
71             }
72              
73 1         11 my $data = { map { $_ => $self->$_ } @access_token_fields };
  5         35  
74 1         18 my $json = $self->_request( 'post', $self->_access_token_url, $data, { token_not_required => 1 } );
75              
76 1 50       11 wantarray ? ( $json->{access_token}, $self->user( $json->{user} ) ) : $json->{access_token};
77             }
78              
79              
80 12     12 1 1047 sub media { shift->_get_obj( 'Media', 'id', shift ) }
81              
82 33   100 33 1 2052 sub user { shift->_get_obj( 'User', 'id', shift // 'self' ) }
83              
84 3     3 1 694 sub location { shift->_get_obj( 'Location', 'id', shift, 1 ) }
85              
86 24     24 1 380 sub tag { shift->_get_obj( 'Tag', 'name', shift ) }
87              
88             sub search {
89 2     2 1 592 my $self = shift;
90 2         5 my $type = shift;
91 2         17 API::Instagram::Search->new( type => $type )
92             }
93              
94              
95             sub popular_medias {
96 2     2 1 3589 my $self = shift;
97 2         5 my $url = "/media/popular";
98 2 50       16 $self->_medias( $url, { @_%2?():@_ } );
99             }
100              
101 6     6   17 sub _comment { shift->_get_obj( 'Media::Comment', 'id', shift ) }
102              
103             #####################################################
104             # Returns cached wanted object or creates a new one #
105             #####################################################
106             sub _get_obj {
107 78     78   172 my ( $self, $type, $key, $code, $optional_code ) = @_;
108              
109 78         227 my $data = { $key => $code };
110 78 100       327 $data = $code if ref $code eq 'HASH';
111 78         187 $code = $data->{$key};
112              
113             # Returns if CODE is not optional and not defined or if it's not a string
114 78 100 100     668 return if (!$optional_code and !defined $code) or ref $code;
      100        
115              
116             # Code used as cache key
117 66   66     429 my $cache_code = md5_hex( $code // $data);
118              
119             # Returns cached value or creates a new object
120 66   33     244 my $return = $self->_cache($type)->{$cache_code} //= ("API::Instagram::$type")->new( $data );
121              
122             # Deletes cache if no-cache is set
123 66 100       16775 delete $self->_cache($type)->{$cache_code} if $self->no_cache;
124              
125 66         548 return $return;
126             }
127              
128             ###################################
129             # Returns a list of Media Objects #
130             ###################################
131             sub _medias {
132 5     5   75 my ($self, $url, $params, $opts) = @_;
133 5   50     536 $params->{count} //= 33;
134 5         13 $params->{url} = $url;
135 5         37 [ map { $self->media($_) } $self->_get_list( { %$params, url => $url }, $opts ) ]
  7         246  
136             }
137              
138             ####################################################################
139             # Returns a list of the requested items. Does pagination if needed #
140             ####################################################################
141             sub _get_list {
142 5     5   21 my $self = shift;
143 5         11 my $params = shift;
144 5         9 my $opts = shift;
145              
146 5   50     23 my $url = delete $params->{url} || return [];
147 5   100     27 my $count = $params->{count} // 999_999_999;
148 5 50       17 $count = 999_999_999 if $count < 0;
149 5         13 $params->{count} = $count;
150              
151 5         19 my $request = $self->_request( 'get', $url, $params, $opts );
152 5         220 my $data = $request->{data};
153              
154             # Keeps requesting if total items is less than requested
155             # and still there is pagination
156 5         21 while ( my $pagination = $request->{pagination} ){
157              
158 2 100       8 last if @$data >= $count;
159 1 50       4 last unless $pagination->{next_url};
160              
161 1         4 $opts->{prepared_url} = 1;
162 1         4 $request = $self->_request( 'get', $pagination->{next_url}, $params, $opts );
163 1         6 push @$data, @{ $request->{data} };
  1         4  
164             }
165              
166 5         25 return @$data;
167             }
168              
169             ##############################################################
170             # Requests the data from the given URL with QUERY parameters #
171             ##############################################################
172             sub _request {
173 8     8   2252 my ( $self, $method, $url, $params, $opts ) = @_;
174              
175             # Verifies access requirements
176 8 100       191 unless ( defined $self->access_token ) {
177 3 100 66     630 if ( !$opts->{token_not_required} or !defined $self->client_id ) {
178 2         304 carp "A valid access_token is required";
179             return {}
180 2         182 }
181             }
182              
183             # If URL is not prepared, prepares it
184 6 100       53 unless ( $opts->{prepared_url} ){
185              
186 5         13 $url =~ s|^/||;
187 5         118 $params->{access_token} = $self->access_token;
188              
189             # Prepares the URL
190 5         138 my $uri = URI->new( $self->_endpoint_url );
191 5         414 $uri->path_segments( $uri->path_segments, split '/', $url );
192 5         587 $uri->query_form($params);
193 5         610 $url = $uri->as_string;
194             }
195              
196             # For debugging purposes
197 6 50       197 print "Requesting: $url$/" if $self->_debug;
198              
199             # Treats response content
200 6         44 my $res = decode_json $self->_ua->$method( $url, [], $params )->decoded_content;
201              
202             # Verifies meta node
203 6         58150 my $meta = $res->{meta};
204 6 50       26 carp "$meta->{error_type}: $meta->{error_message}" if $meta->{code} ne '200';
205              
206 15     15   38875 use Data::Dumper;
  15         147989  
  15         4115  
207             # die Dumper $res;
208 6         21 $res;
209             }
210              
211 21 100   21   87 sub _request_data { shift->_request(@_)->{data} || {} }
212              
213 2     2   35 sub _del { shift->_request_data( 'delete', @_ ) }
214 16     16   160 sub _get { shift->_request_data( 'get', @_ ) }
215 3     3   23 sub _post { shift->_request_data( 'post', @_ ) }
216              
217             ################################
218             # Returns requested cache hash #
219             ################################
220 107     107   1790 sub _cache { shift->_obj_cache->{ shift() } }
221              
222              
223             1;
224              
225             __END__