14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

mod_mruby ngx_mrubyAdvent Calendar 2017

Day 19

ngx_mrubyでSlackのスラッシュコマンドをつくる

Last updated at Posted at 2017-12-19

ただ、レスポンスを動的に組み立てるだけの話です。

botではない、単発のSlackのスラッシュコマンド(Slash Commands)

一応簡単に説明しておきます。スラッシュコマンドとは、/foo [text]のように、Slackのチャンネルで頭にスラッシュをつけて入力することでなにかしら処理をしてもらうためのシンタックスです。

さてこれ、一見Slack Applicationか、botフレームワークの知識が入りそうですが、プリセットのAppSlash Commandsを使えば次のようなゆるゆる仕様で取り扱うことができます。

  • plain/textでレスポンスが来たら、Bodyをそのままチャンネルに投稿する。
  • application/jsonでレスポンスをしたら、Bodyをパースして規定の形式でチャンネルに投稿する。
  • HTTP 200 以外のレスポンスはエラー

Slash Commands | Slack App Directory 2017-12-18 15-38-11.png

これならほぼ絵文字感覚で追加できます。

テキストでシンプルなレスポンス

ではこんなのを作ります。

  • /helloコマンドで、"Hello World"のレスポンス。

まずはngx_mrubyでのhello-world use_caseそのままです。

hello.rb
Nginx.echo "Hello World"
nginx.conf
server {
  location /hello {
    mruby_content_handler /path/to/hello.rb;
  }
}
Access
$ curl http://127.0.0.1/hello
Hello World

slackの設定をします。今回はGETですね。

Image 2017-12-18 15-46-20.png

/helloを実行、と。

Slack - higanworks-idobata 2017-12-18 15-44-20.png

Only visible to youとあるように、このメッセージは自分しか確認できませんが、とりあえず反応してもらえました。結果が他に見えない場合、入力したコマンドもチャンネルには表示されません。

見た目などはSlackの設定で好きなように変更しましょう。今回はカレンダーのネタ出しを私に振ってきた@matsumotoryを使います。

JSONでレスポンス

惜しいことにプレーンテキストでは、コマンドを実行したユーザでしか実行結果が見えません。
このあたりの挙動を変更するのも簡単で、レスポンスをapplication/jsonにしてresponse_typeパラメータをin_channel(デフォルトの挙動はephemeral)にするなどでOKです。

ついでにスラッシュコマンドの引数によって、すこし挙動を変えたりしてみます。

hello2.rb
def args_to_hash(ar)
    h = {}
    ar.each do |a|
        h[a.split('=')[0]] = a.split('=')[1]
    end
    h
end

r = Nginx::Request.new
raw_args = r.args.split('&')
h_args = args_to_hash(raw_args)

resp = {}
resp['response_type'] = 'in_channel'

resp['text'] = h_args['user_name'] + "さん、こんにちは。\n"

if h_args['text'] == nil
    resp['text'] = resp['text'] + "それ、引数がないっていわれませんか?"
else
    resp['text'] = resp['text'] + "ほう、『#{HTTP::URL::decode(h_args['text'])}』、ですか。興味はあります。\n"
end


hout = Nginx::Headers_out.new
hout["Content-type"] = "application/json"

Nginx.echo JSON::stringify(resp)
Nginx.return Nginx::HTTP_OK

組み込みのヘルパーっぽい関数としてもargs_to_hashはある(※)んですが、keyに対するvalueが空っぽの場合(例: &key1=&key2=value2 のようなケースのkey1)にコケるので一旦自前で実装。

こんな感じになりますね。かんたん。

Slack - higanworks-idobata 2017-12-18 16-07-05.png

サンプルコマンド、 ngx_mrubyでテキスト翻訳

では、コマンドに渡したテキストの翻訳でもしてもらいましょう。from to で変換する言語を指定するタイプで。

hello3.rb
r = Nginx::Request.new

