対話型SkypeボットフレームワークUnazuChanのご紹介

対話型IRCボットフレームワークUnazuSanのご紹介 | おそらくはそれさえも平凡な日々 より

プロジェクト立ち上げると色々やってくれる対話型のIRC botが欲しくなるのでAnySanとか使って適当にコピペで作るわけですが、それもタルくなってきたので、対話系のbotフレームワークを簡単に作れるUnazuSanていうのを作りました。

というのを聞いて、Skypeで動くようにしたUnazuChanていうのを作りました。使い方はUnazuSanとほぼ同じです。
akiym/p5-UnazuChan · GitHub

use 5.010;
use warnings;
use utf8;

use UnazuChan;

my $unazu_chan = UnazuChan->new(
    active_chats => ['#anappo2/$d936403094338dbb'],
    respond_all  => 1,
);

$unazu_chan->on_message(
    qr/^\s*unazu_chan:/ => sub {
        my $msg = shift;
        $msg->chat->send_message('うんうん');
    },
    qr/(.)/ => sub {
        my ($msg, $match) = @_;
        say $match;
        say $msg->body;
    },
);

$unazu_chan->on_command(
    help => sub {
        my ($body, @args) = @_;
        warn;
        $body->chat->send_message('help '. ($args[0] || ''));
    }
);

$unazu_chan->run;

通知系はikachanでまかなって対話系はUnazuSanでまかなえば大体プロジェクトでやりたいことはできるんじゃないでしょうか。

通知系はtacochanでまかなって対話系はUnazuChanでまかなえば大体プロジェクトでやりたいことはできるんじゃないでしょうか。


Skypeでも簡単にできますよーという話でした。

Amon2でconfigまわりをいいかんじにする

use Amon2::Config::Simple;
sub load_config {
    my $class = shift;
    my $config = Amon2::Config::Simple->load($class);
    if ($class->debug_mode) {
        Internals::SvREADONLY %$config, 1;
    }
    return $config;
}

こうしておくことでkeyをtypoしたり、設定し忘れてしまったときに泣かずに済みます。
毎回こんな感じで書いてた↓ので楽になりました。

my $conf = $c->config->{'DBI'} // die "missing configuration for 'DBI'";

golangはじめました

最近はgolangがアツいらしい。ちょうどRebuild: 15: After Google Reader, DIY Blogging, The Go language (typester)でtypesterさんがgolangについて触れていたのを聞いて、試しに触っていたがなかなか便利であることがわかった。
golangの印象としては

  • go get、go runにgo build、そしてgo testが便利
  • go fmtのようなコード整形ツールがついてくるのは嬉しい
    • (ただ、インデントがハードタブなのはちょっと時代遅れな気がする)
  • 標準packageが充実しているのが頼もしい
  • golangのマスコットキャラクターであるGopherがかわいい

といった感じ。
f:id:akiym:20130804110138p:plain←かわいい

golangの入門ということで、skkservを実装してみる。skkservというのは、ほとんどの方が使っているであろうSKK(日本語入力システム)の辞書サーバのことである。シンプルなプロトコルなので実装は容易であるため、題材として良さそうだ。
以下メモ書き。

GitHub - akiym/go-skkserv: Lightweight skkserv implementation for golang

packageを作成

How to Write Go Code - The Go Programming Languageを参考にして、packageを作成してみる。ここで注意したいのは、$GOPATH以下にpackageを作成するということ。

% mkdir -p $GOPATH/src/github.com/akiym/go-skkserv

とりあえずざっくりとskkserv.goに処理を書いていく。

package skkserv

import (
	"bufio"
	"log"
	"net"
)

const SkkServVersion = "0.0.1"

type Handler interface {
	Request(text string) ([]string, error)
}

type SKKServ struct {
	Port    string
	Handler Handler
}

func NewServer(port string, handler Handler) *SKKServ {
	server := &SKKServ{
		Port:    port,
		Handler: handler,
	}
	return server
}

func (s *SKKServ) Run() {
	ln, err := net.Listen("tcp", s.Port)
	if err != nil {
		log.Fatal(err)
	}
	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		go s.handleConnection(conn)
	}
}

func (s *SKKServ) handleConnection(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		c, err := reader.ReadByte()
		if err != nil {
			return
		}
		switch c {
		case '0':
			return // end of connection
		case '1':
			buf, err := reader.ReadBytes(' ')
			if err != nil {
				return
			}
			s.serverRequest(conn, buf)
		case '2':
			s.serverVersion(conn)
		case '3':
			s.serverHost(conn)
		}
	}
}

型、メソッド

