こんにちは。PORT株式会社の18卒エンジニアの@rnittaです。
業務ではサーバー/フロント/iOSの開発をしています。
1週間ほど前にAWS LambdaがRubyをサポート(&あらゆる言語対応)というアナウンスがありましたので、
今回はFaaStRubyへの追悼の意を込めて、「LambdaでRuby」ネタを書きます。
今回作るもの
Slackのチャンネルをirb風にします。
irbとはRubyのREPL(Read-Eval-Print Loop)ですが、今回は「Read-Eval-Print」するだけのものを作ります。
そういった意味でirb"風"です。
こういった、SlackにRubyのコードを送信すると
Rubyのコードとして解釈して返してくれるボットを作り、
チャンネルをirb化させます。
以下手順です。
1. Slackにチャンネルを作成
irb化させるチャンネルを作ってください。
2. 発信Webフックを追加
Slackをカスタマイズ > App管理 > カスタムインテグレーション > 発信 Webフック(Outgoing Webhook) > 設定を追加
するとSlackへの投稿をフックに発動するボットを追加できます。
追加したら各項目を適切に設定してください。
「チャンネル」は先程作ったチャンネルに、
「引き金となる言葉」は空のままに、
「URL」も一旦空にしておきます(あとでAPI GatewayのURLを入れます)。
3. AWS Lambdaで関数を作成
「ランタイム」はRubyを選択し、適当な名前をつけて作成します。
関数の編集画面に入ったら、
Designerから高輪API Gatewayを追加します。
API Gatewayの設定はあとでします。
発信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']
に先程作ったボットのトークンを入れておき、リクエスト毎にそのトークンが含まれるか検証します。
↑環境変数はコードの真下のここで設定します。
@johnrengelman We're in AWS so don't have a fixed IP range. But, outgoing webhooks include a "token" which can be used for verification!
— Slack Platform (@SlackAPI) 2015年2月15日
(リクエスト元を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。
マッピングテンプレートを設定したら、
アクションからデプロイすればapiのURLが得られます。
5. 発信Webフックの「URL」にAPIのURLを設定
します。
6. irb風チャンネル完成!
チャンネルに適当なコードを送信して返ってきたら完了です。
Slack_irbはこんなことに使えます
素数判定したり
コードと実行結果が他のユーザーと共有できるので、
公正な発表順の抽選に使ったり
世界の国々の略称を視覚的に確認したり
できます!
まとめ
実用上の話をすれば、irb風のSlackチャンネルの実用性は0ですが、
こういうものを作ってみてAWSのサービスの使い方を覚えたり試行錯誤することが大事なんじゃないでしょうか(適当)
最後になりますが、PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています(Rubyエンジニア以外も)。
もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう!
PORTもくもく会
PORT Advent Calendarはまだまだ続きます。乞うご期待!