Salesforce & AWS Lambda でコミュニティ管理&イベント通知(Discord/Slack)を作った話

前回の記事[メインで利用しているクラウドを AWS から Salesforce に切り替えた話](https://qiita.com/Xudev/items/d5f7822180d6b71c68f9)で Salesforce に色々と移行していると話しましたが、今回は Saleforce を使っていかにコミュニティ管理を楽にできるか模索した話をしようと思います。

## コミュニティ管理について
さて、度々開催させていただいているもくもく会イベント（記事：[陰キャが初めてもくもく会・勉強会を開催した話](https://qiita.com/Xudev/items/c8fad239a37b9fd6af81)を参照）ですが、実はこちらも Salesforce で参加人数やメンバーを管理してます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466211/6afa1cb5-60b6-4626-869a-22693871a5d5.png)

おかげさまで、Connpass のほうで募集すると毎回あっという間に満員御礼になります。
意外と北千住周辺ってエンジニア多いんだなと改めて感じました。

固定で参加していただけるメンバーも増えてくると悩みが出てきます。
いかにこのコミュニティを管理して素早く新イベント開催の通知を届けるかですね。

現状 Discord と Slack のほうでメンバーを集めているため、Bot を作って API を呼び出して自動投稿される方法が考えられます。

## 全体のフロー
ここで改めて全体のフローを確認してみよう。
![構成図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466211/e785b584-56bb-439f-98d5-b5c33f8df95b.png)
自分が Connpass で募集をかけ、ユーザーが Connpass で参加登録し、その情報を Salesforce に統合し、Salesforce から Lambda 関数を呼び出してコミュニティに通知をする。

Salesforce から直接各コミュニティの API を呼び出すことも可能だと思いますが、Salesforce の場合コールアウトする URL を随一リモートサイトに追加しないといけないので、今後インテグレーション先が増えていくと管理しきれないなと思い、Lambda 関数での実装にしました。

できれば全部自動化したいところだが、今回は手動で Salesforce にイベント情報を商談レコードとして作成したときに Lambda 関数を呼び出すところだけを自動化しておきます。

## Lambda 関数 で Bot の作成
まずは最低限動くものを作ろうと思ってたので、下記記事を参考にしました。
[Lambdaで定時刻に呟くDiscord Botを作る](https://qiita.com/eda-kobayashi/items/650db9cefa25b6984db8)

RESTFUL API にしたほうが色々と拡張するときに便利かなと思って rawPath でどの操作をするか切り分けも実装しました。

```Python lmabda_function.py
import json
import requests
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    try:
        order = str(event["rawPath"]).replace("//", "")

        if order == "/event" or order == "event":
            body = json.loads(event["body"])
            content = body["event"]
            channel_id = "NOTICE_CHANNEL_ID"
            payload = {
                "content": "@everyone\n" + content,
                "text": "<!channel>\n" + content
            }
        elif order == "/test" or order == "test":
            body = json.loads(event["body"])
            content = body["event"]
            channel_id = 'TEST_CHANNEL_ID'
            payload = {
                "content": "@everyone\n" + content,
                "text": "<!channel>\n" + content
            }
        else:
            return {
                'statusCode': 502,
                'body': json.dumps('rawPath Error\n' + 'rawPath: ' + str(event["rawPath"]))
            }


        SLACK_WEB_HOOK_URL = 'https://hooks.slack.com/services/YOUR_HOOKS_PARAM'
        dc_url = "https://discord.com/api/channels/" + channel_id + "/messages"
        dc_headers = {
            "Authorization": "Bot YOUR_BOT_TOKEN",
            "Content-Type": "application/json"
        }

        dc_r = requests.post(dc_url, json=payload, headers=dc_headers)
        logger.info(dc_r)
        sl_r = requests.post(SLACK_WEB_HOOK_URL, json=payload)
        logger.info(sl_r)

        return {
            'statusCode': 200,
            'body': json.dumps("Succeed")
        }

    except Exception as e:
        logger.error(str(e))
        return {
            'statusCode': 502,
            'body': json.dumps(str(e) + "\n" +str(event))
        }
```

ここで注意すべきことは、Slack の場合、メッセージ内に `@here` など直書きしても実際にメンションされないので、`<!channel>` や `<!here>` などを使いましょう。

## Salesforce での作業
要件としてはレコード作成時に動くものなので、フローや Apex トリガーの実装が想定されます。
フローはバージョンアップに応じてどんどんわけわかんなくなってくるので、個人では管理しきれない可能性があります。
今回は Apex トリガーを実装しようと思います。

トリガー内で future メソッドを実装するのはベストプラクティスではないので、Salesforce から Lambda 関数を呼び出すために、まずはハンドラークラスを作ります。
```Apex Class EventCommuTriggerHandler.cls
public class EventCommuTriggerHandler {
    @future(callout=true)
    public static void sendNewOpportunitiesToLambda(List<Id> oppIds) {
        List<Opportunity> oppList = [
            SELECT Id, Name, CloseDate, Address__c, EventURL__c
            FROM Opportunity
            WHERE Id IN :oppIds
        ];
        String lambdaUrl = 'https://LAMBDA_URL/event';
        for (Opportunity opp : oppList) {
            if (String.isBlank(opp.Name)) {
                continue;
            }

            HttpRequest req = new HttpRequest();
            req.setEndpoint(lambdaUrl);
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');

            String message = '新イベント募集開始;\n' + opp.Name + '\n開催日時: ' + String.valueOf(opp.CloseDate) + '\n開催場所: ' + opp.Address__c + '\nイベント詳細URL: ' + opp.EventURL__c;

            Map<String, Object> payload = new Map<String, Object>{
                'event' => message
            };
            String body = JSON.serialize(payload);
            req.setBody(body);

            Http http = new Http();
            try {
                HttpResponse res = http.send(req);
                System.debug('Lambda Response Status: ' + res.getStatus());
                System.debug('Lambda Response Body: ' + res.getBody());
            } catch (Exception e) {
                System.debug('Error calling Lambda: ' + e.getMessage());
            }
        }
    }
}
```

商談レコードが作成された時に発動するトリガーを実装
```Apex Trigger EventCommuNotice.trigger
trigger EventCommuNotice on Opportunity (after insert) {
    if (Trigger.isAfter && Trigger.isInsert) {
        List<Id> oppIds = new List<Id>();
        for (Opportunity opp : Trigger.new) {
            oppIds.add(opp.Id);
        }
        EventCommuTriggerHandler.sendNewOpportunitiesToLambda(oppIds);
    }
}
```


## 運用テスト
試しにレコードを作成してみたら実際に通知がされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466211/58bc6723-b82e-42e8-b5d0-37ef1f3af140.png)
若干タイムラグはあるものの、別に急ぐものでもないので、これでいいかなと思います。

## あとがき
今後は参加者統計と商談レコードの作成も自動化したいので、もしかしたら例えば gcal を挟むなども考えていこうかなと思います。
![構成図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466211/9b9ffb74-6874-46e1-b4d6-2986112ea13d.png)