小ネタ

IRC がゆく

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - IRC がゆく
このエントリーをはてなブックマークに追加
開発部 _ です。

みなさんは IRC を使っていますか?
もしくは使ったことがありますか?

ネット上の個人コミュニケーション手段が msn・Yahoo!・Skype などのメッセンジャーが一般的になって久しい昨今、IRC は一部のマニアが使うニッチで古臭い手段という見方も多いようです。
そんな中 livedoor では、日々の運用作業に始まり、開発中のコンテンツ進捗、技術的な意見交換、バグ報告、ランチどこいく?、麻雀やりますよ、今夜のおかずに到るまで日常的に活用されています。
IRC で使われているツールに IRC bot(IRCボット:以下 ボット)と呼ばれる小さなプログラムがあります。
ユーザーの発言に反応したり、定期的に何かを発信したり、嫌なアイツにナニしてくれたり、と作り手の発想次第でいろいろな処理をさせることが出来ます。
時に悪用(ボットネットなど)されるなどネガティヴな印象を与えてしまうこともありますが、うまく使えばとても便利で楽しい IRC ライフが送れるハズです。

ここでは簡単なボットの利用例とサンプルを紹介したいと思います。


■業務でのボット利用案



・サーバ監視
サーバ(マシン)の負荷情報や稼働状況などのステータス通知。

・コンテンツ監視
定期バッチ処理など自動処理の完了通知。

・開発
各種コミット情報の通知。

・人
社長を監視。



■ボット 事始め



Perl で IRC を利用するために、古くは Net::IRC を、ちょっと前までは POEPOE::Component::IRC)を使うのが通例でしたが、最近では AnyEventAnyEvent::IRC::Client)を利用するのがイケメンのようです。

以下にボットが IRC に接続するまでを書いてみます。

use strict;
use AnyEvent;
use AnyEvent::IRC::Client;

# 接続設定
my $channel = '#チャンネル';
my %config = (
    server   => 'IRC サーバ',
    port     => 6667,
    info     => {
        nick => 'ニックネーム',
        real => 'リアルネーム',
    }
);

# 状態変数生成
my $condvar = AnyEvent->condvar;

# IRC イベント
my $irc = AnyEvent::IRC::Client->new;
$irc->reg_cb(
    # 接続時に呼び出されるイベント
    connect => sub {
        my ($irc, $err) = @_;
        if (defined $err) {
            print "connect error: $err\n";
            return;
        }
        print "connected.\n";
    },

    # 接続成功時に呼び出されるイベント
    registered => sub {
        print "registered.\n";
    },

    # 切断時に呼び出されるイベント
    disconnect => sub {
        print "disconnected.\n";
    },
);

# IRC サーバに接続
$irc->connect($config{server}, $config{port}, $config{info});

# $channel に join
$irc->send_srv("JOIN", $channel);

# $channel に notice メッセージを送る
$irc->send_chan($channel, "NOTICE", $channel, "Hello, $channel!");

# イベント待ち
$condvar->recv;

これで IRC サーバ $config{server} の $channel に $config{info}->{nick} なユーザー名で接続(join)し、"Hello, $channel!" と挨拶することが出来ました。 しかし、このままでは本当に挨拶するだけで何もしてくれません。

簡単な例として $channel 内のユーザーが発言したときにレスをするようにしてみます。

$irc->reg_cb(
    # $channel に発言があったとき _public() を実行
    publicmsg => sub {
        my ($irc, $chan, $msg) = @_;
        _public($msg->{params}[1]);
    }
);

sub _public {
    my $msg = shift;
    $irc->send_chan($channel, "PRIVMSG", $channel, $msg . 'だと!?');
}
出力:
<#channel> user: ほげ
<#channel> bot: ほげだと!?

実際にこのボットがいるチャンネルはだいぶウザそうです!

*****
この中に出てくる "JOIN" や "NOTICE" "PRIVMSG" などは IRC を操作するためのコマンドです。詳細は RFC 1459 で定義されています。
本格的に IRC を操ってやろう!という方には必読ですので、少々骨が折れますがぜひ確認してみてください。


■ボットを働かせる



ただレスだけではつまらないので、もう少しまともな処理をさせてみます。
手前味噌ですが、弊社コンテンツ livedoor グルメでは 公式 API を公開しています。興味のある方はぜひお試しください。

