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   5783 use strict;
  5         10  
  5         179  
3 5     5   23 use warnings;
  5         7  
  5         159  
4 5     5   21 use BusyBird::Util qw(set_param future_of);
  5         8  
  5         249  
5 5     5   2389 use BusyBird::Main::PSGI::View;
  5         13  
  5         177  
6 5     5   2612 use Router::Simple;
  5         14230  
  5         142  
7 5     5   2418 use Plack::Request;
  5         185551  
  5         209  
8 5     5   2415 use Plack::Builder ();
  5         26621  
  5         124  
9 5     5   2450 use Plack::App::File;
  5         31904  
  5         146  
10 5     5   32 use Try::Tiny;
  5         9  
  5         323  
11 5     5   25 use JSON qw(decode_json);
  5         10  
  5         41  
12 5     5   616 use Scalar::Util qw(looks_like_number);
  5         8  
  5         219  
13 5     5   22 use List::Util qw(min);
  5         8  
  5         272  
14 5     5   23 use Carp;
  5         8  
  5         203  
15 5     5   26 use Exporter qw(import);
  5         6  
  5         138  
16 5     5   21 use URI::Escape qw(uri_unescape);
  5         11  
  5         215  
17 5     5   24 use Encode qw(decode_utf8);
  5         7  
  5         362  
18 5     5   31 use Future::Q;
  5         7  
  5         129  
19 5     5   22 use POSIX qw(ceil);
  5         10  
  5         70  
