気まぐれ緋夜子

アウトプット力がほしい

perlとslack apiでhubotを実現してみた

hiyoko91.hatenablog.jp

前回の続き。というかこれこそ本番です。

なんでhubotがあったのに自分で実現しなければならないというのなら、会社のslackにはhubotを作る権限がないだけです。

Real Time Messaging API | Slack
優しいSlack APIと説明があるから全然苦労しなかったです。

まずはWebsocket URLを取ります:

    my $res = Furl->new->post('https://slack.com/api/rtm.start', [], +{ token => "xxxxxxxxxxx" });
    die "response fail" unless decode_json($res->content)->{ok};
    my $url = decode_json($res->content)->{url};

clientの部分:

sub connect {
    my ($url, $token) = @_; 

    my ($host) = $url =~ m{wss://(.+)/websocket};
    my $socket = IO::Socket::SSL->new(PeerHost => $host, PeerPort => 443);
    $socket->blocking(0);
    $socket->connect;

    my $ws_client = Protocol::WebSocket::Client->new(url => $url);
    $ws_client->on(read => sub {
        my ($client, $buffer) = @_; 
        Hiyobot::parser($buffer, $token);
    }); 
    $ws_client->on(write => sub {
        my ($client, $buffer) = @_; 
        syswrite $socket, $buffer;
    }); 
    $ws_client->on(connect => sub {
        say 'on_connect';
    }); 
    $ws_client->on(error => sub {
        my ($client, $error) = @_; 
        say 'on_error: ', $error;
    }); 
    $ws_client->connect;

    my $i = 0;
    while (1) {
        my $data = ''; 
        while (my $line = readline $socket) {
            $data .= $line;
            last if $line eq "\r\n";
        }   
        $ws_client->read($data) if $data;
        if ($i++ % 30 == 0) {
            # 定期的にpingしないと接続が切れる
            $ws_client->write('{"type": "ping"}');
        }
        sleep 1;
    }
}

最初のconnect:

{
    "type": "hello"
}

失敗したら、エラーメセッジーが帰ってきます:

{
    "type": "error",
    "error": {
        "code": 1,
        "msg": "Socket URL has expired"
    }
}

成功したら、socketからeventのsocketがもらえます:

{
    "type": "message",
    "ts": "1358878749.000002",
    "user": "U023BECGF",
    "text": "Hello"
}

すべてのEvents種類がhttps://api.slack.com/rtmのEventsの部分で見えます。

ここでちょっとはまったのは、接続は1分か2分ぐらいで切れることです。
先輩に聞いてみたら、定期的にpingしないとだめっぽいです。

$ws_client->write('{"type": "ping"}');

もらったjsonをもう一つのモジュールで処理する

Hiyobot::parser($buffer, $token);

パーサーの部分:

sub parser {
    (my $json, $token) = @_; 

    my $content = decode_json(encode_utf8($json));
    if ($content->{type} eq "message") {
        # ircっぽく全チャンネルのメセッジーをリアルタイムで流す
        my $channel_name = _map_name($content->{channel});
        my $user_name = ""; 
        my $text = ""; 
        unless ($content->{subtype}) {
            $user_name = _map_name($content->{user});
            $text = $content->{text}; 
            say sprintf("#%s <%s> %s", $channel_name, $user_name, encode_utf8($text));
        }   
        elsif ($content->{subtype} eq "bot_message") {
            $user_name = $content->{username};
            $text = $content->{text};
            say sprintf("#%s (bot)<%s> %s", $channel_name, encode_utf8($content->{username}), encode_utf8($text));
        }   
        elsif ($content->{subtype} eq "message_changed") {
            $user_name = _map_name($content->{message}->{edited}->{user});
            $text = $content->{message}->{text};
            say sprintf("#%s (changed)<%s> %s", $channel_name, $user_name, encode_utf8($text));
        }   
        elsif ($content->{subtype} eq "message_deleted") {
            say "#$channel_name !message has been deleted!";
        }   
        elsif ($content->{subtype} eq "pinned_item") {
            $user_name = _map_name($content->{user});
            say sprintf("<%s> pined: %s", $user_name, $content->{attachments}->[0]->{fallback});
        }   
        elsif ($content->{subtype} eq "file_share" or $content->{subtype} eq "file_comment") {
            say encode_utf8($content->{text}); 
        }   
        else {
            #warn Dumper $content;
        }   

おまけっぽいだけど、リアルタイムでjsonを解析して、コマンドラインで出ることができます。
黒い画面版のslack appですね(笑
解析の部分まだ書ける部分いっぱいあります。とりあえずほしい部分だけちょっと書きました。
_map_name($id)はchannelかuserのidをnameに変換するメソッド、前回話したから今回は略します。

botの応答部分

        #応答bot
        if ($text =~ '@hiyobo') {
            if ($text =~ m/^\@hiyobo (.*)/) {
                my $command = $1;
                if ($command eq "help") {
                    #TODO show usage
                    _hiyobo_send($channel_name, "ごめん、まだ書いてない");
                }
                #TODO add command
                elsif ($command =~ "keyword") {
                    my $message;
                    try {
                        _hiyobo_send($channel_name, $message);
                    }
                    catch {
                        _hiyobo_debug($channel_name, $user_name, $text);
                        _hiyobo_debug($_);
                    }
                }
                else {
                    # unknown command 
                    _hiyobo_send($channel_name, $command);
                    _hiyobo_debug($channel_name, $user_name, $text);
                }
            }
            else {
                _hiyobo_send($channel_name, "はい");
                _hiyobo_debug($channel_name, $user_name, $text);
            }
        }

今の仕様は"@hiyobo"だけに反応して
"@hiyobo command"に対応の命令で反応します
そのほかは"はい"で返事します
f:id:hiyoko9191:20150614200234p:plain

# send debug message to debug channel 
sub _hiyobo_debug {
    my $debug_message;
    if (scalar @_ == 1) {
        # debug message
        $debug_message = shift;
    }
    else {
        # where is the message
        my ($channel_name, $user_name, $text) = @_;
        $debug_message = sprintf("%s #%s, mentioned by<%s>:%s", $token, $channel_name, $user_name, $text);
    }
    SlackApi::base({
        mode => "debug",
        text => $debug_message,
    });
}

# response of bot
sub _hiyobo_send {
    my ($channel, $text) = @_;
    SlackApi::base({
            bot     => "hiyobot",
            token   => $token,
            channel => $channel,
            text    => $text,
        });
}

前回作ったSlackApi::baseで返事します

基本機能は多分その辺で一旦終わります。(とりあえずテストを書きます
コマンドへの拡張機能いっぱい考えたから実装するのはすごく楽しみです

コードはgitに上げました。汚いコードだけど、参考になれるとうれしいです。github.com