usage = '引数のことなんですけど、3ついるんですよ。 このようにしてください => `/hello en ja Hello World.`'
MS_KEY = 'MicrosoftTranslatorのAPIキー'
V_TOKEN = 'SLACKがパラメータにつけるTOKEN、任意で変更可能'


def get_token
    http = HttpRequest.new()
    res = http.post(
       'https://api.cognitive.microsoft.com/sts/v1.0/issueToken',
        '{}',
        {
            'Content-Type' => 'application/json',
            'Accept'       => 'application/jwt',
            'Ocp-Apim-Subscription-Key' => MS_KEY
        }
    )
    token = res.body
    token
end


## ここで翻訳。
def trans(from, to, text)
    utext = HTTP::URL::encode(text)
    http = HttpRequest.new()
    res = http.get(
       "https://api.microsofttranslator.com/V2/Http.svc/Translate?category=generalnn&from=#{from}&to=#{to}&text=#{utext}",
        nil,
        { 'Authorization' => 'Bearer ' + get_token }
    )
    t_txt = res.body

    t_txt.match(/<string.*?>(.*)<\/string>$/)[1]
end

def args_to_hash(ar)
    {}.tap do |h|
	    ar.each do |a|
    	    h[a.split('=')[0]] = a.split('=')[1]
	    end
    end
end

raw_args = r.args.split('&')
h_args = args_to_hash(raw_args)


## Nginx.returnからのlambdaについては => http://lamanotrama.hateblo.jp/entry/2015/08/02/005930
Nginx.return -> do
    # Slackからのリクエストであるっぽい、に制限するためトークンをチェック。
    if h_args['token'] != V_TOKEN
        Nginx.errlogger Nginx::LOG_WARN, "NO TOKEN ACCESS"
        return Nginx::HTTP_FORBIDDEN
    end

    resp = {}
    resp['response_type'] = 'ephemeral'

    if h_args['text'] == nil
        resp['text'] = usage
    else
        a_args = h_args['text'].split('+')
        if a_args.length < 3
            resp['text'] = usage
        else         
            t_body = a_args[2..(a_args.length)].join('+')
            resp['text'] = h_args['user_name'] + "さん、こんにちは。\n"
            begin
                resp['text'] = resp['text'] + "『#{HTTP::URL::decode(t_body)}』、そうですね。\n"
                resp['text'] = resp['text'] + "翻訳したら、『#{trans(a_args[0], a_args[1], HTTP::URL::decode(t_body))}』、ですかね。"
                resp['response_type'] = 'in_channel'

            # さすがに例外になりやすいところなのでざっくり拾う
            rescue => e
                Nginx.errlogger Nginx::LOG_ERR, e
                resp['text'] = "`Some error occurerd on calling Microsoft Translate API.` (this message only visible to you.)\n maybe unsupported language.\n See https://msdn.microsoft.com/ja-jp/library/hh456380.aspx."
            end
        end
    end

    hout = Nginx::Headers_out.new
    hout["Content-type"] = "application/json"

    Nginx.echo JSON::stringify(resp)
    return Nginx::HTTP_OK
end.call

Slack - higanworks-idobata 2017-12-18 17-01-20.png

今回使った環境と感想

まず環境。

上記構成にはあまり必然性はなく、Cloud9を触ってるうちに思いついたというだけなので。試してみるならローカル+ngrokで十分です。

さて、たいていこういうのはFaaSでやるのが一般的で、よほどの理由がなければあえてngx_mrubyでやる必要もないでしょう。ただ、slack側のタイムアウトがわりと厳しくて、FaaS側のウォームアップ周り次第でレスポンスが間に合わないことがある、と言う話も聞いたことはあるので意外とありなのかもしれません。

nginxのlocation + rbファイルでわりとやりたい放題、デバッグもまあまあやりやすいので、遊んでるnginxがいたらスラッシュコマンドエンドポイントを生やしても良いかもですよ。

mruby_add_handlerと組み合わせると、ルーズにコマンド追加・管理もできそう。

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?