気まぐれ緋夜子

アウトプット力がほしい

ノート

Mackerel開発におけるScalaとGo、そしてPerl


一枚のPerlスクリプトの中にテストも書かれてる
やりたい。

メモ:perlの文字化け解決法

最近はAPIから取ったjson文字列、とyamlで保存してる文字列が文字化けしたことが頻繁に起こってて、先輩に相談してみたら、いいモジュールを紹介してくれました。

#yaml全体とjson全体をutf8に統一するモジュール
use Data::Recursive::Encode;

my $string = Data::Recursive::Encode->decode_utf8(YAML::LoadFile("xxx.yaml"));
#の感じで使う

#文字列の中身を見るモジュール
use Devel::Peek;
Dump $string;

黒い画面で三つのslackチームのメセッジーを同時に流れてみた

hiyoko91.hatenablog.jp

前回の続きです。

前に書いたものは一つのslackに対して発言を拾い、特定の発言に反応するbotです。
おまけな感じで、黒い画面のslackアプリができました(ちゃんとパーサーすれば
それを眺める途中で、急に多くのslackチームの発言を同時に拾って流れることができるかなという発想が出ました。

やってみれば簡単にできました。

sub connect {
    my $urls = {};
    my $sockets = {};
    my $ws_clients = {};
    foreach my $token (@tokens) {
        $urls->{$token} = SlackApi::get_rtm_socket($token);
        my ($host) = $urls->{$token} =~ m{wss://(.+)/websocket};
        $sockets->{$token} = IO::Socket::SSL->new(PeerHost => $host, PeerPort => 443);
        $sockets->{$token}->blocking(0);
        $sockets->{$token}->connect;

        $ws_clients->{$token} = Protocol::WebSocket::Client->new(url => $urls->{$token});
        $ws_clients->{$token}->on(read => sub {
            my ($client, $buffer) = @_;
            Hiyobot::parser($buffer, $token); #もらったjsonをどこかに処理する
        });
        $ws_clients->{$token}->on(write => sub {
            my ($client, $buffer) = @_;
            syswrite $sockets->{$token}, $buffer;
        });
        $ws_clients->{$token}->on(connect => sub {
            say 'on_connect';
        });
        $ws_clients->{$token}->on(error => sub {
            my ($client, $error) = @_;
            say 'on_error: ', $error;
        });
        $ws_clients->{$token}->connect;
    }
    say "ready!";

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

一つのtokenにつき一つのsocket接続
順番にメセッジーを取るだけです。

たまに再起動のときは最初のresponseがtimeoutで失敗、ちょっと時間を置いて再起動すれば大丈夫です。
あとはあまり多くのslackチームに接続したらすごくtimeoutしやすくなります。

流れるものは前回とほとんど変わってないので略します。

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

perlでslack apiをちょっと使ってみた

最近slackを使う時間が増えて、起きてる時間がほとんどslackを開いています。そろそろ自分用のリマインドbotか面白いものが書きたくなったから、slackのapiをちょっと使ってみました。

api.slack.com

公式のドキュメントです。感想はやればなんでもできる、素晴らしいです。


tokenはhttps://api.slack.com/webで取れます

use Furl;
use HTTP::Request::Common;

my $req = POST 'https://slack.com/api/chat.postMessage',
    'Content' => [
        token   => "xxxxxxxxxxxxxxxxxxx"
        channel => "#test",
        text    => "test",
    ];
my $res = Furl->new->request($req);

でメセッジーが送れます。(https://api.slack.com/methods/chat.postMessage


デフォルトはこんな感じです:
f:id:hiyoko9191:20150614191449p:plain

usernameとicon_urlも送ったら画像と名前が簡単に変更できます:
f:id:hiyoko9191:20150614191748p:plain

as_userをtrueにすれば自分のアカウントで発言できます。
自分がいないPRIVATE GROUPには発言できないです。

自分の場合はslackチームが多いから毎回指定するのも面倒だから、ちょっとyamlにまとめました。

#yaml/config.yaml
slack_token:
   test-slack: "xoxp-xxxxxxxxxxxxxxxxxxx"

#yaml/setting.yaml
bot_type:
  katosan:
    username: "加藤さん"
    icon_url:
      - "https://docs.google.com/uc?id=0Bwf35y4fWlHhNHlvdm5HbkdzRms&ext=.png"
mode:
  test:
    token: "test-slack"
    channel: "test-channel"
    text: "test"
    bot: "katosan"

yamlはこんな感じです。

use YAML;
use Getopt::Long;

my $config = YAML::LoadFile("./yaml/config.yaml");
my $setting = YAML::LoadFile("./yaml/setting.yaml");
...

    my $opts = {};
    GetOptions($opts,
        'mode=s',
        'bot=s',
        'token=s',
        'channel=s',
        'text=s',
        'username=s',
        'icon_url=s',
        'file=s',
        'filename=s',
    );

    #mode
    if ($opts->{mode}) {
        die "no mode $opts->{mode}" unless ($setting->{mode} and $setting->{mode}->{$opts->{mode}});
        my $mode = $setting->{mode}->{$opts->{mode}};
        while (my ($key, $value) = each %$mode) {
            $opts->{$key} //= $value;
        }
    }

    #bot
    if ($opts->{bot} and $setting->{bot_type}) {
        my $bot = $setting->{bot_type}->{$opts->{bot}};
        die "no bot $opts->{bot}" unless $bot;
        $opts->{username} //= $bot->{username};
        unless (ref $bot->{icon_url} eq "ARRAY") {
            $opts->{icon_url} //= $bot->{icon_url};
        }   
        else {
            my $randam = rand(scalar @{$bot->{icon_url}}); 
            $opts->{icon_url} //= $bot->{icon_url}->[int($randam)];
        }   
    }

これからはmodeを指定すれば簡単に発言できます。

$ perl slack-test.pl --mode=test


ファイルも送れます(https://api.slack.com/methods/files.upload)。

    my $post = {
        token    => "xxxxxxxxx",
        channels => $channel_id,
        filename => $opts->{filename},
        file     => [$opts->{file}],
    };

    my $req = POST ('https://slack.com/api/files.upload',
        Content-Type => 'form-data',
        Content      => [
            %$post,
        ]);
    my $res = Furl->new->request($req);

主にAPIで画像をアプデートしたいです。

ちょっとハマったのは主に2点:
1:ここのchannelsはチャンネルの名前じゃなくてIDです
2:ファイルの送り方

slackの仕様では、CHANNELSと PRIVATE GROUPは分けています。
https://api.slack.com/methods/channels.list
https://api.slack.com/methods/groups.list
上の二つのAPIでgroupsとchannelsの参照表がもらえます。

# get channel id by channel name
sub _get_channel_id {
    my ($channel, $token) = @_; 

    my $res_channels = Furl->new->post('https://slack.com/api/channels.list', [], +{ token => $token });
    my %channels  = map { sprintf('#%s', $_->{name}) => $_->{id} } @{decode_json($res_channels->content)->{channels}};
    my $channel_id = $channels{$channel};
    return $channel_id if $channel_id;

    my $res_groups = Furl->new->post('https://slack.com/api/groups.list', [], +{ token => $token });
    my %groups  = map { sprintf('#%s', $_->{name}) => $_->{id} } @{decode_json($res_groups->content)->{groups}};
    $channel_id = $groups{$channel};
    return $channel_id if $channel_id;

    die "can't find channel id";
}

後もいろいろ使いところあるからあらかじめ参照表をyamlに保存します。

# make slack map
sub make_slack_map {
    my %slack_map;
    while (my ($key, $value) = each %{$config->{slack_token}}) {
        my $res_channels = Furl->new->post('https://slack.com/api/channels.list', [], +{ token => $value });
        my $res_groups = Furl->new->post('https://slack.com/api/groups.list', [], +{ token => $value });
        my %channels  = map { $_->{id} => $_->{name} } (@{decode_json($res_channels->content)->{channels}}, @{decode_jso<
        my $res_users = Furl->new->post('https://slack.com/api/users.list', [], +{ token => $value });
        my %users = map { $_->{id} => $_->{name} } @{decode_json($res_users->content)->{members}};
        $slack_map{$key} = {
            channels => \%channels,
            users => \%users,
        };
    }
    YAML::DumpFile("yaml/slack_map.yaml",\%slack_map );
    say "make slack map success";
}