見出し画像

Slackと連携していたWebhook通知をTeamsに連携させる

カシオでサーバ開発に携わっている高橋です。
この記事では、Slackと連携させていたWebhook通知をMicrosoft Teamsに連携させるためにおこなったことについて記載をします。
通知先のTeamsのチーム・チャンネルは作成済の前提です。


背景

これまでチーム内のコミュニケーションや開発に関する様々なやり取りを、Slackでおこなっていました。
カシオとアシックスが共同開発したランニング向けスマートフォンアプリのRunmetrix、ウォーキング向けパーソナルコーチングアプリのWalkmetrixのバックエンドで動いているMetrixサーバの開発・運用においても、下記を参考にしてAWS上で稼働しているMetrixサーバでのエラー発生や、開発中のコード修正のプルリクエスト(PR)発行などをSlackにWebhook通知していました。

諸事情によりSlackが使えなくなりTeamsに移行する必要が発生したため、Webhook通知しているものについてもTeamsへの移行が必要となりました。
また一から作るのは既存リソースがもったいないので、下記の3点を要件に設定してTeamsに移行することにしました。

  1. Slackへの通知で使っていたAWSリソース(EventBridge, SNS, Lambda)はなるべく流用する

  2. Slackに通知していたメッセージのフォーマットはTeamsでも基本的に変更しない

    1. エラー通知:Webhook通知されたメッセージはそのままチャンネルに投稿する
      ※他チームの方がエラー解析に使っており、フォーマットを変更してほしくない要望があった

    2. PR通知:イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明など

  3. PRの通知についてはレビュワーにメンションされるようにする
    ※ Slackのときに取り入れていたため継続したい

参考:システム構成

Metrixサーバのエラー通知やコード修正時のPR通知は、おおよそ下記のようなシステム構成でSlackに届いていました。
今回の対応により、Slackの場所がTeamsに差し替わるイメージです。

Slackには、下記のようなメッセージフォーマットで通知が届いていました。

エラー発生時:

アプリからアップロードされたデータの処理中にエラー発生したときの通知

PR時:

PR発行/PR更新/マージ/コメントのときの通知

エラー発生時のWebhook通知とPR時のWebhook通知で行ったことが違う箇所があるので、以降それぞれに記載します。


エラー発生時のWebhook通知をTeams移行する

実際に移行するにあたって実施したことは下記の3点です。

  1. Teamsワークフローの作成、Webhook用URLの取得

  2. LambdaのWebhook通知先URLを変更

  3. Teamsワークフローを修正し、チャンネルにメッセージが流れるようにする

以降、順を追って説明します。

1. Teamsワークフローの作成、Webhook用URLの取得

Teamsの左側に並んでいるアイコンから 、"Workflows" を選択します。
※なければ、三点リーダーのアイコンから探す

"新しいフロー"を選択し、"Webhook要求を受信するとチャネルに投稿する"を選択します。

表示されるダイアログでフロー名を入力、Teamsチーム・チャネルを選択してフローを作成すると、Webhook用URLを取得することができます。
後で使うので控えておきましょう。

2. LambdaのWebhook通知先URLを変更

Webhook通知するLambdaからSlackに通知するのに使っているURLを1.で取得したURLに差し替えればOKです。

3. Teamsワークフローを修正し、チャンネルにメッセージが流れるようにする

2までの作業で、作成したTeamsワークフローにメッセージが到達するようになるので、Teamsチャンネルにメッセージが投稿されるようにワークフローを修正します。

Teamsのワークフローの画面に戻り、1.で作成したワークフローを編集します。
例として、"{text: {key1: value1, key2: value2}}"というフォーマットのリクエストボディをWebhookで通知するものとします。

  • ワークフロー作成時点で入っている"Send each adaptive card"は削除

  • 「新しいステップ」から"JSONの解析"を追加(「組み込み」タブから探すとすぐ見つかる)

    • コンテンツ:本文 ※「動的なコンテンツの追加」から追加

    • スキーマ:(「サンプルから作成」で送信するメッセージのサンプルを貼り付ければ生成してくれる)

  • 続けて、「新しいステップ」から"チャットまたはチャネルでメッセージを投稿する"を追加

    • 投稿者:フローボット

    • 投稿先:チャネル

    • チーム:(投稿先チャネルのチーム)

    • チャネル:(投稿先チャネル)

    • メッセージ:text(メッセージのルートキー) ※「動的なコンテンツの追加」から追加

