ConnpassのAPIを利用し、Mattermostからイベント検索

# 経緯
[Connpass](https://connpass.com/)をよく利用させて頂いており、[API](https://connpass.com/about/api/)があることを見つけました。
[Mattermost](https://mattermost.com/)へイベントのキーワードを入力し、結果が取得できたら便利かなと思い立ちました。
Pythonのコードは特に拙いのでより良い点がありましたらご教示をお願い致します。

# 概要図
1. 指定のチャンネルへ検索したいキーワードを入力
2. Mattermostの外向きウェブフックへ設定したコールバックURLへメッセージ送信
3. API GatewayがメッセージをLambdaへパススルー
4. Lambdaがメッセージ内から検索キーワードを元にconnpassのイベントサーチAPIを実行
5. Connpass のイベントサーチAPIから帰ってきた結果からLambdaにてイベントURLのみMattermost（内向きウェブフック）へ送信
6. Mattermostの指定のチャンネルにLambdaからイベントURLが送信される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316662/80ce6296-788b-7393-28fe-fc5232e0656f.png)

# 設定

## Lambda

### Python3
```python
import os
import json
import urllib.request
from urllib.parse import urlparse
import time
import datetime

Contact_Slack = os.environ['SLACK']
# Lambdaの環境変数に設定されたMattermostの内向きウェブフックのURLを変数へ

url = "https://connpass.com/api/v1/event/?keyword=東京都"
# Connpass APIのURLを指定。全国の結果が帰ってきてしまうので、キーワードへ東京都を追加。

p = urlparse(url)
query = urllib.parse.quote_plus(p.query, safe='=&')
url = '{}://{}{}{}{}{}{}{}{}'.format(
    p.scheme, p.netloc, p.path,
    ';' if p.params else '', p.params,
    '?' if p.query else '', query,
    '#' if p.fragment else '', p.fragment)
# URLにマルチバイトの文字を扱えない為、フォーマットを変更

def post_slack(message):
# post_slack関数に渡された内容をMattermostの内向きウェブフックへ送る関数です。

    msg = str(message)
    SLACK_POST_URL = Contact_Slack
    
    send_data = {
        "username": "Slack-test",
        "text": msg
    }
# usernameは不要かもしれません。

    send_text = "payload=" + json.dumps(send_data)

    request = urllib.request.Request(
        Contact_Slack, 
        data=send_text.encode('utf-8'), 
        method="POST"
    )

    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

def lambda_handler(event, context):
# API Gateway経由でMattermostから送られたデータをConnpassのAPIへ投げます。
    print(event)
    keyword_list = event["text"]
    print(keyword_list)
    keyword = list(map(str, keyword_list.split(" ")))
# キーワードは半角スペース区切りにしているので、分割しlist型で変数へ。
    try:

        for events in keyword:
            dic = {}
            print(events)
            post_slack(events)
# post_slack関数へeventsを送信
            events = urllib.parse.quote(events)
# キーワードが日本語の場合があるので、エンコードを実施
            api = url + "," + events
# 入力したキーワードは東京都と合わせて二つ目の為、URLとキーワードの間に,を入れる
            print(api)
            req = urllib.request.Request(api)
            with urllib.request.urlopen(req) as res:
                data = res.read().decode('utf-8')
# キーワードごとにConnpassのAPIへ投げ、結果をdataへ格納

            events = json.loads(data)
            for key in events["events"]:
                eve = key["event_url"]
                started = key["started_at"]
                print(eve)
                print(started)
                dic.setdefault(started, eve)
# イベントのスタート日時とイベントURLを格納

            print(dic)
            dct = sorted(dic.items())
# スタート日時でソート
            print(dct)
# 検索結果は過去分のイベントが含まれている場合がある為、以下の処理でMattermostへ送らないようにします。

            for k, v in sorted(dic.items()):
# ソートは冗長な記載のような
                print(str(k) + ": " + str(v))
                k = k.replace("+09:00", "")
# イベントのスタート日時がISO-8601形式（2012-04-17T18:30:00+09:00）な為、+09:00を消します。
                #print(k)
                dt = datetime.datetime.fromisoformat(k)
# 文字型からdatetime型へ
                #print(dt)
                if dt > datetime.datetime.today():
# もし、スタート日時が今日以降であればpost_slack関数とLogへ出力
                    print(str(v))
                    post_slack(v)
            print("END")
            post_slack("END")
        print("ALL END")
        post_slack("ALL END")

    except:
        print("ERROR")
        post_slack("ERROR")
```

### 環境変数
SLACK：Mattermost内向きウェブフック

### タイムアウト
1分

### その他
デフォルト


## Mattermost

### 外向きウェブフック
MattermostからAPI GatewayのURLを設定

タイトル：任意
Content-Type: application/json
チャンネル：キーワード入力するチャンネル
トリガーワード：なし
コールバックURL：API GatewayのURL

### 内向きウェブフック
Lambdaから結果を貰うURLの設定

タイトル：任意
チャンネル：結果を貰うチャンネル（ループしそうなので外向きウェブフックと分けました。）


## API Gateway

### APIの作成

|項目|値|
|---|---|
|プロトコル|REST|
|新しい API の作成|新しい API|
|API 名*|任意|
|説明|任意|
|エンドポイントタイプ|リージョン|

### メゾットの作成
アクション→メゾットの作成

|項目|値|
|---|---|
|メゾット|POST|
|統合タイプ| Lambda 関数|
|Lambda プロキシ統合の使用|チェックなし|
|Lambda リージョン|Lambda作成したリージョン|
| デフォルトタイムアウトの使用|チェックあり|

### ステージの作成
アクション→ステージの作成

|項目|値|
|---|---|
|ステージ名*|任意|
|ステージの説明|任意|
|デプロイメント*|任意|

ここのステージに表示されるURL呼び出しのURLがMattermostの外向きウェブフックへ設定するURLになります。

### リソースポリシー
このままだとなんの制限も入っていない為、API Gatewayを呼び出せるIPアドレスに制限をかけます。
作成したAPIの画面内にリソースポリシーがある為、選択し以下のjsonを入れ保存します。


```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "メゾットリクエストのARN",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "アクセス元IP/サブネットマスク"
                }
            }
        }
    ]
}
```

保存が完了したら、アクションからAPIをデプロイさせ、反映させてください。

## 設定は以上です。
Mattermostからキーワードを入力し、指定のチャンネルへURLが受信できれば完成となります。

# 良いところ
- Mattermostへのキーワード入力がトリガー
- キーワードの複数に対応
- 過去分のイベントは送信されない
- 日付でソートされて送信される
- Mattermostなのでスマホからでも利用できる

# 躓いた事
- 東京都が全角でconnpassのAPIを叩けない
    - 参考にさせて頂いた記事　
        - [python3で日本語を含むURL(日本語URL)にurllibでアクセスする際、htmlで勝手にencodeされてエラーするので回避策をメモ](https://qiita.com/mix/items/87d094414e46f857de45)
- MattermostからAPI Gatewayの通信がうまくいかない
    - API Gatewayのマッピングテンプレートをデフォルトのままにする。
        - Slack等で類似の事をされている記事がたくさんあり、そちらではマッピングテンプレートの追加をされていますが私のケースではLambdaへ連携されなかった為、デフォルトのままにしています。
        - リソースポリシーはデプロイしないと反映されない。
            - 保存して満足してましたが、制限がかかっていませんでした。
        - リソースポリシーはサブネットマスクの表記までいる（気がする）
- 通知内容がごちゃごちゃする
    -  MattermostはURLを見やすくしてくれるので、URLのみ送信することにした。
    -  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316662/93a00e85-f6b0-ce8e-10d5-879cf6c4f7ed.png)
- 過去分のイベントが送られてくる
    - Lambdaで判定したいが、ISO-8601形式（2012-04-17T18:30:00+09:00）な為、比較ができない
        - +09:00を消した。
- 送られてくるイベントの日時がバラバラなので近い日付から順番に送られるようにしたい
    - Lambdaでスタート日時でソート

# まとめ
Mattermostの記事が少なく、API Gatewayとうまく連携取れない時が大変でした。
（どっち側が悪いのか検討がづけられず・・・）
Pythonの勉強になりました。