golangはどのような言語なのかを知るために、まずは型とメソッドについて調べてみる。golangは型に対してメソッドが生やせる。そしてメソッド名を大文字で始めることで、packageの外から呼び出せるようになる(エクスポートされる)。基本は以上。
基本的な形:

type SKKServ struct {
	Port    string
	Handler Handler
}

func NewServer(port string, handler Handler) *SKKServ {
	server := &SKKServ{
		Port:    port,
		Handler: handler,
	}
	return server
}

これだけでNewServerというメソッドを定義することができる。
別にメソッドを生やすことができるのはstructだけではなく、「型に対して」である。したがってstringに対しても同じようなことができる。

package main

import (
    "fmt"
)

type MyString string

func (m MyString) Censor() string {
    s := make([]byte, len(m))
    for i := 0; i < len(m); i++ {
        switch c := m[i]; c {
        case 'u':
            s[i] = '*'
        default:
            s[i] = c
        }
    }
    return string(s)
}

func main() {
    var foo MyString = "fuck"
    fmt.Println(foo.Censor())
}

ここで注意したいのはstringそのものに対して、メソッドを生やすことはできないということ。実際にやってみても

cannot define new methods on non-local type string

と言われてしまう。

エラーハンドリング、defer

golangには一般的なプログラミング言語でおなじみの「例外」がないらしい。代わりに、戻り値としてerrorを返すことでエラーハンドリングさせる形式になっている。
(本質はpanic、recoverという仕組みだが、ここでは触れない)
例えば、このようにエラーハンドリングができる。

		c, err := reader.ReadByte()
		if err != nil {
			return
		}

しかし、ここで気にしたいのはファイルディスクリプタをクローズ必要があるということだ。errorが返ってきたときにいちいちクローズするのは大変だ。

		c, err := reader.ReadByte()
		if err != nil {
			conn.Close()
			return
		}

		// ...
		if err != nil {
			conn.Close()
			return
		}

		// ...

なんて書いていたら日が暮れてしまう。try-catch-finallyであるところのfinallyの処理を書きたい。そこでdeferという仕組みが用意されている。deferを利用することで、returnする直前に特定の処理を実行することができる。

	defer conn.Close()

あらかじめdeferでクローズする処理を書いておけば、あとは気にする必要はない。NO MORE 悩み無用だ。
golangのエラー処理はほかの言語に慣れている身としては扱いにくいように思えたが、もろもろの処理は割と書きやすい気がする(少し貧弱かもしれないけど)。
参考: Defer, Panic, and Recover - The Go Blog

interface

今回の場合、リクエストを処理するhandlerを定義できるような設計にしたい。そこでinterfaceという仕組みを利用する。
このように書いておくことで、Request()というメソッドが実装されていることを明示することができる。

type Handler interface {
	Request(text string) ([]string, error)
}

これがまた面白くて、外部からどう利用するのかというと:

type GoogleIMESKK struct{}

func (s *GoogleIMESKK) Request(text string) ([]string, error) {
	words, err := Transliterate(text)
	if err != nil {
		return nil, err
	}
	return words, nil
}

func main() {
	var server = skkserv.NewServer(":55100", &GoogleIMESKK{})
	server.Run()
}

skkserv.NewServer()はHandlerを受け取るようにしていたので、そこにRequest()を実装した型をつっこむだけ。
これはinterfaceのほんの一部でしかなく、詳しいことはGo の interface 設計 - Block Rockin’ Codesに書いてある。

interface型

interface型はどんな型でも受け取れる。interface型を利用する具体例としては、JSONのパースがある:

	dec := json.NewDecoder(resp.Body)
	var w [][]interface{}
	if err := dec.Decode(&w); err != nil {
		return nil, err
	}
	for _, v := range w[0][1].([]interface{}) {
		word := v.(string)
		result, ok := enc.ConvertStringOK(word)
		if ok {
			words = append(words, result)
		}
	}

interface型でアサーションして、stringに当て直すあたりがよくできている。

テストを書く

これまでskkserv.goを書いていたが、テストはskkserv_test.goに書いていく。見ればわかると思うが*_test.goという名前のファイルがテストコードになる。あとはimport "testing"して、"Test"で始まるメソッドを定義しておけば、go testでテストを走らせることができる。

