slackにスタンプが押されたら、Trelloにカードを追加する

# この記事について
slackのスタンプでTrelloにカード追加を追加します。
lambdaを使ったサーバーレス構成の勉強もかねてやってみました。


# 全体図
こうなることを目指していきます。
<img width="868" alt="スクリーンショット 2020-06-01 21.23.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/ebce59e6-b40e-c25f-94b8-92df6d2a4395.png">

# サーバー（AWS）側の設定
## lambdaの作成
AWSのアプリケーションで、「Lambda」を選択して、「関数の作成」を選びます。
適当な名前をつけて、ぽちぽちで作成できます。
rubyが好きなので、rubyで書きます。
<img width="1334" alt="スクリーンショット 2020-06-01 21.45.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/769b7798-0ab3-8304-0ba9-cd0e69e87157.png">

lambdaが作成されます。
中身は変えずに、そのままにしておきます。
<img width="1414" alt="スクリーンショット 2020-06-01 21.55.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/160873a0-782b-a044-d777-505991e581d3.png">
<img width="1422" alt="スクリーンショット 2020-06-01 21.55.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/3d6a05af-34e7-dad9-3a03-343e7e6146a0.png">


## API-Gatewayの作成
lambdaは、サーバーレスでロジックを実行することができますが、
このロジックを実行するためのリクエストURLが必要です。そこにAPI-Gatewayが利用できます。

これもぽちぽちのみで完了します。
lambdaのコンソールから、自動で生成できます。
「トリガーを追加」をクリック
「Api Gateway」を選択して、セキュリティは「オープン」を選択して、追加します。
<img width="666" alt="スクリーンショット 2020-06-01 21.59.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/baefce5a-0227-facd-b18c-6afb69613cf4.png">

## 疎通の確認
1. API-Gatawayを開くと、先ほど作成したlambdaと同名のapiが作成されています。
<img width="683" alt="スクリーンショット 2020-06-02 9.30.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/48ef07e8-dfb8-c471-e1b2-fd04bc62688a.png">

2. 名前をクリックして、URLとルートを確認します。
<img width="675" alt="スクリーンショット 2020-06-02 9.32.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/4e27199c-267b-389f-c993-3a5920f396ba.png">
<img width="382" alt="スクリーンショット 2020-06-02 9.34.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/70870053-cefd-e84c-fc36-f8e677918754.png">

3. コンソールでリクエストしてみる

"Hello from Lambda!"と表示されれば成功です。

```bash
$curl https://{API ID}.execute-api.ap-northeast-1.amazonaws.com/slack-reaction-trello
"Hello from Lambda!"%
```

# Slack Events APIの設定
## Appの作成
slackで何かしらのアクションを拾う場合は、Events APIを使います。
https://api.slack.com/apps にアクセスして、「Create New App」を押します。

以下の2つを入力して「OK」します。
・ App名（適当）
・ workspace

登録できると、こんな画面が表示されますので、「Event Subscriptions」を選択します。
<img width="1418" alt="スクリーンショット 2020-06-01 21.35.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/31b85805-5273-0556-99ac-5728bf4e2ccc.png">

## challenge認証を成功させる
「Event Subscriptions」の画面で、URLを単純に入力すると、エラー
<img width="733" alt="スクリーンショット 2020-06-02 9.43.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/4d314eac-b119-b5e7-2283-d757b5403d84.png">


Event APIを作成する際、challenge認証が要求されます。
lambda側で、毎度異なった値が送信されている、"challenge"パラメータをresponseとして返却する必要があります。

requestのbodyは、引数の"event"内に格納されていますので、そちらを返却値にセットするだけです。

```ruby:lambda_function.rb
def lambda_handler(event:, context:)
    # 送信されたbodyをそのまま返却する
    { statusCode: 200, body: JSON.generate(event["body"]) }
end
```

slackで、「Retry」すると、成功が表示されます。
<img width="423" alt="スクリーンショット 2020-06-02 10.00.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/14f9146d-e1f1-da52-68e0-905e2b96311a.png">

