6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINE WORKSAdvent Calendar 2022

Day 20

LINE WORKS で GitHub の Webhook 通知を受け取る

Last updated at Posted at 2022-12-19

この記事は LINE WORKS Advent Calender 2022 20 日目の記事です。

LINE WORKS で GitHub の通知を受け取る

GitHub で対象のリポジトリにプッシュされた際の Webhook による通知を、
LINE WORKS のトークルームに送信するための Bot を開発してみます🙌

LINE WORKS Bot 開発

今回の技術スタックをさらっとご紹介です💁‍♂️🌟

  • Node.js
  • AWS API Gateway
  • AWS Lambda
  • AWS CDK

これにプラスで GitHub の Webhook の知識が(少し)あれば OK です◎

なお、今回は LINE WORKS のアドベントカレンダーなので、
LINE WORKS の Bot 開発に関する部分の解説に焦点を当てたいと思います。

Bot 開発の準備

API 2.0 を使用するために、アプリの新規追加をおこないます。

LINE WORKS Developer Console > API 2.0 の「アプリの新規追加」からアプリを追加します。

追加の流れに沿って、Service AccountPrivate Key の発行までおこないます。

また、OAuth Scopes では bot を選択します。

lineworks_アプリ追加.png

今回は Bot 登録も画面からおこないます。基本的に必須項目を入力すれば OK です!

対話型の Bot ではないので、Callback URL は Off のままとします。

また、複数の開発メンバーと Bot のトークルームを作成できるように、「複数人のトークルームに招待可」を有効にしています。

1:1 のトークで良い場合は無効のままで良いかと思いますので、ここはお好みでどうぞ😉

Bot IDは登録が完了すると発行されます。

lineworks_bot.png

Bot ソースコード

ソースコードは GitHub で公開しています。要チェックです🫰

Lambda 関数

メインどころのコードを解説していきたいと思います!

GithubWebhookFunc.ts
import {APIGatewayProxyEvent, APIGatewayProxyHandler} from "aws-lambda";
import SecretType from "./types/Secret.type";
import * as jwt from "jsonwebtoken";
import axios from "axios";
import UserAccountAuthType from "./types/AccessToken.type";

// @ts-ignore
const handler: APIGatewayProxyHandler = async (event: APIGatewayProxyEvent) => {
    console.info(event);

    const githubEvent = event.headers["X-GitHub-Event"];
    if (githubEvent !== "ping" && githubEvent !== "push") {
        return  {
            statusCode: 400,
        };
    }

    // '204 No Content' returned on event when setting up Webhook.
    if (githubEvent === "ping") {
        return  {
            statusCode: 204,
        };
    }

    const query = event.queryStringParameters;
    if (query === null) {
        return  {
            statusCode: 400,
        };
    }
    const channelId = query.channelId;
    if (channelId === undefined) {
        return  {
            statusCode: 400,
        };
    }

    const requestBody = event.body;
    console.info(requestBody);

    if (requestBody === null) {
        return  {
            statusCode: 400,
        };
    }

    const webhookBody: {
        repository: {
            name: string;
        };
        pusher: {
            name: string;
        };
        compare: string;
    } = JSON.parse(requestBody);

    const secret = init();
    const accessToken = await genAccessToken(secret);

    const text = `${webhookBody.pusher.name} committed in ${webhookBody.repository.name} repository.`;
    await sendMessageWithActions(accessToken, secret.lineWorksBotId, channelId, text, webhookBody.compare);

    return {
        statusCode: 201,
    };
};

const init = (): SecretType => {
    return {
        lineWorksBotId: process.env.LINE_WORKS_BOT_ID ?? "",
        lineWorksClientId: process.env.LINE_WORKS_CLIENT_ID ?? "",
        lineWorksClientSecret: process.env.LINE_WORKS_CLIENT_SECRET ?? "",
        lineWorksDomainId: process.env.LINE_WORKS_DOMAIN_ID ?? "",
        lineWorksPrivateKey: process.env.LINE_WORKS_PRIVATE_KEY ?? "",
        lineWorksServiceAccount: process.env.LINE_WORKS_SERVICE_ACCOUNT ?? ""
    }
}

/**
 * Generate JWT
 * @param secret {SecretType} Secret in environment variables
 */
const genJwt = (secret: SecretType): string => {
    const payload = {
        iss: secret.lineWorksClientId,
        sub: secret.lineWorksServiceAccount,
        iat: Date.now(),
        exp: Date.now() + 3600,
    };
    const privateKey = secret.lineWorksPrivateKey.replace(/\\n/g, '\n');

    return jwt.sign(payload, privateKey, {algorithm: "RS256"});
}

/**
 * Generate Access token
 * @param secret {SecretType} Secret in environment variables
 */