以下が、クチコミ検索 API を用いた、最新クチコミ1件を取得しお知らせする機能のサンプルになります。

use XML::LibXML;
use Encode;
use LWP::UserAgent;
use URI;

sub _public {
    my ($msg) = @_;
    eval {
        if ($msg eq "新着クチコミ") {
            _shinchaku_kuchikomi();
        }
    };
}

sub _shinchaku_kuchikomi {
    my $res = _kuchikomi_api();
    return if !$res->is_success;

    my $xml   = XML::LibXML->new->parse_string($res->decoded_content);
    my @nodes = $xml->findnodes('//evaluation');
    my $node  = $nods[0];
    my $title = encode_utf8 $node->findvalue('title');
    my $user  = encode_utf8 $node->findvalue('user/nickname');
    my $plink = encode_utf8 $node->findvalue('permalink');
    my $date  = encode_utf8 $node->findvalue('date/created');
    my $line  = sprintf "%s by %s (%s)", $title, $user, $plink

    $irc->send_chan($channel, "NOTICE", $channel, $line);
    return;
}

sub _kuchikomi_api {
    my $api = "http://api.gourmet.livedoor.com/v1.0/evaluation/";
    my %query = (api_key => 'API-KEY');
    my $lwp = LWP::UserAgent->new;
    $lwp->agent('gourmet-bot/1.00');
    $lwp->timeout(5);
    my $uri = URI->new($api);
    $uri->query_form(%query);
    return $lwp->get($uri->as_string);
}
出力:
<#channel> shiro: 新着クチコミ
<#channel> bot: 究極の至高的美味な何か by yuzan (http://gourmet.livedoor.com/...)

さらに AnyEvent の timer を使って定期的に処理させることも出来ます。

my $timer;
$timer = AnyEvent->timer(
    after    => 1,
    interval => 60,
    cb       => sub {
        __shinchaku_kuchikomi();
    }
);

after 後に開始し interval 周期で cb の処理が実行されます。


■最後に



こよなく IRC を愛する livedoor は IRC サーバを提供、支援しています。

さぁ皆さんも IRC ワールドへ。

裏jQuery - 特殊なTriggerを作ってみよう

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 裏jQuery - 特殊なTriggerを作ってみよう
このエントリーをはてなブックマークに追加
こんにちは。開発部でインターフェースエンジニアをやっております油井(あぶい)です。ライブドアでは主にjavascriptを中心としたクライアントサイド側の開発をやっております。

今回は裏jQueryと題しまして、普段から単にユーザーとして使っているだけでは決して知ることができないjQueryの裏技を紹介したいと思います。

注意
  1. この記事で扱うjQueryは最新版の1.4で動かすことを前提としています(一つ前のバージョンである1.3.2でも動くことは検証済みです)。
  2. サンプルで使うjQueryセレクタの書き方は「jQuery」で統一しています。「$」に置き換えて読んでもらってもかまいません。

はじめに - jQueryで扱うイベントやトリガー

javascriptがふんだんに使われた画面遷移の発生しないウェブアプリケーションではブラウザ上で発生するイベントやトリガーをうまく扱いこなすということが必須となります。ここでいうイベントやトリガーとは例えば「ページのロードが完了した」や「ページ上でクリックイベントが発生した」などのことです。

// ページのロードが完了した
jQuery(function () {
alert('done!');
})

// ページ上のどこかがクリックされた
jQuery( document ).bind('click', function () {
alert('clicked me!');
});

これらのイベントはブラウザにあらかじめ用意された、言い方を変えると特別に何も手を加えなくても扱えるイベントであるといえます。jQueryはそれらを簡潔かつ複数のブラウザで同じように動く書き方を提供してくれているにすぎません。

そこから一歩進んで「ajax通信が終了した」や「アニメーションが終了した」などのイベントの扱い方はどうでしょうか。もちろんそれらはjQueryが提供する公の機能として、決められたオプションや引数を指定することによって簡単に扱うことができます。

// ajax通信が終了した
jQuery.post('[api]', {params}, function () {
alert('ajax finished');
}, 'json');

/*
アニメーションが終了した(h1タグの位置を上部から数えて100px左端から数えて200pxの位置まで1秒かけて移動させる)
*/
jQuery("h1").animate({
top : 100,
left : 200
}, 1000, function () {
alert('animation finished');
});