最終形のワークフローは下記のような感じになります。
properties.text.typeをobjectにして内部構造を記述すればtext内部も解析してくれますが、textの文字列がそのままTeamsの投稿に表示されればよいのでstringにしています。

通知されたメッセージをそのままTeamsの投稿に出すだけなら、ワークフローはシンプルです。
実際に通知させてみた結果は下記です。

Slackに通知していたときと、おおよそ同じにできました。
投稿者が「Workflow経由の〇〇(ワークフロー作成者の名前)」になっているのが個人的にイマイチですが、ここまででWebhook通知をTeamsで受け取ることはできます。


PR時のWebhook通知をTeams移行する

PR時のWebhook通知については、

  1. イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明などをわかりやすく表示する

  2. レビュワーにメンションされるようにする

の2点を満たす必要があります。
また、PRに関しては「発行」「更新」「コメント追加」「マージ」「却下」それぞれに応じてメッセージを変えたいので、追加作業が必要です。
PR時のWebhook通知の移行にあたっては下記4点を実施しました。

  1. Teamsワークフローの作成、Webhook用URLの取得

  2. LambdaのWebhook通知先URLを変更

  3. Lambdaのコード変更

  4. 通知メッセージを解析し、チャンネルに流れるメッセージを整形するようにTeamsワークフローを修正する

1.と2.まではエラー発生時の通知とやることが同じなので割愛して、以降では3.から説明します。

3. Lambdaのコード変更

プルリクエスト通知用のLambdaのコードを下記サイトを参考に書きます。

コードは下記のような感じ。

  • lambda_handler:Lambda実行時のエントリーポイント

  • get_message:EventBridgeから渡されたイベントを解釈してTeamsに通知するメッセージを整形

  • post_teams:TeamsにWebhook通知

import json
import os
import re
import requests
import sys
from logging import getLogger, INFO ,StreamHandler

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

TEAMS_WEBHOOK_URL = os.environ['WEBHOOK_URL']
REGION = os.environ['REGION']
REPOSITORY_URL_BASE = os.environ['REPOSITORY_URL_BASE']


def lambda_handler(event, context) -> None:
    logger.info('event:')
    logger.info(event)
    message_dict_str = json.dumps(event, ensure_ascii=False)
    logger.info('message:')
    logger.info(message_dict_str)
    msg_title, msg_title_fixed, detail = get_message(event)
    post_teams(msg_title, msg_title_fixed, detail)

