File Coverage

blib/lib/BusyBird/Main/PSGI.pm
Criterion Covered Total %
statement 223 229 97.3
branch 42 56 75.0
condition 19 25 76.0
subroutine 41 41 100.0
pod 1 1 100.0
total 326 352 92.6


line stmt bran cond sub pod time code
1             package BusyBird::Main::PSGI;
2 5     5   6224 use v5.8.0;
  5         13  
  5         230  
3 5     5   21 use strict;
  5         5  
  5         146  
4 5     5   18 use warnings;
  5         8  
  5         146  
5 5     5   21 use BusyBird::Util qw(set_param future_of);
  5         38  
  5         262  
6 5     5   2282 use BusyBird::Main::PSGI::View;
  5         135  
  5         231  
7 5     5   3302 use Router::Simple;
  5         16860  
  5         192  
8 5     5   2988 use Plack::Request;
  5         229743  
  5         221  
9 5     5   2920 use Plack::Builder ();
  5         29255  
  5         155  
10 5     5   2752 use Plack::App::File;
  5         35983  
  5         198  
11 5     5   50 use Try::Tiny;
  5         13  
  5         364  
12 5     5   33 use JSON qw(decode_json);
  5         11  
  5         47  
13 5     5   764 use Scalar::Util qw(looks_like_number);
  5         11  
  5         354  
14 5     5   40 use List::Util qw(min);
  5         10  
  5         406  
15 5     5   30 use Carp;
  5         8  
  5         329  
16 5     5   29 use Exporter 5.57 qw(import);
  5         142  
  5         214  
17 5     5   31 use URI::Escape qw(uri_unescape);
  5         7  
  5         324  
18 5     5   31 use Encode qw(decode_utf8);
  5         9  
  5         506  
19 5     5   32 use Future::Q;
  5         13  
  5         194  
20 5     5   30 use POSIX qw(ceil);
  5         9  
  5         54  