jQueryのajaxではさらに、ajaxSetupを使うことによって「ajax通信が開始した」「ajax通信が失敗した」などのajaxのライフサイクルに応じて発生するイベントを簡単に扱うことができるようになっています。

複数のajax通信、アニメーションの終了を検知する方法1(スマートでない方法)

ではそこからさらに一歩進んで「同時に実行された全てのajax通信が終了した」や「同時に実行された全てのアニメーションが終了した」さらに「同時に実行されたajax通信とアニメーションが終了した」となるとどうでしょうか。
残念ながら現行のjQueryでは複数のajax通信・アニメーションを同時に実行する機能は提供されていません。ということで単一のajax(またはアニメーション)を扱うときのように、オプションや引数を渡すだけで簡単に扱うことはできないということになります。
ではどうするのかというと、普通に考えられるのは、複数並べた単一のajax通信(またはアニメーション)のコールバックが呼ばれる度に実行数を示すカウンタをカウントアップしていき、トータル実行数と同じ数になったタイミングで指定したコールバックを呼ぶコードを書くことです。
言葉で説明すると難しいので、複数のajax通信を同時に実行して、それらの全てが終了したタイミングで指定したコールバック関数を呼ぶサンプルを以下に示します。

var total = 2,
count = 0,
callback = function () {
alert('done');
},
tid = setInterval(function () {
if ( total == count ) {
clearInterval(tid), ( tid = null ), setTimeout(callback, 0);
}
}, 200);
jQuery.post('/api/012', function () { count++ }, 'json');
jQuery.get('/api/xyz', function () { count++ }, 'json');

はい、これでうまくいきました!といきたいところですが、このやり方の弱点は、全てのajax通信が終了したということを知るために事前にリクエストの総数を知っておかなければならないことです。つまり同期を取りたいajax通信の総数を間違ってしまうといつまで経ってもコールバック関数が呼ばれないということになりかねません。

その弱点を補った、事前に総リクエスト数を知らなくてもいいサンプルは以下の通りです。

function multi_ajax (apis, callback) {
var total = apis.length,
count = 0,
tid = setInterval(function () {
if ( total == count ) {
clearInterval(tid), ( tid = null ), setTimeout(callback, 0);
}
}, 200);
apis.forEach(function (api) {
jQuery(api, function () {
count++;
}, 'json')
});
}

multi_ajax([
"/api/012",
"/api/xyz"
], function () {
alert("done");
});

はい、これで解決しました!、、といきたいところですが、このやり方の弱点は個別に違うオプションや通信方式(post, get, getScript, getJSON, ajax)を使いたいときに引数の指定が複雑になるとともに、それを扱う関数も複雑になることです。

では、複数のajax通信(またはアニメーション)が終了するタイミングをスマートに知る方法はないのでしょうか?

複数のajax通信、アニメーションの終了を検知する方法2(スマート?な方法)

前置きが長くなりました。ここからが本題です。結論からいいますと、jQueryには公にはされていない特殊な変数を内部に持っています。公にされていないというのは、普通に使う分には必要がない(公にすると逆に混乱を招く)、もしくは内部で行われているロジックを成立させるためだけに存在しているということです。つまりソースを直接読まないと知れない情報であるといえます。

これまで散々いってきたajax通信とアニメーションのステータスを示す特殊な変数は以下の通りです。

jQuery.active
 jQuery.ajaxが呼ばれる度にカウントアップされます。そして終了と同時にカウントダウンされます。

jQuery.timers
 アニメーションが発生する度にそのアニメーションを実行する関数が蓄えられます。そしてアニメーションが終了すると同時に該当する関数が削除されます。

つまり、これらの値を監視することによって、同時に実行されたajax通信、またはアニメーションの終了を検知することができるというわけです。

Triggerクラスを作成してみる

ということで実際に先ほどの変数を監視するTriggerクラスを作ってみました。どのライブラリにも依存していないのでコピペするだけで使うことができるようになっています。

