3 # $Id: keitairc,v 1.25 2004-08-29 12:24:21 morimoto Exp $
5 # Copyright (c) 2003 Jun Morimoto <morimoto@xantia.citroen.org>
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.25 2004-08-29 12:24:21 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;
27 my $config = AppConfig->new(
31 ARGCOUNT => ARGCOUNT_ONE,
34 qw(irc_nick irc_username irc_desc
35 irc_server irc_port irc_password
37 web_port web_title web_lines web_root
38 web_username web_password show_newmsgonly)
41 $config->file('/etc/keitairc');
42 $config->file($ENV{'HOME'} . '/.keitairc');
46 if(defined $config->web_root){
47 $docroot = $config->web_root;
50 # join
\e$B$7$F$$$k%A%c%M%k$NL>>N$r5-O?$9$k%O%C%7%e
\e(B
53 #
\e$B%A%c%M%k$N2qOCFbMF$r5-O?$9$k%O%C%7%e
\e(B
54 my (%channel_buffer, %channel_recent);
56 #
\e$B3F%A%c%M%k$N:G=*%"%/%;%9;~9o!":G?7H/8@;~9o
\e(B
66 POE::Component::IRC->new('keitairc');
68 _start => \&on_irc_start,
69 irc_join => \&on_irc_join,
70 irc_part => \&on_irc_part,
71 irc_public => \&on_irc_public,
72 irc_notice => \&on_irc_notice,
73 irc_ctcp_action => \&on_irc_ctcp_action,
76 # web server component
77 POE::Component::Server::TCP->new(
79 Port => $config->web_port,
80 ClientFilter => 'POE::Filter::HTTPD',
81 ClientInput => \&on_web_request
87 ################################################################
89 my $kernel = $_[KERNEL];
90 $kernel->post('keitairc' => 'register' => 'all');
91 $kernel->post('keitairc' => 'connect' => {
92 Nick => $config->irc_nick,
93 Username => $config->irc_username,
94 Ircname => $config->irc_desc,
95 Server => $config->irc_server,
96 Port => $config->irc_port,
97 Password => $config->irc_password
101 ################################################################
103 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
106 # chop off after the gap (bug workaround of madoka)
108 my $canon_channel = &canon_name($channel);
110 $channel_name{$canon_channel} = $channel;
111 unless ($who eq $config->irc_nick) {
112 &add_message($channel, undef, "$who joined");
116 ################################################################
118 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
121 # chop off after the gap (bug workaround of POE::Filter::IRC)
123 my $canon_channel = &canon_name($channel);
125 if ($who eq $config->irc_nick) {
126 delete $channel_name{$canon_channel};
128 &add_message($channel, undef, "$who leaves");
132 ################################################################
134 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
136 $channel = $channel->[0];
137 $msg = Jcode->new($msg, 'jis')->euc;
138 &add_message($channel, $who, $msg);
141 ################################################################
143 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
145 $channel = $channel->[0];
146 $msg = Jcode->new($msg, 'jis')->euc;
147 &add_message($channel, $who, $msg);
150 ################################################################
151 sub on_irc_ctcp_action{
152 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
154 $channel = $channel->[0];
155 $msg = sprintf('* %s %s', $who, Jcode->new($msg, 'jis')->euc);
156 &add_message($channel, '', $msg);
159 ################################################################
160 # $msg
\e$B$O
\e(B EUC
\e$B$K$J$C$F$$$k$O$:
\e(B
161 # $channel
\e$B$O
\e(B jis
\e$B$G$-$F$k$>
\e(B
163 my($channel, $who, $msg) = @_;
167 $message = sprintf('%s %s> %s', &now, $who, $msg);
169 $message = sprintf('%s %s', &now, $msg);
172 my $canon_channel = &canon_name($channel);
173 my @tmp = split("\n", $channel_buffer{$canon_channel});
176 my @tmp2 = split("\n", $channel_recent{$canon_channel});
177 push @tmp2, $message;
179 if(@tmp > $config->web_lines){
180 $channel_buffer{$canon_channel} =
181 join("\n", splice(@tmp, -$config->web_lines));
183 $channel_buffer{$canon_channel} = join("\n", @tmp);
186 if(@tmp2 > $config->web_lines){
187 $channel_recent{$canon_channel} =
188 join("\n", splice(@tmp2, -$config->web_lines));
190 $channel_recent{$canon_channel} = join("\n", @tmp2);
193 $mtime{$canon_channel} = time;
196 $unread_lines{$canon_channel} = scalar(@tmp2);
198 if($unread_lines{$canon_channel} > $config->web_lines){
199 $unread_lines{$canon_channel} = $config->web_lines;
203 ################################################################
205 my ($sec,$min,$hour) = localtime(time);
206 sprintf('%02d:%02d', $hour, $min);
209 ################################################################
218 ################################################################
220 my $accesskey = shift;
223 sprintf('%d ', $accesskey);
229 ################################################################
235 for my $canon_channel (sort {
236 $mtime{$b} <=> $mtime{$a};
237 }(keys(%channel_name))){
238 $channel = $channel_name{$canon_channel};
240 $buf .= &label($accesskey);
243 $buf .= sprintf('<a accesskey="%1d" href="%s%s">%s</a>',
246 uri_escape($channel),
247 &compact_channel_name($channel));
249 $buf .= sprintf('<a href="%s%s">%s</a>',
251 uri_escape($channel),
252 &compact_channel_name($channel));
258 if($unread_lines{$canon_channel}){
259 $buf .= sprintf(' <a href="%s%s,recent">%d</a>',
261 uri_escape($channel),
262 $unread_lines{$canon_channel});
267 $buf .= qq(0 <a href="$docroot" accesskey="0">refresh list</a><br>);
269 if(grep($unread_lines{$_}, keys %unread_lines)){
270 $buf .= qq(* <a href="$docroot,recent" accesskey="*">recent</a><br>);
273 $buf .= qq( - keitairc $version);
277 ################################################################
278 #
\e$B%A%c%M%kL>>N$rC;$+$/$9$k
\e(B
279 sub compact_channel_name{
282 # #name:*.jp
\e$B$r
\e(B %name
\e$B$K
\e(B
287 #
\e$BKvHx$NC1FH$N
\e(B @
\e$B$O<h$k
\e(B (for multicast.plm)
293 ################################################################
297 tr/A-Z[\\]^/a-z{|}~/;
302 ################################################################
307 my @src = (reverse(split("\n", shift)))[0 .. $config->web_lines];
315 unless(s,\b(https?://[!-;=-\177]+)\b,<a href="$1">$1</a>,g){
316 unless(s|\b(www\.[!-\177]+)\b|<a href="http://$1">$1</a>|g){
318 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){
319 s|\b(\w[\w.+=-]+\@[\w.-]+[\w]\.[\w]{2,4})\b|<a href="mailto:$1">$1</a>|g;
329 '<pre>' . join("\n", @buf) . '</pre>';
332 ################################################################
334 my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
336 # Filter::HTTPD sometimes generates HTTP::Response objects.
337 # They indicate (and contain the response for) errors that occur
338 # while parsing the client's HTTP request. It's easiest to send
339 # the responses as they are and finish up.
340 if($request->isa('HTTP::Response')){
341 $heap->{client}->put($request);
342 $kernel->yield('shutdown');
347 unless(defined($config->au_subscriber_id) &&
348 ($request->header('x-up-subno') eq $config->au_subscriber_id)) {
349 if(defined($config->web_username)){
350 unless($request->headers->authorization_basic eq
351 $config->web_username . ':' . $config->web_password){
352 my $response = HTTP::Response->new(401);
353 $response->push_header(WWW_Authenticate =>
354 qq(Basic Realm="keitairc"));
355 $heap->{client}->put($response);
356 $kernel->yield('shutdown');
362 my $uri = $request->uri;
363 my $content = '<html><head>';
364 $content .= '<meta http-equiv="Cache-Control" content="max-age=0" />';
366 # POST
\e$B$5$l$F$-$?$b$N$OH/8@
\e(B
367 if($request->method =~ /POST/i){
368 my $message = $request->content;
370 $message =~ s/\+/ /g;
371 $message = uri_unescape($message);
373 if(length($message)){
375 my $channel = uri_unescape($uri);
376 $poe_kernel->post('keitairc',
378 Jcode->new($channel)->jis,
379 Jcode->new($message)->jis);
380 &add_message($channel, $config->irc_nick,
381 Jcode->new($message)->euc);
382 $message_added = true;
386 # store and remove attached options from uri
389 my @opts = split(',', $uri);
391 grep($option{$_} = $_, @opts);
396 $content .= '<title>' . $config->web_title . '</title>';
397 $content .= '</head>';
398 $content .= '<body>';
401 # recent messages on every channel
402 for my $channel (sort keys %channel_name){
403 my $canon_channel = &canon_name($channel);
404 if(defined($channel_name{$canon_channel}) &&
405 length($channel_recent{$canon_channel})){
406 $content .= "<b>$channel_name{$canon_channel}</b>";
407 $content .= sprintf(' <a href="%s%s">more..</a><br>',
408 $docroot, uri_escape($channel));
409 $content .= &render($channel_recent{$canon_channel});
410 $unread_lines{$canon_channel} = 0;
411 $channel_recent{$canon_channel} = '';
415 $content .= qq(<a accesskey="8" href="$docroot">ch list[8]</a>);
418 $content .= &index_page;
421 # channel conversation
425 # Apart from the the requirement that the first character
426 # being either '&', '#', '+' or '!' (hereafter called "channel
427 # prefix"). The only restriction on a channel name is that it
428 # SHALL NOT contain any spaces (' '), a control G (^G or ASCII
429 # 7), a comma (',' which is used as a list item separator by
430 # the protocol). Also, a colon (':') is used as a delimiter
431 # for the channel mask. The exact syntax of a channel name is
432 # defined in "IRC Server Protocol" [IRC-SERVER].
434 # so we use white space as separator character of channel name
435 # and command argument.
437 my $channel = uri_unescape($uri);
439 $content .= '<title>' . $config->web_title . ": $channel</title>";
440 $content .= '</head>';
441 $content .= '<body>';
443 $content .= '<a name="1"></a>';
444 $content .= '<a accesskey="7" href="#1"></a>';
446 $content .= sprintf('<form action="%s%s" method="post">',
447 $docroot, uri_escape($channel));
448 $content .= '<input type="text" name="m" size="10">';
449 $content .= '<input type="submit" accesskey="1" value="OK[1]">';
450 $content .= qq(<a accesskey="8" href="$docroot">ch list[8]</a><br>);
451 # $content .= '<input type="submit" accesskey="1" value="聆">';
452 $content .= '</form>';
454 my $canon_channel = &canon_name($channel);
455 if(defined($channel_name{$canon_channel})){
456 if(defined($channel_buffer{$canon_channel}) &&
457 length($channel_buffer{$canon_channel})){
458 $content .= '<a accesskey="9" href="#2"></a>';
459 if($option{recent} ||
460 (defined($config->show_newmsgonly) && $message_added)){
461 $content .= &render($channel_recent{$canon_channel});
462 $content .= sprintf('<a accesskey="5" href="%s%s">more[5]</a>',
463 $docroot, uri_escape($channel));
465 $content .= &render($channel_buffer{$canon_channel});
467 $content .= '<a name="2"></a>';
469 $content .= 'no message here yet';
472 $content .= 'no such channel';
476 $message_added = false;
478 # clear unread counter
479 $unread_lines{$canon_channel} = 0;
481 # clear recent messages buffer
482 $channel_recent{$canon_channel} = '';
485 $content .= '</body></html>';
487 my $response = HTTP::Response->new(200);
488 $response->push_header('Content-type', 'text/html; charset=Shift_JIS');
489 $response->content(Jcode->new($content)->sjis);
490 $heap->{client}->put($response);
491 $kernel->yield('shutdown');