File Coverage

blib/lib/WebService/Zendesk.pm
Criterion Covered Total %
statement 39 302 12.9
branch 0 114 0.0
condition 0 27 0.0
subroutine 13 35 37.1
pod 17 17 100.0
total 69 495 13.9


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