Edited at

GASを使ってbacklogの通知をslackに流す

More than 1 year has passed since last update.


この記事の対象


  • プロジェクトマネジメントツールにbacklogを使ってる

  • なんでもslackにまとめる文化がある

  • 無料が良い

  • 難しい設定したくない

というチームに。


こんな感じです

スクリーンショット 2018-05-15 14.08.46.png

コメント、変更点、(descriptionに変更があれば)descriptionの3点が表示されます。

このworkspaceではまいまいが通知してくれてます。女神。

ちなみにSlack上でmaimaiという名前で絵文字登録してるので:maimai:でこの絵文字が出ますw

slackの設定です。


コード

var postUrl = PropertiesService.getScriptProperties().getProperty('maimai'); //GASのプロパティに登録してるslackのwebhook URL。直で文字列として書いても良いです!

var BASEURL = "https://{teamName}.backlog.com/"; //backlog開いたときのurlのbase部分。{teamName}に値入れてください
var slackMemberSheetId = ""; //slackのidとbacklogのidを変換するのに使ってるスプレッドシートのid
// 上のシートでの列番号。Aが0で、Gが6
var colSlackMemberId = 0;
var colBacklogMemberId = 6;
var alertMessage = ":maimai_3:「Backlogからのお知らせだよ〜!」"; //botからのメッセージ。癒されるやつを入れてください。

var backlog_changes_status = {
"1":"未対応",
"2":"処理中",
"3":"処理済み",
"4":"完了"
};

var backlog_changes_priority = {
"2":"",
"3":"",
"4":""
};

var backlog_changes_resolution = {
"":"",
"0":"対応済み",
"1":"対応しない",
"2":"無効",
"3":"重複",
"4":"再現しない"
};