func TestRequest(t *testing.T) {
	server := NewServer(":55100", &TestHandler{})
	go server.Run()

	// ...

まとめ

少し雑になったが、とりあえずgolangでskkservを実装することができた。
ここにあるほとんどのことはgolang.orgを見れば書いてある。日本語の情報もgolang.jpに詳しく書いてあるので心配はいらない。
とりあえず少し触ってみただけだったがgolangは素晴らしい言語だと思った。今後も使っていこう。

SQL Injectionを学ぶためにSQLiPuzzleというサービスを作ってみた

SQLiPuzzle

SQL Injectionについての知識が足りなかったので、ちょろっと作ってみた。まだすべての問題を用意できていないので未完成。
難易度としては初級レベルで、知っている人ならすぐに解ける。TODO: SQLインジェクションゴルフ - なんと3文字で認証回避が可能に | 徳丸浩の日記


それとHerokuでアプリを動かしたかったからというのもある。
構成はAmon2でゴリゴリ書くといった感じで、Herokuのbuildpackにはmiyagawaさんのmiyagawa/heroku-buildpack-perl · GitHubを使っている。
とりあえず軽く動かしてみたけど、悪くはない。git pushするだけでデプロイしてすぐに動くし、無料でここまでできるのは素晴らしい。

とりあえず動かしてみたので、あとでコードをGithubに置く予定。

Skype に issue 番号に反応する bot がいると捗る

"IRC に issue 番号に反応する bot がいると捗る" らしいので - @soh335 memo より

use strict;
use warnings;
use Skype::Any;
use AnyEvent::HTTP;
use JSON::XS;
use Try::Tiny;
use HTTP::Request::Common;

my $owner;
my $repos;

my $github_user;
my $github_password;

my $skype = Skype::Any->new(name => 'issue-chan');

$skype->message_received(sub {
    my ($msg) = @_;
    my @numbers = $msg->body =~ /#(\d+)/g;

    for my $number ( @numbers ) {
        my $req = GET sprintf(
            "https://api.github.com/repos/%s/%s/issues/%d",
            $owner,
            $repos,
            $number
        );
        $req->authorization_basic($github_user, $github_password);
        my %headers = map { $_ => $req->header($_) } $req->headers->header_field_names;

        my $g; $g = http_get $req->uri, headers => \%headers, sub {
            my ($body, $hdr) = @_;
            return unless $hdr->{Status} =~ /^2/;

            try {
                my $json = decode_json $body;
                $msg->chat->send_message(
                    sprintf "#%d %s %s", $json->{number}, $json->{title}, $json->{html_url}
                );
            }
            catch {
                warn $_;
            }
            finally {
                undef $g;
            };
        };
    }
});

$skype->run;

Skype::Any、ちょろっと書き直したやつを手元のリポジトリに放置しているのでひどい。
あとSkype botをうまく運用する方法とかまとめておきたいけど気力がなくて、これも放置しております。

sudoをしっかり読もう

あなたが初めてsudoコマンドを実行したときに出力されたメッセージを(読みました|覚えています)か?

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

以下の通り。

  1. 他人のプライバシーを尊重しよう
  2. 入力する前によく考えよう
  3. 「大いなる力には、大いなる責任が伴う」*1

sudoは何でもできてしまうということです。他人を傷つけてしまうかもしれないし、自分を殺してしまうかもしれない。

perl-5.18のhash randomizationについて

perl-5.18.0がリリースされました。

大きな変更点としてhash randomization(ハッシュのランダム化)が挙げられます。each(), keys(), values()の出力結果がランダムになるというものです。ちなみにprint %hash;したときもランダムになります。これによってテストがこけると言われていますが、ただテストがこけるだけなのか、そうではないとかというのが疑問だと思います。
実際にはほとんど問題ありません(テストがこけるだけかもしれません)。しかし、(ときどき)動かなくなるものも存在します。気をつけるポイントはひとつです。ハッシュはできる限りsortしてから扱うことです。

実例

Test::Difflet::is_deeply()にはhash randomizationに関するバグがありました。Data-Difflet-0.08ではすでにfixされています。

  • Data::Dumperの出力結果を比較することでデータ構造の比較を実現している
  • hash randomizationによってData::Dumperの出力結果がランダムになった
    • 配慮しているつもりだったが、SortkeysとするところをSortKeysとしていた
--- a/lib/Test/Difflet.pm
+++ b/lib/Test/Difflet.pm
@@ -41,7 +41,7 @@ sub _eq_deeply {
     my ($a, $b) = @_;
     local $Data::Dumper::Terse = 1;
     local $Data::Dumper::Indent = 0;
-    local $Data::Dumper::SortKeys = 1;
+    local $Data::Dumper::Sortkeys = 1;
     return Dumper($a) eq Dumper($b);
 }


蛇足ですが、Data::Dumperの出力がランダムになったのでデバッグするときにData::Dumperを使ってるんだよねーといった人は戸惑うかもしれません。
local $Data::Dumper::Sortkeys = 1;しておくようなsnippetを書いておくと捗りそうです。

snippet dumper
    use Data::Dumper; local $Data::Dumper::Sortkeys = 1; warn Dumper(${1:code});