この記事は 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 Account と Private Key の発行までおこないます。
また、OAuth Scopes では bot を選択します。
今回は Bot 登録も画面からおこないます。基本的に必須項目を入力すれば OK です!
対話型の Bot ではないので、Callback URL は Off のままとします。
また、複数の開発メンバーと Bot のトークルームを作成できるように、「複数人のトークルームに招待可」を有効にしています。
1:1 のトークで良い場合は無効のままで良いかと思いますので、ここはお好みでどうぞ😉
Bot IDは登録が完了すると発行されます。
Bot ソースコード
ソースコードは GitHub で公開しています。要チェックです🫰
Lambda 関数
メインどころのコードを解説していきたいと思います!
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}
また、その他の設定は以下のように変更します。
- Content tyep: application/json
- Which events would you like to trigger this webhook?: Just the push event
通知を受け取ってみる
GitHub の画面上で直接ファイルを編集してコミット・プッシュして、通知が届くか確認してみます!
無事、プッシュ時の通知を受け取ることができましたー🍻🥂🍾
さいごに
他のサービスと連携できる Bot を何か作れないかなと思い、GitHub の Webhook 通知を受け取ってみました。
この記事では GitHub ですが、Webhook 機能が提供されているサービスであれば、同じような仕組みで連携ができそうですよね!
他のサービス連携しまくって、より便利な LINE WORKS にしちゃっていましょう🥳
以上です🫰✨