const genAccessToken = async (secret: SecretType): Promise<string> => {
    const jwt = genJwt(secret);

    const params = new URLSearchParams({
        assertion: jwt,
        grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
        client_id: secret.lineWorksClientId,
        client_secret: secret.lineWorksClientSecret,
        scope: "bot",
    });

    const response = await axios.post("https://auth.worksmobile.com/oauth2/v2.0/token", params);
    const auth = response.data as UserAccountAuthType;

    return auth.access_token;
}

/**
 * Send a message with action to the talk room
 * @param accessToken {string} access token
 * @param botId {string} bot id
 * @param channelId {string} channel id
 * @param text {string} text message
 * @param uri {string} GitHub comparing changes URL
 */
const sendMessageWithActions = async (
    accessToken: string,
    botId: string,
    channelId: string,
    text: string,
    uri :string,
): Promise<void> => {
    try {
        const headers = {
            Authorization: `Bearer ${accessToken}`
        }

        const url = `https://www.worksapis.com/v1.0/bots/${botId}/channels/${channelId}/messages`;
        const response = await axios.post(url, {
            content: {
                type: "button_template",
                contentText: text,
                actions: [{
                    type: "uri",
                    label: "Comparing changes",
                    uri: uri
                }]
            }
        }, { headers });
        console.info(response);
    } catch(error) {
        console.error(error);
    }
}

export { handler };

User Account認証 (OAuth) に関する処理で、アクセストークンを発行しているのは genAccessToken 関数です。

上記関数内で genJwt 関数を呼び出して、JWT の生成をしています。

そして、Botによるトークルームへのメッセージ送信は sendMessageWithActions 関数でおこなっています。

今回はユーザーではなくトークルームにメッセージを送信するため、
https://www.worksapis.com/v1.0/bots/${botId}/channels/${channelId}/messages に対して POST でリクエストを送信します。

channelId は URL クエリパラメータで受け取るようにしています。

メッセージには GitHub の Comparing changes 画面に遷移するボタンをつけたいので、
button_template をタイプのメッセージを利用します。

また、ハードコーディングを避けたい認証情報などは Lambda の環境変数 に定義しておき、init 関数で取得しています。

認証情報は環境変数に定義するより AWS Secrets Manager を利用する方が望ましいようです。
今回は楽な実装方法ということで環境変数に定義しています🙇

GitHub の Webhook 通知については、プッシュ時に送られてくる以下の情報をメッセージとして送るために利用しています。

  • repository.name: 対象のリポジトリ名
  • pusher.name: プッシュしたユーザー名
  • compare: Comparing changes 画面の URL

デプロイと環境変数の設定

AWS CDK コマンドでデプロイをおこない、デプロイされた Lambda 関数の環境変数を設定します。

npm i
cdk synth --quiet
cdk deploy

デプロイが完了したら、Lambda のコンソール画面から以下の認証情報を環境変数に設定します。

すべて LINE WORKS Developer Console で確認・コピーできます。

  • LINE_WORKS_BOT_ID
    • Bot ID
  • LINE_WORKS_CLIENT_ID
    • Client ID
  • LINE_WORKS_CLIENT_SECRET
    • Client Secret
  • LINE_WORKS_DOMAIN_ID
    • Domain ID
  • LINE_WORKS_PRIVATE_KEY
    • Private Key
  • LINE_WORKS_SERVICE_ACCOUNT
    • Service Account

Private Key については、JWT 生成時にエラーが発生したため、\nを追加し置換処理を入れました。
例) -----BEGIN PRIVATE KEY-----\nABC...XYZ\n-----END PRIVATE KEY-----

Github Webhooks の設定

トークルームの作成

チャンネルID(トークルームID)を先に作っておく必要があるため、トークルームを作成します。

チャンネルIDをコピーして控えておきます。

GitHub Settings

GitHub で通知を受け取りたいリポジトリの Settings > Webhoooks で、Payload URL に API Gateway の URL を設定します。

クエリパラメータ channelId を忘れないようにしましょう。

https://{example}.execute-api.ap-northeast-1.amazonaws.com/prod/github-webhooks?channelId={チャンネルID}

github_webhooks.png

また、その他の設定は以下のように変更します。

  • Content tyep: application/json
  • Which events would you like to trigger this webhook?: Just the push event

通知を受け取ってみる

GitHub の画面上で直接ファイルを編集してコミット・プッシュして、通知が届くか確認してみます!

lineworks_github.gif

無事、プッシュ時の通知を受け取ることができましたー🍻🥂🍾

さいごに

他のサービスと連携できる Bot を何か作れないかなと思い、GitHub の Webhook 通知を受け取ってみました。

この記事では GitHub ですが、Webhook 機能が提供されているサービスであれば、同じような仕組みで連携ができそうですよね!

他のサービス連携しまくって、より便利な LINE WORKS にしちゃっていましょう🥳

以上です🫰✨

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?