// backlogからのjsonを整形
function makeChatMessage(body) {
var msgObj = new Object();
var label = "";
var bl_key = "";
var bl_summary = "";
var bl_comment = "";
var bl_url = "";
var bl_to ="";
var bl_changes = "";
var bl_description = "";

switch (body.type) {
case 1:
label = "追加";
bl_key = "["+body.project.projectKey+"-"+body.content.key_id+"]";
bl_summary = "" + body.content.summary + "";
bl_url = BASEURL+"view/"+body.project.projectKey+"-"+body.content.key_id;
bl_description = body.content.description;
break;
case 2:
label = "更新";
bl_key = "["+body.project.projectKey+"-"+body.content.key_id+"]";
bl_summary = "" + body.content.summary + "";
bl_url = BASEURL+"view/"+body.project.projectKey+"-"+body.content.key_id;
if(body.content.changes.length > 0){
for(var c = 0; c < body.content.changes.length; c++){
switch (body.content.changes[c].field){
case 'status':
bl_changes += "・状態を「"+backlog_changes_status[body.content.changes[c].old_value]+"」から「"+backlog_changes_status[body.content.changes[c].new_value]+"」に変更しました。\n";
break;
case 'priority':
bl_changes += "・優先度を「"+backlog_changes_priority[body.content.changes[c].old_value]+"」から「"+backlog_changes_priority[body.content.changes[c].new_value]+"」に変更しました。\n";
break;
case 'assigner':
bl_changes += "・担当者を"+body.content.changes[c].old_value+"から"+body.content.changes[c].new_value+"に変更しました。\n";
break;
case 'component':
bl_changes += "・カテゴリを「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'description':
bl_changes += "・説明文を変更しました。\n";
bl_description += body.content.changes[c].new_value;
var stringChangeLength = body.content.changes[c].new_value.length-body.content.changes[c].old_value.length;
var oldarr = body.content.changes[c].old_value.split("");
var newarr = body.content.changes[c].new_value.split("");
if(stringChangeLength==0){
for(var i in oldarr){
if(oldarr[i]!=newarr[i]){
if(newarr[i-1]=="["&&newarr[i]=="x"){
msgObj = false;
return msgObj;
}
}
}
}
break;
case 'issueType':
bl_changes += "・issueTypeを「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'summary':
bl_changes += "・課題名を「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'limitDate':
bl_changes += "・期限日を「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'startDate':
bl_changes += "・開始日を「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'resolution':
bl_changes += "・完了理由を「"+backlog_changes_resolution[body.content.changes[c].old_value]+"」から「"+backlog_changes_resolution[body.content.changes[c].new_value]+"」に変更しました。\n";
break;
case 'version':
bl_changes += "・発生バージョンを「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'milestone':
bl_changes += "・マイルストーンを「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'attachment':
bl_changes += body.content.changes[c].new_value+"を添付しました。\n";
break;
case 'estimatedHours':
bl_changes += "・予定時間を「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
case 'actualHours':
bl_changes += "・実績時間を「"+body.content.changes[c].old_value+"」から「"+body.content.changes[c].new_value+"」に変更しました。\n";
break;
default:
return;
}
}
if(body.content.comment.content){
bl_comment += body.content.comment.content;
}
}
break;
case 3:
label = "コメント";
bl_key = "["+body.project.projectKey+"-"+body.content.key_id+"]";
bl_summary = "" + body.content.summary + "";
bl_url = BASEURL+"view/"+body.project.projectKey+"-"+body.content.key_id+"#comment-"+body.content.comment.id;
bl_comment = body.content.comment.content;
break;
case 14:
label = "課題まとめて更新";
bl_key = "";
bl_summary = "";
bl_url = BASEURL+"projects/"+body.project.projectKey;
bl_comment = body.createdUser.name+"さんが課題をまとめて操作しました。";
break;
case 5:
label = "Wiki追加";
bl_key = "";
bl_summary = ""+body.content.name+"";
bl_url = BASEURL+"alias/wiki/"+body.content.id;
bl_comment = body.createdUser.name+"さんがWikiページを追加しました。";
break;
case 6:
label = "Wiki更新";
bl_key = "";
bl_summary = ""+body.content.name+"";
bl_url = BASEURL+"alias/wiki/"+body.content.id;
bl_comment = body.createdUser.name+"さんがWikiページを更新しました。";
break;
case 11:
label = "SVNコミット";
bl_key = "[r"+body.content.rev+"]";
bl_summary = "";
bl_url = BASEURL+"rev/"+body.project.projectKey+"/"+body.content.rev;
bl_comment = body.content.comment;
break;
case 12:
label = "Gitプッシュ";
var git_rev = body.content.revisions[0].rev;
git_rev = git_rev.substr(0,10);
bl_key = "["+git_rev+"]";
bl_summary = "";
bl_url = BASEURL+"git/"+body.project.projectKey+"/"+body.content.repository.name+"/"+body.content.revision_type+"/"+body.content.revisions[0].rev;
bl_comment = body.content.revisions[0].comment;
break;
case 18:
label = "プルリクエスト追加";
bl_key = "( 担当:"+body.content.assignee.name+" )";
bl_summary = ""+body.content.summary+"";
bl_url = BASEURL+"git/"+body.project.projectKey+"/"+body.content.repository.name+"/pullRequests/"+body.content.number;
bl_comment = body.content.description;
break;
case 19:
label = "プルリクエスト更新";
bl_key = "( 担当:"+body.content.assignee.name+" )";
bl_summary = ""+body.content.summary+"";
bl_url = BASEURL+"git/"+body.project.projectKey+"/"+body.content.repository.name+"/pullRequests/"+body.content.number;
bl_comment = body.content.description;
break;
case 20:
label = "プルリクエストコメント";
bl_key = "( 担当:"+body.content.assignee.name+" )";
bl_summary = "";
bl_url = BASEURL+"git/"+body.project.projectKey+"/"+body.content.repository.name+"/pullRequests/"+body.content.number+"#comment-"+body.content.comment.id;
bl_comment = body.content.comment.content;
break;

default:
return;
}

if(bl_comment=="//"){
msgObj = false;
return msgObj;
}

if(body.notifications.length > 0){
bl_to += "_to ";
for(var i = 0; i < body.notifications.length; i++){
bl_to += "@"+body.notifications[i].user.nulabAccount.uniqueId+" ";
}
}

if(label){
msgObj['message'] = alertMessage+"\n";

if(bl_to != ""){
msgObj['message'] += bl_to+"\n";
}

msgObj['message'] += bl_key+" "
+ label
+ bl_summary
+ " by "
+ body.createdUser.name
+ "\n "+bl_url;

msgObj['comment'] = bl_comment;
msgObj['description'] = bl_description;
msgObj['changes'] = bl_changes;
}
return msgObj;
}