## bot eventsの追加
どんなeventが発生したら、このAppを起動するかを設定してきます。
今回は、スタンプによるeventの発火を狙いたいので、
「Subscribe to bot events」内で、`reaction_add`を選択して、save changesします。
<img width="894" alt="スクリーンショット 2020-06-02 12.30.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/8e09ec9c-8aca-feca-b5df-8be4153fe710.png">

## eventが発火されるかの確認
workspaceにAppをインストールします。
<img width="664" alt="スクリーンショット 2020-06-02 12.39.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/7c0c4f21-9a1d-cc34-1247-4f9b83b3c8fa.png">

するとworkspace上で、上記のbotをチャンネルに招待することができるようになります。
このbotが招待されているチャンネルのみで、このeventは発火されるようになります。

早速チャンネルに、botを招待してみます。

```
/invite @{bot名}
```

<img width="333" alt="スクリーンショット 2020-06-02 12.47.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/ff3ea489-e7f5-8aae-28f9-97ff0610d9cf.png">

スタンプを押してみます。
<img width="272" alt="スクリーンショット 2020-06-02 12.52.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/db12686a-b666-93f0-9c44-84215dd4916c.png">


lambdaコンソール > モニタリング > cludwatchLogsのログを確認
で、実行ログを確認できるので見てみます。

<img width="681" alt="スクリーンショット 2020-06-02 12.50.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/504b2830-45ee-c05a-95fc-e91b62a4df52.png">

新たなログが作成されていました。
イベントの発火もこれで完了です。

# Trello API用のtokenの準備
## アクセスキーの準備
下記の3つを準備します。
こちらの記事を参考にさせていただきました。
https://qiita.com/isseium/items/8eebac5b79ff6ed1a180

- TrelloAPIの access_token
- TrelloAPIの secret_key
- 追加したいリストの list_id

取得できたら、lambdaの環境変数に保存しておきます。
<img width="732" alt="スクリーンショット 2020-06-02 13.11.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/0b3dcb8e-e158-f5af-ecc4-183ea7b6325f.png">

## 試しにカードを作成させてみる
ここまできたら、Trelloにカードが追加できるようになります。
lambdaのロジックを修正して、カードを作成させます。

```ruby:lambda_function.rb
require 'json'
require 'net/http' #追加

def lambda_handler(event:, context:)
    params = { #追加
         key: ENV['trello_access_key'],
        token: ENV['trello_access_token'],
        idList: ENV['trello_target_list_id'],
        name: 'slackのスタンプによるカード追加'
    }
    uri = URI.parse("https://trello.com/1/cards") #追加
    response = Net::HTTP.post_form(uri, params) #追加
    
    { statusCode: 200, body: JSON.generate(event["body"]) }
end
```

再度、同じチャンネルでスタンプを押してみます。
Trelloにカードが追加されれば、成功です。

# slack APIによるメッセージ・スレッドの取得
あとはイベントからメッセージを取得して、TrelloAPI叩けば完了だー。わーい。
意気揚々と、送信されてきたeventのパラーメータを確認すると、、、

```json
// id系はxxxxでマスクしています。
{
  "token"=>"xxxx",
  "team_id"=>"xxxx",
  "api_app_id"=>"xxxx",
  "event"=>
  {
    "type"=>"reaction_added",
    "user"=>"xxxx",
    "item"=>
    {
      "type"=>"message",
      "channel"=>"xxxx",
      "ts"=>"1591069623.000700"
    },
    "reaction"=>"trello",
    "event_ts"=>"1591071843.001100"
  },
  "type"=>"event_callback",
  "event_id"=>"xxxx",
  "event_time"=>1591071843,
  "authed_users"=>["xxxx"]
}
```

そう、メッセージの情報は含まれていないのです。。。😭
ついでに先に言ってしまうと、メッセージにスタンプが押された場合と、スレッドにスタンプが押された場合で内容の取得方法が異なっていました。

