File Coverage

blib/lib/WebService/Zendesk.pm
Criterion Covered Total %
statement 39 305 12.7
branch 0 114 0.0
condition 0 27 0.0
subroutine 13 36 36.1
pod 17 17 100.0
total 69 499 13.8


line stmt bran cond sub pod time code
1             package WebService::Zendesk;
2             # ABSTRACT: API interface to Zendesk
3 1     1   1723 use Moose;
  1         462932  
  1         6  
4 1     1   7686 use MooseX::Params::Validate;
  1         78422  
  1         6  
5 1     1   1147 use MooseX::WithCache;
  1         115562  
  1         6  
6 1     1   1468 use File::Spec::Functions; # catfile
  1         765  
  1         89  
7 1     1   748 use MIME::Base64;
  1         722  
  1         69  
8 1     1   6 use File::Path qw/make_path/;
  1         2  
  1         56  
9 1     1   3987 use LWP::UserAgent;
  1         72019  
  1         35  
10 1     1   7 use HTTP::Request;
  1         3  
  1         25  
11 1     1   8 use HTTP::Headers;
  1         1  
  1         23  
12 1     1   787 use JSON::MaybeXS;
  1         24220  
  1         133  
13 1     1   1204 use YAML;
  1         10051  
  1         55  
14 1     1   847 use URI::Encode qw/uri_encode/;
  1         12638  
  1         60  
15 1     1   6 use Encode;
  1         3  
  1         4771  