21              
22              
23             our @EXPORT_OK = qw(create_psgi_app);
24              
25             sub create_psgi_app {
26 45     45 1 1592 my ($main_obj) = @_;
27 45         385 my $self = __PACKAGE__->_new(main_obj => $main_obj);
28 45         183 return $self->_to_app;
29             }
30              
31             sub _new {
32 45     45   144 my ($class, %params) = @_;
33 45         386 my $self = bless {
34             router => Router::Simple->new,
35             view => undef, ## lazy build
36             }, $class;
37 45         632 $self->set_param(\%params, "main_obj", undef, 1);
38 45         182 $self->_build_routes();
39 45         2449 return $self;
40             }
41              
42             sub _to_app {
43 45     45   77 my $self = shift;
44 45         205 my $sharedir = $self->{main_obj}->get_config("sharedir_path");
45 45         259 $sharedir =~ s{/+$}{};
46             return Plack::Builder::builder {
47 45     45   2185 Plack::Builder::enable 'ContentLength';
48 45         8324 Plack::Builder::mount '/static' => Plack::App::File->new(
49             root => File::Spec->catdir($sharedir, 'www', 'static')
50             )->to_app;
51 45         3536 Plack::Builder::mount '/' => $self->_my_app;
52 45         506 };
53             }
54              
55             sub _my_app {
56 45     45   96 my ($self) = @_;
57             return sub {
58 185     185   1076169 my ($env) = @_;
59 185   66     1500 $self->{view} ||= BusyBird::Main::PSGI::View->new(main_obj => $self->{main_obj}, script_name => $env->{SCRIPT_NAME});
60 185 100       1235 if(my $dest = $self->{router}->match($env)) {
61 179         18684 my $req = Plack::Request->new($env);
62 179         1767 my $code = $dest->{code};
63 179         348 my $method = $dest->{method};
64 179 50       1043 return defined($code) ? $code->($self, $req, $dest) : $self->$method($req, $dest);
65             }else {
66 6         595 return $self->{view}->response_notfound();
67             }
68 45         316 };
69             }
70              
71             sub _build_routes {
72 45     45   73 my ($self) = @_;
73 45         302 my $tl_mapper = $self->{router}->submapper(
74             '/timelines/{timeline}', {}
75             );
76 45         1527 $tl_mapper->connect('/statuses.{format}',
77             {method => '_handle_tl_get_statuses'}, {method => 'GET'});
78 45         7172 $tl_mapper->connect('/statuses.json',
79             {method => '_handle_tl_post_statuses'}, {method => 'POST'});
80 45         4508 $tl_mapper->connect('/ack.json',
81             {method => '_handle_tl_ack'}, {method => 'POST'});
82 45         4141 $tl_mapper->connect('/updates/unacked_counts.json',
83             {method => '_handle_tl_get_unacked_counts'}, {method => 'GET'});
84 45         4459 $tl_mapper->connect($_, {method => '_handle_tl_index'}) foreach "", qw(/ /index.html /index.htm);
85 45         13904 $self->{router}->connect('/updates/unacked_counts.json',
86             {method => '_handle_get_unacked_counts'}, {method => 'GET'});
87 45         2773 foreach my $path ("/", "/index.html") {
88 90         2601 $self->{router}->connect($path, {method => '_handle_get_timeline_list'}, {method => 'GET'});
89             }
90             }
91              
92             sub _get_timeline_name {
93 152     152   241 my ($dest) = @_;
94 152         303 my $name = $dest->{timeline};
95 152 50       403 $name = "" if not defined($name);
96 152         422 $name =~ s/\+/ /g;
97 152         468 return decode_utf8(uri_unescape($name));
98             }
99              
100             sub _get_timeline {
101 135     135   268 my ($self, $dest) = @_;
102 135         424 my $name = _get_timeline_name($dest);
103 135         4825 my $timeline = $self->{main_obj}->get_timeline($name);
104 135 100       1555 if(!defined($timeline)) {
105 4         38 die qq{No timeline named $name};
106             }
107 131         241 return $timeline;
108             }
109              
110             sub _handle_tl_get_statuses {
111 70     70   193 my ($self, $req, $dest) = @_;
112             return sub {
113 70     70   4227 my $responder = shift;
114 70         91 my $timeline;
115 70         340 my $only_statuses = !!($req->query_parameters->{only_statuses});
116 70 100 100     7507 my $format = !defined($dest->{format}) ? ""
    50          
117             : (lc($dest->{format}) eq "json" && $only_statuses) ? "json_only_statuses"
118             : $dest->{format};
119             Future::Q->try(sub {
120 70         3061 $timeline = $self->_get_timeline($dest);
121 69   100     203 my $count = $req->query_parameters->{count} || 20;
122 69 50 33     1064 if(!looks_like_number($count) || int($count) != $count) {
123 0         0 die "count parameter must be an integer\n";
124             }
125 69   100     164 my $ack_state = $req->query_parameters->{ack_state} || 'any';
126 69         550 my $max_id = decode_utf8($req->query_parameters->{max_id});
127 69         1057 return future_of($timeline, "get_statuses",
128             count => $count, ack_state => $ack_state, max_id => $max_id);
129             })->then(sub {
130 65         13624 my $statuses = shift;
131 65         447 $responder->($self->{view}->response_statuses(
132             statuses => $statuses, http_code => 200, format => $format,
133             timeline_name => $timeline->name
134             ));
135             })->catch(sub {
136 5         2471 my ($error, $is_normal_error) = @_;
137 5 100       58 $responder->($self->{view}->response_statuses(
    100          
138             error => "$error", http_code => ($is_normal_error ? 500 : 400), format => $format,
139             ($timeline ? (timeline_name => $timeline->name) : ())
140             ));
141 70         794 });
142 70         854 };
143             }
144              
145             sub _handle_tl_post_statuses {
146 31     31   69 my ($self, $req, $dest) = @_;
147             return sub {
148 31     31   1972 my $responder = shift;
149             Future::Q->try(sub {
150 31         1412 my $timeline = $self->_get_timeline($dest);
151 30         139 my $posted_obj = decode_json($req->content);
152 29 100       29906 if(ref($posted_obj) ne 'ARRAY') {
153 5         18 $posted_obj = [$posted_obj];
154             }
155 29         175 return future_of($timeline, "add_statuses", statuses => $posted_obj);
156             })->then(sub {
157 27         6177 my $added_num = shift;
158 27         252 $responder->($self->{view}->response_json(200, {count => $added_num + 0}));
159             })->catch(sub {
160 4         1873 my ($e, $is_normal_error) = @_;
161 4 100       43 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
162 31         351 });
163 31         396 };
164             }
165              
166             sub _handle_tl_ack {
167 28     28   70 my ($self, $req, $dest) = @_;
168             return sub {
169 28     28   1686 my $responder = shift;
170             Future::Q->try(sub {
171 28         1292 my $timeline = $self->_get_timeline($dest);
172 27         56 my $max_id = undef;
173 27         42 my $ids = undef;
174 27 100       126 if($req->content) {
175 23         22497 my $body_obj = decode_json($req->content);
176 23 50       619 if(ref($body_obj) ne 'HASH') {
177 0         0 die "Response body must be an object.\n";
178             }
179 23         67 $max_id = $body_obj->{max_id};
180 23         66 $ids = $body_obj->{ids};
181             }
182 27         537 return future_of($timeline, "ack_statuses", max_id => $max_id, ids => $ids);
183             })->then(sub {
184 25         5083 my $acked_num = shift;
185 25         261 $responder->($self->{view}->response_json(200, {count => $acked_num + 0}));
186             })->catch(sub {
187 3         1328 my ($e, $is_normal_error) = @_;
188 3 100       33 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
189 28         310 });
190 28         340 };
191             }
192              
193             sub _handle_tl_get_unacked_counts {
194 6     6   12 my ($self, $req, $dest) = @_;
195             return sub {
196 6     6   278 my $responder = shift;
197             Future::Q->try(sub {
198 6         253 my $timeline = $self->_get_timeline($dest);
199 5         18 my $query_params = $req->query_parameters;
200 5         388 my %assumed = ();
201 5 100       17 if(defined $query_params->{total}) {
202 3         11 $assumed{total} = delete $query_params->{total};
203             }
204 5         16 foreach my $query_key (keys %$query_params) {
205 5 50       18 next if !looks_like_number($query_key);
206 5 50       46 next if int($query_key) != $query_key;
207 5         13 $assumed{$query_key} = $query_params->{$query_key};
208             }
209 5         18 my $ret_future = Future::Q->new;
210             $timeline->watch_unacked_counts(assumed => \%assumed, callback => sub {
211 5         5 my ($error, $w, $unacked_counts) = @_;
212 5         24 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
213 5 50       60 if($error) {
214 0         0 $ret_future->reject($error, 1);
215             }else {
216 5         20 $ret_future->fulfill($unacked_counts);
217             }
218 5         106 });
219 5         29 return $ret_future;
220             })->then(sub {
221 5         651 my ($unacked_counts) = @_;
222 5         28 $responder->($self->{view}->response_json(200, {unacked_counts => $unacked_counts}));
223             })->catch(sub {
224 1         265 my ($e, $is_normal_error) = @_;
225 1 50       10 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
226 6         50 });
227 6         62 };
228             }
229              
230             sub _handle_get_unacked_counts {
231 17     17   35 my ($self, $req, $dest) = @_;
232             return sub {
233 17     17   958 my $responder = shift;
234             Future::Q->try(sub {
235 17         658 my $query_params = $req->query_parameters;
236 17         2305 my $level = $query_params->{level};
237 17 100 33     125 if(not defined($level)) {
    50 66        
238 3         6 $level = "total";
239             }elsif($level ne 'total' && (!looks_like_number($level) || int($level) != $level)) {
240 0         0 die "level parameter must be an integer\n";
241             }
242 17         37 my %assumed = ();
243 17         53 foreach my $query_key (keys %$query_params) {
244 62 100       677 next if substr($query_key, 0, 3) ne 'tl_';
245 46         138 $assumed{decode_utf8(substr($query_key, 3))} = $query_params->{$query_key};
246             }
247 17         217 my $ret_future = Future::Q->new;
248             $self->{main_obj}->watch_unacked_counts(
249             level => $level, assumed => \%assumed, callback => sub {
250 15         20 my ($error, $w, $tl_unacked_counts) = @_;
251 15         53 $w->cancel(); ## immediately cancel the watcher to prevent multiple callbacks
252 15 50       428 if($error) {
253 0         0 $ret_future->reject($error, 1);
254             }else {
255 15         53 $ret_future->fulfill($tl_unacked_counts);
256             }
257             }
258 17         372 );
259 15         98 return $ret_future;
260             })->then(sub {
261 15         2064 my ($tl_unacked_counts) = @_;
262 15         84 $responder->($self->{view}->response_json(200, {unacked_counts => $tl_unacked_counts}));
263             })->catch(sub {
264 2         883 my ($e, $is_normal_error) = @_;
265 2 50       20 $responder->($self->{view}->response_json(($is_normal_error ? 500 : 400), {error => "$e"}));
266 17         144 });
267 17         168 };
268             }
269              
270             sub _handle_tl_index {
271 17     17   37 my ($self, $req, $dest) = @_;
272 17         69 return $self->{view}->response_timeline(_get_timeline_name($dest));
273             }
274              
275             sub _handle_get_timeline_list {
276 10     10   24 my ($self, $req, $dest) = @_;
277             return sub {
278 10     10   951 my $responder = shift;
279             Future::Q->try(sub {
280 10         682 my $num_per_page = $self->{main_obj}->get_config('timeline_list_per_page');
281 112         1577 my @timelines = grep {
282 10         53 !$self->{main_obj}->get_timeline_config($_->name, "hidden")
283             } $self->{main_obj}->get_all_timelines();
284 10 50       101 if(@timelines == 0) {
285 0         0 die "No visible timeline. Probably you must configure config.psgi to create a timeline.";
286             }
287 10         92 my $page_num = ceil(scalar(@timelines) / $num_per_page);
288 10         21 my $cur_page = 0;
289 10         91 my $query = $req->query_parameters;
290 10 100       1167 if(defined $query->{page}) {
291 7 100 100     118 if(!looks_like_number($query->{page}) || $query->{page} < 0 || $query->{page} >= $page_num) {
      100        
292 4         51 die "Invalid page parameter\n";
293             }
294 3         8 $cur_page = $query->{page};
295             }
296 6         94 my @target_timelines = @timelines[($cur_page * $num_per_page) .. min(($cur_page+1) * $num_per_page - 1, $#timelines)];
297 24         728 return Future::Q->needs_all(map { future_of($_, "get_unacked_counts") } @target_timelines)->then(sub {
298 6         3160 my (@unacked_counts_list) = @_;
299 24         78 my @timeline_unacked_counts = map {
300 6         59 +{ name => $target_timelines[$_]->name, counts => $unacked_counts_list[$_] }
301             } 0 .. $#target_timelines;
302 6         63 $responder->( $self->{view}->response_timeline_list(
303             timeline_unacked_counts => \@timeline_unacked_counts,
304             total_page_num => $page_num,
305             cur_page => $cur_page
306             ) );
307 6         19 });
308             })->catch(sub {
309 4         1148 my ($error, $is_normal_error) = @_;
310 4 50       42 $responder->($self->{view}->response_error_html(
311             ($is_normal_error ? 500 : 400), $error
312             ));
313 10         184 });
314 10         159 };
315             }
316              
317             1;
318              
319              
320             __END__