File Coverage

blib/lib/BusyBird/Main/PSGI.pm
Criterion Covered Total %
statement 220 226 97.3
branch 42 56 75.0
condition 19 25 76.0
subroutine 40 40 100.0
pod 1 1 100.0
total 322 348 92.5


line stmt bran cond sub pod time code
1             package BusyBird::Main::PSGI;
2 5     5   4722 use strict;
  5         6  
  5         175  
3 5     5   26 use warnings;
  5         7  
  5         156  
4 5     5   21 use BusyBird::Util qw(set_param future_of);
  5         6  
  5         260  
5 5     5   2016 use BusyBird::Main::PSGI::View;
  5         11  
  5         180  
6 5     5   2436 use Router::Simple;
  5         13374  
  5         130  
7 5     5   2273 use Plack::Request;
  5         181498  
  5         162  
8 5     5   2367 use Plack::Builder ();
  5         23845  
  5         107  
9 5     5   2051 use Plack::App::File;
  5         28091  
  5         152  
10 5     5   32 use Try::Tiny;
  5         7  
  5         299  
11 5     5   21 use JSON qw(decode_json);
  5         7  
  5         38  
12 5     5   643 use Scalar::Util qw(looks_like_number);
  5         10  
  5         206  
13 5     5   21 use List::Util qw(min);
  5         12  
  5         260  
14 5     5   20 use Carp;
  5         7  
  5         282  
15 5     5   39 use Exporter qw(import);
  5         6  
  5         152  
16 5     5   18 use URI::Escape qw(uri_unescape);
  5         11  
  5         197  
17 5     5   21 use Encode qw(decode_utf8);
  5         6  
  5         334  
18 5     5   33 use Future::Q;
  5         8  
  5         121  
19 5     5   19 use POSIX qw(ceil);
  5         10  
  5         76  
