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

こんにちは。PORT株式会社の18卒エンジニアの@rnittaです。
業務ではサーバー/フロント/iOSの開発をしています。

1週間ほど前に[AWS LambdaがRubyをサポート（&あらゆる言語対応）](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-use-any-programming-language-and-share-common-components/)というアナウンスがありましたので、
今回は[FaaStRuby](https://faastruby.io/)への追悼の意を込めて、「LambdaでRuby」ネタを書きます。

# 今回作るもの
Slackのチャンネルをirb風にします。
irbとはRubyのREPL(Read-Eval-Print Loop)ですが、今回は「Read-Eval-Print」するだけのものを作ります。
そういった意味でirb"風"です。

今回のゴール：
<img width="407" alt="スクリーンショット 2018-12-05 20.20.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/3e71f391-1b29-747c-36c9-e84817af20cd.png">

こういった、SlackにRubyのコードを送信すると
Rubyのコードとして解釈して返してくれるボットを作り、
チャンネルをirb化させます。

以下手順です。

# 1. Slackにチャンネルを作成
irb化させるチャンネルを作ってください。

# 2. 発信Webフックを追加
`Slackをカスタマイズ > App管理 > カスタムインテグレーション > 発信 Webフック(Outgoing Webhook) > 設定を追加`
するとSlackへの投稿をフックに発動するボットを追加できます。
追加したら各項目を適切に設定してください。

「チャンネル」は先程作ったチャンネルに、
「引き金となる言葉」は空のままに、
「URL」も一旦空にしておきます（あとでAPI GatewayのURLを入れます）。

# 3. AWS Lambdaで関数を作成
「ランタイム」はRubyを選択し、適当な名前をつけて作成します。
<img width="870" alt="スクリーンショット 2018-12-05 20.33.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/a5ff5dd7-ea86-753f-a863-36966c0d68ec.png">

関数の編集画面に入ったら、
Designerから~~高輪~~API Gatewayを追加します。
API Gatewayの設定はあとでします。
<img width="961" alt="スクリーンショット 2018-12-05 20.35.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/caa60e7e-0317-3c42-8cf5-f755d9b2c0df.png">

発信Webフックで飛んできたリクエストの本文をコードとして実行して、
json形式で返す処理を書きます。
Hashを返せばjsonで出力してくれます。
リクエストやレスポンスの形式は発信Webフックの「発信ペイロードとレスポンス」を参考にします。

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

```ruby
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']`に先程作ったボットのトークンを入れておき、リクエスト毎にそのトークンが含まれるか検証します。
<img width="859" alt="スクリーンショット 2018-12-05 20.52.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/af67f096-611f-d20d-3214-6466c71c628c.png">
↑環境変数はコードの真下のここで設定します。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr"><a href="https://twitter.com/johnrengelman?ref_src=twsrc%5Etfw">@johnrengelman</a> We&#39;re in AWS so don&#39;t have a fixed IP range. But, outgoing webhooks include a &quot;token&quot; which can be used for verification!</p>&mdash; Slack Platform (@SlackAPI) <a href="https://twitter.com/SlackAPI/status/567110311476350976?ref_src=twsrc%5Etfw">2015年2月15日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

（リクエスト元を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`

```VTL
#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](https://qiita-image-store.s3.amazonaws.com/0/265793/74a52058-740b-5937-6fea-2bed53a7719e.png)
アクションからデプロイすればapiのURLが得られます。

# 5. 発信Webフックの「URL」にAPIのURLを設定
します。

<img width="1028" alt="スクリーンショット 2018-12-05 21.27.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/9395b8c3-1567-306e-8b60-80977422738a.png">


# 6. irb風チャンネル完成！

<img width="213" alt="スクリーンショット 2018-12-05 21.28.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/08fba079-66fc-b247-28b4-0593a28d73b0.png">

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

# Slack_irbはこんなことに使えます
素数判定したり
> <img width="249" alt="スクリーンショット 2018-12-05 21.40.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/4a77a5f8-7c30-d557-b4b7-981134878a54.png">


コードと実行結果が他のユーザーと共有できるので、
公正な発表順の抽選に使ったり
> <img width="375" alt="スクリーンショット 2018-12-05 21.34.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/763861a5-c804-edc1-bef2-2071a2d88adf.png">


世界の国々の略称を視覚的に確認したり
> <img width="1141" alt="スクリーンショット 2018-12-05 21.43.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/265793/5136c2c6-98e2-7c83-2295-d8882f13143d.png">


**できます！**

# まとめ

実用上の話をすれば、irb風のSlackチャンネルの実用性は0ですが、
こういうものを作ってみてAWSのサービスの使い方を覚えたり試行錯誤することが大事なんじゃないでしょうか（適当）

---

最後になりますが、PORT株式会社では[自社サービスを支えてくれる優秀なRubyエンジニアを募集しています](https://www.theport.jp/)(Rubyエンジニア以外も)。
もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう！
[PORTもくもく会](https://freestyle-mokumoku.connpass.com/)

[PORT Advent Calendar](https://qiita.com/advent-calendar/2018/port)はまだまだ続きます。乞うご期待！