Kayako-RestAPI/lib/Kayako/RestAPI.pm
package Kayako::RestAPI;
$Kayako::RestAPI::VERSION = '0.07';
# ABSTRACT: Perl library for working with L<Kayako REST API|https://kayako.atlassian.net/wiki/display/DEV/Kayako+REST+API>. Tested with
use common::sense;
use Mojo::UserAgent;
use Digest::SHA qw(hmac_sha256_base64);
use File::Slurp;
use XML::LibXML::Simple;
use utf8;
use Data::Dumper;
my $xs = XML::LibXML::Simple->new();
say "Load local lib";
sub new {
my $class = shift;
my $o = {};
$o->{auth_hash} = shift;
$o->{xml2json_options} = shift;
$o->{xml2json_options} =
{ content_key => 'text', pretty => 1, attribute_prefix => 'attr_' }
if not defined $o->{xml2json_options};
$o->{ua} = Mojo::UserAgent->new;
bless $o, $class;
return $o;
}
sub _prepare_request {
my $self = shift;
my @alphabet = ( ( 'a' .. 'z' ), ( 'A' .. 'Z' ), 0 .. 9 );
my $len = 10;
my $salt = join '', map { $alphabet[ rand(@alphabet) ] } 1 .. $len;
my $digest =
hmac_sha256_base64( $salt, $self->{auth_hash}{secret_key} ) . '=';
my $hash = {
"apikey" => $self->{auth_hash}{api_key},
"salt" => $salt,
"signature" => $digest
};
return $hash;
}
sub xml2obj {
my ( $self, $xml ) = @_;
local $SIG{__DIE__} = sub {
my $die_str = '';
if ( ref $_[0] eq 'XML::LibXML::Error' ) {
$die_str = "XML::LibXML::Error\n";
$die_str .= "Message : " . $_[0]->message();
}
else {
$die_str = $_[0] . "\n";
}
$die_str .= "Last res: \n" . $self->last_res;
die $die_str;
};
$xs->XMLin($xml);
}
# abstract api GET/POST/PUT/DELETE _query. return plain xml
sub _query {
my ( $self, $method, $route, $params ) = @_;
$params->{e} = $route;
my %hash = ( %{ $self->_prepare_request }, %$params );
my $res =
$self->{ua}->$method( $self->{auth_hash}{api_url} => form => \%hash )
->res;
if ( $res->is_success ) {
my $xml = $res->body;
$self->{last_res} = $res;
# $xml =~ s/([\x00-\x09]+)|([\x0B-\x1F]+)//g;
return $xml;
}
elsif ( $res->code == 301 ) {
die "Moved Permanently: " . $res->headers->location;
}
else {
die "HTTP Error: " . $res->code;
}
}
sub last_res {
shift->{last_res};
}
sub get {
my ( $self, $route, $params ) = @_;
$self->_query( 'get', $route, $params );
}
sub put {
my ( $self, $route, $params ) = @_;
$self->_query( 'put', $route, $params );
}
sub post {
my ( $self, $route, $params ) = @_;
$self->_query( 'post', $route, $params );
}
sub delete {
my ( $self, $route, $params ) = @_;
$self->_query( 'delete', $route, $params );
}
sub get_hash {
my ( $self, $route, $params ) = @_;
my $xml = $self->get( $route, $params );
return $self->xml2obj($xml);
}
sub get_ticket_xml {
my ( $self, $ticket_id, $params ) = @_;
return $self->get( "/Tickets/Ticket/" . $ticket_id . "/", $params );
}
sub get_ticket_hash {
my ( $self, $ticket_id ) = @_;
my $xml = $self->get_ticket_xml($ticket_id);
my $hash = {};
if ( $xml eq 'Ticket not Found' || $xml eq '' ) {
die "Ticket not Found";
# $hash->{'ticket_id'} = $ticket_id;
# $hash->{'is_found'} = 0;
}
else {
$hash = $self->xml2obj($xml)->{ticket};
}
return $hash;
}
sub change_ticket_owner {
my ( $self, $ticket_id, $new_owner_id ) = @_;
my $content_key = $self->{xml2json_options}{content_key};
my $old_data = $self->get_ticket_hash($ticket_id);
$self->put(
"/Tickets/Ticket/" . $ticket_id . "/",
{ ownerstaffid => $new_owner_id }
);
my $new_data = $self->get_ticket_hash($ticket_id);
my $res = {
old => {
ownerstaffid => $old_data->{ownerstaffid}{$content_key},
ownerstaffname => $old_data->{ownerstaffname}{$content_key}
},
new => {
ownerstaffid => $new_data->{ownerstaffid}{$content_key},
ownerstaffname => $new_data->{ownerstaffname}{$content_key}
}
};
return $res;
}
sub make_unassigned {
my ( $self, $ticket_id ) = @_;
$self->change_ticket_owner( $ticket_id, 0 );
}
sub create_ticket {
my ( $self, $params ) = @_;
my $xml = $self->post( '/Tickets/Ticket/', $params );
$self->xml2obj($xml);
}
sub filter_fields {
my ( $self, $a ) = @_; # array of hashes
my $key = $self->{xml2json_options}{content_key};
for my $j (@$a) {
$j = { map { $_ => $j->{$_}{$key} }
grep { exists $j->{$_} } qw/id title module/ };
}
return $a;
}
# Convert nested hash with id keys into array of hashes. For compatibility with old API
# Usage is same as filter_fields
sub _postprocess_libxml {
my ( $self, $hash ) = @_; # array of hashes
my @res;
for my $k ( sort keys %$hash ) {
# my $j = { map { $_ => $hash->{$k} } grep { exists $hash->{$k} } qw/id title module fullname/ };
# warn "Element:".Dumper $j;
my $j = {
id => $k,
title => $hash->{$k}{title},
module => $hash->{$k}{module}
};
push @res, $j;
# push @res, $j;
}
return \@res;
}
sub get_departements {
my $self = shift;
my $xml = $self->get('/Base/Department/');
$self->xml2obj($xml)->{department};
}
sub get_departements_old {
my $self = shift;
$self->_postprocess_libxml( $self->get_departements );
}
sub get_ticket_statuses {
my $self = shift;
my $xml = $self->get('/Tickets/TicketStatus/');
$self->xml2obj($xml)->{ticketstatus};
}
sub get_ticket_statuses_old {
my $self = shift;
$self->_postprocess_libxml( $self->get_ticket_statuses );
}
sub get_ticket_priorities {
my $self = shift;
my $xml = $self->get('/Tickets/TicketPriority/');
$self->xml2obj($xml)->{ticketpriority};
}
sub get_ticket_priorities_old {
my $self = shift;
$self->_postprocess_libxml( $self->get_ticket_priorities );
}
sub get_ticket_types {
my $self = shift;
my $xml = $self->get('/Tickets/TicketType/');
$self->xml2obj($xml)->{tickettype}; # array
}
sub get_ticket_types_old {
my $self = shift;
$self->_postprocess_libxml( $self->get_ticket_types );
}
sub get_staff {
my $self = shift;
my $xml = $self->get('/Base/Staff/');
$self->xml2obj($xml)->{staff}; # array
}
sub get_staff_old {
my $self = shift;
$self->_postprocess_libxml( $self->get_staff );
}
# HTTP mocks
# perl -Ilib -MData::Dumper -MKayako::RestAPI -E 'print Dumper Kayako::RestAPI::_samples();'
sub _samples {
return [
{
method => 'get',
route => '/Tickets/Ticket/1000/',
sample_file => 'ticket.xml',
params => {}
},
{
method => 'get',
route => '/Base/Department/',
sample_file => 'departments.xml'
},
{
method => 'get',
route => '/Tickets/TicketStatus/',
sample_file => 'ticket_status.xml'
},
{
method => 'get',
route => '/Tickets/TicketPriority/',
sample_file => 'ticket_priority.xml'
},
{
method => 'get',
route => '/Tickets/TicketType/',
sample_file => 'ticket_type.xml'
},
{
method => 'get',
route => '/Base/Staff/',
sample_file => 'staff.xml'
}
];
}
# generate ethalon data to t/lib/Kayako/samples
# perl -Ilib -MData::Dumper -MKayako::RestAPI -E 'print Dumper Kayako::RestAPI::_generate_t_samples();'
sub _generate_t_samples {
my @samples = @{ __PACKAGE__->_samples };
my $k = Kayako::RestAPI->new(
{
api_url => '',
api_key => '',
secret_key => ''
}
);
for my $s (@samples) {
my $data = $k->_query( $s->{method}, $s->{route} );
write_file( 't/lib/Kayako/samples/' . $s->{sample_file},
{ binmode => ':raw' }, $data );
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Kayako::RestAPI - Perl library for working with L<Kayako REST API|https://kayako.atlassian.net/wiki/display/DEV/Kayako+REST+API>. Tested with
=head1 VERSION
version 0.07
=head1 SYNOPSIS
use Kayako::RestAPI;
my $kayako_api = Kayako::RestAPI->new({
"api_url" => '',
"api_key" => '',
"secret_key" => ''
}, {
content_key => 'text',
pretty => 1,
attribute_prefix => 'attr_'
});
$kayako_api->get($route, $params); # $params is optional hashref
$kayako_api->post($route, $params);
$kayako_api->put($route, $params);
$kayako_api->delete($route, $params);
$kayako_api->get('/Base/Department'); # list of all departements
my $ticket_id = 1000;
$kayako_api->get_ticket_xml($ticket_id);
$kayako_api->get_ticket_hash($ticket_id);
$kayako_api->create_ticket({
subject => 'Test ticket',
fullname => 'Pavel Serikov',
email => 'someuser@gmail.com',
contents => 'Hello, world!',
departmentid => 5,
ticketstatusid => 4,
ticketpriorityid => 1,
tickettypeid => 5,
autouserid => 1
});
You can test you controller with L<API Test Controller|https://kayako.atlassian.net/wiki/display/DEV/API+Test+Controller>
Attention: since version 0.06 (migration from XML::XML2JSON to XML::LibXML::Simple) response structure of following methods was changed from array to hash
get_ticket_hash
get_departements
get_ticket_statuses
get_ticket_priorities
get_ticket_types
If you need to use old structure please add _old suffix to method ;)
WORK UNDER THIS MODULE IS IN PROGRESS, HELP WANTED, ESPECIALLY FOR WRITING DOCS
=head1 METHODS
=head2 xml2obj
Convert xml API response to hash using XML::XML2JSON::xml2obj method
my $xml = $kayako_api->get('/Some/Endpoint');
my $hash = $kayako_api->xml2obj($xml);
Can potentially crash is returned xml isn't valid (when XML::XML2JSON dies)
=head2 last_res
Get latest result from user agent. For debug purpose
=head2 get_hash
Wrapper under abstract GET API query, return hash
$kayako_api->get_hash('/Some/API/Endpoint');
Can potentially crash is returned xml isn't valid (when XML::XML2JSON dies)
=head2 get_ticket_xml
Get info about ticket in native XML
$kayako_api->get_ticket_xml($ticket_id);
=head2 get_ticket_hash
$kayako_api->get_ticket_hash($ticket_id);
=head2 change_ticket_owner
$kayako_api->change_ticket_owner($ticket_id, $new_owner_id);
=head2 make_unassigned
$kayako_api->make_unassigned($ticket_id);
equalent to
$kayako_api->change_ticket_owner($ticket_id, 0);
=head2 create_ticket
Check a list of required arguments here: L<https://kayako.atlassian.net/wiki/display/DEV/REST+-+Ticket#REST-Ticket-POST/Tickets/Ticket>
=head2 filter_fields
THIS METHOD LEFT HERE FOR COMPATIBILITY AND WILL BE REMOVED IN FUTURE RELEASES
Filter fields of API request result and trim content_key added by XML::XML2JSON
By default leave only id, title and module fields
my $arrayref = $kayako_api->get_hash('/Some/API/Endpoint');
$kayako_api->filter_fields($arrayref);
=head2 get_departements_old
$kayako_api->get_departements();
Return an arrayref of hashes with title, module and id keys like
[
{
'module' => 'tickets',
'title' => 'Hard drives department',
'id' => '5'
},
{
'id' => '6',
'module' => 'tickets',
'title' => 'Flash drives department'
}
]
API endpoint is /Base/Department/
=head2 get_ticket_statuses_old
$kayako_api->get_ticket_statuses();
Return an arrayref of hashes with title and id keys like
[
{
'title' => 'In progress',
'id' => '1'
},
{
'title' => 'Closed',
'id' => '3'
},
{
'id' => '4',
'title' => 'New'
}
]
API endpoint is /Tickets/TicketStatus/
=head2 get_ticket_priorities_old
$kayako_api->get_ticket_priorities();
Return an arrayref of hashes with title and id keys like
[
{
'title' => 'Normal',
'id' => '1'
},
{
'id' => '3',
'title' => 'Urgent'
},
]
API endpoint is /Tickets/TicketPriority/
=head2 get_ticket_types_old
$kayako_api->get_ticket_types();
Return an arrayref of hashes with title and id keys like
[
{
'id' => '1',
'title' => 'Case'
},
{
'id' => '3',
'title' => 'Bug'
},
{
'id' => '5',
'title' => 'Feedback'
}
];
API endpoint is /Tickets/TicketType/
=head2 get_staff_old
$kayako_api->get_staff();
Return an arrayref of hashes with keys like firstname, lastname, username etc.
E.g.
[
{ ... },
{
'id' => { 'text' => '12' },
'firstname' => { 'text' => 'Pavel' },
'email' => { 'text' => 'pavelsr@cpan.org' },
'lastname' => { 'text' => 'Serikov' },
'enabledst' => { 'text' => '0'},
'username' => { 'text' => 'pavelsr' },
'isenabled' => { 'text' => '1' },
'staffgroupid' => { 'text' => '4' },
'greeting' => {},
'timezone' => {},
'designation' => { 'text' => 'TS' },
'mobilenumber' => {},
'signature' => {},
'fullname' => { 'text' => 'Pavel Serikov' }
}
]
API endpoint is /Base/Staff/
=head1 AUTHOR
Pavel Serikov <pavelsr@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Pavel Serikov.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut