File Coverage

blib/lib/WebService/Pushover.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package WebService::Pushover;
2 1     1   504 use strict;
  1         2  
  1         31  
3              
4 1     1   486 use Moo;
  1         13051  
  1         5  
5              
6             binmode STDOUT, ":encoding(UTF-8)";
7              
8 1     1   1202 use Carp;
  1         2  
  1         57  
9 1     1   875 use DateTime;
  1         108436  
  1         41  
10 1     1   671 use DateTime::Format::Strptime;
  1         6117  
  1         75  
11 1     1   11 use File::Spec;
  1         1  
  1         20  
12 1     1   361 use WebService::Simple;
  0            
  0            
13             use WebService::Simple::Parser::JSON;
14             use Params::Validate qw( :all );
15             use Readonly;
16             use URI;
17              
18             use version; our $VERSION = qv('1.0.0');
19              
20             # Module implementation here
21              
22             # constants
23             Readonly my $REGEX_FORMAT => '^(?:json|xml)$';
24             Readonly my $REGEX_TOKEN => '^[A-Za-z0-9]{30}$';
25             Readonly my $REGEX_DEVICE => '^[A-Za-z0-9_-]{0,25}$';
26             Readonly my $REGEX_NUMERIC => '^\d+$';
27             Readonly my $REGEX_SOUNDS => '^(?:pushover|bike|bugle|cashregister|classical|cosmic|falling|gamelan|incoming|intermission|magic|mechanical|pianobar|siren|spacealarm|tugboat|alien|climb|persistent|echo|updown|none)$';
28              
29             Readonly my $SIZE_TITLE => 50;
30             Readonly my $SIZE_MESSAGE => 512;
31             Readonly my $SIZE_URL => 200;
32             Readonly my $SIZE_RETRY => 30;
33             Readonly my $SIZE_EXPIRE => 86400;
34              
35             has debug => (
36             is => 'ro',
37             default => sub { 0 },
38             coerce => sub { $_[0] ? 1 : 0 },
39             );
40              
41             # NB: We can't call this 'user', as there's already a method called that.
42             has user_token => (
43             is => 'ro',
44             required => 0,
45             isa => sub { $_[0] =~ /$REGEX_TOKEN/ or die "Invalid user token: $_[0]" },
46             );
47              
48             has api_token => (
49             is => 'ro',
50             required => 0,
51             isa => sub { $_[0] =~ /$REGEX_TOKEN/ or die "Invalid api token: $_[0]" },
52             );
53              
54             has base_url => (
55             default => "https://api.pushover.net",
56             is => 'ro',
57             );
58              
59             has _urls => (
60             is => 'ro',
61             default => sub {
62             return {
63             messages => '/1/messages.json',
64             users => '/1/users/validate.json',
65             receipts => '/1/receipts/$receipt$.json',
66             sounds => '/1/sounds.json',
67             };
68             },
69             );
70              
71             has api => (
72             is => 'lazy',
73             );
74              
75             sub _build_api {
76             my ($self) = @_;
77             return WebService::Simple->new(
78             response_parser => WebService::Simple::Parser::JSON->new,
79             base_url => $self->base_url,
80             debug => $self->debug,
81             );
82             }
83              
84             has specs => (
85             is => 'lazy',
86             );
87              
88             sub _build_specs {
89             my $self = shift();
90             my $SPECS = {
91             token => {
92             type => SCALAR,
93             regex => qr/$REGEX_TOKEN/,
94             },
95             user => {
96             type => SCALAR,
97             regex => qr/$REGEX_TOKEN/,
98             },
99             device => {
100             optional => 1,
101             type => SCALAR,
102             regex => qr/$REGEX_DEVICE/,
103             },
104             receipt => {
105             type => SCALAR,
106             regex => qr/$REGEX_TOKEN/, # yes, receipts are formatted like tokens
107             },
108             callback => {
109             optional => 1,
110             type => SCALAR,
111             callbacks => {
112             "valid URL" => sub {
113             my $url = shift;
114             my $uri = URI->new( $url );
115             defined( $uri->as_string() );
116             },
117             },
118             },
119             title => {
120             optional => 1,
121             type => SCALAR,
122             callbacks => {
123             "$SIZE_TITLE characters or fewer" => sub { length( shift() ) <= $SIZE_TITLE },
124             },
125             },
126             message => {
127             type => SCALAR,
128             callbacks => {
129             "$SIZE_MESSAGE characters or fewer" => sub { length( shift() ) <= $SIZE_MESSAGE },
130             },
131             },
132             timestamp => {
133             optional => 1,
134             type => SCALAR,
135             callbacks => {
136             "Unix epoch timestamp" => sub {
137             my $timestamp = shift;
138             my $strp = DateTime::Format::Strptime->new(
139             pattern => '%s',
140             time_zone => "floating",
141             on_error => "undef",
142             );
143             defined( $strp->parse_datetime( $timestamp ) );
144             },
145             },
146             },
147             priority => {
148             optional => 1,
149             type => SCALAR,
150             callbacks => {
151             "valid or undefined" => sub {
152             my $priority = shift;
153             my( %priorities ) = (
154             0 => 'valid',
155             1 => 'valid',
156             -1 => 'valid',
157             2 => 'valid',
158             );
159             ( ! defined( $priority ) )
160             or exists $priorities{$priority};
161             },
162             },
163             },
164             url => {
165             optional => 1,
166             type => SCALAR,
167             callbacks => {
168             "valid URL" => sub {
169             my $url = shift;
170             my $uri = URI->new( $url );
171             defined( $uri->as_string() );
172             },
173             },
174             },
175             url_title => {
176             optional => 1,
177             type => SCALAR,
178             callbacks => {
179             "$SIZE_TITLE characters or fewer" => sub { length( shift() ) <= $SIZE_TITLE },
180             },
181             },
182             sound => {
183             optional => 1,
184             type => SCALAR,
185             regex => qr/$REGEX_SOUNDS/,
186             },
187             retry => {
188             optional => 1,
189             type => SCALAR,
190             callbacks => {
191             "numeric" => sub { shift() =~ /$REGEX_NUMERIC/ },
192             "$SIZE_RETRY seconds or more" => sub { shift() >= $SIZE_RETRY },
193             }
194             },
195             expire => {
196             optional => 1,
197             type => SCALAR,
198             callbacks => {
199             "numeric" => sub { shift() =~ /$REGEX_NUMERIC/ },
200             "$SIZE_EXPIRE seconds or fewer" => sub { shift() <= $SIZE_EXPIRE },
201             }
202             },
203             };
204              
205             my %messages_spec = (
206             token => $SPECS->{token},
207             user => $SPECS->{user},
208             device => $SPECS->{device},
209             title => $SPECS->{title},
210             message => $SPECS->{message},
211             timestamp => $SPECS->{timestamp},
212             priority => $SPECS->{priority},
213             callback => $SPECS->{callback},
214             sound => $SPECS->{sound},
215             retry => $SPECS->{retry},
216             expire => $SPECS->{expire},
217             url => $SPECS->{url},
218             url_title => $SPECS->{url_title},
219             );
220              
221             my %users_spec = (
222             token => $SPECS->{token},
223             user => $SPECS->{user},
224             device => $SPECS->{device},
225             );
226              
227             my %receipts_spec = (
228             token => $SPECS->{token},
229             receipt => $SPECS->{receipt},
230             );
231              
232             my %sounds_spec = (
233             token => $SPECS->{token},
234             );
235              
236             return {
237             messages => \%messages_spec,
238             users => \%users_spec,
239             receipts => \%receipts_spec,
240             sounds => \%sounds_spec,
241             };
242             }
243              
244             sub _apicall {
245             my ($self, $method, $call, @rest) = @_;
246              
247             my $spec = $self->specs->{$call}
248             or croak( "'$call' is not a supported API call." );
249             my $url = $self->_urls->{$call}
250             or croak( "'$call' is not a supported API call." );
251             my $params = validate( @rest, $spec );
252              
253             while ($url =~ /\$(\S+?)\$/) {
254             my $arg = $1;
255             my $val = delete($params->{$arg}) || "";
256             $url =~ s/\$$arg\$/$val/g;
257             }
258              
259             return $self->api->$method($url, $params)->parse_response;
260             }
261              
262             sub message {
263             my ($self, %opts) = @_;
264              
265             return $self->_apicall(post => 'messages',
266             user => $self->user_token,
267             token => $self->api_token,
268             %opts,
269             );
270             }
271              
272             sub user {
273             my ($self, %opts) = @_;
274              
275             return $self->_apicall(post => 'users',
276             user => $self->user_token,
277             token => $self->api_token,
278             %opts,
279             );
280             }
281              
282             sub receipt {
283             my ($self, %opts) = @_;
284              
285             return $self->_apicall(get => 'receipts',
286             token => $self->api_token,
287             %opts,
288             );
289             }
290              
291             sub sounds {
292             my ($self, %opts) = @_;
293              
294             return $self->_apicall(get => 'sounds',
295             token => $self->api_token,
296             %opts,
297             );
298             }
299              
300             # ok, add some backwards compatibility
301             before 'push' => sub {
302             carp( "The 'push' method is deprecated in WebService::Pushover v0.1.0, and will be removed in a future release. Please use the 'message' method instead." );
303             };
304              
305             sub push {
306             my $self = shift;
307             $self->message( @_ );
308             }
309              
310             before 'tokens' => sub {
311             carp( "The 'tokens' method is deprecated in WebService::Pushover v0.1.0, and will be removed in a future release. Please use the 'user' method instead." );
312             };
313              
314             sub tokens {
315             my $self = shift;
316             $self->user( @_ );
317             }
318              
319             1; # Magic true value required at end of module
320             __END__