Ruby
irb
Slack
slackbot
AWSLambda
PORTDay 6

AWS Lambdaでirb風のSlackチャンネルを作る

こんにちは。PORT株式会社の18卒エンジニアの@rnittaです。

業務ではサーバー/フロント/iOSの開発をしています。

1週間ほど前にAWS LambdaがRubyをサポート(&あらゆる言語対応)というアナウンスがありましたので、

今回はFaaStRubyへの追悼の意を込めて、「LambdaでRuby」ネタを書きます。


今回作るもの

Slackのチャンネルをirb風にします。

irbとはRubyのREPL(Read-Eval-Print Loop)ですが、今回は「Read-Eval-Print」するだけのものを作ります。

そういった意味でirb"風"です。

今回のゴール:

スクリーンショット 2018-12-05 20.20.33.png

こういった、SlackにRubyのコードを送信すると

Rubyのコードとして解釈して返してくれるボットを作り、

チャンネルをirb化させます。

以下手順です。


1. Slackにチャンネルを作成

irb化させるチャンネルを作ってください。


2. 発信Webフックを追加

Slackをカスタマイズ > App管理 > カスタムインテグレーション > 発信 Webフック(Outgoing Webhook) > 設定を追加

するとSlackへの投稿をフックに発動するボットを追加できます。

追加したら各項目を適切に設定してください。

「チャンネル」は先程作ったチャンネルに、

「引き金となる言葉」は空のままに、

「URL」も一旦空にしておきます(あとでAPI GatewayのURLを入れます)。


3. AWS Lambdaで関数を作成

「ランタイム」はRubyを選択し、適当な名前をつけて作成します。

スクリーンショット 2018-12-05 20.33.43.png

関数の編集画面に入ったら、

Designerから高輪API Gatewayを追加します。

API Gatewayの設定はあとでします。

スクリーンショット 2018-12-05 20.35.54.png

発信Webフックで飛んできたリクエストの本文をコードとして実行して、

json形式で返す処理を書きます。

Hashを返せばjsonで出力してくれます。

リクエストやレスポンスの形式は発信Webフックの「発信ペイロードとレスポンス」を参考にします。

そしてインラインでコード書きます。

require 'uri'

require 'cgi'

# 出力先をstringオブジェクトにする
require 'stringio'
$stdout = StringIO.new

def execute(code)
# 文字列をrubyコードとして実行して返す
eval(code, binding, 'port_inc.rb')
rescue => error
error
end

# このメソッドが最初に呼ばれる eventにはリクエストの本文がHashで入っている
def lambda_handler(event:, context:)
# tokenが一致しなければ弾く 本来はオーソライザでやったほうがよさそう
return unless event['token'] == ENV['SLACK_TOKEN']
# チャンネルにはこのボット自体の投稿をフックにしてリクエストが飛ぶので、ボットの投稿は無視する
return if event['user_name'] == 'slackbot'

# デコードする
decodedcode = CGI.unescapeHTML(URI.decode_www_form_component(event['text']).tr(%q(“”‘’), %q(""'')))
# slackに投稿する本文を生成する
output = "#{$stdout.string}=> #{execute(decodedcode).to_s}"
# なぜかグローバル変数がリクエストをまたいで共有されるので空にする
$stdout.reopen('')
{ text: output, username: 'ruby' }
rescue => error
$stdout.reopen('')
{ text: 'エラー: ' + error.to_s, username: 'ruby' }
end


注意すべき点としては、

やたらめったら誰彼構わず世界中から叩けると困る気がするので、

(ハードコードしてもいいですが) ENV['SLACK_TOKEN']に先程作ったボットのトークンを入れておき、リクエスト毎にそのトークンが含まれるか検証します。

スクリーンショット 2018-12-05 20.52.05.png

↑環境変数はコードの真下のここで設定します。

(リクエスト元をIPで絞る事はできないようです。)


注意すべき点②は、

slackから飛んでくるリクエスト本文は入力したものとは多少異なります。

まずapplication/x-www-form-urlencodedという形式でエンコードされています。

たとえば半角スペースは +に置き換わってたりします。

これを URI.decode_www_form_componentでデコードし、

また、実体参照でエスケープされているので CGI.unescapeHTMLで戻してあげます。

また、Slackクライアントの仕様で(少なくともmac版は ?)クォーテーションが全角のものに置換されるので、

String#trで置換します。


注意すべき点③は、

発信Webフックにおいて、

slackに投稿されたメッセージの投稿者の名前のキーは user_nameですが、

それに対する返信の投稿者名のキーは usernameで、

その値は bot_nameとして採用されるという正気とは思えない仕様です。

大変混乱しました。

ボットからの投稿の user_nameは常に slackbotなので、

return if event['user_name'] == 'slackbot'

無限ループを回避できます。


4. API Gatewayを設定してデプロイする

先程関数に紐づけたAPI Gatewayを「Amazon API Gateway コンソール」から編集します。

適当にPOSTアクションつくって、

マッピングテンプレートをいい感じにします。

application/x-www-form-urlencoded

#set($parameters = $input.path('$').split("&"))

{
#foreach( $keyValue in $parameters )
#set($data = $keyValue.split("="))
"$data[0]": "$data[1]" #if( $foreach.hasNext ),#end
#end
}

application/x-www-form-urlencoded形式のリクエストをjson形式にマッピングしています。

#が頭にある行が処理で、ない行が出力だそうです。VTL。

マッピングテンプレートを設定したら、

スクリーンショット 2018-12-05 21.24.16.png

アクションからデプロイすればapiのURLが得られます。


5. 発信Webフックの「URL」にAPIのURLを設定

します。

スクリーンショット 2018-12-05 21.27.38.png


6. irb風チャンネル完成!

スクリーンショット 2018-12-05 21.28.16.png

チャンネルに適当なコードを送信して返ってきたら完了です。


Slack_irbはこんなことに使えます

素数判定したり


スクリーンショット 2018-12-05 21.40.14.png


コードと実行結果が他のユーザーと共有できるので、

公正な発表順の抽選に使ったり


スクリーンショット 2018-12-05 21.34.54.png


世界の国々の略称を視覚的に確認したり


スクリーンショット 2018-12-05 21.43.40.png


できます!


まとめ

実用上の話をすれば、irb風のSlackチャンネルの実用性は0ですが、

こういうものを作ってみてAWSのサービスの使い方を覚えたり試行錯誤することが大事なんじゃないでしょうか(適当)


最後になりますが、PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています(Rubyエンジニア以外も)。

もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう!

PORTもくもく会

PORT Advent Calendarはまだまだ続きます。乞うご期待!