AnyEvent::IRC つかってみた

ただ作ってみたくなったので実装してみました。発言されたURLのタイトルとContent-Typeを返すボットです。

http://github.com/punytan/mobijiro

名前はアレを真似てつけました。

レスポンスの文字コード

AnyEvent::HTTP::http_get で取得すると、 UTF-8 だったり、 EUC-JP であったりと、 body の文字コードが何か検討がつかない。なので、 HTTP::Message の decoded_content を使うことでそのあたりの処理を楽にしています。その役割を担うのが Tatsumaki::HTTPClient。

ソース

use strict;
use warnings;
use feature qw/say/;
use Data::Dumper;
use Encode;
use Data::Validate::URI qw/is_uri/;
use AnyEvent;
use AnyEvent::HTTP;
use AnyEvent::IRC;
use AnyEvent::IRC::Connection;
use AnyEvent::IRC::Client;
use Tatsumaki::HTTPClient;
use Web::Scraper;

our $CONFIG = {
    ch => '#',
    server => 'irc.freenode.net',
    port => 6667,
    info => {
        nick => '',
        user => '',
        real => 'the bot',
    },
};

#-----

our $CONNECTION = 0;
our $LT = scalar localtime;

my @conf = ( $CONFIG->{server}, $CONFIG->{port}, $CONFIG->{info} );

my $cv = AnyEvent->condvar;

my $ua = Tatsumaki::HTTPClient->new;

my $cl;

my $ltw; $ltw = AnyEvent->timer(
    after    => 1,
    interval => 1,
    cb       => sub { $LT = scalar localtime; },
);

my $t; $t = AnyEvent->timer (after => 5, interval=> 30, cb => sub {
    say "[ $LT ] connection status : $CONNECTION";
    connect_to_server() unless $CONNECTION;
});

my $scraper = scraper {
    process '/html/head/title', 'title' => 'TEXT';
};

$cv->recv;

sub connect_to_server {
    say "[ $LT ] start connection";

    $cl = AnyEvent::IRC::Client->new;
    $cl->reg_cb(
        'connect' => sub {
            my ( $cl, $err ) = @_;
            if ( defined $err ) {
                print "[ $LT ] Connect ERROR! => $err\n";
                $CONNECTION = 0;
                $cv->broadcast;
            }
            else {
                print "[ $LT ] Connected! Yay!\n";
                $CONNECTION = 1;
            }
        },
        registered => sub {
            my ($self) = @_;
            print "[ $LT ] registered!\n";
            $cl->enable_ping (60);

            $cl->send_srv("JOIN", $CONFIG->{ch});
            $cl->send_chan($CONFIG->{ch}, "NOTICE", $CONFIG->{ch}, "hi, i'm a bot!");
        },
        irc_001 => sub {
            say "[ $LT ] irc_001";
        },
        irc_privmsg => sub {
            my ($self, $msg) = @_;
            my $message = decode_utf8 $msg->{params}[1];
            process_msg($message);
        },
        disconnect => sub {
            $CONNECTION = 0;
            print "[ $LT ] Oh, got a disconnect: $_[1], exiting...\n";
        }
    );

    $cl->connect(@conf);
}