def get_message(message: dict) -> (str, str, list):
    msg_title = message['detail-type']
    event = message['detail']['event']
    # PRへのコメント
    if (event == "commentOnPullRequestCreated" or event == "commentOnPullRequestUpdated"):
        # msg_title
        msg_title_fixed = 'プルリクエストにコメントされました'
        # detail
        user_arn = message['detail']['callerUserArn']
        user_arn_splitted = user_arn.split('/')
        user_name = user_arn_splitted[1] + '/' + user_arn_splitted[2]

        repo_name = message['detail']['repositoryName']

        pull_request_id = message['detail']['pullRequestId']
        url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + '/activity' + REGION

        detail = {
            'event':msg_title_fixed,
            'repo_name':repo_name,
            'id':pull_request_id,
            'author':user_name,
            'url':url
            }
    # PR作成/更新
    elif (event == "pullRequestCreated" or event == "pullRequestSourceBranchUpdated"):
        # msg_title
        msg_title_fixed = 'プルリクエストが作成されました' if message['detail']['event'] == "pullRequestCreated" else 'プルリクエストが更新されました'
        # detail
        repo_name = message['detail']['repositoryNames'][0]
        src_branch = message['detail']['sourceReference'][10:] # ref/heads/を削る
        dst_branch = message['detail']['destinationReference'][10:] # ref/heads/を削る
        author_arn = message['detail']['author']
        author_arn_splitted = author_arn.split('/')
        author_name = author_arn_splitted[1] + '/' + author_arn_splitted[2]
        description = message['detail']['description'] if 'description' in message['detail'] else 'No Description'
        pull_request_id = message['detail']['pullRequestId']
        url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + REGION

        title = message['detail']['title'] if 'title' in message['detail'] else 'No Title'
        title_split = title.split() if 'title' in message['detail'] else 'No Title'
        reviewer = set_reviewer(title_split[0])
        title_fixed = title_split[1] if reviewer != '' else title

        detail = {
            'event': msg_title_fixed,
            'title': title_fixed,
            'reviewer': reviewer,
            'repo_name':repo_name,
            'id':pull_request_id,
            'src_branch':src_branch,
            'dst_branch':dst_branch,
            'author':author_name,
            'url':url,
            'description':description
            }
    # PRマージ/却下
    else:
        # msg_title
        msg_title_fixed = ''
        if event == "pullRequestMergeStatusUpdated":
            msg_title_fixed = 'プルリクエストがマージされました'
        elif event == "pullRequestStatusChanged":
            msg_title_fixed = 'プルリクエストが却下されました'
        else:
            msg_title_fixed = event
        # detail
        repo_name = message['detail']['repositoryNames'][0]
        pull_request_id = message['detail']['pullRequestId']
        url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + REGION
        user_arn = message['detail']['callerUserArn']
        user_arn_splitted = user_arn.split('/')
        user_name = user_arn_splitted[1] + '/' + user_arn_splitted[2]

        description = message['detail']['description'] if 'description' in message['detail'] else 'No Description'

        detail = {
            'event': msg_title_fixed,
            'repo_name':repo_name,
            'id':pull_request_id,
            'author':user_name,
            'url':url,
            'description':description,
            }

    return msg_title, msg_title_fixed, detail

def set_reviewer(user):
    # メールアドレスとslackのときのメンション名が違う人がいたのでその置換用(なくていいが使い勝手のために残した)
    user_list = {
        '@xxxxx': '@yyyyy'
    }
    user = user.replace("@", "@")
    reviewer = user_list.get(user,user).replace("@", "") + "@casio.co.jp"
    return reviewer if user.startswith('@') else ''

def post_teams(msg_title: str, msg_title_fixed: str, detail: list) -> None:

    payload = {
        '@type': 'MessageCard',
        "@context": "http://schema.org/extensions",
        "themeColor": "0076D7",
        "summary": f'{msg_title}',
        "sections": {
            "activityTitle": 'CodeCommit Notifiction',
            "activitySubtitle": f'{msg_title}',
            "facts": detail,
            "markdown": 'true',
        }
    }

    logger.info('post message:')
    logger.info(json.dumps(payload))

    try:
        response = requests.post(TEAMS_WEBHOOK_URL, data=json.dumps(payload), headers={'Content-Type': 'application/json'})
    except requests.exceptions.RequestException as e:
        print(e)
    else:
        print(response.status_code)

※Lambdaの環境変数に下記を設定しています

  • WEBHOOK_URL:控えておいたWebhook用URL

  • REGION:?region=ap-northeast-1

  • REPOSITORY_URL_BASE:https://ap-northeast-1.console.aws.amazon.com/codesuite/codecommit/repositories/

上記コードで扱っているイベント以外に何があるか、EventBridgeから渡されるデータ構造が知りたい方は下記を参照ください。

実際にPRを発行した場合、Teamsに到達するリクエストボディは下記のようなフォーマットになります。
更新などの際は"summary"と"sections"の中身が変わります。

{
  "@type": "MessageCard",
  "@context": "http://schema.org/extensions",
  "themeColor": "0076D7",
  "summary": "CodeCommit Pull Request State Change",
  "sections": {
    "activityTitle": "CodeCommit Notifiction",
    "activitySubtitle": "CodeCommit Pull Request State Change",
    "facts": {
      "event": "プルリクエストが作成されました",
      "title": "タイトル",
      "reviewer": "レビュワーのメールアドレス",
      "repo_name": "リポジトリ名",
      "id": "PRのID",
      "src_branch": "ソースブランチ名",
      "dst_branch": "ターゲットブランチ名",
      "author": "発行した人",
      "url": "PRのURL",
      "description": "PR内容に関する説明"
    },
    "markdown": "true"
  }
}