なのでメッセージの取得をできるようにします。

## 権限の追加
「OAuth & Permissions」のページ内、「Scopes」に`channels:history`を追加します。
※もし、privateチャンネルで利用したい場合は`groups:history`の追加が必要です。

<img width="754" alt="スクリーンショット 2020-06-02 14.32.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/579cb596-28a8-7577-b9e3-b9931352d317.png">

## slack access-tokenの取得
`OAuth Access Token`をcopyして、trelloのキーと同じように、lambdaの環境変数に保存します。
<img width="944" alt="スクリーンショット 2020-06-02 14.34.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/b0a704f6-05d2-f180-89ff-a1e7d442f9ed.png">

## lambdaでskack apiを利用してメッセージの内容を取得する
結果だけ記載します。
綺麗に書き直してはいないので、ご容赦ください。。。

```ruby:lambda_function.rb
require 'json'
require 'net/http'

def lambda_handler(event:, context:)
    event_hash = JSON.parse(event['body'])

    # trelloスタンプの場合に実行する
    stamp = event_hash['event']['reaction']
    if stamp == 'trello'
        
        slack_channel_id = event_hash['event']['item']['channel']
        #slackメッセージ取得
        slack_uri = URI.parse('https://slack.com/api/channels.history')
        slack_params = {
            channel: slack_channel_id,
            token: ENV['slack_access_token'],
            oldest: event_hash['event']['item']['ts'],
            latest: event_hash['event']['item']['ts'],
            inclusive: true
        }
        slack_res = Net::HTTP.post_form(slack_uri, slack_params)
        slack_res_hash = JSON.parse(slack_res.body)
        
        # 取得できなかったらスレッドなので再度取得
        unless slack_res_hash['ok']
            slack_uri = URI.parse('https://slack.com/api/conversations.replies')
            slack_params = {
                channel: slack_channel_id,
                token: ENV['slack_access_token'],
                ts: event_hash['event']['item']['ts']
            }
            slack_res = Net::HTTP.post_form(slack_uri, slack_params)
            slack_res_hash = JSON.parse(slack_res.body)
        end
        
        params = {
            key: ENV['trello_access_key'],
            token: ENV['trello_access_token'],
            idList: ENV['trello_target_list_id'],
            name: slack_res_hash['messages'].first['text']
        }
        uri = URI.parse("https://trello.com/1/cards")
        response = Net::HTTP.post_form(uri, params)
    end
    
    { statusCode: 200, body: JSON.generate(event["body"]) }
end
```

# 動作確認
以上で、スタンプを押した時に、Trelloにカードを追加できるようになりました！
動作を確認してみましょう。

メッセージと、スレッドに、それぞれtrelloスタンプを押します。

<img width="329" alt="スクリーンショット 2020-06-02 14.44.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/1ff82656-b783-0656-2bf5-3500954cbe01.png">

すると、
<img width="451" alt="スクリーンショット 2020-06-02 14.45.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330639/6d1e8b72-77f7-5298-b02e-5c5af9c7fa92.png">

無事、Trelloにカードが追加されました。

# 注意など
## lambdaの冪等生性について
上記で、カードを追加できるように放ったのですが運用しているとカードが2つ作成される現状が見られました。
調べてみると、こちらはLambdaの仕様で、Lambdaの実行は最低1回以上実行されることが保証されているだけであって、処理の冪等性については、サーバーロジック側で担保が必要とのことでした。

DynamoDBなどを利用して、タイムスタンプでゴニョゴニョするのが良いらしいです。
[アプリケーションにおける不整合性とデータ損失を防ぐために Lambda 関数を冪等にするにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-function-idempotent/#:~:text=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6%E3%80%81%E5%86%AA%E7%AD%89%E6%80%A7,%E8%A8%AD%E8%A8%88%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82
)

今回の場合は、別に2つ作成されても消せば良いのでそこまでの対応はしませんでした。