File Coverage

blib/lib/Net/ThirtySevenSignals/Highrise.pm
Criterion Covered Total %
statement 31 33 93.9
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 42 44 95.4


line stmt bran cond sub pod time code
1             # $Id: Backpack.pm 29 2008-07-13 10:35:47Z dave $
2              
3             =head1 NAME
4              
5             Net::ThirtySevenSignals::Highrise - Perl extension for talking 37Signals' Highrise API
6              
7             =head1 SYNOPSIS
8              
9             use Net::ThirtySevenSignals::Highrise;
10              
11             my $hr = Net::ThirtySevenSignals::Highrise(
12             user => $your_highrise_url_prefix,
13             token => $your_highrise_api_token,
14             ssl => $use_ssl);
15              
16             # Fill out a Perl data structure with information about
17             my $people = $hr->list_all_people;
18              
19             # return a hashref of people
20             my $people = $hr->people_list_by_criteria(email => 'danny@example.com');
21              
22              
23             =head1 DESCRIPTION
24              
25             Net::ThirtySevenSignals::Highrise provides a thin Perl wrapper around the Highrise API
26             (L). Currently it only implements a very few
27             API points.
28              
29             The API is unstable at this time.
30              
31              
32             =head2 Getting Started
33              
34             In order to use the Highrise API, you'll need to have a Highrise
35             API token. And in order to get one of those, you'll need a Highrise
36             account. But then again, the API will be pretty useless to you if
37             you don't have a Highrise account to manipulate with it.
38              
39             You can get a Highrise account from L.
40              
41             =head2 Highrise API
42              
43             The Highrise API is based on XML over HTTP. You send an XML message
44             over HTTP to the Highrise server and the server sends a response to
45             you which is also in XML. The format of the various XML requests and
46             responses are defined at L
47              
48             This module removes the need to deal with any XML. You create an
49             object to talk to the Highrise server and call methods on that object
50             to manipulate your data. The values returned from Highrise
51             are converted to Perl data structures before being handed back to
52             you (although it is also possible to get back the raw XML).
53              
54             =head1 XML
55              
56             To get the XML back from the API, you can pass the parameter C
57             =E 0> to the C function.
58              
59             =cut
60              
61             package Net::ThirtySevenSignals::Highrise;
62             {
63             $Net::ThirtySevenSignals::Highrise::VERSION = '0.03';
64             }
65              
66 1     1   76640 use 5.006;
  1         3  
  1         36  
67 1     1   980 use utf8;
  1         11  
  1         6  
68 1     1   40 use strict qw(vars subs);
  1         7  
  1         33  
69 1     1   5 no warnings;
  1         1  
  1         47  
70 1         170 use warnings qw(FATAL closed threads internal debugging pack malloc
71             portable prototype inplace io pipe unpack regexp
72             deprecated glob digit printf layer
73 1     1   4 reserved taint closure semicolon);
  1         2  
74 1     1   5 no warnings qw(exec newline unopened);
  1         2  
  1         38  
75              
76 1     1   5 use Carp;
  1         2  
  1         80  
77 1     1   3286 use LWP::UserAgent;
  1         124285  
  1         43  
78 1     1   13 use URI;
  1         2  
  1         28  
79 1     1   6 use HTTP::Request;
  1         3  
  1         29  
80 1     1   584 use XML::Simple;
  0            
  0            