20              
21              
22             our @EXPORT_OK = qw(create_psgi_app);
23              
24             sub create_psgi_app {
25 45     45 1 1228 my ($main_obj) = @_;
26 45         286 my $self = __PACKAGE__->_new(main_obj => $main_obj);
27 45         155 return $self->_to_app;
28             }
29              
30             sub _new {
31 45     45   110 my ($class, %params) = @_;
32 45         312 my $self = bless {
33             router => Router::Simple->new,
34             view => undef, ## lazy build
35             }, $class;
36 45         463 $self->set_param(\%params, "main_obj", undef, 1);
37 45         135 $self->_build_routes();
38 45         1905 return $self;
39             }
40              
41             sub _to_app {
42 45     45   69 my $self = shift;
43 45         168 my $sharedir = $self->{main_obj}->get_config("sharedir_path");
44 45         212 $sharedir =~ s{/+$}{};
45             return Plack::Builder::builder {
46 45     45   1717 Plack::Builder::enable 'ContentLength';
47 45         6159 Plack::Builder::mount '/static' => Plack::App::File->new(
48             root => File::Spec->catdir($sharedir, 'www', 'static')
49             )->to_app;
50 45         2673 Plack::Builder::mount '/' => $self->_my_app;
51 45         379 };
52             }
53              
54             sub _my_app {
55 45     45   69 my ($self) = @_;
56             return sub {
57 185     185   826612 my ($env) = @_;
58 185   66     1046 $self->{view} ||= BusyBird::Main::PSGI::View->new(main_obj => $self->{main_obj}, script_name => $env->{SCRIPT_NAME});
59 185 100       831 if(my $dest = $self->{router}->match($env)) {
60 179         14454 my $req = Plack::Request->new($env);
61 179         1420 my $code = $dest->{code};
62 179         311 my $method = $dest->{method};
63 179 50       855 return defined($code) ? $code->($self, $req, $dest) : $self->$method($req, $dest);
64             }else {
65 6         509 return $self->{view}->response_notfound();
66             }
67 45         214 };
68             }
69              
70             sub _build_routes {
71 45     45   65 my ($self) = @_;
72 45         196 my $tl_mapper = $self->{router}->submapper(
73             '/timelines/{timeline}', {}
74             );
75 45         1291 $tl_mapper->connect('/statuses.{format}',
76             {method => '_handle_tl_get_statuses'}, {method => 'GET'});
77 45         5475 $tl_mapper->connect('/statuses.json',
78             {method => '_handle_tl_post_statuses'}, {method => 'POST'});
79 45         3485 $tl_mapper->connect('/ack.json',
80             {method => '_handle_tl_ack'}, {method => 'POST'});
81 45         3218 $tl_mapper->connect('/updates/unacked_counts.json',
82             {method => '_handle_tl_get_unacked_counts'}, {method => 'GET'});
83 45         3424 $tl_mapper->connect($_, {method => '_handle_tl_index'}) foreach "", qw(/ /index.html /index.htm);
84 45         10618 $self->{router}->connect('/updates/unacked_counts.json',
85             {method => '_handle_get_unacked_counts'}, {method => 'GET'});
86 45         1892 foreach my $path ("/", "/index.html") {
87 90         1996 $self->{router}->connect($path, {method => '_handle_get_timeline_list'}, {method => 'GET'});
88             }
89             }
90              
91             sub _get_timeline_name {
92 152     152   211 my ($dest) = @_;
93 152         256 my $name = $dest->{timeline};
94 152 50       383 $name = "" if not defined($name);
95 152         394 $name =~ s/\+/ /g;
96 152         465 return decode_utf8(uri_unescape($name));
97             }
98              
99             sub _get_timeline {
100 135     135   204 my ($self, $dest) = @_;
101 135         308 my $name = _get_timeline_name($dest);
102 135         4162 my $timeline = $self->{main_obj}->get_timeline($name);
103 135 100       1358 if(!defined($timeline)) {
104 4         37 die qq{No timeline named $name};
105             }
106 131         253 return $timeline;
107             }
108              
109             sub _handle_tl_get_statuses {
110 70     70   118 my ($self, $req, $dest) = @_;
111             return sub {
112 70     70   3196 my $responder = shift;
113 70         98 my $timeline;
114 70         220 my $only_statuses = !!($req->query_parameters->{only_statuses});
115 70 100 100     5952 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         2510 $timeline = $self->_get_timeline($dest);
120 69   100     160 my $count = $req->query_parameters->{count} || 20;
121 69 50 33     881 if(!looks_like_number($count) || int($count) != $count) {
122 0         0 die "count parameter must be an integer\n";
123             }
124 69   100     153 my $ack_state = $req->query_parameters->{ack_state} || 'any';
125 69         480 my $max_id = decode_utf8($req->query_parameters->{max_id});
126 69         816 return future_of($timeline, "get_statuses",
127             count => $count, ack_state => $ack_state, max_id => $max_id);
128             })->then(sub {
129 65         10799 my $statuses = shift;
130 65         345 $responder->($self->{view}->response_statuses(
131             statuses => $statuses, http_code => 200, format => $format,
132             timeline_name => $timeline->name
133             ));
134             })->catch(sub {
135 5         1588 my ($error, $is_normal_error) = @_;
136 5 100       45 $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         606 });
141 70         667 };
142             }
143              
144             sub _handle_tl_post_statuses {
145 31     31   71 my ($self, $req, $dest) = @_;
146             return sub {
147 31     31   1646 my $responder = shift;
148             Future::Q->try(sub {
149 31         1251 my $timeline = $self->_get_timeline($dest);
150 30         119 my $posted_obj = decode_json($req->content);
151 29 100       36261 if(ref($posted_obj) ne 'ARRAY') {
152 5         12 $posted_obj = [$posted_obj];
153             }
154 29         133 return future_of($timeline, "add_statuses", statuses => $posted_obj);
155             })->then(sub {
156 27         5434 my $added_num = shift;
157 27         227 $responder->($self->{view}->response_json(200, {count => $added_num + 0}));
158             })->catch(sub {
159 4         1174 my ($e, $is_normal_error) = @_;
160 4 100       25 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
161 31         304 });
162 31         323 };
163             }
164              
165             sub _handle_tl_ack {
166 28     28   58 my ($self, $req, $dest) = @_;
167             return sub {
168 28     28   1287 my $responder = shift;
169             Future::Q->try(sub {
170 28         964 my $timeline = $self->_get_timeline($dest);
171 27         39 my $max_id = undef;
172 27         42 my $ids = undef;
173 27 100       114 if($req->content) {
174 23         18806 my $body_obj = decode_json($req->content);
175 23 50       541 if(ref($body_obj) ne 'HASH') {
176 0         0 die "Response body must be an object.\n";
177             }
178 23         54 $max_id = $body_obj->{max_id};
179 23         63 $ids = $body_obj->{ids};
180             }
181 27         416 return future_of($timeline, "ack_statuses", max_id => $max_id, ids => $ids);
182             })->then(sub {
183 25         3580 my $acked_num = shift;
184 25         182 $responder->($self->{view}->response_json(200, {count => $acked_num + 0}));
185             })->catch(sub {
186 3         784 my ($e, $is_normal_error) = @_;
187 3 100       21 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
188 28         217 });
189 28         242 };
190             }
191              
192             sub _handle_tl_get_unacked_counts {
193 6     6   9 my ($self, $req, $dest) = @_;
194             return sub {
195 6     6   224 my $responder = shift;
196             Future::Q->try(sub {
197 6         345 my $timeline = $self->_get_timeline($dest);
198 5         16 my $query_params = $req->query_parameters;
199 5         340 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       15 next if !looks_like_number($query_key);
205 5 50       16 next if int($query_key) != $query_key;
206 5         9 $assumed{$query_key} = $query_params->{$query_key};
207             }
208 5         19 my $ret_future = Future::Q->new;
209             $timeline->watch_unacked_counts(assumed => \%assumed, callback => sub {
210 5         7 my ($error, $w, $unacked_counts) = @_;
211 5         22 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
212 5 50       53 if($error) {
213 0         0 $ret_future->reject($error, 1);
214             }else {
215 5         15 $ret_future->fulfill($unacked_counts);
216             }
217 5         82 });
218 5         27 return $ret_future;
219             })->then(sub {
220 5         1137 my ($unacked_counts) = @_;
221 5         25 $responder->($self->{view}->response_json(200, {unacked_counts => $unacked_counts}));
222             })->catch(sub {
223 1         260 my ($e, $is_normal_error) = @_;
224 1 50       7 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
225 6         42 });
226 6         44 };
227             }
228              
229             sub _handle_get_unacked_counts {
230 17     17   30 my ($self, $req, $dest) = @_;
231             return sub {
232 17     17   850 my $responder = shift;
233             Future::Q->try(sub {
234 17         629 my $query_params = $req->query_parameters;
235 17         1852 my $level = $query_params->{level};
236 17 100 33     104 if(not defined($level)) {
    50 66        
237 3         3 $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         32 my %assumed = ();
242 17         48 foreach my $query_key (keys %$query_params) {
243 62 100       607 next if substr($query_key, 0, 3) ne 'tl_';
244 46         113 $assumed{decode_utf8(substr($query_key, 3))} = $query_params->{$query_key};
245             }
246 17         182 my $ret_future = Future::Q->new;
247             $self->{main_obj}->watch_unacked_counts(
248             level => $level, assumed => \%assumed, callback => sub {
249 15         22 my ($error, $w, $tl_unacked_counts) = @_;
250 15         45 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
251 15 50       203 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         314 );
258 15         74 return $ret_future;
259             })->then(sub {
260 15         1655 my ($tl_unacked_counts) = @_;
261 15         74 $responder->($self->{view}->response_json(200, {unacked_counts => $tl_unacked_counts}));
262             })->catch(sub {
263 2         790 my ($e, $is_normal_error) = @_;
264 2 50       13 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
265 17         114 });
266 17         152 };
267             }
268              
269             sub _handle_tl_index {
270 17     17   21 my ($self, $req, $dest) = @_;
271 17         57 return $self->{view}->response_timeline(_get_timeline_name($dest));
272             }
273              
274             sub _handle_get_timeline_list {
275 10     10   18 my ($self, $req, $dest) = @_;
276             return sub {
277 10     10   521 my $responder = shift;
278             Future::Q->try(sub {
279 10         382 my $num_per_page = $self->{main_obj}->get_config('timeline_list_per_page');
280 112         896 my @timelines = grep {
281 10         39 !$self->{main_obj}->get_timeline_config($_->name, "hidden")
282             } $self->{main_obj}->get_all_timelines();
283 10 50       59 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         19 my $cur_page = 0;
288 10         40 my $query = $req->query_parameters;
289 10 100       651 if(defined $query->{page}) {
290 7 100 100     73 if(!looks_like_number($query->{page}) || $query->{page} < 0 || $query->{page} >= $page_num) {
      100        
291 4         26 die "Invalid page parameter\n";
292             }
293 3         5 $cur_page = $query->{page};
294             }
295 6         61 my @target_timelines = @timelines[($cur_page * $num_per_page) .. min(($cur_page+1) * $num_per_page - 1, $#timelines)];
296 24         451 return Future::Q->needs_all(map { future_of($_, "get_unacked_counts") } @target_timelines)->then(sub {
297 6         1579 my (@unacked_counts_list) = @_;
298 24         49 my @timeline_unacked_counts = map {
299 6         22 +{ name => $target_timelines[$_]->name, counts => $unacked_counts_list[$_] }
300             } 0 .. $#target_timelines;
301 6         41 $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         11 });
307             })->catch(sub {
308 4         577 my ($error, $is_normal_error) = @_;
309 4 50       20 $responder->($self->{view}->response_error_html(
310             ($is_normal_error ? 500 : 400), $error
311             ));
312 10         102 });
313 10         88 };
314             }
315              
316             1;
317              
318              
319             __END__