File Coverage

blib/lib/API/Zendesk.pm
Criterion Covered Total %
statement 33 275 12.0
branch 0 104 0.0
condition 0 33 0.0
subroutine 11 32 34.3
pod 17 17 100.0
total 61 461 13.2


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