CodeCommitでPR発行する際にタイトルを"@xxxx タイトル"(xxxxの部分はメールアドレスの@より前)のようにすることで、Lambdaコードのset_reviewer関数において"reviewer"にレビュワーのメールアドレスが入るようにしており、これをメンションのために使います。 ("@xxxx"を付けなければ"reviewer"がnullになり、メンションされない)

4. 通知メッセージを解析し、チャンネルに流れるメッセージを整形するようにTeamsワークフローを修正する

上で挙げた、

  1. イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明などをわかりやすく表示する

  2. レビュワーにメンションされるようにする

を実現するために、ワークフローを修正していきます。
条件分岐がいくつか必要で、メンションされるようにする必要もあるため、ワークフローを頑張って作ります。

箇条書きで説明すると長くなるので、まず最終形のワークフローの画像から。
条件分岐させるために"条件"、メンションのために"ユーザーの@mentionトークンを取得する"のアクションを使います。

JSONの解析のスキーマは、下記のようになります。
長いですが、Teamsに届いたメッセージをコピーして「サンプルから作成」でペーストすれば勝手に作ってくれますのでご安心を。

{
    "type": "object",
    "properties": {
        "@@type": {
            "type": "string"
        },
        "@@context": {
            "type": "string"
        },
        "themeColor": {
            "type": "string"
        },
        "summary": {
            "type": "string"
        },
        "sections": {
            "type": "object",
            "properties": {
                "activityTitle": {
                    "type": "string"
                },
                "activitySubtitle": {
                    "type": "string"
                },
                "facts": {
                    "type": "object",
                    "properties": {
                        "event": {
                            "type": "string"
                        },
                        "title": {
                            "type": "string"
                        },
                        "reviewer": {
                            "type": "string"
                        },
                        "repo_name": {
                            "type": "string"
                        },
                        "id": {
                            "type": "string"
                        },
                        "src_branch": {
                            "type": "string"
                        },
                        "dst_branch": {
                            "type": "string"
                        },
                        "author": {
                            "type": "string"
                        },
                        "url": {
                            "type": "string"
                        },
                        "description": {
                            "type": "string"
                        }
                    }
                },
                "markdown": {
                    "type": "string"
                }
            }
        }
    }
}

ポイントは下記の2点です。

  • JSON解析で"sections"配下も解析させ、その結果から個々のイベントに合わせて表示したいキーを抜き出してメッセージを整形

  • "ユーザーの@mentionトークンを取得する"のアクションにレビュワーのメールアドレスを渡すようにし、メッセージに"@mention"を含めることでレビュワーの人にメンションされるようにする

実際にTeamsに投稿されるメッセージは、下記のような感じになります。
Slackのときのメッセージに近くなり、メンションも無事付いています。

当たり前ですが、通知されたメッセージをそのまま投稿するよりもわかりやすいです。
本当はURLやDescriptionでリンクを挿入できるようにしたかったのですが、文字列扱いで表示されてしまいました・・・
とりあえずコピーしてブラウザに貼り付ければいいので、保留しました。


おわりに

以上の作業を行うことで、無事SlackからTeamsに移行することができました。
メッセージの投稿者名やURLにうまくリンクが張られないなど、個人的に納得がいっていないところもありますが、そのうち時間があるときにまた調べてみようかなと思います。


【前回のテックブログはコチラ】
カシオの「G-SHOCK MOVE」や「CASIO WATCHES」という時計につながるスマートフォンアプリの保守や仕様設計の担当者が、さらにカシオの時計を楽しく使っていただくため、スマートフォンアプリの機能の一つである「ワールドタイム機能」について、紹介しています。

【カシオのソフトウェア採用についてはコチラ】


この記事が参加している募集