var Trigger = (function () {
function isArray (obj) {
return Object.prototype.toString.call(obj) == "[object Array]";
}
function each (obj, callback) {
for ( var i in obj ) {
callback(i, obj[i], obj);
}
}
function map (ary, callback, thisObject) {
var res = [];
each(ary, function (i, val) {
res[i] = callback.call(thisObject,val,i,this);
});
return res
}
function every (ary, callback, thisObject) {
for(var i=0,len=ary.length;i<len;i++) {
if(!callback.call(thisObject,ary[i],i,ary)) return false;
}
return true
}

var tid = null;
function COND (statement, callback) {
if ( tid != null ) clearInterval(tid), tid = null;
tid = setInterval(function () {
(isArray( statement ) ? every(statement, function (_cond) {
return _cond();
}) : statement()) && ( clearInterval(tid), tid = null, setTimeout(callback, 0) );
}, 200);
};
var events = {
allAnimated: function () {
return !$.timers.length;
},
ajaxFullComplete: function () {
return !$.active;
}
}, triggers = {
multi: function (names, callback) {
return COND
(
isArray( names ) ? map(names, function (name) {
return events[name] || function () { return true };
}) : names, callback
);
},
util: {
regist: function (name, statement) {
triggers[name] = function (callback) {
COND(statement, callback);
}
return {
after: function (callback) {
triggers[name](callback);
}
}
}
}
};
each(events, function (name, statement) {
triggers.util.regist.apply(null, arguments);
});
return triggers;
})();

Triggerクラスの使い方

Triggerクラスの使い方は以下の通りです。

同時に実行された全てのajax通信が終了した後にcallbackを呼びたい場合
 Trigger.ajaxFullComplete(callback);

同時に実行された全てのアニメーションが終了した後にcallbackを呼びたい場合
 Trigger.allAnimated(callback);

同時に実行された全てのajax通信とアニメーションが終了した後にcallbackを呼びたい場合
 Trigger.multi(['ajaxFullComplete', 'allAnimated'], callback);

自分で新しくトリガーを作成したいとき
 Trigger.util.regist(name, cond); // Trigger[name](callback);

新しく作成したトリガーを続けて使用したいとき
 Trigger.util.regist(name, cond).after(callback);

Triggerクラスのサンプルコード

atomフィードを並列に読み込み、表示した後に総ロード時間を表示するサンプルコードを書いてみました。

サンプルでは
  1. jQuery本体の読み込みを検知するためのloaded_jQueryというトリガー
  2. 並列で動いているajax通信の終了を検知するためのTrigger.ajaxFullCompleteというトリガー
この二つを使用しています。

コードの下にあるボタンを押すと、サンプルコードを動かすことができます。

/* Triggerクラスは既に読み込まれているものとする*/
Trigger.util.regist('loaded_jQuery', function () { return typeof jQuery != 'undefined' })
.after(function () {
var blogs = ['techblog', 'ld_directors'],
start = +new Date;
Trigger.ajaxFullComplete(function () {
jQuery.each(blogs, function (i, blog_id) {
var total_time = ( +new Date - start ) / 1000;
jQuery("#load_status").html('ロード時間:' + total_time + '秒');
})
});
jQuery.each(blogs, function (i, blog_id) {
jQuery('<div id="' + blog_id + '_feed"></div>').appendTo("#feeds_box");
jQuery.get('/'+blog_id+'/atom.xml', function (xml) {
var entries = xml.getElementsByTagName('entry'),
mtitle = xml.getElementsByTagName('title')[0];
mtitle = mtitle.text || mtitle.textContent;
var entry, title, link, lists = [];
for ( var i = 0, l = entries.length; i < l; i++ ) {
entry = entries[i], title = entry.getElementsByTagName('title')[0], link = entry.getElementsByTagName('link')[0];
title = title.text || title.textContent, link = link.getAttribute('href');
lists.push('<li><a href="' + link + '" target="_blank">' + title + '</a></li>');
}
jQuery('#'+blog_id+"_feed").html( mtitle + '<ul>' + lists.join('') + '</ul>' );
}, 'xml');
});
});

var s = document.createElement('script');
s.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js';
document.body.appendChild(s);





終わりに

以上、裏jQueryと題しまして、複数のajax通信、アニメーションの終了を検知する方法とそれを扱うTriggerクラスを紹介しました。いかがでしたでしょうか。他にもjQueryのこんな裏技を知ってるよ!という方は是非とも教えてくださいね。

それでは今年もよろしくお願い致します。