sub process_msg {
    my $msg = shift;

    my @url_list = $msg =~ m{(http://[\S]+)}g;

    for my $url (@url_list) {
        say "[ $LT ] $url";

        if (is_uri($url)) {
            $ua->get($url, timeout => 3, sub {
                    my $res = shift;

                    my $info = {};
                    my $decoded_content = $res->decoded_content;

                    $info->{content_type} = $res->headers->content_type;

                    if ($res->is_success) {
                        my $data = $scraper->scrape($res->decoded_content);
                        $info->{title} = $data->{title};
                    } else {
                        $info->{title} = 'NO TITLE';
                    }

                    my $msg = encode_utf8(" $info->{title} [ Content-Type: $info->{content_type} ] ");

                    $cl->send_chan($CONFIG->{ch}, "NOTICE", $CONFIG->{ch}, "$msg");
                }
            );
        } 
        else {
            $cl->send_chan($CONFIG->{ch}, "NOTICE", $CONFIG->{ch}, "$url is not valid URL");
        }
    }
}

__END__

参考

http://d.hatena.ne.jp/tokuhirom/20090712/1247408876


perlcasual #02 でLTしてきました

         ,. -‐'''''""¨¨¨ヽ
         (.___,,,... -ァァフ|          あ…ありのまま 今 起こった事を話すぜ!
          |i i|    }! }} //|
         |l、{   j} /,,ィ//|       『 dankogai さんと makamaka さんの前で
        i|:!ヾ、_ノ/ u {:}//ヘ        Encode モジュールと絡めて JSON モジュールの LT をした 』
        |リ u' }  ,ノ _,!V,ハ |
       /´fト、_{ル{,ィ'eラ , タ人        な… 何を言ってるのか わからねーと思うが
     /'   ヾ|宀| {´,)⌒`/ |<ヽトiゝ        おれも何をしたのかわからなかった…
    ,゙  / )ヽ iLレ  u' | | ヾlトハ〉
     |/_/  ハ !ニ⊇ '/:}  V:::::ヽ        頭がどうにかなりそうだった…
    // 二二二7'T'' /u' __ /:::::::/`ヽ
   /'´r -―一ァ‐゙T´ '"´ /::::/-‐  \    Wide character in print at だとか malformed JSON string だとか
   / //   广¨´  /'   /:::::/´ ̄`ヽ ⌒ヽ    そんなチャチなもんじゃあ 断じてねえ
  ノ ' /  ノ:::::`ー-、___/::::://       ヽ  }
_/`丶 /:::::::::::::::::::::::::: ̄`ー-{:::...       イ  もっと恐ろしいものの片鱗を味わったぜ…

資料

JSON

http://docs.google.com/present/view?id=df7ztscv_5gnr9w4cv

Data::Dumperの資料

お蔵入りになった資料です

http://docs.google.com/present/view?id=df7ztscv_1fj593sdc

補足

http://www.donzoko.net/cgi-bin/tdiary/20100422.html

LTまとめ

http://webtech-walker.com/archive/2010/04/22143413.html

http://d.hatena.ne.jp/sfujiwara/20100421/1271868858

まとめ

懇親会も含めて楽しかったです!

主催のyusukebeさんをはじめ、関係者の皆様ありがとうございました。

内容は参加された方がまとめられているのでそちらへのリンクで代替します

http://wo.skr.jp/mt/2010/04/perl-02.html

http://blog.nobjas.net/2010/04/%E3%82%AB%E3%82%B8%E3%83%A5%E3%82%A2%E3%83%ABperl-02-%E6%98%A5%E3%81%AEperl%E3%83%95%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A5%E3%83%9E%E3%83%B3%E3%82%A2%E3%83%AF%E3%83%BC-%E3%82%92%E7%B5%82%E3%81%88/

http://d.hatena.ne.jp/ymko/20100422/1271900930

戦利品


JSONモジュールの encode_json / decode_json と to_json / from_json について調べてみた

JSONモジュールの encode_json / decode_json と to_json / from_json について調べてみた。

間違いがあった場合は指摘お願いします。

encode_json / decode_json

encode_json / decode_json は Encodeモジュールの encode_utf8 / decode_utf8 にJSONのシリアライザー・デシリアライザーがくっ付いたようなもの。

to_json / from_json

その一方、to_json / from_json は引数・戻り値共に flagged utf8 で扱う。JSONのシリアライザー・デシリアライザーのみ。

よって、エンコード周りに関しては自分で責任を持ってやる必要がある。

utf8(追記:コメントの指摘により加筆修正)

OOインターフェイスには utf8 メソッドがある。

引数が true もしくは「引数なし」では Enable となり、下記のようになる。 Disable にするには0(falseになる値)をセットする。

get_utf8 で現在の状態が確認できる。デフォルトはDisable

JSON->new->get_utf8 ? print "true" : print "false"; # false
encode_json と JSON->new->utf8->encode は同義
$json_text = encode_json $perl_scalar;
$json_text = JSON->new->utf8->encode ($perl_scalar);
decode_json と JSON->new->utf8->decode ($json_text) は同義
$perl_scalar = decode_json $json_text;
$perl_scalar = JSON->new->utf8->decode ($json_text);
to_json と JSON->new->encode は同義
$json_text = to_json($perl_scalar);
$json_text = JSON->new->encode($perl_scalar);
from_json と JSON->decode は同義
$perl_scalar = from_json($json_text);
$perl_scalar = JSON->decode($json_text);

サンプルコード

#!/usr/bin/perl

use strict;
use warnings;
use utf8;
use feature qw/say/;
use Encode;
use Data::Dumper;
use JSON;

my $v_1 = {
    name => 'いろはにほへと',
    value => 'ちりぬるを',
};

my $v_2 = {
    name => 'わかよたれそ',
    value => 'つねならむ',
};

my $v_3 = {
    name => 'うゐのおくやま',
    value => 'けふこへて',
};

$v_1->{name}  = encode_utf8 $v_1->{name};
$v_1->{value} = encode_utf8 $v_1->{value};

say 'Wrong!!! ' . encode_json $v_1; # 多重エンコードしていることになる

say "Right: " .  encode_json $v_2;

say "Right: " .  encode_utf8 to_json($v_3);

my $to_json = to_json($v_3);
print Dumper "Right: ", from_json $to_json;

exit;

1;
__END__

# output

Wrong!!! {"value":"ã&#129;¡ã&#130;&#138;ã&#129;¬ã&#130;&#139;ã&#130;&#146;","name":"ã&#129;&#132;ã&#130;&#141;ã&#129;¯ã&#129;&#171;ã&#129;&#187;ã&#129;¸ã&#129;¨"}
Right: {"value":"つねならむ","name":"わかよたれそ"}
Right: {"value":"けふこへて","name":"うゐのおくやま"}

$VAR1 = 'Right: ';
$VAR2 = {
          'value' => "\x{3051}\x{3075}\x{3053}\x{3078}\x{3066}",
          'name' => "\x{3046}\x{3090}\x{306e}\x{304a}\x{304f}\x{3084}\x{307e}"
        };

追記(2010.04.18)

あわせてこちらも

http://www.donzoko.net/cgi-bin/tdiary/20100406.html#p0

http://www.donzoko.net/cgi-bin/tdiary/20080329.html#p01

latin1とasciiについてmakamakaさんに教えていただきました。

http://togetter.com/li/15145


AnyEvent::HTTP で Cookie を扱う

ここ数週間ずっと悩んでいたことがようやく解決した。ドキュメントも付けておいたのでgitsからとってきて適当にやってください。

http://gist.github.com/351087


github はじめました

http://github.com/punytan


AnyEvent の疑問点 2 つ

AnyEventのエラー処理

下記のコードがメインのイベントループである時、 on_error と on_eof の場合にイベントループが終了する。Twitter Streaming API との接続が切れた場合のエラー処理として、再接続を試みるにはどんな方法があるか思いつかない

my $cv = AE::cv;
my $streamer = AnyEvent::Twitter::Stream->new(
    username => $config->{username},
    password => $config->{password},
    method   => 'filter',
    track    => $word,
    on_tweet => sub { on_tweet_cb(@_); },
    on_error => sub { warn shift; $cv->send; },
    on_eof   => sub { $cv->send; },
);
$cv->recv;

AE::HTTP with Cookies

ドキュメントには

cookie_jar => $hash_ref

Passing this parameter enables (simplified) cookie-processing, loosely based on the original netscape specification.

The $hash_ref must be an (initially empty) hash reference which will get updated automatically. It is possible to save the cookie_jar to persistent storage with something like JSON or Storable, but this is not recommended, as expiry times are currently being ignored.

Note that this cookie implementation is not of very high quality, nor meant to be complete. If you want complete cookie management you have to do that on your own. cookie_jar is meant as a quick fix to get some cookie-using sites working. Cookies are a privacy disaster, do not use them unless required to.

とあるので、自分で処理しなければいけない。

LWP::UserAgent ライクに cookie_jar を使う方法はあるのだろうか。


Tatsumaki::HTTPClient というモジュールの紹介

前回のエントリに関して、 LivePostHandler で新しい項目を通知するときは POST を使ったほうが良い、と指摘されたものの、 nicoalert.pl の方で POST するのに必須なAnyEvent::HTTP ::http_post の使い方がわからなかったので get で実装していました。

AE::HTTP を使おうとすると結構骨が折れる作業になります。

現状の AnyEvent::HTTP はなんと HTTP ::Request オブジェクトからのリクエスト送信に対応していないため、自分ですべてのリクエストを組み立てる必要があるようです。

http://unknownplace.org/memo/2009/07/13/1/

このような仕様なので、 AE::HTTP を使うのは非常につらい事になりますが、 miyagawa さんから Tatsumaki::HTTPClient という AnyEvent::HTTP のラッパーがあるのでそれ使うと良いという情報をいただきました。 Tatsumaki::HTTPClient を使うといろいろとうまくやってくれます。ドキュメントがないのが非常に残念ですが、

http://github.com/miyagawa/Tatsumaki/blob/master/lib/Tatsumaki/HTTPClient.pm を読めばやっていることがわかります。

使い方

HTTP ::Request::Common の引数を普通に書いたあとにコールバックを指定してやれば良い。すばらしい!

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Encode;
use Data::Dumper;
use Tatsumaki::HTTPClient;
use AnyEvent;

my $cv = AE::cv;

my $callback = sub {
    print Dumper \@_;
    print "Done\n";
    $cv->send;
};

my $client = Tatsumaki::HTTPClient->new;
$client->post(
    'http://www.futomi.com/cgi-bin/lecture/form/posttest.cgi',
    [
        name   => 'Gisle Aas',
        email  => 'gisle@aas.no',
        gender => 'M',
        born   => '1964',
        perc   => '3%',
    ],
    $callback
);

$cv->recv;

exit;
__END__

Tatsumaki と Twiggy 使ってみた

Tatsumaki の eg/chat をほとんどそのまま流用しました。

概要

Tatsumaki

Non-blocking web framework based on Plack and AnyEvent

Twiggy

AnyEvent HTTP server for PSGI (like Thin)

ニコ生アラートサーバ

ニコニコ生放送というサービスを使って開始された番組の情報が XMLSocket の形式で送られるもの。

とりあえずデモ

(deprecated)

仕組み

  • live.html
  • live.psgi
  • nicoalert.pl

nicoalert.pl でアラートサーバに接続し、取得したものを GET パラメータでサーバに送る。 live.psgi で long/poll の管理と新規番組の情報を扱う。 live.html で Comet 。

live.html

http://github.com/beppu/jquery-ev でCometを実装

live.psgi

eg/chat/app.psgi をほぼそのまま

use strict;
use warnings;
use Tatsumaki;
use Tatsumaki::Error;
use Tatsumaki::Application;

package LivePollHandler;
use base qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use Tatsumaki::MessageQueue;

sub get {
    my ($self, $channel) = @_;
    my $mq = Tatsumaki::MessageQueue->instance($channel);
    my $client_id = $self->request->param('client_id')
        or Tatsumaki::Error::HTTP->throw(500, "'client_id' needed");
    $mq->poll_once($client_id, sub { $self->on_new_event(@_) });
}

sub on_new_event {
    my($self, @events) = @_;
    $self->write(\@events);
    $self->finish;
}

package LivePostHandler;
use base qw(Tatsumaki::Handler);
use Encode;
use AnyEvent::HTTP;
use XML::Simple;

sub get {
    my($self, $channel) = @_;

    my $v = $self->request->parameters;
    my $lv_num = $v->{lv};
    http_request(
        GET     => "http://live.nicovideo.jp/api/getstreaminfo/lv$lv_num",
        timeout => 3,
        on_body => sub {
            my ($body, $hdr) = @_;
            return if (!defined $body);
            my $res = XMLin(decode_utf8 $body);
            $res->{type} = 'message';
            my $mq = Tatsumaki::MessageQueue->instance($channel);
            $mq->publish($res);
            $self->write({ success => 1 });
        }
    );
}

package LiveHomeHandler;
use base qw(Tatsumaki::Handler);
sub get {
    my ($self) = @_;
    $self->render('live.html');
}

package main;
use File::Basename;

my $chat_re = 'stream';
my $app = Tatsumaki::Application->new([
    "/($chat_re)/poll" => 'LivePollHandler',
    "/($chat_re)/post" => 'LivePostHandler',
    "/($chat_re)" => 'LiveHomeHandler',
]);
$app->template_path(dirname(__FILE__) . "/templates");
$app->static_path(dirname(__FILE__) . "/static");

$app;
nicoalert.pl
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Encode;
use AnyEvent;
use AnyEvent::Socket;
use AnyEvent::HTTP;
use AnyEvent::Handle;
use XML::Simple;
use Data::Dumper;
use LWP::UserAgent;

my $server = get_alertinfo();

my $cv = AE::cv;
my $connection = tcp_connect(
    $server->{addr},
    $server->{port},
    sub { connection_cb(@_); }
);
$cv->recv;

sub socket_read_cb {
    my ($handle, $chat_tag) = @_;
    my $decoded_chat_tag = decode_utf8 $chat_tag;
    if ($decoded_chat_tag =~ />(.*)</) {
        my ($lv_num, $co_num, $user_id) = split/,/, $1;
        if (defined $lv_num && defined $co_num && defined $user_id) {
            http_request(
                GET => "http://live.linknode.net:8080/stream/post?lv=$lv_num",
                timeout => 3,
                on_body => sub { print Dumper \@_; }
            );
        }
    }
    $handle->push_read(line => "\0", sub{ socket_read_cb(@_); } );
}

sub connection_cb {
    my ($fh) = @_ or die $!;
    my $thread_tag = qq/<thread thread="$server->{thread}" version="20061206" res_from="-1" \/>\0/;

    my $handle; $handle = new AnyEvent::Handle(
        fh       => $fh,
        on_error => sub {
            warn "Error $_[2]\n";
            $_[0]->destroy;
        },
        on_eof   => sub {
            $handle->destroy;
            warn "Done\n";
        }
    );       
    $handle->push_write($thread_tag);
    $handle->push_read(line => "\0", sub { socket_read_cb(@_); });
}

sub get_alertinfo {
    my $url = 'http://live.nicovideo.jp/api/getalertinfo';

    my $ua = LWP::UserAgent->new();
    my $res = $ua->get($url);
    die "$res->status_line" unless ($res->is_success);
    my $xml = XMLin($res->decoded_content);
    die "Fatal: Server status $xml->{status}" if ($xml->{status} ne 'ok');
    return $xml->{ms};
}

起動

plackup -p 8080 -s Twiggy live.psgi > /dev/null 2>&1 &
perl nicoalert.pl > /dev/null &

追記

LivePostHandlerのgetをpostに書き換えてnicoalert.plのsocket_read_cb()のhttp_requestの部分をTatsumaki::HTTPClientで処理するように変更しました

追記2

http://blog.plackperl.org/2010/03/comet-applcation-with-tatsumaki-and-twiggy.html

こちらで紹介していただきました。


VPSでcpanminusを使う

Linode という VPS を契約したついでにちょうど良いので巷で話題の cpanminus を使ってみた。

依存関係もなく、何の設定もいらないので非常に楽でよい。これはおすすめ。

VSP 環境だったので、一部嵌ったところがありましたが、 miyagawa さんと lestrrat さんの的確なアドバイスによりすぐに解決できました。感謝>< その内容も含めてまとめておきました。VPS で使うときの参考になれば幸いです。

Debian をデプロイしたのでディストリごとに適宜読み替えてください。

cpanminus

CPAN::Shell の軽量版で、概要は xaicron さんのスライド http://blog.livedoor.jp/xaicron/archives/51015507.html がまとまってます。

VPS では最小限の構成で OS がデプロイされる

これのため cc やらなんやらがなかったり、いろいろ不便するので先に必要なものをインストールしておく。

apt-get install build-essential

http://packages.debian.org/lenny/build-essential

expat のインストール

XML や RSS は扱う確率が高いので expat をインストールしておく。 XML::RSS が XML::Parser に依存していて、 XML::Parser は expat に依存している。

# cd /usr/local/src
# wget http://prdownloads.sourceforge.net/expat/expat-2.0.1.tar.gz
# tar zxvf expat-2.0.1.tar.gz
# cd expat-2.0.1
# ./configure --prefix=/usr/local
# make && make install

cpanminus のインストール

apt-get で git を入れ、

$ git clone git://github.com/miyagawa/cpanminus.git
$ cd cpanminus
$ perl Makefile.PL
$ make install # or sudo make install if you're non root

ですんなりインストール完了。

モジュールのインストール

root なら

cpanm AnyEvent Plack XML::RSS

でパスの通ったところにうまくやってくれる。

cpanminus++

その他


PerlのTruth/Falsehood

Perl の truth

Programming Perl Chapter 1 より。

Truth in Perl is always evaluated in a scalar context.

  • Any string is true except for "" and "0".
  • Any number is true except for 0.
  • Any reference is true.
  • Any undefined value is false.

どう判定されるか

Truth and Falsehood

The number 0, the strings '0' and '' , the empty list () , and undef are all false in a boolean context. All other values are true. Negation of a true value by ! or not returns a special false value. When evaluated as a string it is treated as '' , but as a number, it is treated as 0.

http://perldoc.perl.org/perlsyn.html#Truth-and-Falsehood

よって、以下の結果

0          # false
1          # true
10 - 10    # false
0.00       # false
"0"        # false
""         # false
"0.00"     # true!!!
"0.00" + 0 # false
\$a        # true
undef()    # false

"0.00"がtrueで"0.00" + 0がfalse。これは嵌りポイント。


« 10 »