16              
17             our $VERSION = 0.021;
18              
19             =head1 NAME
20              
21             WebService::Zendesk
22              
23             =head1 DESCRIPTION
24              
25             Manage Zendesk connection, get tickets etc. This is a work-in-progress - we have only written
26             the access methods we have used so far, but as you can see, it is a good template to extend
27             for all remaining API endpoints. I'm totally open for any pull requests! :)
28              
29             This module uses MooseX::Log::Log4perl for logging - be sure to initialize!
30              
31             =head1 ATTRIBUTES
32              
33             =cut
34              
35             with "MooseX::Log::Log4perl";
36              
37             =over 4
38              
39             =item cache
40              
41             Optional.
42              
43             Provided by MooseX::WithCache - optionally pass a cache object to cache and avoid unnecessary requests
44              
45             =cut
46              
47             has 'cache' => (
48             is => 'ro',
49             required => 0,
50             trigger => \&_setup_cache,
51             );
52              
53             sub _setup_cache {
54 0     0     my( $self, $cache ) = @_;
55 0           my $backend = ref( $cache );
56              
57 0           with 'MooseX::WithCache' => {
58             backend => $backend,
59             };
60             }
61              
62             =item zendesk_token
63              
64             Required.
65              
66             =cut
67             has 'zendesk_token' => (
68             is => 'ro',
69             isa => 'Str',
70             required => 1,
71             );
72              
73             =item zendesk_username
74              
75             Required.
76              
77             =cut
78             has 'zendesk_username' => (
79             is => 'ro',
80             isa => 'Str',
81             required => 1,
82             );
83              
84             =item default_backoff
85             Optional. Default: 10
86             Time in seconds to back off before retrying request.
87             If a 429 response is given and the Retry-Time header is provided by the api this will be overridden.
88             =cut
89             has 'default_backoff' => (
90             is => 'ro',
91             isa => 'Int',
92             required => 1,
93             default => 10,
94             );
95              
96             =item retry_on_status
97             Optional. Default: [ 429, 500, 502, 503, 504 ]
98             Which http response codes should we retry on?
99             =cut
100             has 'retry_on_status' => (
101             is => 'ro',
102             isa => 'ArrayRef',
103             required => 1,
104             default => sub{ [ 429, 500, 502, 503, 504 ] },
105             );
106              
107             =item max_tries
108             Optional. Default: undef
109             Limit maximum number of times a query should be attempted before failing. If undefined then unlimited retries
110             =cut
111             has 'max_tries' => (
112             is => 'ro',
113             isa => 'Int',
114             );
115              
116             =item zendesk_api_url
117              
118             Required.
119              
120             =cut
121             has 'zendesk_api_url' => (
122             is => 'ro',
123             isa => 'Str',
124             required => 1,
125             );
126              
127             =item user_agent
128              
129             Optional. A new LWP::UserAgent will be created for you if you don't already have one you'd like to reuse.
130              
131             =cut
132              
133             has 'user_agent' => (
134             is => 'ro',
135             isa => 'LWP::UserAgent',
136             required => 1,
137             lazy => 1,
138             builder => '_build_user_agent',
139              
140             );
141              
142             has '_zendesk_credentials' => (
143             is => 'ro',
144             isa => 'Str',
145             required => 1,
146             lazy => 1,
147             builder => '_build_zendesk_credentials',
148             );
149            
150             has 'default_headers' => (
151             is => 'ro',
152             isa => 'HTTP::Headers',
153             required => 1,
154             lazy => 1,
155             builder => '_build_default_headers',
156             );
157              
158             sub _build_user_agent {
159 0     0     my $self = shift;
160 0           $self->log->debug( "Building zendesk useragent" );
161 0           my $ua = LWP::UserAgent->new(
162             keep_alive => 1
163             );
164             # $ua->default_headers( $self->default_headers );
165 0           return $ua;
166             }
167              
168             sub _build_default_headers {
169 0     0     my $self = shift;
170 0           my $h = HTTP::Headers->new();
171 0           $h->header( 'Content-Type' => "application/json" );
172 0           $h->header( 'Accept' => "application/json" );
173 0           $h->header( 'Authorization' => "Basic " . $self->_zendesk_credentials );
174 0           return $h;
175             }
176              
177             sub _build_zendesk_credentials {
178 0     0     my $self = shift;
179 0           return encode_base64( $self->zendesk_username . "/token:" . $self->zendesk_token );
180             }
181              
182             =back
183              
184             =head1 METHODS
185              
186             =over 4
187              
188             =item init
189              
190             Create the user agent and credentials. As these are built lazily, initialising manually can avoid
191             errors thrown when building them later being silently swallowed in try/catch blocks.
192              
193             =cut
194              
195             sub init {
196 0     0 1   my $self = shift;
197 0           my $ua = $self->user_agent;
198 0           my $credentials = $self->_zendesk_credentials;
199             }
200              
201             =item get_incremental_tickets
202              
203             Access the L<Incremental Ticket Export|https://developer.zendesk.com/rest_api/docs/core/incremental_export#incremental-ticket-export> interface
204              
205             !! Broken !!
206              
207             =cut
208             sub get_incremental_tickets {
209 0     0 1   my ( $self, %params ) = validated_hash(
210             \@_,
211             size => { isa => 'Int', optional => 1 },
212             );
213 0           my $path = '/incremental/ticket_events.json';
214             my @results = $self->_paged_get_request_from_api(
215             field => '???', # <--- TODO
216             method => 'get',
217             path => $path,
218             size => $params{size},
219 0           );
220              
221 0           $self->log->debug( "Got " . scalar( @results ) . " results from query" );
222 0           return @results;
223              
224             }
225              
226             =item search
227              
228             Access the L<Search|https://developer.zendesk.com/rest_api/docs/core/search> interface
229              
230             Parameters
231              
232             =over 4
233              
234             =item query
235              
236             Required. Query string
237              
238             =item sort_by
239              
240             Optional. Default: "updated_at"
241              
242             =item sort_order
243              
244             Optional. Default: "desc"
245              
246             =item size
247              
248             Optional. Integer indicating the number of entries to return. The number returned may be slightly larger (paginating will stop when this number is exceeded).
249              
250             =back
251              
252             Returns array of results.
253              
254             =cut
255             sub search {
256 0     0 1   my ( $self, %params ) = validated_hash(
257             \@_,
258             query => { isa => 'Str' },
259             sort_by => { isa => 'Str', optional => 1, default => 'updated_at' },
260             sort_order => { isa => 'Str', optional => 1, default => 'desc' },
261             size => { isa => 'Int', optional => 1 },
262             );
263 0           $self->log->debug( "Searching: $params{query}" );
264 0           my $path = '/search.json?query=' . uri_encode( $params{query} ) . "&sort_by=$params{sort_by}&sort_order=$params{sort_order}";
265              
266 0           my %request_params = (
267             field => 'results',
268             method => 'get',
269             path => $path,
270             );
271 0 0         $request_params{size} = $params{size} if( $params{size} );
272 0           my @results = $self->_paged_get_request_from_api( %request_params );
273             # TODO - cache results if tickets, users or organizations
274              
275 0           $self->log->debug( "Got " . scalar( @results ) . " results from query" );
276 0           return @results;
277             }
278              
279             =item get_comments_from_ticket
280              
281             Access the L<List Comments|https://developer.zendesk.com/rest_api/docs/core/ticket_comments#list-comments> interface
282              
283             Parameters
284              
285             =over 4
286              
287             =item ticket_id
288              
289             Required. The ticket id to query on.
290              
291             =back
292              
293             Returns an array of comments
294              
295             =cut
296             sub get_comments_from_ticket {
297 0     0 1   my ( $self, %params ) = validated_hash(
298             \@_,
299             ticket_id => { isa => 'Int' },
300             );
301              
302 0           my $path = '/tickets/' . $params{ticket_id} . '/comments.json';
303 0           my @comments = $self->_paged_get_request_from_api(
304             method => 'get',
305             path => $path,
306             field => 'comments',
307             );
308 0           $self->log->debug( "Got " . scalar( @comments ) . " comments" );
309 0           return @comments;
310             }
311              
312             =item download_attachment
313              
314             Download an attachment.
315              
316             Parameters
317              
318             =over 4
319              
320             =item attachment
321              
322             Required. An attachment HashRef as returned as part of a comment.
323              
324             =item dir
325              
326             Directory to download to
327              
328             =item force
329              
330             Force overwrite if item already exists
331              
332             =back
333              
334             Returns path to the downloaded file
335              
336             =cut
337              
338             sub download_attachment {
339 0     0 1   my ( $self, %params ) = validated_hash(
340             \@_,
341             attachment => { isa => 'HashRef' },
342             dir => { isa => 'Str' },
343             force => { isa => 'Bool', optional => 1 },
344             );
345            
346 0           my $target = catfile( $params{dir}, $params{attachment}{file_name} );
347 0           $self->log->debug( "Downloading attachment ($params{attachment}{size} bytes)\n" .
348             " URL: $params{attachment}{content_url}\n target: $target" );
349              
350             # Deal with target already exists
351             # TODO Check if the local size matches the size which we should be downloading
352 0 0         if( -f $target ){
353 0 0         if( $params{force} ){
354 0           $self->log->info( "Target already exist, but downloading again because force enabled: $target" );
355             }else{
356 0           $self->log->info( "Target already exist, not overwriting: $target" );
357 0           return $target;
358             }
359             }
360            
361             # Empty headers so we don't get a http 406 error
362 0           my $headers = $self->default_headers->clone();
363 0           $headers->header( 'Content-Type' => '' );
364 0           $headers->header( 'Accept' => '' );
365             $self->_request_from_api(
366             method => 'GET',
367             uri => $params{attachment}{content_url},
368 0           headers => $headers,
369             fields => { ':content_file' => $target },
370             );
371 0           return $target;
372             }
373              
374             =item add_response_to_ticket
375              
376             Shortcut to L<Updating Tickets|https://developer.zendesk.com/rest_api/docs/core/tickets#updating-tickets> specifically for adding a response.
377              
378             =over 4
379              
380             =item ticket_id
381              
382             Required. Ticket to add response to
383              
384             =item public
385              
386             Optional. Default: 0 (not public). Set to "1" for public response
387              
388             =item response
389              
390             Required. The text to be addded to the ticket as response.
391              
392             =back
393              
394             Returns response HashRef
395              
396             =cut
397             sub add_response_to_ticket {
398 0     0 1   my ( $self, %params ) = validated_hash(
399             \@_,
400             ticket_id => { isa => 'Int' },
401             public => { isa => 'Bool', optional => 1, default => 0 },
402             response => { isa => 'Str' },
403             );
404              
405             my $body = {
406             "ticket" => {
407             "comment" => {
408             "public" => $params{public},
409             "body" => $params{response},
410             }
411             }
412 0           };
413             return $self->update_ticket(
414             body => $body,
415             ticket_id => $params{ticket_id},
416 0           );
417              
418             }
419              
420             =item update_ticket
421              
422             Access L<Updating Tickets|https://developer.zendesk.com/rest_api/docs/core/tickets#updating-tickets> interface.
423              
424             =over 4
425              
426             =item ticket_id
427              
428             Required. Ticket to add response to
429              
430             =item body
431              
432             Required. HashRef of valid parameters - see link above for details.
433              
434             =back
435              
436             Returns response HashRef
437              
438             =cut
439             sub update_ticket {
440 0     0 1   my ( $self, %params ) = validated_hash(
441             \@_,
442             ticket_id => { isa => 'Int' },
443             body => { isa => 'HashRef' },
444             no_cache => { isa => 'Bool', optional => 1 }
445             );
446              
447 0           my $encoded_body = encode_json( $params{body} );
448 0 0         $self->log->trace( "Submitting:\n" . $encoded_body ) if $self->log->is_trace;
449              
450             my $response = $self->_request_from_api(
451             method => 'PUT',
452 0           path => '/tickets/' . $params{ticket_id} . '.json',
453             body => $encoded_body,
454             );
455 0 0         $self->cache_set( 'ticket-' . $params{ticket_id}, $response->{ticket} ) unless( $params{no_cache} );
456 0           return $response;
457             }
458              
459             =item get_ticket
460              
461             Access L<Getting Tickets|https://developer.zendesk.com/rest_api/docs/core/tickets#getting-tickets> interface.
462              
463             =over 4
464              
465             =item ticket_id
466              
467             Required. Ticket to get
468              
469             =item no_cache
470              
471             Disable cache get/set for this operation
472              
473             =back
474              
475             Returns ticket HashRef
476              
477             =cut
478             sub get_ticket {
479 0     0 1   my ( $self, %params ) = validated_hash(
480             \@_,
481             ticket_id => { isa => 'Int' },
482             no_cache => { isa => 'Bool', optional => 1 }
483             );
484            
485             # Try and get the info from the cache
486 0           my $ticket;
487 0 0         $ticket = $self->cache_get( 'ticket-' . $params{ticket_id} ) unless( $params{no_cache} );
488 0 0         if( not $ticket ){
489 0           $self->log->debug( "Ticket info not cached, requesting fresh: $params{ticket_id}" );
490             my $info = $self->_request_from_api(
491             method => 'GET',
492 0           path => '/tickets/' . $params{ticket_id} . '.json',
493             );
494            
495 0 0 0       if( not $info or not $info->{ticket} ){
496 0           $self->log->logdie( "Could not get ticket info for ticket: $params{ticket_id}" );
497             }
498 0           $ticket = $info->{ticket};
499             # Add it to the cache so next time no web request...
500 0 0         $self->cache_set( 'ticket-' . $params{ticket_id}, $ticket ) unless( $params{no_cache} );
501             }
502 0           return $ticket;
503             }
504              
505             =item get_many_tickets
506              
507             Access L<Show Many Organizations|https://developer.zendesk.com/rest_api/docs/core/organizations#show-many-organizations> interface.
508              
509             =over 4
510              
511             =item ticket_ids
512              
513             Required. ArrayRef of ticket ids to get
514              
515             =item no_cache
516              
517             Disable cache get/set for this operation
518              
519             =back
520              
521             Returns an array of ticket HashRefs
522              
523             =cut
524             sub get_many_tickets {
525 0     0 1   my ( $self, %params ) = validated_hash(
526             \@_,
527             ticket_ids => { isa => 'ArrayRef' },
528             no_cache => { isa => 'Bool', optional => 1 }
529             );
530              
531             # First see if we already have any of the ticket in our cache - less to get
532 0           my @tickets;
533             my @get_tickets;
534 0           foreach my $ticket_id ( @{ $params{ticket_ids} } ){
  0            
535 0           my $ticket;
536 0 0         $ticket = $self->cache_get( 'ticket-' . $ticket_id ) unless( $params{no_cache} );
537 0 0         if( $ticket ){
538 0           $self->log->debug( "Found ticket in cache: $ticket_id" );
539 0           push( @tickets, $ticket );
540             }else{
541 0           push( @get_tickets, $ticket_id );
542             }
543             }
544              
545             # If there are any tickets remaining, get these with a bulk request
546 0 0         if( scalar( @get_tickets ) > 0 ){
547 0           $self->log->debug( "Tickets not in cache, requesting fresh: " . join( ',', @get_tickets ) );
548              
549             #limit each request to 100 tickets per api spec
550 0           my @split_tickets;
551 0           push @split_tickets, [ splice @get_tickets, 0, 100 ] while @get_tickets;
552              
553 0           foreach my $cur_tickets ( @split_tickets ) {
554             my @result= $self->_paged_get_request_from_api(
555             field => 'tickets',
556             method => 'get',
557 0           path => '/tickets/show_many.json?ids=' . join( ',', @{ $cur_tickets } ),
  0            
558             );
559 0           foreach( @result ){
560 0           $self->log->debug( "Writing ticket to cache: $_->{id}" );
561 0 0         $self->cache_set( 'ticket-' . $_->{id}, $_ ) unless( $params{no_cache} );
562 0           push( @tickets, $_ );
563             }
564             }
565             }
566 0           return @tickets;
567             }
568              
569             =item get_organization
570              
571             Get a single organization by accessing L<Getting Organizations|https://developer.zendesk.com/rest_api/docs/core/organizations#list-organizations>
572             interface with a single organization_id. The get_many_organizations interface detailed below is more efficient for getting many organizations
573             at once.
574              
575             =over 4
576              
577             =item organization_id
578              
579             Required. Organization id to get
580              
581             =item no_cache
582              
583             Disable cache get/set for this operation
584              
585             =back
586              
587             Returns organization HashRef
588              
589             =cut
590             sub get_organization {
591 0     0 1   my ( $self, %params ) = validated_hash(
592             \@_,
593             organization_id => { isa => 'Int' },
594             no_cache => { isa => 'Bool', optional => 1 }
595             );
596              
597 0           my $organization;
598 0 0         $organization = $self->cache_get( 'organization-' . $params{organization_id} ) unless( $params{no_cache} );
599 0 0         if( not $organization ){
600 0           $self->log->debug( "Organization info not in cache, requesting fresh: $params{organization_id}" );
601             my $info = $self->_request_from_api(
602             method => 'GET',
603 0           path => '/organizations/' . $params{organization_id} . '.json',
604             );
605 0 0 0       if( not $info or not $info->{organization} ){
606 0           $self->log->logdie( "Could not get organization info for organization: $params{organization_id}" );
607             }
608 0           $organization = $info->{organization};
609              
610             # Add it to the cache so next time no web request...
611 0 0         $self->cache_set( 'organization-' . $params{organization_id}, $organization ) unless( $params{no_cache} );
612             }
613 0           return $organization;
614             }
615              
616             =item get_many_organizations
617              
618             =over 4
619              
620             =item organization_ids
621              
622             Required. ArrayRef of organization ids to get
623              
624             =item no_cache
625              
626             Disable cache get/set for this operation
627              
628             =back
629              
630             Returns an array of organization HashRefs
631              
632             =cut
633             #get data about multiple organizations.
634             sub get_many_organizations {
635 0     0 1   my ( $self, %params ) = validated_hash(
636             \@_,
637             organization_ids => { isa => 'ArrayRef' },
638             no_cache => { isa => 'Bool', optional => 1 }
639             );
640              
641             # First see if we already have any of the organizations in our cache - less to get
642 0           my @organizations;
643             my @get_organization_ids;
644 0           foreach my $org_id ( @{ $params{organization_ids} } ){
  0            
645 0           my $organization;
646 0 0         $organization = $self->cache_get( 'organization-' . $org_id ) unless( $params{no_cache} );
647 0 0         if( $organization ){
648 0           $self->log->debug( "Found organization in cache: $org_id" );
649 0           push( @organizations, $organization );
650             }else{
651 0           push( @get_organization_ids, $org_id );
652             }
653             }
654              
655             # If there are any organizations remaining, get these with a single request
656 0 0         if( scalar( @get_organization_ids ) > 0 ){
657 0           $self->log->debug( "Organizations not in cache, requesting fresh: " . join( ',', @get_organization_ids ) );
658 0           my @result= $self->_paged_get_request_from_api(
659             field => 'organizations',
660             method => 'get',
661             path => '/organizations/show_many.json?ids=' . join( ',', @get_organization_ids ),
662             );
663              
664             #if an org is not found it is dropped from the results so we need to check for this and show an warning
665 0 0         if ( $#result != $#get_organization_ids ) {
666 0           foreach my $org_id ( @get_organization_ids ){
667 0           my $org_found = grep { $_->{id} == $org_id } @result;
  0            
668 0 0         unless ( $org_found ) {
669 0           $self->log->warn( "The following organization id was not found in Zendesk: $org_id");
670             }
671             }
672             }
673 0           foreach( @result ){
674 0           $self->log->debug( "Writing organization to cache: $_->{id}" );
675 0 0         $self->cache_set( 'organization-' . $_->{id}, $_ ) unless( $params{no_cache} );
676 0           push( @organizations, $_ );
677             }
678             }
679 0           return @organizations;
680             }
681              
682              
683             =item update_organization
684              
685             Use the L<Update Organization|https://developer.zendesk.com/rest_api/docs/core/organizations#update-organization> interface.
686              
687             =over 4
688              
689             =item organization_id
690              
691             Required. Organization id to update
692              
693             =item details
694              
695             Required. HashRef of the details to be updated.
696              
697             =item no_cache
698              
699             Disable cache set for this operation
700              
701             =back
702              
703             returns the
704             =cut
705             sub update_organization {
706 0     0 1   my ( $self, %params ) = validated_hash(
707             \@_,
708             organization_id => { isa => 'Int' },
709             details => { isa => 'HashRef' },
710             no_cache => { isa => 'Bool', optional => 1 }
711             );
712              
713             my $body = {
714             "organization" =>
715             $params{details}
716 0           };
717              
718 0           my $encoded_body = encode_json( $body );
719 0 0         $self->log->trace( "Submitting:\n" . $encoded_body ) if $self->log->is_trace;
720             my $response = $self->_request_from_api(
721             method => 'PUT',
722 0           path => '/organizations/' . $params{organization_id} . '.json',
723             body => $encoded_body,
724             );
725 0 0 0       if( not $response or not $response->{organization}{id} == $params{organization_id} ){
726 0           $self->log->logdie( "Could not update organization: $params{organization_id}" );
727             }
728              
729 0 0         $self->cache_set( 'organization-' . $params{organization_id}, $response->{organization} ) unless( $params{no_cache} );
730              
731 0           return $response->{organization};
732             }
733              
734             =item list_organization_users
735              
736             Use the L<List Users|https://developer.zendesk.com/rest_api/docs/core/users#list-users> interface.
737              
738             =over 4
739              
740             =item organization_id
741              
742             Required. Organization id to get users from
743              
744             =item no_cache
745              
746             Disable cache set/get for this operation
747              
748             =back
749              
750             Returns array of users
751              
752             =cut
753             sub list_organization_users {
754 0     0 1   my ( $self, %params ) = validated_hash(
755             \@_,
756             organization_id => { isa => 'Int' },
757             no_cache => { isa => 'Bool', optional => 1 }
758             );
759              
760             # for caching we store an array of user ids for each organization and attempt to get these from the cache
761 0 0         my $user_ids_arrayref = $self->cache_get( 'organization-users-ids-' . $params{organization_id} ) unless( $params{no_cache} );
762 0           my @users;
763              
764 0 0         if( $user_ids_arrayref ){
765 0           $self->log->debug( sprintf "Users from cache for organization_id: %u", scalar( @{ $user_ids_arrayref } ), $params{organization_id} );
  0            
766             #get the data for each user in the ticket array
767             my @user_data = $self->get_many_users (
768             user_ids => $user_ids_arrayref,
769             no_cache => $params{no_cache},
770 0           );
771 0           push (@users, @user_data);
772              
773             }else{
774 0           $self->log->debug( "Requesting users fresh for organization: $params{organization_id}" );
775             @users = $self->_paged_get_request_from_api(
776             field => 'users',
777             method => 'get',
778 0           path => '/organizations/' . $params{organization_id} . '/users.json',
779             );
780              
781 0           $user_ids_arrayref = [ map{ $_->{id} } @users ];
  0            
782              
783 0 0         $self->cache_set( 'organization-users-ids-' . $params{organization_id}, $user_ids_arrayref ) unless( $params{no_cache} );
784 0           foreach( @users ){
785 0           $self->log->debug( "Writing ticket to cache: $_->{id}" );
786 0 0         $self->cache_set( 'user-' . $_->{id}, $_ ) unless( $params{no_cache} );
787             }
788             }
789 0           $self->log->debug( sprintf "Got %u users for organization: %u", scalar( @users ), $params{organization_id} );
790              
791 0           return @users;
792             }
793              
794              
795             =item get_many_users
796              
797             Access L<Show Many Users|https://developer.zendesk.com/rest_api/docs/core/users#show-many-users> interface.
798              
799             =over 4
800              
801             =item user_ids
802              
803             Required. ArrayRef of user ids to get
804              
805             =item no_cache
806              
807             Disable cache get/set for this operation
808              
809             =back
810              
811             Returns an array of user HashRefs
812              
813             =cut
814              
815             sub get_many_users {
816 0     0 1   my ( $self, %params ) = validated_hash(
817             \@_,
818             user_ids => { isa => 'ArrayRef' },
819             no_cache => { isa => 'Bool', optional => 1 }
820             );
821              
822             # First see if we already have any of the user in our cache - less to get
823 0           my @users;
824             my @get_users;
825 0           foreach my $user_id ( @{ $params{user_ids} } ){
  0            
826 0           my $user;
827 0 0         $user = $self->cache_get( 'user-' . $user_id ) unless( $params{no_cache} );
828 0 0         if( $user ){
829 0           $self->log->debug( "Found user in cache: $user_id" );
830 0           push( @users, $user );
831             }else{
832 0           push( @get_users, $user_id );
833             }
834             }
835              
836             # If there are any users remaining, get these with a bulk request
837 0 0         if( scalar( @get_users ) > 0 ){
838 0           $self->log->debug( "Users not in cache, requesting fresh: " . join( ',', @get_users ) );
839              
840             #limit each request to 100 users per api spec
841 0           my @split_users;
842 0           push @split_users, [ splice @get_users, 0, 100 ] while @get_users;
843              
844 0           foreach my $cur_users (@split_users) {
845             my @result= $self->_paged_get_request_from_api(
846             field => 'users',
847             method => 'get',
848 0           path => '/users/show_many.json?ids=' . join( ',', @{ $cur_users } ),
  0            
849             );
850 0           foreach( @result ){
851 0           $self->log->debug( "Writing user to cache: $_->{id}" );
852 0 0         $self->cache_set( 'user-' . $_->{id}, $_ ) unless( $params{no_cache} );
853 0           push( @users, $_ );
854             }
855             }
856             }
857 0           return @users;
858             }
859              
860              
861             =item update_user
862              
863             Use the L<Update User|https://developer.zendesk.com/rest_api/docs/core/users#update-user> interface.
864              
865             =over 4
866              
867             =item user_id
868              
869             Required. User id to update
870              
871             =item details
872              
873             Required. HashRef of the details to be updated.
874              
875             =item no_cache
876              
877             Disable cache set for this operation
878              
879             =back
880              
881             returns the
882             =cut
883             sub update_user {
884 0     0 1   my ( $self, %params ) = validated_hash(
885             \@_,
886             user_id => { isa => 'Int' },
887             details => { isa => 'HashRef' },
888             no_cache => { isa => 'Bool', optional => 1 }
889             );
890              
891             my $body = {
892             "user" => $params{details}
893 0           };
894              
895 0           my $encoded_body = encode_json( $body );
896 0 0         $self->log->trace( "Submitting:\n" . $encoded_body ) if $self->log->is_trace;
897             my $response = $self->_request_from_api(
898             method => 'PUT',
899 0           path => '/users/' . $params{user_id} . '.json',
900             body => $encoded_body,
901             );
902              
903 0 0 0       if( not $response or not $response->{user}{id} == $params{user_id} ){
904 0           $self->log->logdie( "Could not update user: $params{user_id}" );
905             }
906              
907 0 0         $self->cache_set( 'user-' . $params{user_id}, $response->{user} ) unless( $params{no_cache} );
908              
909 0           return $response->{user};
910             }
911              
912             =item list_user_assigned_tickets
913              
914             Use the L<List assigned tickets|https://developer.zendesk.com/rest_api/docs/core/tickets#listing-tickets> interface.
915              
916             =over 4
917              
918             =item user_id
919              
920             Required. User id to get assigned tickets from
921              
922             =item no_cache
923              
924             Disable cache set/get for this operation
925              
926             =back
927              
928             Returns array of tickets
929              
930             =cut
931             sub list_user_assigned_tickets {
932 0     0 1   my ( $self, %params ) = validated_hash(
933             \@_,
934             user_id => { isa => 'Int' },
935             no_cache => { isa => 'Bool', optional => 1 }
936             );
937              
938             #for caching we store an array of ticket ids under the user, then look at the ticket cache
939 0 0         my $ticket_ids_arrayref = $self->cache_get( 'user-assigned-tickets-ids-' . $params{user_id} ) unless( $params{no_cache} );
940 0           my @tickets;
941 0 0         if( $ticket_ids_arrayref ){
942 0           $self->log->debug( sprintf "Tickets from cache for user: %u", scalar( @{ $ticket_ids_arrayref } ), $params{user_id} );
  0            
943             #get the data for each ticket in the ticket array
944             @tickets = $self->get_many_tickets (
945             ticket_ids => $ticket_ids_arrayref,
946             no_cache => $params{no_cache},
947 0           );
948             }else{
949 0           $self->log->debug( "Requesting tickets fresh for user: $params{user_id}" );
950             @tickets = $self->_paged_get_request_from_api(
951             field => 'tickets',
952             method => 'get',
953 0           path => '/users/' . $params{user_id} . '/tickets/assigned.json',
954             );
955 0           $ticket_ids_arrayref = [ map{ $_->{id} } @tickets ];
  0            
956              
957 0 0         $self->cache_set( 'user-assigned-tickets-ids-' . $params{user_id}, $ticket_ids_arrayref ) unless( $params{no_cache} );
958              
959 0           foreach( @tickets ){
960 0           $self->log->debug( "Writing ticket to cache: $_->{id}" );
961 0 0         $self->cache_set( 'ticket-' . $_->{id}, $_ ) unless( $params{no_cache} );
962             }
963             }
964 0           $self->log->debug( sprintf "Got %u assigned tickets for user: %u", scalar( @tickets ), $params{user_id} );
965              
966 0           return @tickets;
967             }
968              
969              
970             =item clear_cache_object_id
971              
972             Clears an object from the cache.
973              
974             =over 4
975              
976             =item user_id
977              
978             Required. Object id to clear from the cache.
979              
980             =back
981              
982             Returns whether cache_del was successful or not
983              
984             =cut
985             sub clear_cache_object_id {
986 0     0 1   my ( $self, %params ) = validated_hash(
987             \@_,
988             object_id => { isa => 'Str' }
989             );
990              
991 0           $self->log->debug( "Clearing cache id: $params{object_id}" );
992 0           my $foo = $self->cache_del( $params{object_id} );
993              
994 0           return $foo;
995             }
996              
997             sub _paged_get_request_from_api {
998 0     0     my ( $self, %params ) = validated_hash(
999             \@_,
1000             method => { isa => 'Str' },
1001             path => { isa => 'Str' },
1002             field => { isa => 'Str' },
1003             size => { isa => 'Int', optional => 1 },
1004             body => { isa => 'Str', optional => 1 },
1005             );
1006 0           my @results;
1007 0           my $page = 1;
1008 0           my $response = undef;
1009             do{
1010             $response = $self->_request_from_api(
1011             method => 'GET',
1012 0 0         path => $params{path} . ( $params{path} =~ m/\?/ ? '&' : '?' ) . 'page=' . $page,
1013             );
1014 0           push( @results, @{ $response->{$params{field} } } );
  0            
1015 0           $page++;
1016 0   0       }while( $response->{next_page} and ( not $params{size} or scalar( @results ) < $params{size} ) );
      0        
1017              
1018 0           return @results;
1019             }
1020              
1021              
1022             sub _request_from_api {
1023 0     0     my ( $self, %params ) = validated_hash(
1024             \@_,
1025             method => { isa => 'Str' },
1026             path => { isa => 'Str', optional => 1 },
1027             uri => { isa => 'Str', optional => 1 },
1028             body => { isa => 'Str', optional => 1 },
1029             headers => { isa => 'HTTP::Headers', optional => 1 },
1030             fields => { isa => 'HashRef', optional => 1 },
1031            
1032             );
1033 0           my $url;
1034 0 0         if( $params{uri} ){
    0          
1035 0           $url = $params{uri};
1036             }elsif( $params{path} ){
1037 0           $url = $self->zendesk_api_url . $params{path};
1038             }else{
1039 0           $self->log->logdie( "Cannot request without either a path or uri" );
1040             }
1041              
1042             my $request = HTTP::Request->new(
1043             $params{method},
1044             $url,
1045 0   0       $params{headers} || $self->default_headers,
1046             );
1047 0 0         $request->content( $params{body} ) if( $params{body} );
1048              
1049 0           $self->log->debug( "Requesting from Zendesk: " . $request->uri );
1050 0 0         $self->log->trace( "Request:\n" . Dump( $request ) ) if $self->log->is_trace;
1051              
1052 0           my $response;
1053 0           my $retry = 1;
1054 0           my $try_count = 0;
1055 0           do{
1056 0           my $retry_delay = $self->default_backoff;
1057 0           $try_count++;
1058             # Fields are a special use-case for GET requests:
1059             # https://metacpan.org/pod/LWP::UserAgent#ua-get-url-field_name-value
1060 0 0         if( $params{fields} ){
1061 0 0         if( $request->method ne 'GET' ){
1062 0           $self->log->logdie( 'Cannot use fields unless the request method is GET' );
1063             }
1064 0           my %fields = %{ $params{fields} };
  0            
1065 0           my $headers = $request->headers();
1066 0           foreach( keys( %{ $headers } ) ){
  0            
1067 0           $fields{$_} = $headers->{$_};
1068             }
1069 0           $self->log->trace( "Fields:\n" . Dump( \%fields ) );
1070 0           $response = $self->user_agent->get(
1071             $request->uri(),
1072             %fields,
1073             );
1074             }else{
1075 0           $response = $self->user_agent->request( $request );
1076             }
1077 0 0         if( $response->is_success ){
1078 0           $retry = 0;
1079             }else{
1080 0 0         if( grep{ $_ == $response->code } @{ $self->retry_on_status } ){
  0            
  0            
1081 0 0         if( $response->code == 429 ){
1082             # if retry-after header exists and has valid data use this for backoff time
1083 0 0 0       if( $response->header( 'Retry-After' ) and $response->header('Retry-After') =~ /^\d+$/ ) {
1084 0           $retry_delay = $response->header('Retry-After');
1085             }
1086 0           $self->log->warn( sprintf( "Received a %u (Too Many Requests) response with 'Retry-After' header... going to backoff and retry in %u seconds!",
1087             $response->code,
1088             $retry_delay,
1089             ) );
1090             }else{
1091 0           $self->log->warn( sprintf( "Received a %u: %s ... going to backoff and retry in %u seconds!",
1092             $response->code,
1093             $response->decoded_content,
1094             $retry_delay
1095             ) );
1096             }
1097             }else{
1098 0           $retry = 0;
1099             }
1100              
1101 0 0         if( $retry == 1 ){
1102 0 0 0       if( not $self->max_tries or $self->max_tries > $try_count ){
1103 0           $self->log->debug( sprintf( "Try %u failed... sleeping %u before next attempt", $try_count, $retry_delay ) );
1104 0           sleep( $retry_delay );
1105             }else{
1106 0           $self->log->debug( sprintf( "Try %u failed... exceeded max_tries (%u) so not going to retry", $try_count, $self->max_tries ) );
1107 0           $retry = 0;
1108             }
1109             }
1110             }
1111             }while( $retry );
1112              
1113 0 0         $self->log->trace( "Last zendesk response:\n", Dump( $response ) ) if $self->log->is_trace;
1114 0 0         if( not $response->is_success ){
1115 0           $self->log->logdie( "Zendesk Error: http status:". $response->code .' '. $response->message . ' Content: ' . $response->content);
1116             }
1117 0 0         if( $response->decoded_content ){
1118 0           return decode_json( encode( 'utf8', $response->decoded_content ) );
1119             }
1120 0           return;
1121             }
1122              
1123              
1124             1;
1125              
1126             =back
1127              
1128             =head1 COPYRIGHT
1129              
1130             Copyright 2015, Robin Clarke
1131              
1132             =head1 AUTHOR
1133              
1134             Robin Clarke <robin@robinclarke.net>
1135              
1136             Jeremy Falling <projects@falling.se>
1137