// slackに投げる
function postSlack(e,message,changes,description,comment){
// 引用コメント部分整形
var attachments_comment_opts = "";
if(comment){
attachments_comment_opts = {
"color": "#42ce9f",
"text" : comment
};
}

var attachments_description_opts = "";
if(description){
attachments_description_opts = {
"color": "#00FFFF",
"text" : description
};
}

var attachments_changes_opts = "";
if(changes){
attachments_changes_opts = {
"color": "#FFA500",
"text" : changes
};
}

var channelId = e.parameter.channelId;

var jsonData =
{
"text" : message,
"attachments" : [ attachments_changes_opts, attachments_description_opts , attachments_comment_opts ],
"channel" : channelId
};
var payload = JSON.stringify(jsonData);

var options =
{
"method" : "post",
"contentType" : "application/json",
"payload" : payload
};

UrlFetchApp.fetch(postUrl, options);
}

// シートを配列で取得
function getSlackMemberArray() {
var Sheet = SpreadsheetApp.openById(slackMemberSheetId);
var arr = Sheet.getSheetValues(1, 1, Sheet.getLastRow(), Sheet.getLastColumn());
return arr;
}

// Slackのメンション化
function get_mention(text){
var slackMemberArray = getSlackMemberArray();
for(var i in slackMemberArray){
if(slackMemberArray[i][colBacklogMemberId]==""){

}else{
var backlogId = new RegExp('@'+slackMemberArray[i][colBacklogMemberId], 'g');
text = text.replace(backlogId, '<@'+slackMemberArray[i][colSlackMemberId]+'> ');
}
}
return text;
}

// Main Handler
function doPost(e) {
var body;
var slackObj = new Object();
if(e.postData.contents){
// json整形・メッセージ作成
body = JSON.parse(e.postData.contents);

slackObj = makeChatMessage(body);

// Slack投稿
if(slackObj){
postSlack(e,get_mention(slackObj['message']), get_mention(slackObj['changes']), get_mention(slackObj['description']), get_mention(slackObj['comment']));
}
}else{
Logger.log('対象チャンネルが指定されていないか、データが取得できません。');
}
}


IDを管理するSpreadSheet

こんな感じです!

スクリーンショット 2018-05-22 15.10.04.png


設定

上のコードの1~7行目を書き換えます。

* 1行目はslack のincoming webhook のurl。今回はGASのプロパティに設定している。

ファイル->プロジェクトのプロパティ->スクリプトのプロパティから設定。

* 3行目。スプレッドシートのid取得はググってくださいw

* 5,6行目。idを変換するためにスプレッドシートの列を指定。

* 7行目。癒されるメッセージを!

GASに書き換えたコードをコピペして、

公開->webアプリケーションとして導入

から、アプリケーションにアクセスできるユーザーを全員(匿名ユーザー含む)にして公開します。

またこのときに出てくる実行用urlをbacklog側で使用します。


backlog側の設定


  1. プロジェクトごとにwebhookの設定が必要

  2. webhook作成、上で取得した実行用urlの後尾に?channelId=ASDFASDという形でパラメータをつけて、webhook urlとして設定。(ASDFASDはテキトーですw)

これはプロジェクトごとに違うslackのchannelに通知を送るために必要です!

1つだったとしてもchannelIdは指定してください!


slack側の設定

Appsでincoming webhookの設定をします。投稿先channelはどこでも可!

そのときに取得できるwebhook url をコードの1行目で使ってます。


slackのchannel idの取得の仕方

slack api channel.listsから。

ググれば出てきますw