初めてのサーバーレス(AWS Lambda) ~なろう小説更新bot on Slack~

# 授業で、AWSのLambdaを使ってSlackボット的なものを学び、なろう小説のAPIを使ったbot的なものを作ったので備忘録まとめます！
![スクリーンショット 2019-09-27 13.06.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/a4fd94b2-e071-1d43-a664-1349ef6f729e.png)


## 環境
PC: MacBook Air

## 参考記事

[文の途中で改行する／if文も値を返す ― コーディングミスを防ぐには？（1）](https://www.buildinsider.net/language/rubytips/0002)
[Rails5+heroku+nokogiriで特定のサイトのみスクレイピングが出来ない](https://teratail.com/questions/134715)
[なろうデベロッパー](https://dev.syosetu.com/man/api/)

---

(:bee:以下実装メモ:bee:)

今回作ったのは、
Slackの特定のチャンネルで、`narou_test:n8139dg`というように投稿すると、
あらかじめ引き金として設定している`narou_test:`をOutgoing Webhookが拾い、
Api Gatewayが呼ばれてLambdaを呼び出し、
Lambdaでなろう小説APIを叩いて情報を取得し、結果をSlackに投稿する
といったものです。

![スクリーンショット 2019-09-28 17.31.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/54290aaf-a955-580c-da7b-0d2abf79ea61.png)



#### その１　AWS Lambda関数の作成

まずはAWSコンソール画面で、サービス一覧からLambdaを検索、左のメニュー画面から、[関数]を選択し、右上の[関数の作成]を押下

![スクリーンショット 2019-09-27 16.27.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/9e1ee1b6-1ac4-e05e-6432-52238e506cf6.png)

[一から作成]を選択し、関数名・ランタイムは適宜適当に、今回は関数名をtestNarouBot,ランタイムをruby 2.5にして、あとはいじらずに[関数の作成]を押下

![スクリーンショット 2019-09-27 16.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/fa2dc882-ce74-a7fe-3b0e-d606b66b77bb.png)

作成できたら、とりあえず右上の[テスト]を押下する
最初はテスト内容を作成する画面になるが、疎通確認したいだけなので、適当にイベント名をつけて内容はHello Worldのままで[作成]

![スクリーンショット 2019-09-27 16.36.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/8c165fd1-0131-9613-eeb0-ea70f0c4e27d.png)

そうしたら、作成したテストが選択されている状態になるので、再び右上の[テスト]を押下すると、今度はちゃんと実行されて、結果が一番上と、関数編集画面の右下半分エリアに表示される

![スクリーンショット 2019-09-27 19.04.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/3464ff2d-2cda-9bc8-6e75-7967bdaeaa6b.png)

特にエラーなく、テストができたら、いったんLambdaは置いておく

#### その２　Amazon　API Gatewayの作成

次に、API Gatewayを作成する
サービス一覧からAPI Gatewayを検索して別タブで開き、[APIの作成]を押下

![スクリーンショット 2019-09-27 19.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/fe4e0cd9-2cbb-7d5c-0ab4-dbced8f10b7a.png)

API名だけ適当に（今回はslack_narou）入れてあとはそのままで[APIの作成]を押下

![スクリーンショット 2019-09-27 19.19.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/f393d7ca-8ed6-4727-c939-1e5d14613472.png)

[アクション]→[リソースの作成]を押下し、

![スクリーンショット 2019-09-27 19.21.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/e577ec0e-cdef-6de4-9ce5-9c55421653f7.png)

適当にリソース名をつけ、あとはそのままで[リソースの作成]を押下

![スクリーンショット 2019-09-27 19.23.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/407271ee-010c-072f-e944-852f7d737a4f.png)

次に、[メソッドの作成]を押下し、

![スクリーンショット 2019-09-27 19.24.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/1c0d10c5-486f-82d4-a089-fe7181ffe0e8.png)

[POST]を選択したら、左横のチェックマーク:ballot_box_with_check: をクリックして確定する

![スクリーンショット 2019-09-27 19.26.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/f972ff55-eb57-7ba3-e089-fc5f19c0831b.png)

確定すると、右側にセットアップ情報が出てくるので、Lambda関数の部分だけ、
先ほど作ったLambda関数名を入れて、他はそのままに[保存]を押下する
すると、

```
Lambda 関数に権限を追加する
API Gateway に、Lambda 関数を呼び出す権限を与えようとしています
```
上記のようなアラートが出るので、[OK]をクリックする

![スクリーンショット 2019-09-27 19.28.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/ca2e412f-fb30-e4af-6a63-855dbc76ef13.png)

画面が切り替わるので、次に、[統合リクエスト]をクリックし、[マッピングテンプレート]を開き、
[テンプレートが定義されていない場合 (推奨)] にラジオボタンを入れ、[マッピングテンプレートの追加]を押下、
Content-Typeに

`application/x-www-form-urlencoded`

を入れてチェックマーク:ballot_box_with_check: をクリックして確定し、
下にスクロールして下記内容を丸っと貼り付けて[保存]をクリックする

（slackから受け取るパラメータは application/x-www-form-urlencoded だけど、API Gatewayはjsonで受けるため、変換が必要になる）

```
## convert HTML POST data or HTTP GET query string to JSON

## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path('$'))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}
```
マッピングの登録までできたら、POSTメソッドを選択した状態で
[アクション]→[APIのデプロイ]をクリック

![スクリーンショット 2019-09-27 19.42.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/2c22f1d6-6f2e-8dfb-cd8b-248a214bbe27.png)

デプロイされるステージは初回なので[新しいステージ]を選択し、
ステージ名などは適当に入力し[デプロイ]を押下

![スクリーンショット 2019-09-27 19.51.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/29647a1c-634b-53a2-9250-c0a790e78683.png)


デプロイが終わったら、[ステージ]のstageを展開すると、先ほどデプロイしたリソース名の下にPOSTメソッドが出てくるので、それをクリックすると、
`URL の呼び出し`としてデプロイしたAPI Gatewayを利用するためのURLが表示されるのでこれをコピーしてメモする

#### その３　SlackのOutgoing Webhookの設定

API GatewayのURLをゲットできたら、次はSlack側を設定する
Slack の管理画面をWebサイトで開くため、Slack Appの右上の歯車マークから、[アプリを追加する]をクリック

![スクリーンショット 2019-09-28 0.23.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/516edfd5-125b-fa2e-3435-e302dd901a99.png)

右上の[Appディレクトリを表示]を押下すると、Webページに遷移する

![スクリーンショット 2019-09-28 0.23.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/d344b346-95b8-1bc7-d8d3-46a14ac44db5.png)

検索バーから　Outgoing Webhook を検索して詳細ページを開く

![スクリーンショット 2019-09-28 0.24.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/a913b9a1-69fe-5ba4-93c6-09c3b0b9b80f.png)

[設定を追加]をクリック

![スクリーンショット 2019-09-28 0.28.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/4ce143dd-8a49-5157-3370-b6c86a51faf6.png)

[Outgoing Webhookインテグレーションの追加]をクリックする

![スクリーンショット 2019-09-28 0.28.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/39a84a6a-5cff-d3cf-e16f-965453cfa664.png)

インテグレーションの各設定を行う

・チャンネル
は今回は　#times_omori　を選択（botを使いたいチャンネルを選択すればOK）

・引き金となる言葉
は今回は　narou_test:　を設定（narou_test:を頭につけて投稿すると、Webhookが反応しbotが返ってくるようになるイメージ）

・URL
は先ほどデプロイしてコピーした、/narou にPOSTするためのURLを貼り付ける

・トークン
このトークンはコピーしておく（あとで、Lambdaが動く際に、このトークンをチェックすることにより、自分で設定したWebhookからのみ、Lambdaが動くように制御するために使う）

・説明ラベル
SlackでAppの説明欄に表示される文言なので適当に書いておく

・名前をカスタマイズ
・アイコンをカスタマイズする
botが返ってくる時に表示される名前や表示アイコンの設定なので適当なのを設定しておく

諸々設定したら、[設定を保存する]を押下したら、Slack側の設定は完了

#### その４　Lambdaの編集

再度Lambdaのコンソール画面を開き、Api Gatewayが追加されているのを確認できたら、次はLambdaにbot用の処理などを書いていく


```
# 処理の内容としてはざっとこんな感じ
1. Webhookから受け取ったテキストからnarouに対してAPIリクエスト 
2. 1のレスポンスを整形
3. 整形したテキストを返し、Slackポスト
```

まず、先ほどWebhookの設定時にコピーしたトークンを環境変数の部分に、
SLACK_OUTGOING_TOKEN　という変数名で登録し、忘れぬうちに左上の[保存]をクリックしておく

![スクリーンショット 2019-09-28 0.49.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/ead47678-160c-6e07-2624-6ed4a1a9f361.png)

わかりやすくするために、
`lambda_function.rb`という最初に作られているファイル名クリックし、`narou_main.rb`という名前に変更する

次に、最初から書かれている`lambda_handler`というファンクション名も、わかりやすく`narou_bot`に変更し、右上の [ハンドラ]の部分は
最初に実行するファイル名（識別子なし）.最初に実行するメソッド名
になっているので、
`narou_main.narou_bot`に変更する
end手前の一行をSlackに返答される形式({"text": "hoge"})に書き換え[保存]

```narou_main.rb
{ statusCode: 200, body: JSON.generate('Hello from Lambda!') }
```

↓

```narou_main.rb
{ "text": JSON.generate('Hello from Lambda!') }
```

```(※修正後全体)narou_main.rb
require 'json'

def narou_bot(event:, context:)
    # TODO implement
    { "text": JSON.generate('Hello from Lambda!') }
end
```


![スクリーンショット 2019-09-28 0.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/986b9a78-6cc0-9e9c-457b-4c64f83987aa.png)

この状態で、いったんSlackから、引き金ワードの`narou_test:`を叩いてみると、


![スクリーンショット 2019-09-28 1.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/71b80879-73d7-0bfa-1d4e-106bbe7e7725.png)

ちゃんとSlackに返ってくることが確認できた！

次に、Slackトークンのチェックも追加してみてきちんと返ってくることを再度確認する

```(※修正後全体)narou_main.rb
require 'json'

def narou_bot(event:, context:)
    
    #Slackトークンチェック
    raise InvalidToken unless ENV["SLACK_OUTGOING_TOKEN"] == event["token"]
    
    # TODO implement
    { "text": JSON.generate('Hello from Lambda!') }
end
```

返ってくること確認できたら、次はSlackに投稿された文言を取得してHello from Lambda!の代わりに返してみる

```(※修正後全体)narou_main.rb
require 'json'

def narou_bot(event:, context:)
    
    #Slackトークンチェック
    raise InvalidToken unless ENV["SLACK_OUTGOING_TOKEN"] == event["token"]
    
    #event["text"]でSlack投稿の内容まるっと取得できる
    tmp = event["text"]
    
    # TODO implement
    { "text": JSON.generate(tmp) }
end

```

![スクリーンショット 2019-09-28 1.14.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/0b869921-b305-b111-5b54-06dfd7201299.png)


できた！！
次にncodeを指定して抜き出してみる

```(※修正後全体)narou_main.rb
require 'json'

def narou_bot(event:, context:)
    
    #Slackトークンチェック
    raise InvalidToken unless ENV["SLACK_OUTGOING_TOKEN"] == event["token"]
    
    #event["text"]でSlack投稿の内容まるっと取得できる
    tmp = event["text"]
    
    #ncode部分のみを取得（narou_test:が引き金で、今の所ncodeは7文字だけど、余裕を持って抜き出しておく）
    ncode = tmp[11,23]
    
    # TODO implement
    { "text": "指定されたncode→ " + ncode }
end

```
![スクリーンショット 2019-09-28 1.20.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/9c2c5ebf-41e7-c241-ffdb-b37c2bd47316.png)

できた！
あとはこのncodeを使って、なろう小説APIで情報を取得してきて整形して、Slackに返すようにするだけ！


```(※修正後全体)narou_main.rb
require 'json'
require 'net/http'

def narou_bot(event:, context:)
    
    #Slackトークンチェック
    raise InvalidToken unless ENV["SLACK_OUTGOING_TOKEN"] == event["token"]
    
    #event["text"]でSlack投稿の内容まるっと取得できる
    tmp = event["text"]
    
    #ncode部分のみを取得（narou_test:が引き金で、今の所ncodeは7文字だけど、余裕を持って抜き出しておく）
    ncode = tmp[11,23]
    
    #なろう小説APIをリクエスト
    uri = URI.parse('https://api.syosetu.com/novelapi/api/?of=t-n-w-ga-gl&ncode=' + ncode + '&out=json')
    response = Net::HTTP.get(uri)
    
    #取得した結果をparseする
    dat = JSON.parse(response)
    
    reply = "[タイトル]" + dat[1]["title"] \
    + "\n[ncode]" + dat[1]["ncode"] \
    + "\n[作者]" + dat[1]["writer"] \
    + "\n[最終更新日]" + dat[1]["general_lastup"] \
    + "\n[最新話数]" + dat[1]["general_all_no"].to_s
    
    # TODO implement
    { "text": reply }
end

```

![スクリーンショット 2019-09-28 1.32.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/81c2698a-66ee-5ae8-07d4-1b050800bb6d.png)

できた！！

詰まったところとしては下記あたり

・投げているなろう小説のAPIのパラメータは
[なろうデベロッパー](https://dev.syosetu.com/man/api/)をみながら設定
（ `of=t-n-w-ga-gl` の部分は欲しい情報を`-`で繋げるような仕組みだった）
（一番後ろの `&out=json` を最初つけ忘れていて、文字コードエラーでだいぶ引っかかったので、要注意:ghost:）

・rubyでの改行の仕方がわからず調べた（`\`を後ろにつければ繋げられる）

・`general_all_no`だけ数値で、rubyでは文字列と数値を連結させる場合は数値の後ろに`.to_s`をつけなければエラーになってしまうとこも知らなかったのでつまづいた


あとは、もうちょっとみやすく使いやすくなるように、ちょっと表示を調整して、、、

```(※最終版全体)narou_main.rb
require 'json'
require 'net/http'

def narou_bot(event:, context:)
    
    #Slackトークンチェック
    raise InvalidToken unless ENV["SLACK_OUTGOING_TOKEN"] == event["token"]
    
    #event["text"]でSlack投稿の内容まるっと取得できる
    tmp = event["text"]
    
    #ncode部分のみを取得（narou_test:が引き金で、今の所ncodeは7文字だけど、余裕を持って抜き出しておく）
    ncode = tmp[11,23]
    
    #なろう小説APIをリクエスト
    uri = URI.parse('https://api.syosetu.com/novelapi/api/?of=t-n-w-ga-gl&ncode=' + ncode + '&out=json')
    response = Net::HTTP.get(uri)
    
    #取得した結果をparseする
    dat = JSON.parse(response)
    
    lastup_url = "https://ncode.syosetu.com/" + ncode + "/" + dat[1]["general_all_no"].to_s

    reply = "[" + dat[1]["ncode"] + "] " + dat[1]["title"] \
    + " / " + dat[1]["writer"] \
    + "\n最終更新日 " + dat[1]["general_lastup"] \
    + "\n最新話 " + dat[1]["general_all_no"].to_s \
    + "\n →見にいく:broccoli: " + lastup_url
    
    # TODO implement
    { "text": reply }
end

```
![スクリーンショット 2019-09-28 1.48.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/6d5f963c-8f9c-7dd8-ff9e-b237cd62b19e.png)

完成！！！

---

(:alien:振り返り:alien:)

ほんとは最新更新話をスクレイピングして題名と、最初の１５文字くらい表示できたらいいなと思って、`open-uri`などを使ってやってみていたのですが、
403で弾かれてしまい、調べてみると、なろう小説サイト自体がamazonなどからのアクセス？を弾いているらしい記事を見つけたので今回はスクレイピングは諦めました。。

あと、最初にSlackで投稿した内容を確認するために、

reply = event["text"]
{ "text": reply }

にして　narou_test:

とSlackで投稿したら、当たり前ですが永久ループ状態(botとして返ってきた`narou_test:`が再び引き金となってしまう)になってしまい一瞬テンパリました。
一生止まらなかったらどうしよう:scream:と。。。。

![スクリーンショット 2019-09-28 2.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463891/cdd0f686-396d-5130-1eac-7d52257672bf.png)

（すぐに、他の内容が返ってくるようにLambdaの記述書き換えて止めることができました:wrench:）


AWS Lambdaは聞いたことはあるけどなんか難しそうな名前だな、、と思っていたのですが、Slackbotが予想より簡単に作れて、とても便利だと感じました！

まだまだ難しい使い方はできないですが、選択肢が増えて良かったです。

（※今回やった内容だとセキュリティ面に関してはあまり意識を向けておらず弱いので、実践よりはとりあえずLambda触ってみる用、といった感じのサンプルと思っていただけますと幸いですprz）

以上！