3 # $Id: keitairc,v 1.30 2006-08-03 07:19:47 morimoto Exp $
5 # Copyright (c) Jun Morimoto <morimoto@mrmt.net>
6 # This program is covered by the GNU General Public License 2
8 # Depends: libjcode-pm-perl, libpoe-component-irc-perl,
9 # liburi-perl, libwww-perl, libappconfig-perl
11 my $rcsid = q$Id: keitairc,v 1.30 2006-08-03 07:19:47 morimoto Exp $;
12 my ($version) = $rcsid =~ m#,v ([0-9.]+)#;
17 use POE::Component::Server::TCP;
18 use POE::Filter::HTTPD;
19 use POE::Component::IRC;
22 use AppConfig qw(:argcount);
24 use constant true => 1;
25 use constant false => 0;
26 use constant cookie_ttl => 86400*3; # 3 days
28 my $config = AppConfig->new(
32 ARGCOUNT => ARGCOUNT_ONE,
35 qw(irc_nick irc_username irc_desc
36 irc_server irc_port irc_password
37 au_subscriber_id au_pcsv use_cookie
38 web_port web_title web_lines web_root
39 web_username web_password show_newmsgonly)
42 $config->file('/etc/keitairc');
43 $config->file($ENV{'HOME'} . '/.keitairc');
47 if(defined $config->web_root){
48 $docroot = $config->web_root;
51 # join
\e$B$7$F$$$k%A%c%M%k$NL>>N$r5-O?$9$k%O%C%7%e
\e(B
54 # join
\e$B$7$F$$$k%A%c%M%k$NL>>N$r5-O?$9$k%O%C%7%e
\e(B
57 #
\e$B%A%c%M%k$N2qOCFbMF$r5-O?$9$k%O%C%7%e
\e(B
58 my (%channel_buffer, %channel_recent);
60 #
\e$B3F%A%c%M%k$N:G=*%"%/%;%9;~9o!":G?7H/8@;~9o
\e(B
70 POE::Component::IRC->new('keitairc');
72 _start => \&on_irc_start,
73 irc_join => \&on_irc_join,
74 irc_part => \&on_irc_part,
75 irc_public => \&on_irc_public,
76 irc_notice => \&on_irc_notice,
77 irc_topic => \&on_irc_topic,
78 irc_332 => \&on_irc_topicraw,
79 irc_ctcp_action => \&on_irc_ctcp_action,
82 # web server component
83 POE::Component::Server::TCP->new(
85 Port => $config->web_port,
86 ClientFilter => 'POE::Filter::HTTPD',
87 ClientInput => \&on_web_request
93 ################################################################
95 my $kernel = $_[KERNEL];
96 $kernel->post('keitairc' => 'register' => 'all');
97 $kernel->post('keitairc' => 'connect' => {
98 Nick => $config->irc_nick,
99 Username => $config->irc_username,
100 Ircname => $config->irc_desc,
101 Server => $config->irc_server,
102 Port => $config->irc_port,
103 Password => $config->irc_password
107 ################################################################
109 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
112 # chop off after the gap (bug workaround of madoka)
114 my $canon_channel = &canon_name($channel);
116 $channel_name{$canon_channel} = $channel;
117 unless ($who eq $config->irc_nick) {
118 &add_message($channel, undef, "$who joined");
122 ################################################################
124 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
127 # chop off after the gap (bug workaround of POE::Filter::IRC)
129 my $canon_channel = &canon_name($channel);
131 if ($who eq $config->irc_nick) {
132 delete $channel_name{$canon_channel};
134 &add_message($channel, undef, "$who leaves");
138 ################################################################
140 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
142 $channel = $channel->[0];
143 $msg = Jcode->new($msg, 'jis')->euc;
144 &add_message($channel, $who, $msg);
147 ################################################################
149 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
151 $channel = $channel->[0];
152 $msg = Jcode->new($msg, 'jis')->euc;
153 &add_message($channel, $who, $msg);
156 ################################################################
158 my ($kernel, $who, $channel, $topic) = @_[KERNEL, ARG0 .. ARG2];
160 $topic = Jcode->new($topic, 'jis')->euc;
161 &add_message($channel, undef, "$who set topic: $topic");
162 $topic{&canon_name($channel)} = $topic;
165 ################################################################
167 my ($kernel, $raw) = @_[KERNEL, ARG1];
168 my ($channel, $topic) = split(/ :/, $raw, 2);
169 $topic{&canon_name($channel)} = $topic;
172 ################################################################
173 sub on_irc_ctcp_action{
174 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
176 $channel = $channel->[0];
177 $msg = sprintf('* %s %s', $who, Jcode->new($msg, 'jis')->euc);
178 &add_message($channel, '', $msg);
181 ################################################################
182 # $msg
\e$B$O
\e(B EUC
\e$B$K$J$C$F$$$k$O$:
\e(B
183 # $channel
\e$B$O
\e(B jis
\e$B$G$-$F$k$>
\e(B
185 my($channel, $who, $msg) = @_;
189 $message = sprintf('%s %s> %s', &now, $who, $msg);
191 $message = sprintf('%s %s', &now, $msg);
194 my $canon_channel = &canon_name($channel);
195 my @tmp = split("\n", $channel_buffer{$canon_channel});
198 my @tmp2 = split("\n", $channel_recent{$canon_channel});
199 push @tmp2, $message;
201 if(@tmp > $config->web_lines){
202 $channel_buffer{$canon_channel} =
203 join("\n", splice(@tmp, -$config->web_lines));
205 $channel_buffer{$canon_channel} = join("\n", @tmp);
208 if(@tmp2 > $config->web_lines){
209 $channel_recent{$canon_channel} =
210 join("\n", @tmp2[1 .. $config->web_lines]);
212 $channel_recent{$canon_channel} = join("\n", @tmp2);
215 $mtime{$canon_channel} = time;
218 $unread_lines{$canon_channel} = scalar(@tmp2);
220 if($unread_lines{$canon_channel} > $config->web_lines){
221 $unread_lines{$canon_channel} = $config->web_lines;
225 ################################################################
227 my ($sec,$min,$hour) = localtime(time);
228 sprintf('%02d:%02d', $hour, $min);
231 ################################################################
240 ################################################################
242 my $accesskey = shift;
245 sprintf('%d ', $accesskey);
251 ################################################################
257 for my $canon_channel (sort {
258 $mtime{$b} <=> $mtime{$a};
259 }(keys(%channel_name))){
260 $channel = $channel_name{$canon_channel};
262 $buf .= &label($accesskey);
265 $buf .= sprintf('<a accesskey="%1d" href="%s%s">%s</a>',
268 uri_escape($channel),
269 &compact_channel_name($channel));
271 $buf .= sprintf('<a href="%s%s">%s</a>',
273 uri_escape($channel),
274 &compact_channel_name($channel));
280 if($unread_lines{$canon_channel}){
281 $buf .= sprintf(' <a href="%s%s,recent">%s</a>',
283 uri_escape($channel),
284 $unread_lines{$canon_channel});
289 $buf .= qq(0 <a href="$docroot" accesskey="0">refresh list</a><br>);
291 if(grep($unread_lines{$_}, keys %unread_lines)){
292 $buf .= qq(* <a href="$docroot,recent" accesskey="*">recent</a><br>);
296 $buf .= qq(# <a href="$docroot,topics" accesskey="#">topics</a><br>);
299 $buf .= qq( - keitairc $version);
303 ################################################################
304 #
\e$B%A%c%M%kL>>N$rC;$+$/$9$k
\e(B
305 sub compact_channel_name{
308 # #name:*.jp
\e$B$r
\e(B %name
\e$B$K
\e(B
313 #
\e$BKvHx$NC1FH$N
\e(B @
\e$B$O<h$k
\e(B (for multicast.plm)
319 ################################################################
323 tr/A-Z[\\]^/a-z{|}~/;
328 ################################################################
332 push @buf, sprintf('<a href="%s">%s</a>', $url, $url);
333 if(defined $config->au_pcsv && $ENV{HTTP_USER_AGENT} =~ /^KDDI-/){
334 push @buf, sprintf('<a href="device:pcsiteviewer?url=%s">[PCSV]</a>', $url);
336 push @buf, sprintf('<a href="http://www.google.com/gwt/n?u=%s&hl=ja&mrestrict=xhtml|chtml&lr=&inlang=ja&client=ms-kddi-jp">[GWT]</a>', uri_escape($url));
340 ################################################################
345 my @src = (reverse(split("\n", shift)))[0 .. $config->web_lines];
353 unless(s|\b(https?://[!-;=-\177]+)\b|link_url($1)|eg){
354 unless(s|\b(www\.[!-\177]+)\b|link_url($1)|eg){
356 unless(s|\b(0\d{1,3})([-(]?)(\d{2,4})([-)]?)(\d{4})\b|<a href="tel:$1$3$5">$1$2$3$4$5</a>|g){
357 s|\b(\w[\w.+=-]+\@[\w.-]+[\w]\.[\w]{2,4})\b|<a href="mailto:$1">$1</a>|g;
367 '<pre>' . join("\n", @buf) . '</pre>';
370 ################################################################
372 my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
374 # Filter::HTTPD sometimes generates HTTP::Response objects.
375 # They indicate (and contain the response for) errors that occur
376 # while parsing the client's HTTP request. It's easiest to send
377 # the responses as they are and finish up.
378 if($request->isa('HTTP::Response')){
379 $heap->{client}->put($request);
380 $kernel->yield('shutdown');
385 my $cookie_authorized;
386 if($config->use_cookie){
388 for(split(/; */, $request->header('Cookie'))){
389 my ($name, $value) = split(/=/);
390 $value =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('C', hex($1))/eg;
391 $cookie{$name} = $value;
394 if($cookie{username} eq $config->web_username &&
395 $cookie{passwd} eq $config->web_password){
396 $cookie_authorized = true;
401 unless($cookie_authorized){
402 unless(defined($config->au_subscriber_id) &&
403 $request->header('x-up-subno') eq $config->au_subscriber_id){
404 if(defined($config->web_username)){
405 unless($request->headers->authorization_basic eq
406 $config->web_username . ':' . $config->web_password){
407 my $response = HTTP::Response->new(401);
408 $response->push_header(WWW_Authenticate =>
409 qq(Basic Realm="keitairc"));
410 $heap->{client}->put($response);
411 $kernel->yield('shutdown');
418 my $uri = $request->uri;
419 my $content = '<html><head>';
420 $content .= '<meta http-equiv="Cache-Control" content="max-age=0" />';
422 # POST
\e$B$5$l$F$-$?$b$N$OH/8@
\e(B
423 if($request->method =~ /POST/i){
424 my $message = $request->content;
426 $message =~ s/\+/ /g;
427 $message = uri_unescape($message);
429 if(length($message)){
431 my $channel = uri_unescape($uri);
432 $poe_kernel->post('keitairc',
434 Jcode->new($channel)->jis,
435 Jcode->new($message)->jis);
436 &add_message($channel, $config->irc_nick,
437 Jcode->new($message)->euc);
438 $message_added = true;
442 # store and remove attached options from uri
445 my @opts = split(',', $uri);
447 grep($option{$_} = $_, @opts);
452 $content .= '<title>' . $config->web_title . '</title>';
453 $content .= '</head>';
454 $content .= '<body>';
457 # recent messages on every channel
458 for my $canon_channel (sort keys %channel_name){
459 my $channel = $channel_name{$canon_channel};
460 if(length($channel) &&
461 length($channel_recent{$canon_channel})){
462 $content .= '<b>' . Jcode->new($channel_name{$canon_channel})->euc . '</b>';
463 $content .= sprintf(' <a href="%s%s">more..</a><br>',
464 $docroot, uri_escape($channel));
465 $content .= &render($channel_recent{$canon_channel});
466 $unread_lines{$canon_channel} = 0;
467 $channel_recent{$canon_channel} = '';
471 $content .= qq(<a accesskey="8" href="$docroot">ch list[8]</a>);
472 }elsif($option{topics}){
473 # topic on every channel
474 for my $canon_channel (sort keys %channel_name){
475 my $channel = $channel_name{$canon_channel};
477 $content .= sprintf(' <a href="%s%s">%s</a><br>',
478 $docroot, uri_escape($channel),
479 Jcode->new($channel_name{$canon_channel})->euc);
480 $content .= &escape(Jcode->new($topic{$canon_channel})->euc);
484 $content .= qq(<br><a accesskey="8" href="$docroot">ch list[8]</a>);
487 $content .= &index_page;
490 # channel conversation
494 # Apart from the the requirement that the first character
495 # being either '&', '#', '+' or '!' (hereafter called "channel
496 # prefix"). The only restriction on a channel name is that it
497 # SHALL NOT contain any spaces (' '), a control G (^G or ASCII
498 # 7), a comma (',' which is used as a list item separator by
499 # the protocol). Also, a colon (':') is used as a delimiter
500 # for the channel mask. The exact syntax of a channel name is
501 # defined in "IRC Server Protocol" [IRC-SERVER].
503 # so we use white space as separator character of channel name
504 # and command argument.
506 my $channel = uri_unescape($uri);
508 $content .= '<title>' . $config->web_title . ": $channel</title>";
509 $content .= '</head>';
510 $content .= '<body>';
512 $content .= '<a name="1"></a>';
513 $content .= '<a accesskey="7" href="#1"></a>';
515 $content .= sprintf('<form action="%s%s" method="post">',
516 $docroot, uri_escape($channel));
517 $content .= '<input type="text" name="m" size="10">';
518 $content .= '<input type="submit" accesskey="1" value="OK[1]">';
519 $content .= qq(<a accesskey="8" href="$docroot">ch list[8]</a><br>);
520 $content .= '</form>';
522 my $canon_channel = &canon_name($channel);
523 if(defined($channel_name{$canon_channel})){
524 if(defined($channel_buffer{$canon_channel}) &&
525 length($channel_buffer{$canon_channel})){
526 $content .= '<a accesskey="9" href="#2"></a>';
527 if($option{recent} ||
528 (defined($config->show_newmsgonly) && $message_added)){
529 $content .= &render($channel_recent{$canon_channel});
530 $content .= sprintf('<a accesskey="5" href="%s%s">more[5]</a>',
531 $docroot, uri_escape($channel));
533 $content .= &render($channel_buffer{$canon_channel});
535 $content .= '<a accesskey="9" href="#2"></a>';
536 $content .= '<a name="2"></a>';
538 $content .= 'no message here yet';
541 $content .= 'no such channel';
545 $message_added = false;
547 # clear unread counter
548 $unread_lines{$canon_channel} = 0;
550 # clear recent messages buffer
551 $channel_recent{$canon_channel} = '';
554 $content .= '</body></html>';
556 my $response = HTTP::Response->new(200);
558 if($config->use_cookie){
559 my ($sec, $min, $hour, $mday, $mon, $year, $wday) =
560 localtime(time + cookie_ttl);
562 sprintf('%.3s, %.2d-%.3s-%.4s %.2d:%.2d:%.2d',
563 qw(Sun Mon Tue Wed Thu Fri Sat)[$wday],
565 qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon],
570 $response->push_header('Set-Cookie',
571 sprintf("username=%s; expires=%s; \n",
572 $config->web_username, $expiration));
573 $response->push_header('Set-Cookie',
574 sprintf("passwd=%s; expires=%s; \n",
575 $config->web_password, $expiration));
578 $response->push_header('Content-type', 'text/html; charset=Shift_JIS');
579 $response->content(Jcode->new($content)->sjis);
580 $heap->{client}->put($response);
581 $kernel->yield('shutdown');