弊社では以前から Backlog と Typetalk で社内業務を管理してます。
参考: 創業時から15年間「徹底したリモートワーク」を追求する企業の取り組み、コミュニケーションの要は「Backlog」
とても便利なのですがヌーラボアカウントの所属組織が違う Backlog と Typetalk を連携することができません(2021年4月現在)。
昔は別組織の Backlog と Typetalk を連携できたのに、今は自組織の Backlog プロジェクトとしか連携できなくなってしまったんです。
自組織の Backlog は大体ブラウザで開いてますが、他組織の Backlog は開いてないことが多いので、他組織の Backlog とこそ連携してほしい。
(お客様の Backlog に入って進めているプロジェクトもあるので、結構他組織の Backlog プロジェクトも多いんです)
なければ作ればいいということで Backlog の Webhook と Typetalk のボットを使って、Backlog に課題立てられたりしたら、Typetalk に通知する仕組みを Lambda + API Gateway で実装しました。
Backlog の Webhook を受け取って Typetalk のボットに渡す
この部分は Lambda で実装しました。
こんな感じです。特に追加の npm パッケージは使ってないので handler.js
一つでいいです。
参考: Backlogから送信されるPOSTリクエストの内容〜Webhook〜
'use strict';
const https = require('https');
function unescapeHTML(str) {
return '> ' + str.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/&/g,'&')
.replace(/[\n\r]+/g, "\n> ");
}
function is_object(mixedVar) {
// discuss at: https://locutus.io/php/is_object/
// original by: Kevin van Zonneveld (https://kvz.io)
// improved by: Legaev Andrey
// improved by: Michael White (https://getsprink.com)
if (Object.prototype.toString.call(mixedVar) === '[object Array]') {
return false
}
return mixedVar !== null && typeof mixedVar === 'object'
}
async function sendRequest(opts,replyData){
// https://ky-yk-d.hatenablog.com/entry/2018/07/16/011748
return new Promise(((resolve,reject)=>{
let req = https.request(opts, (response) => {
response.setEncoding('utf8');
let body = '';
response.on('data', (chunk)=>{
body += chunk;
});
response.on('end', ()=>{
resolve(body);
});
}).on('error', (err)=>{
reject(err);
});
req.write(replyData);
req.end();
}));
};
module.exports.helloTypetalk = async (event) => {
const { queryStringParameters, body } = event;
// event.body をパースする
return new Promise( (resolve, reject) => {
try {
resolve({
topicId: queryStringParameters.topicId,
typetalkToken: queryStringParameters.typetalkToken,
body: JSON.parse(body)
});
}
catch (err) {
reject(err);
}
})
// Backlog から受け取った情報をいい感じに編集する
// 参考: https://qiita.com/rekooom/items/455c1e8ec247e3cb8abb
.then(data => {
const { project, content, createdUser, notifications } = data.body;
// 課題キー
let keyId = content.key_id ? `${project.projectKey}-${content.key_id}` : project.projectKey;
if (is_object(content.comment) && content.comment.id) {
keyId += `#comment-${content.comment.id}`;
}
// 通知タイプ
let notificationType = '';
switch(data.body.type) {
case (1): notificationType = '課題を追加'; break;
case (2): notificationType = '課題を更新'; break;
case (3): notificationType = '課題にコメント'; break;
case (14): notificationType = '課題をまとめて更新'; break;
case (17): notificationType = 'お知らせを追加'; break;
case (5): notificationType = 'wiki を追加'; break;
case (6): notificationType = 'wiki を更新'; break;
case (7): notificationType = 'wiki を削除'; break;
case (8): notificationType = 'ファイルを追加'; break;
case (9): notificationType = 'ファイルを更新'; break;
case (10): notificationType = 'ファイルを削除'; break;
case (11): notificationType = 'Subversion にコミット'; break;
case (12): notificationType = 'Git にプッシュ'; break;
case (13): notificationType = 'Git リポジトリを作成'; break;
case (15): notificationType = 'プロジェクトに参加'; break;
case (16): notificationType = 'プロジェクトから脱退'; break;
case (18): notificationType = 'プルリクエストを追加'; break;
case (19): notificationType = 'プルリクエストを更新'; break;
case (20): notificationType = 'プルリクエストにコメント'; break;
case (22): notificationType = '発生バージョン/マイルストーンを追加'; break;
case (23): notificationType = '発生バージョン/マイルストーンを更新'; break;
case (24): notificationType = '発生バージョン/マイルストーンを削除'; break;
default: notificationType = '何をしたかわからないけど、なにかを実行';
}
notificationType = encodeURIComponent(`${createdUser.name} さんが ${notificationType}しました。`);
// タイトル
let summary = content.summary ? content.summary : (content.name ? content.name : (content.repository ? content.repository.name : ''));
let title = encodeURIComponent(`${keyId} ${summary}`);
// メッセージ内容
let message;
if (content.comment) {
message = is_object(content.comment) && content.comment.content ? content.comment.content : content.comment;
} else {
message = content.description ? content.description : (content.name ? content.name : '');
}
message = encodeURIComponent(unescapeHTML(message));
// お知らせした人
let notify = '';
notifications.forEach(notification => {
const { user } = notification;
if (notify) { notify += ', '; }
notify += user.name;
if (user.nulabAccount) { notify += ` (@${user.nulabAccount.uniqueId})`; }
});
if (notify) {
notify = `\n` + encodeURIComponent(`お知らせした人: ${notify}`);
}
data.postDataStr = `${notificationType}\n${title}\n\n${message}${notify}`;
return Promise.resolve(data);
})
// Typetalk ボットを叩く
.then(data => {
const contentBody = `message=${data.postDataStr}`;
const options = {
host: 'typetalk.com',
port: 443,
path: `/api/v1/topics/${data.topicId}`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(contentBody),
'X-TYPETALK-TOKEN': data.typetalkToken
}
};
return sendRequest(options, contentBody);
})
.then(body => Promise.resolve({
'statusCode': 200,
'body': JSON.stringify(body)
}))
.catch(err => {
console.log(err);
return Promise.reject({
'statusCode': 500,
'body': JSON.stringify(err)
});
});
};
Serverless Framework で API Gateway を用意する
serverless.yml
は、こんな感じで大丈夫です。
参考: Serverless FrameworkでAPIGateway・Lambda・DynamoDBを構築する
service: backlog-gateway
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: '2'
provider:
name: aws
runtime: nodejs14.x
lambdaHashingVersion: 20201221
timeout: 300
memorySize: 256
# you can overwrite defaults here
stage: prd
region: us-west-2
# you can add packaging information here
package:
exclude:
- package.json
- package-lock.json
- event.json
functions:
helloTypetalk:
handler: handler.helloTypetalk
timeout: 30
events:
- httpApi:
path: /typetalk/post
method: post
これで https://example.execute-api.us-west-2.amazonaws.com/typetalk/post
みたいな感じの Webhook 用 URL ができます。
やったね。
あとは Typetalk のトピック設定でボットを追加してあげて、Backlog のプロジェクト設定の「インテグレーション」で Webhook を追加すれば良いです。
Backlog に Webhook として登録するURLは https://{API Gateway のドメイン}/typetalk/post?topicid={トピックID}&typetalktoken={Typetalk Token}
となります。
クエリストリングは以下の通りです。
-
{トピックID}
: Typetalk ボットの「メッセージの取得と投稿の URL」の/topics/
以降の数値 -
{Typetalk Token}
: Typetalk ボットの「Typetalk Token」
良い Typetalk を!