File Coverage

blib/lib/API/Zendesk.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


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