3 # $Id: keitairc,v 1.21 2004-07-24 08:54:51 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.21 2004-07-24 08:54:51 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 my $config = AppConfig->new(
28 ARGCOUNT => ARGCOUNT_ONE,
31 qw(irc_nick irc_username irc_desc
32 irc_server irc_port irc_password
33 web_port web_title web_lines web_root
34 web_username web_password show_newmsgonly)
37 $config->file('/etc/keitairc');
38 $config->file($ENV{'HOME'} . '/.keitairc');
42 if(defined $config->web_root){
43 $docroot = $config->web_root;
46 # join
\e$B$7$F$$$k%A%c%M%k$NL>>N$r5-O?$9$k%O%C%7%e
\e(B
49 #
\e$B%A%c%M%k$N2qOCFbMF$r5-O?$9$k%O%C%7%e
\e(B
50 my (%channel_buffer, %channel_recent);
52 #
\e$B3F%A%c%M%k$N:G=*%"%/%;%9;~9o!":G?7H/8@;~9o
\e(B
59 my ($send_chk, $update_chk);
62 POE::Component::IRC->new('keitairc');
64 _start => \&on_irc_start,
65 irc_join => \&on_irc_join,
66 irc_part => \&on_irc_part,
67 irc_public => \&on_irc_public,
68 irc_notice => \&on_irc_notice,
69 irc_ctcp_action => \&on_irc_ctcp_action,
72 # web server component
73 POE::Component::Server::TCP->new(
75 Port => $config->web_port,
76 ClientFilter => 'POE::Filter::HTTPD',
77 ClientInput => \&on_web_request
83 ################################################################
85 my $kernel = $_[KERNEL];
86 $kernel->post('keitairc' => 'register' => 'all');
87 $kernel->post('keitairc' => 'connect' => {
88 Nick => $config->irc_nick,
89 Username => $config->irc_username,
90 Ircname => $config->irc_desc,
91 Server => $config->irc_server,
92 Port => $config->irc_port,
93 Password => $config->irc_password
97 ################################################################
99 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
102 # chop off after the gap (bug workaround of madoka)
105 $channel_name{$channel}++;
106 unless ($who eq $config->irc_nick) {
107 &add_message($channel, undef, "$who joined");
111 ################################################################
113 my ($kernel, $who, $channel) = @_[KERNEL, ARG0, ARG1];
116 # chop off after the gap (bug workaround of POE::Filter::IRC)
119 if ($who eq $config->irc_nick) {
120 delete $channel_name{$channel};
122 &add_message($channel, undef, "$who leaves");
126 ################################################################
128 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
130 $channel = $channel->[0];
131 $msg = Jcode->new($msg, 'jis')->euc;
132 &add_message($channel, $who, $msg);
135 ################################################################
137 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
139 $channel = $channel->[0];
140 $msg = Jcode->new($msg, 'jis')->euc;
141 &add_message($channel, $who, $msg);
144 ################################################################
145 sub on_irc_ctcp_action{
146 my ($kernel, $who, $channel, $msg) = @_[KERNEL, ARG0 .. ARG2];
148 $channel = $channel->[0];
149 $msg = sprintf('* %s %s', $who, Jcode->new($msg, 'jis')->euc);
150 &add_message($channel, '', $msg);
153 ################################################################
154 # $msg
\e$B$O
\e(B EUC
\e$B$K$J$C$F$$$k$O$:
\e(B
155 # $channel
\e$B$O
\e(B jis
\e$B$G$-$F$k$>
\e(B
157 my($channel, $who, $msg) = @_;
161 $message = sprintf('%s %s> %s', &now, $who, $msg);
163 $message = sprintf('%s %s', &now, $msg);
166 my @tmp = split("\n", $channel_buffer{$channel});
169 my @tmp2 = split("\n", $channel_recent{$channel});
170 push @tmp2, $message;
172 if(@tmp > $config->web_lines){
173 $channel_buffer{$channel} =
174 join("\n", splice(@tmp, -$config->web_lines));
176 $channel_buffer{$channel} = join("\n", @tmp);
179 if(@tmp2 > $config->web_lines){
180 $channel_recent{$channel} =
181 join("\n", splice(@tmp2, -$config->web_lines));
183 $channel_recent{$channel} = join("\n", @tmp2);
186 $mtime{$channel} = time;
189 $unread{$channel} = scalar(@tmp2);
191 if ($unread{$channel} > $config->web_lines) {
192 $unread{$channel} = $config->web_lines;
196 ################################################################
198 my ($sec,$min,$hour) = localtime(time);
199 sprintf('%02d:%02d', $hour, $min);
202 ################################################################
211 ################################################################
213 my $accesskey = shift;
216 sprintf('%d ', $accesskey);
222 ################################################################
227 for my $channel (sort {
228 $mtime{$b} <=> $mtime{$a};
229 }(keys(%channel_name))){
231 $buf .= &label($accesskey);
234 $buf .= sprintf('<a accesskey="%1d" href="%s%s">%s</a>',
237 uri_escape($channel),
238 &compact_channel_name($channel));
240 $buf .= sprintf('<a href="%s%s">%s</a>',
242 uri_escape($channel),
243 &compact_channel_name($channel));
249 if($unread{$channel} > 0){
250 $buf .= sprintf(' <a href="%s%s.update">%d</a>',
252 uri_escape($channel),
258 $buf .= qq(0 <a href="$docroot" accesskey="0">refresh list</a><br>);
259 $buf .= qq( - keitairc $version);
263 ################################################################
264 #
\e$B%A%c%M%kL>>N$rC;$+$/$9$k
\e(B
265 sub compact_channel_name{
268 # #name:*.jp
\e$B$r
\e(B %name
\e$B$K
\e(B
273 #
\e$BKvHx$NC1FH$N
\e(B @
\e$B$O<h$k
\e(B (for multicast.plm)
279 ################################################################
284 my @src = (reverse(split("\n", shift)))[0 .. $config->web_lines];
292 unless(s,\b(https?://[!-;=-\177]+)\b,<a href="$1">$1</a>,g){
293 unless(s|\b(www\.[!-\177]+)\b|<a href="http://$1">$1</a>|g){
295 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){
296 s|\b(\w[\w.+=-]+\@[\w.-]+[\w]\.[\w]{2,4})\b|<a href="mailto:$1">$1</a>|g;
306 '<pre>' . join("\n", @buf) . '</pre>';
309 ################################################################
311 my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
313 # Filter::HTTPD sometimes generates HTTP::Response objects.
314 # They indicate (and contain the response for) errors that occur
315 # while parsing the client's HTTP request. It's easiest to send
316 # the responses as they are and finish up.
317 if($request->isa('HTTP::Response')){
318 $heap->{client}->put($request);
319 $kernel->yield('shutdown');
323 if(defined($config->web_username)){
324 unless($request->headers->authorization_basic eq
325 $config->web_username . ':' . $config->web_password){
327 my $response = HTTP::Response->new(401);
328 $response->push_header(WWW_Authenticate =>
329 qq(Basic Realm="keitairc"));
330 $heap->{client}->put($response);
331 $kernel->yield('shutdown');
336 my $uri = $request->uri;
337 my $content = '<html><head>';
338 $content .= '<meta http-equiv="Cache-Control" content="max-age=0" />';
340 # POST
\e$B$5$l$F$-$?$b$N$OH/8@
\e(B
341 if($request->method =~ /POST/i){
342 my $message = $request->content;
344 $message =~ s/\+/ /g;
345 $message = uri_unescape($message);
347 if(length($message)){
349 my $channel = uri_unescape($uri);
350 $poe_kernel->post('keitairc',
352 Jcode->new($channel)->jis,
353 Jcode->new($message)->jis);
354 &add_message($channel, $config->irc_nick,
355 Jcode->new($message)->euc);
357 # set flag for "send message"
363 $content .= '<title>' . $config->web_title . '</title>';
364 $content .= '</head>';
365 $content .= '<body>';
366 $content .= &index_page;
370 $update_chk = ($uri =~ /.*.update/);
371 if ($update_chk eq 1) {
375 my $channel = uri_unescape($uri);
377 $content .= '<title>' . $config->web_title . ": $channel</title>";
378 $content .= '</head>';
379 $content .= '<body>';
381 $content .= '<a name="1"></a>';
382 $content .= '<a accesskey="7" href="#1"></a>';
384 $content .= sprintf('<form action="%s%s" method="post">',
385 $docroot, uri_escape($channel));
386 $content .= '<input type="text" name="m" size="10">';
387 $content .= '<input type="submit" accesskey="1" value="OK[1]">';
388 $content .= qq(<a accesskey="8" href="$docroot">back[8]</a><br>);
389 # $content .= '<input type="submit" accesskey="1" value="聆">';
390 $content .= '</form>';
392 if(defined($channel_name{$channel})){
393 if(defined($channel_buffer{$channel}) &&
394 length($channel_buffer{$channel})){
395 $content .= '<a accesskey="9" href="#2"></a>';
396 if ((($update_chk eq 1)||((defined $config->show_newmsgonly) && ($send_chk eq 1)))) {
397 $content .= &render($channel_recent{$channel});
398 $content .= sprintf('<a accesskey="5" href="%s%s">
399 ..more[5]</a>', $docroot, uri_escape($channel));
401 $content .= &render($channel_buffer{$channel});
403 $content .= '<a name="2"></a>';
405 $content .= 'no message here yet';
408 $content .= "no such channel";
414 # clear unread counter
415 $unread{$channel} = 0;
417 # clear recent messages buffer
418 $channel_recent{$channel} = '';
421 $content .= '</body></html>';
423 my $response = HTTP::Response->new(200);
424 $response->push_header('Content-type', 'text/html; charset=Shift_JIS');
425 $response->content(Jcode->new($content)->sjis);
426 $heap->{client}->put($response);
427 $kernel->yield('shutdown');