20              
21              
22             our @EXPORT_OK = qw(create_psgi_app);
23              
24             sub create_psgi_app {
25 45     45 1 1030 my ($main_obj) = @_;
26 45         263 my $self = __PACKAGE__->_new(main_obj => $main_obj);
27 45         161 return $self->_to_app;
28             }
29              
30             sub _new {
31 45     45   157 my ($class, %params) = @_;
32 45         347 my $self = bless {
33             router => Router::Simple->new,
34             view => undef, ## lazy build
35             }, $class;
36 45         480 $self->set_param(\%params, "main_obj", undef, 1);
37 45         159 $self->_build_routes();
38 45         1929 return $self;
39             }
40              
41             sub _to_app {
42 45     45   86 my $self = shift;
43 45         177 my $sharedir = $self->{main_obj}->get_config("sharedir_path");
44 45         235 $sharedir =~ s{/+$}{};
45             return Plack::Builder::builder {
46 45     45   1843 Plack::Builder::enable 'ContentLength';
47 45         6377 Plack::Builder::mount '/static' => Plack::App::File->new(
48             root => File::Spec->catdir($sharedir, 'www', 'static')
49             )->to_app;
50 45         2821 Plack::Builder::mount '/' => $self->_my_app;
51 45         383 };
52             }
53              
54             sub _my_app {
55 45     45   67 my ($self) = @_;
56             return sub {
57 185     185   839037 my ($env) = @_;
58 185   66     1065 $self->{view} ||= BusyBird::Main::PSGI::View->new(main_obj => $self->{main_obj}, script_name => $env->{SCRIPT_NAME});
59 185 100       822 if(my $dest = $self->{router}->match($env)) {
60 179         14493 my $req = Plack::Request->new($env);
61 179         1417 my $code = $dest->{code};
62 179         317 my $method = $dest->{method};
63 179 50       786 return defined($code) ? $code->($self, $req, $dest) : $self->$method($req, $dest);
64             }else {
65 6         578 return $self->{view}->response_notfound();
66             }
67 45         254 };
68             }
69              
70             sub _build_routes {
71 45     45   56 my ($self) = @_;
72 45         192 my $tl_mapper = $self->{router}->submapper(
73             '/timelines/{timeline}', {}
74             );
75 45         1226 $tl_mapper->connect('/statuses.{format}',
76             {method => '_handle_tl_get_statuses'}, {method => 'GET'});
77 45         5527 $tl_mapper->connect('/statuses.json',
78             {method => '_handle_tl_post_statuses'}, {method => 'POST'});
79 45         3700 $tl_mapper->connect('/ack.json',
80             {method => '_handle_tl_ack'}, {method => 'POST'});
81 45         3189 $tl_mapper->connect('/updates/unacked_counts.json',
82             {method => '_handle_tl_get_unacked_counts'}, {method => 'GET'});
83 45         3567 $tl_mapper->connect($_, {method => '_handle_tl_index'}) foreach "", qw(/ /index.html /index.htm);
84 45         10933 $self->{router}->connect('/updates/unacked_counts.json',
85             {method => '_handle_get_unacked_counts'}, {method => 'GET'});
86 45         2008 foreach my $path ("/", "/index.html") {
87 90         2207 $self->{router}->connect($path, {method => '_handle_get_timeline_list'}, {method => 'GET'});
88             }
89             }
90              
91             sub _get_timeline_name {
92 152     152   261 my ($dest) = @_;
93 152         235 my $name = $dest->{timeline};
94 152 50       340 $name = "" if not defined($name);
95 152         278 $name =~ s/\+/ /g;
96 152         368 return decode_utf8(uri_unescape($name));
97             }
98              
99             sub _get_timeline {
100 135     135   201 my ($self, $dest) = @_;
101 135         284 my $name = _get_timeline_name($dest);
102 135         3947 my $timeline = $self->{main_obj}->get_timeline($name);
103 135 100       1220 if(!defined($timeline)) {
104 4         35 die qq{No timeline named $name};
105             }
106 131         249 return $timeline;
107             }
108              
109             sub _handle_tl_get_statuses {
110 70     70   137 my ($self, $req, $dest) = @_;
111             return sub {
112 70     70   3378 my $responder = shift;
113 70         91 my $timeline;
114 70         234 my $only_statuses = !!($req->query_parameters->{only_statuses});
115 70 100 100     5960 my $format = !defined($dest->{format}) ? ""
    50          
116             : (lc($dest->{format}) eq "json" && $only_statuses) ? "json_only_statuses"
117             : $dest->{format};
118             Future::Q->try(sub {
119 70         2534 $timeline = $self->_get_timeline($dest);
120 69   100     173 my $count = $req->query_parameters->{count} || 20;
121 69 50 33     865 if(!looks_like_number($count) || int($count) != $count) {
122 0         0 die "count parameter must be an integer\n";
123             }
124 69   100     159 my $ack_state = $req->query_parameters->{ack_state} || 'any';
125 69         493 my $max_id = decode_utf8($req->query_parameters->{max_id});
126 69         799 return future_of($timeline, "get_statuses",
127             count => $count, ack_state => $ack_state, max_id => $max_id);
128             })->then(sub {
129 65         11179 my $statuses = shift;
130 65         325 $responder->($self->{view}->response_statuses(
131             statuses => $statuses, http_code => 200, format => $format,
132             timeline_name => $timeline->name
133             ));
134             })->catch(sub {
135 5         1584 my ($error, $is_normal_error) = @_;
136 5 100       40 $responder->($self->{view}->response_statuses(
    100          
137             error => "$error", http_code => ($is_normal_error ? 500 : 400), format => $format,
138             ($timeline ? (timeline_name => $timeline->name) : ())
139             ));
140 70         558 });
141 70         670 };
142             }
143              
144             sub _handle_tl_post_statuses {
145 31     31   63 my ($self, $req, $dest) = @_;
146             return sub {
147 31     31   1632 my $responder = shift;
148             Future::Q->try(sub {
149 31         1216 my $timeline = $self->_get_timeline($dest);
150 30         137 my $posted_obj = decode_json($req->content);
151 29 100       26065 if(ref($posted_obj) ne 'ARRAY') {
152 5         11 $posted_obj = [$posted_obj];
153             }
154 29         121 return future_of($timeline, "add_statuses", statuses => $posted_obj);
155             })->then(sub {
156 27         5475 my $added_num = shift;
157 27         247 $responder->($self->{view}->response_json(200, {count => $added_num + 0}));
158             })->catch(sub {
159 4         1400 my ($e, $is_normal_error) = @_;
160 4 100       30 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
161 31         314 });
162 31         342 };
163             }
164              
165             sub _handle_tl_ack {
166 28     28   58 my ($self, $req, $dest) = @_;
167             return sub {
168 28     28   1364 my $responder = shift;
169             Future::Q->try(sub {
170 28         1027 my $timeline = $self->_get_timeline($dest);
171 27         43 my $max_id = undef;
172 27         42 my $ids = undef;
173 27 100       115 if($req->content) {
174 23         18927 my $body_obj = decode_json($req->content);
175 23 50       564 if(ref($body_obj) ne 'HASH') {
176 0         0 die "Response body must be an object.\n";
177             }
178 23         66 $max_id = $body_obj->{max_id};
179 23         47 $ids = $body_obj->{ids};
180             }
181 27         390 return future_of($timeline, "ack_statuses", max_id => $max_id, ids => $ids);
182             })->then(sub {
183 25         3713 my $acked_num = shift;
184 25         166 $responder->($self->{view}->response_json(200, {count => $acked_num + 0}));
185             })->catch(sub {
186 3         962 my ($e, $is_normal_error) = @_;
187 3 100       29 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
188 28         233 });
189 28         297 };
190             }
191              
192             sub _handle_tl_get_unacked_counts {
193 6     6   10 my ($self, $req, $dest) = @_;
194             return sub {
195 6     6   279 my $responder = shift;
196             Future::Q->try(sub {
197 6         361 my $timeline = $self->_get_timeline($dest);
198 5         17 my $query_params = $req->query_parameters;
199 5         365 my %assumed = ();
200 5 100       17 if(defined $query_params->{total}) {
201 3         9 $assumed{total} = delete $query_params->{total};
202             }
203 5         16 foreach my $query_key (keys %$query_params) {
204 5 50       19 next if !looks_like_number($query_key);
205 5 50       14 next if int($query_key) != $query_key;
206 5         10 $assumed{$query_key} = $query_params->{$query_key};
207             }
208 5         20 my $ret_future = Future::Q->new;
209             $timeline->watch_unacked_counts(assumed => \%assumed, callback => sub {
210 5         8 my ($error, $w, $unacked_counts) = @_;
211 5         18 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
212 5 50       52 if($error) {
213 0         0 $ret_future->reject($error, 1);
214             }else {
215 5         16 $ret_future->fulfill($unacked_counts);
216             }
217 5         88 });
218 5         26 return $ret_future;
219             })->then(sub {
220 5         566 my ($unacked_counts) = @_;
221 5         23 $responder->($self->{view}->response_json(200, {unacked_counts => $unacked_counts}));
222             })->catch(sub {
223 1         255 my ($e, $is_normal_error) = @_;
224 1 50       8 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
225 6         40 });
226 6         51 };
227             }
228              
229             sub _handle_get_unacked_counts {
230 17     17   29 my ($self, $req, $dest) = @_;
231             return sub {
232 17     17   803 my $responder = shift;
233             Future::Q->try(sub {
234 17         594 my $query_params = $req->query_parameters;
235 17         1950 my $level = $query_params->{level};
236 17 100 33     112 if(not defined($level)) {
    50 66        
237 3         6 $level = "total";
238             }elsif($level ne 'total' && (!looks_like_number($level) || int($level) != $level)) {
239 0         0 die "level parameter must be an integer\n";
240             }
241 17         26 my %assumed = ();
242 17         54 foreach my $query_key (keys %$query_params) {
243 62 100       609 next if substr($query_key, 0, 3) ne 'tl_';
244 46         116 $assumed{decode_utf8(substr($query_key, 3))} = $query_params->{$query_key};
245             }
246 17         207 my $ret_future = Future::Q->new;
247             $self->{main_obj}->watch_unacked_counts(
248             level => $level, assumed => \%assumed, callback => sub {
249 15         18 my ($error, $w, $tl_unacked_counts) = @_;
250 15         41 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
251 15 50       361 if($error) {
252 0         0 $ret_future->reject($error, 1);
253             }else {
254 15         43 $ret_future->fulfill($tl_unacked_counts);
255             }
256             }
257 17         331 );
258 15         81 return $ret_future;
259             })->then(sub {
260 15         1850 my ($tl_unacked_counts) = @_;
261 15         66 $responder->($self->{view}->response_json(200, {unacked_counts => $tl_unacked_counts}));
262             })->catch(sub {
263 2         906 my ($e, $is_normal_error) = @_;
264 2 50       16 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
265 17         141 });
266 17         151 };
267             }
268              
269             sub _handle_tl_index {
270 17     17   26 my ($self, $req, $dest) = @_;
271 17         48 return $self->{view}->response_timeline(_get_timeline_name($dest));
272             }
273              
274             sub _handle_get_timeline_list {
275 10     10   16 my ($self, $req, $dest) = @_;
276             return sub {
277 10     10   529 my $responder = shift;
278             Future::Q->try(sub {
279 10         378 my $num_per_page = $self->{main_obj}->get_config('timeline_list_per_page');
280 112         994 my @timelines = grep {
281 10         45 !$self->{main_obj}->get_timeline_config($_->name, "hidden")
282             } $self->{main_obj}->get_all_timelines();
283 10 50       58 if(@timelines == 0) {
284 0         0 die "No visible timeline. Probably you must configure config.psgi to create a timeline.";
285             }
286 10         56 my $page_num = ceil(scalar(@timelines) / $num_per_page);
287 10         16 my $cur_page = 0;
288 10         34 my $query = $req->query_parameters;
289 10 100       640 if(defined $query->{page}) {
290 7 100 100     72 if(!looks_like_number($query->{page}) || $query->{page} < 0 || $query->{page} >= $page_num) {
      100        
291 4         22 die "Invalid page parameter\n";
292             }
293 3         8 $cur_page = $query->{page};
294             }
295 6         72 my @target_timelines = @timelines[($cur_page * $num_per_page) .. min(($cur_page+1) * $num_per_page - 1, $#timelines)];
296 24         447 return Future::Q->needs_all(map { future_of($_, "get_unacked_counts") } @target_timelines)->then(sub {
297 6         1622 my (@unacked_counts_list) = @_;
298 24         42 my @timeline_unacked_counts = map {
299 6         19 +{ name => $target_timelines[$_]->name, counts => $unacked_counts_list[$_] }
300             } 0 .. $#target_timelines;
301 6         67 $responder->( $self->{view}->response_timeline_list(
302             timeline_unacked_counts => \@timeline_unacked_counts,
303             total_page_num => $page_num,
304             cur_page => $cur_page
305             ) );
306 6         12 });
307             })->catch(sub {
308 4         576 my ($error, $is_normal_error) = @_;
309 4 50       18 $responder->($self->{view}->response_error_html(
310             ($is_normal_error ? 500 : 400), $error
311             ));
312 10         99 });
313 10         89 };
314             }
315              
316             1;
317              
318              
319             __END__