81             # use Log::Log4perl qw( get_logger );
82              
83             # my $logger = get_logger();
84              
85              
86             my %data = (
87             'people_list_all' =>
88             {
89             url => '/people.xml',
90             return_key => 'person',
91             },
92            
93             'person_get' =>
94             {
95             url => '/people/[P:id].xml',
96             },
97              
98              
99             tag_add =>{
100             url => '/[P:subjectType]/[P:subjectID]/tags.xml',
101             method => "POST",
102             req => '[P:tagName]'
103             },
104              
105             'person_create' =>
106             {
107             url => '/people.xml?reload=true',
108             req => '
109            
110             [P:firstName]
111             [P:lastName]
112             [P:companyName]
113            
114              
115             [% IF P:emailAddress %]
116            
117            
118            
[P:emailAddress]
119             Work
120            
121            
122             [% END %]
123              
124              
125             [% IF P:workPhone %]
126            
127            
128             [P:workPhone]
129             Work
130            
131            
132             [% END %]
133              
134            
135             '
136             },
137              
138             'person_destroy' =>
139             {
140             method => "DELETE",
141             url => '/people/[P:id].xml',
142             },
143              
144             'people_list_by_criteria' =>
145             {
146             url => '/people/search.xml',
147             return_key => 'person',
148             },
149              
150             'tags_list_all' =>
151             {
152             url => '/tags.xml',
153             return_key=>'tag',
154             },
155            
156            
157             'tags_list_for_subject' =>
158             {
159             url => '/[P:subjectType]/[P:subjectID]/tags.xml',
160             required_params => [qw(subjectType subjectID )],
161             return_key => 'tag',
162             },
163              
164             'create_page' =>
165             {
166             url => '/ws/pages/new',
167             req => '
168             [S:token]
169            
170             [P:title]
171             [P:description]
172            
173             '
174             },
175             'show_page' =>
176             {
177             url => '/ws/page/[P:id]',
178             req => '
179             [S:token]
180             '
181             },
182             'destroy_page' =>
183             {
184             url => '/ws/page/[P:id]/destroy',
185             req => '
186             [S:token]
187             '
188             },
189             'update_title' =>
190             {
191             url => '/ws/page/[P:id]/update_title',
192             req => '
193             [S:token]
194             [P:title]
195             '
196             },
197             update_body =>
198             {
199             url => '/ws/page/[P:id]/update_body',
200             req => '
201             [S:token]
202             [P:description]
203             '
204             },
205             'duplicate_page' =>
206             {
207             url => '/ws/page/[P:id]/duplicate',
208             req => '
209             [S:token]
210             '
211             },
212             'link_page' =>
213             {
214             url => '/ws/page/[P:to_page]/link',
215             req => '
216             [S:token]
217             [P:link_page]
218             '
219             },
220             'unlink_page' =>
221             {
222             url => '/ws/page/[P:from_page]/link',
223             req => '
224             [S:token]
225             [P:link_page]
226             '
227             },
228             'share_people' =>
229             {
230             url => '/ws/page/[P:id]/share',
231             req => '
232             [S:token]
233            
234             [P:people]
235            
236             '
237             },
238             'make_page_public' =>
239             {
240             url => '/ws/page/[P:id]/share',
241             req => '
242             [S:token]
243            
244             [P:public]
245            
246             '
247             },
248             'unshare_friend_page' =>
249             {
250             url => '/ws/page/[P:id]/unshare_friend_page',
251             req => '
252             [S:token]
253             '
254             },
255             'email_page' =>
256             {
257             url => '/ws/page/[P:id]/email',
258             req => '
259             [S:token]
260             '
261             },
262             'list_all_items' =>
263             {
264             url => '/ws/page/[P:page_id]/items/list',
265             req => '
266             [S:token]
267             '
268             },
269             'create_item' =>
270             {
271             url => '/ws/page/[P:page_id]/items/add',
272             req => '
273             [S:token]
274            
275             [P:item]
276            
277             '
278             },
279             'update_item' =>
280             {
281             url => '/ws/page/[P:page_id]/items/update/[P:id]',
282             req => '
283             [S:token]
284            
285             [P:item]
286            
287             '
288             },
289             'toggle_item' =>
290             {
291             url => '/ws/page/[P:page_id]/items/toggle/[P:id]',
292             req => '
293             [S:token]
294             '
295             },
296             'destroy_item' =>
297             {
298             url => '/ws/page/[P:page_id]/items/destroy/[P:id]',
299             req => '
300             [S:token]
301             '
302             },
303             'move_item' =>
304             {
305             url => '/ws/page/[P:page_id]/items/move/[P:id]',
306             req => '
307             [S:token]
308             [P:direction]
309             '
310             },
311             'list_all_notes' =>
312             {
313             url => '/ws/page/[P:page_id]/notes/list',
314             req => '
315             [S:token]
316             '
317             },
318             'create_note' =>
319             {
320             url => '/ws/page/[P:page_id]/notes/create',
321             req => '
322             [S:token]
323            
324             [P:title]
325             [P:body]
326            
327             '
328             },
329             'update_note' =>
330             {
331             url => '/ws/page/[P:page_id]/notes/update/[P:id]',
332             req => '
333             [S:token]
334            
335             [P:title]
336             [P:body]
337            
338             '
339             },
340             'destroy_note' =>
341             {
342             url => '/ws/page/[P:page_id]/notes/destroy/[P:id]',
343             req => '
344             [S:token]
345             '
346             },
347             'get_tag_pages' =>
348             {
349             url => '/ws/tags/[P:page_id]',
350             req => '
351             [S:token]
352             '
353             },
354             'set_page_tags' =>
355             {
356             url => '/ws/page/[P:page_id]/tags/tag',
357             req => '
358             [S:token]
359             [P:tags]
360             '
361             },
362             'upcoming_reminders' =>
363             {
364             url => '/ws/reminders',
365             req => '
366             [S:token]
367             '
368             },
369             'create_reminder' =>
370             {
371             url => '/ws/reminders/create',
372             req => '
373             [S:token]
374            
375             [P:content]
376             [P:remind_at]
377            
378             '
379             },
380             'update_reminder' =>
381             {
382             url => '/ws/reminders/update/[P:id]',
383             req => '
384             [S:token]
385            
386             [P:content]
387             [P:remind_at]
388            
389             '
390             },
391             'destroy_reminder' =>
392             {
393             url => '/ws/reminders/destroy/[P:id]',
394             req => '
395             [S:token]
396             '
397             },
398             'list_all_emails' =>
399             {
400             url => '/ws/page/[P:page_id]/emails/list',
401             req => '
402             [S:token]
403             '
404             },
405             'show_email' =>
406             {
407             url => '/ws/page/[P:page_id]/emails/show/[P:id]',
408             req => '
409             [S:token]
410             '
411             },
412             'destroy_email' =>
413             {
414             url => '/ws/page/[P:page_id]/emails/destroy/[P:id]',
415             req => '
416             [S:token]
417             '
418             },
419             'export' =>
420             {
421             url => '/ws/account/export',
422             req => '
423             [S:token]
424             '
425             },
426             'list_all_lists' =>
427             {
428             url => '/ws/page/[P:page_id]/lists/list',
429             req => '
430             [S:token]
431             '
432             },
433             'list_this_list' =>
434             {
435             url => '/ws/page/[P:page_id]/items/list?list_id=[P:list_id]',
436             req => '
437             [S:token]
438             '
439             },
440             'create_list' =>
441             {
442             url => '/ws/page/[P:page_id]/lists/add',
443             req => '
444             [S:token]
445             [P:title]
446             '
447             },
448             'update_list' =>
449             {
450             url => '/ws/page/[P:page_id]/lists/update/[P:list_id]',
451             req => '
452             [S:token]
453            
454             [P:title]
455            
456             '
457             },
458             'destroy_list' =>
459             {
460             url => '/ws/page/[P:page_id]/lists/destroy/[P:list_id]',
461             req => '
462             [S:token]
463             '
464             },
465             'create_list_item' =>
466             {
467             url => '/ws/page/[P:page_id]/items/add?list_id=[P:list_id]',
468             req => '
469             [S:token]
470            
471             [P:item]
472            
473             '
474             },
475             );
476              
477             =head1 METHODS
478              
479             =head2 $hr = Net::ThirtySevenSignals::Highrise->new(token => $token, user => $user, [forcearray => 0], ssl => 0);
480              
481             Creates a new Net::ThirtySevenSignals::Highrise object. All communication with the
482             Highrise server is made through this object.
483              
484             Takes two mandatory arguments, your Highrise API token and your
485             Highrise username. Returns the new Net:Highrise object.
486              
487             There is also an optional parameter, forcearray. This controls the
488             value of the C parameter that is used by C. The
489             default value is 1.
490              
491             If the C parameter is provided, then communication will take
492             place over SSL. This is required for Plus and Premium accounts.
493              
494             =cut
495              
496             sub new {
497             my $class = shift;
498             my %params = @_;
499              
500             my $self;
501             $self->{debug} = $params{debug};
502             $self->{token} = $params{token}
503             || croak "No Highrise API token passed Net::ThirtySevenSignals::Highrise::new\n";
504             $self->{user} = $params{user}
505             || croak "No Highrise API user passed Net::ThirtySevenSignals::Highrise::new\n";
506              
507             $self->{protocol} = $params{ssl} ? 'https' : 'http';
508              
509             $self->{forcearray} = $params{forcearray} || 1;
510              
511             my $ua = $self->{ua} = LWP::UserAgent->new;
512              
513             $ua->env_proxy;
514             $ua->default_header('X-POST-DATA-FORMAT' => 'xml');
515              
516             $ua->credentials($self->{user} . ".highrisehq.com:443","Application", $self->{token}, 'X');
517             $ua->credentials($self->{user} . ".highrisehq.com:80","Application", $self->{token}, 'X');
518             if( $self->{debug}){
519             $ua->add_handler("request_send",sub{warn(shift->dump);return });
520             $ua->add_handler("response_done",sub{warn(shift->dump);return });
521             }
522              
523             $self->{base_url} = "$self->{protocol}://$self->{user}.highrisehq.com";
524              
525             return bless $self, $class;
526             }
527              
528             =head2 $pages = $hr->people_list_all([xml => 1]);
529              
530             Get a list of all of your Highrise people. Returns a Perl data structure
531             unless the C parameter is true, in which case it returns the raw
532             XML as returned by the Highrise server.
533              
534             =cut
535              
536             sub people_list_all {
537             my $self = shift;
538             my %params = @_;
539              
540             my $req_data = $data{people_list_all};
541             my $url = $self->{base_url} . $req_data->{url};
542              
543             my $req = HTTP::Request->new('GET', $url);
544              
545             my $structure = $self->_call(%params, req => $req);
546             return $structure if $params{xml};
547             return $structure->{$req_data->{return_key}};
548             }
549              
550              
551              
552             =head2 $people = $hr->people_list_by_criteria([xml => 1], city => 'Oakland',country=>'US'...);
553              
554             Returns a collection of people that match the criteria passed
555             in. Available criteria are:
556              
557             city
558             state
559             country
560             zip
561             phone
562             email
563              
564             Returns an arrayref or undef
565             unless the C parameter is true, in which case it returns the raw
566             XML as returned by the Highrise server.
567              
568             =cut
569              
570             sub people_list_by_criteria{
571             my $self = shift;
572             my %params = @_;
573              
574             my $req_data = $data{people_list_by_criteria};
575             my $url = $self->{base_url} . $req_data->{url};
576              
577             my $expandedURL = new URI($self->_expand($url, %params));
578             my %criteria = ();
579             for my $key (qw(city state country zip phone email)){
580             $criteria{"criteria[$key]"} = $params{$key} if exists $params{$key};
581             }
582             $expandedURL->query_form(%criteria);
583             my $req = HTTP::Request->new('GET', $expandedURL);
584              
585             my $structure = $self->_call(%params, req => $req);
586             return $structure if $params{xml};
587             return $structure->{$req_data->{return_key}};
588             }
589              
590              
591             =head2 $tags = $hr->tags_list_all([xml => 1]);
592              
593             Get a list of all of your Highrise tags. Returns a Perl data structure
594             unless the C parameter is true, in which case it returns the raw
595             XML as returned by the Highrise server.
596              
597             =cut
598              
599             sub tags_list_all {
600             my $self = shift;
601             my %params = @_;
602              
603             my $req_data = $data{tags_list_all};
604             my $url = $self->{base_url} . $req_data->{url};
605              
606             my $req = HTTP::Request->new('GET', $url);
607              
608             my $structure = $self->_call(%params, req => $req);
609             return $structure if $params{xml};
610             return $structure->{$req_data->{return_key}};
611              
612             }
613              
614             =head2 $tags = $hr->tags_list_for_subject([xml => 1]);
615              
616             Get a list of all of your Highrise tags. Returns a Perl data structure
617             unless the C parameter is true, in which case it returns the raw
618             XML as returned by the Highrise server.
619              
620             =cut
621              
622             sub tags_list_for_subject {
623             my $self = shift;
624             my %params = @_;
625              
626             my $req_data = $data{tags_list_for_subject};
627             if( $req_data->{required_params} ){
628             for my $name (@{$req_data->{required_params}}){
629             die "missing required param $name" unless defined $params{$name};
630             }
631              
632             }
633             my $url = $self->{base_url} . $req_data->{url};
634             my $expandedURL = $self->_expand($url, %params);
635              
636             my $req = HTTP::Request->new('GET', $expandedURL);
637              
638             my $structure = $self->_call(%params, req => $req);
639             return $structure if $params{xml};
640             return $structure->{$req_data->{return_key}};
641             }
642              
643              
644              
645             =head2 $pages = $hr->person_get(id=> 123455,
646             [xml => 1]);
647              
648             Get a list of all of your Highrise people. Returns a Perl data structure
649             unless the C parameter is true, in which case it returns the raw
650             XML as returned by the Highrise server.
651              
652             returns a $person hashref or die()s if the person does not exist;
653              
654             =cut
655              
656             sub person_get {
657             my $self = shift;
658             my %params = @_;
659              
660             my $req_data = $data{person_get};
661             my $url = $self->{base_url} . $req_data->{url};
662             my $expandedURL = $self->_expand($url, %params);
663             my $req = HTTP::Request->new('GET', $expandedURL);
664              
665             return $self->_call(%params, req => $req);
666             }
667              
668              
669              
670             =head2 $pages = $hr->person_create(
671             [xml => 1]);
672              
673             Create a person
674             unless the C parameter is true, in which case it returns the raw
675             XML as returned by the Highrise server.
676             Pass in parameters with keys:
677             firstName
678             lastName
679             companyName
680             emailAddress
681             emailAddress
682              
683             =cut
684              
685             sub person_create {
686             my $self = shift;
687             my %params = @_;
688              
689             my $req_data = $data{person_create};
690             my $url = $self->{base_url} . $req_data->{url};
691             my $expandedURL = $self->_expand($url, %params);
692             warn "url = $url, expanded = $expandedURL" if $self->{debug};
693             my $req = HTTP::Request->new('POST', $expandedURL);
694             my %encodedParams = ();
695             while (my ($key, $value) = each %params){
696             $value =~ s/&/&/g;
697             $value =~ s/
698             $value =~ s/>/>/g;
699             $encodedParams{$key} = $value;
700             }
701             $req->content($self->_expand($req_data->{req}, %encodedParams));
702              
703             return $self->_call(%params, req => $req);
704             }
705              
706              
707             =head2 $pages = $hr->tag_add( $subject, $subjectType, $tagName );
708              
709             add a tag to an item.
710             $subject should be a perl structure returned from one of person_get, company_get etc.
711             $subjectType should be one of
712             people
713            
714             =cut
715              
716             sub tag_add {
717             my $self = shift;
718             my ($subject, $subjectType, $tagName ) = @_;
719              
720             my $req_data = $data{tag_add};
721             use Data::Dumper;
722             warn "tag add. Subject = ".Dumper($subject);
723             my %params = ( subjectType => $subjectType,
724             tagName => $tagName,
725             subjectID => $subject->{id}[0]{content},
726             );
727             my $url = $self->{base_url} . $req_data->{url};
728              
729             my $expandedURL = $self->_expand($url, %params);
730             my $req = HTTP::Request->new('POST', $expandedURL);
731             my %encodedParams = ();
732             while (my ($key, $value) = each %params){
733             $value =~ s/&/&/g;
734             $value =~ s/
735             $value =~ s/>/>/g;
736             $encodedParams{$key} = $value;
737             }
738             $req->content($self->_expand($req_data->{req}, %encodedParams));
739              
740             return $self->_call(%params, req => $req);
741             }
742              
743              
744             =head2 $pages = $hr->person_destroy();
745              
746             Destroy a person. either returns undef or die()s.
747              
748             Pass in parameters with keys:
749             id => the personid to be destroyed
750             =cut
751              
752             sub person_destroy {
753             my $self = shift;
754             my %params = @_;
755              
756             my $req_data = $data{person_destroy};
757             my $url = $self->{base_url} . $req_data->{url};
758             my $expandedURL = $self->_expand($url, %params);
759             my $req = HTTP::Request->new($req_data->{method}, $expandedURL);
760              
761             if( $req_data->{req}){
762             my %encodedParams = ();
763             while (my ($key, $value) = each %params){
764             $value =~ s/&/&/g;
765             $value =~ s/
766             $value =~ s/>/>/g;
767             $encodedParams{$key} = $value;
768             }
769             $req->content($self->_expand($req_data->{req}, %encodedParams));
770             }
771            
772             $self->_call(%params, req => $req, xml=>1);
773             return ;
774             }
775              
776             =head2 $page = $hr->create_page(title => $title,
777             [description => $desc, xml => 1]);
778              
779             Create a new Highrise page with the given title and (optional)
780             description. Returns a Perl data structure unless the C parameter is
781             true, in which case it returns the raw XML as returned by the Highrise server.
782              
783             =cut
784              
785             sub _call {
786             my $self = shift;
787             my %params = @_;
788              
789             my $resp = $self->{ua}->request($params{req});
790             unless( $resp->is_success){
791             die "Request Failed: ".$resp->status_line."\t".$resp->content;
792             }
793             my $xml = $resp->content;
794             if( $self->{debug}){
795             print "received xml: $xml\n";
796             }
797              
798             if ($params{xml}) {
799             return $xml;
800             } else {
801             my $data = XMLin($xml, ForceArray => $self->{forcearray});
802             return $data;
803             }
804             }
805              
806             sub _expand {
807             my $self = shift;
808             my $string = shift;
809             my %params = @_;
810             my $startTag = qr"\Q[%\E";
811             my $endTag = "%]";
812             $string =~ s{ $startTag \s* IF \s* P:(\w+) \s* $endTag
813             (.+?)
814             $startTag \s* END \s* $endTag
815             } {
816             my ($condParam, $ifClause ) = ($1,$2);
817             if( $params{$1} ){
818             $2;
819             }
820             else{
821             '';
822             }
823             }sexg;
824              
825             $string =~ s/\[S:(\w+)]/$self->{$1}/g;
826             $string =~ s/\[P:(\w+)]/$params{$1}/g;
827             # warn "expanded is $string\n";
828             return $string;
829             }
830              
831              
832              
833              
834             =head2 $url = $hr->person_url($personHash);
835              
836             Create an URL pointing at a person page.
837              
838             =cut
839              
840             sub person_url{
841             my $self = shift;
842             my ($person) = @_;
843             return sprintf ("http://%s.highrisehq.com/people/%d", $self->{user}, $person->{id}->[0]->{content});
844             }
845              
846              
847              
848              
849             =head1 TO DO
850              
851             =over 4
852              
853             =item *
854              
855             Improve documentation (I know, it's shameful)
856              
857             =item *
858              
859             More tests
860              
861             =back
862              
863             =head1 AUTHOR
864              
865             Danny Sadinoff Edanny@sadinoff.comE
866              
867             derived directly from Dave Cross's Net::Backpack
868              
869             Please feel free to email me to tell me how you are using the module.
870              
871             =head1 BUGS
872              
873             the API is incomplete, to say the least.
874              
875             Please report bugs by email to danny@sadinoff.com
876              
877             =head1 LICENSE AND COPYRIGHT
878              
879             Copyright (c) 2005,2010, Dave Cross, Danny Sadinoff. All Rights Reserved.
880              
881             This script is free software; you can redistribute it and/or
882             modify it under the same terms as Perl itself.
883              
884             =head1 SEE ALSO
885              
886             L, L
887              
888             =cut
889              
890              
891              
892             1;
893             __END__