Edited at

ChatWorkのWebhook+GASで担当者に自動タスク化

どうもこんにちは。

株式会社サイトビジットの横沢です。

弊社ではチャットツールとしてChatWorkを使っているのですが、

2017年11月1日にChatWorkがWebhookの提供を開始したということで、CSの作業を自動化してみました。


内容


困ってたこと

CS担当が電話を受けて担当者が不在の場合、ChatWorkで担当者にTOをつけて送っていたが、


  • TOつけるのめんどくさい

  • 担当者の対応が抜ける

という問題が発生していました。


やったこと


運用



  • 受電メモ窓を作成して、CSの方にはここにメモって送信してもらうようにした


  • 問い合わせ対応窓を作成して、ビジネスサイドの人はここのタスクをウォッチしてもらうようにした


    • 対応が終了したらタスクを完了してもらった




システム


  • ChatoWorkに適当にメモって送信したら、テキストを解析して担当者のタスクに自動的に追加されるようにした


    • 該当者がいない場合、とりあえずCSの責任者にタスクを追加して、責任者が適宜割り振るようにした




詳細


事前準備

頑張って書こうと思ったけどこのページ見た方が早い。笑


  • 適当なスプレッドシートからGASを作成して、ウェブアプリケーションのURLを取得


    • とりあえず以下のコードがあればWebhookを受け取れる(Google様ありがとう)



function doPost(e) {

var json = JSON.parse(e.postData.contents);
doChatwork(json.webhook_event);
}


  • ChatWorkのAPI Tokenを発行した


    • 弊社は当然のようにビジネスアカウントなので、管理者にAPI Tokenを発行したので承認してくだされとお願いする必要ありましたが、個人利用なら必要ない(はず)




  • Webhookの設定


    • 現時点(2018年3月10日)だと、メッセージ作成メッセージ更新のみだが、今回はメッセージ作成をチョイス

    • ルームIDは受電メモ窓のIDを入れる




  • logという名前のシート作成

  • 社員のデータをスプレッドシートに準備



    • Memberというシート名にする

    • これもGASで取ってきたけど今回は割愛(体力の限界)



列名
意味

name
社員の名前

chatwork_id
社員のChatWorkID

condition
適当な単語をカンマ(,)区切りで(この単語がメモに入ってたら担当者になる)

guardian
担当者が該当しない場合に TRUE の人が担当者になる

単語の意味とかは気にしないでください(英語できない)

こんな感じで準備終了。受電メモ窓に何かメッセージ入れればWebhook発動される(はず)


利用するシートの準備

LOG_SHEET = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("log");

MEMBER_SHEET = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Member");

なんやかんや色んな所で使いそうだったのでとりあえずグローバル変数に


ChatWork関連のメソッドをモジュール化(postだけ抜粋)

var Chatwork = {

apiKey: "***** 適宜自分のAPI Key *******",
apiUrl: "https://api.chatwork.com/v2",

post: function(apiPath, payload){
var url = this.apiUrl + apiPath;
var options = {
headers: {
"X-ChatWorkToken": this.apiKey
},
'method': 'POST',
'payload': payload
}
var response = UrlFetchApp.fetch(url, options);
},

// タスクを追加
addTask: function(roomId, message, toIds) {
var apiPath = "/rooms/" + roomId + "/tasks";
var payload = {
body: message,
to_ids: toIds
}
this.post(apiPath, payload);
}
}

apiPathpayloadを変更すれば、新しい窓作ったりメッセージ送信したりできます。


スプレッドシートのデータを持ってくるメソッド

var Common = {

// ヘッダー情報をとってくる
getHeaders: function(sheet) {
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues();
return headers[0];
},
// ヘッダーと値を突合してハッシュを生成
getHashFromHeaders: function(headers, items) {
var hash = {};
for(var i in headers) {
hash[headers[i]] = items[i];
}
return hash;
}
}

スプレッドシートからデータ持ってくると2次元配列で返ってくるので、扱いがやや面倒です。

なので、1行目のヘッダー情報を配列に付加してオブジェクトに変換する目的。

余裕でリファクタリング対象

var Member = {

// メンバー全員を配列で持ってくる
getAllList: function(memberSheet) {
var memberList = memberSheet.getRange(2, 1, memberSheet.getLastRow()-1, memberSheet.getLastColumn()).getValues();
var headers = Common.getHeaders(memberSheet);
var members = []
for(var i in memberList) {
var member = Common.getHashFromHeaders(headers, memberList[i])
members.push(member);
}
return members;
},
// 担当者を選択する
selectIdList: function(text) {
var allMembers = this.getAllList(MEMBER_SHEET);
var selectMember = [];
for(var i in allMembers) {
var member = allMembers[i];
var conditionArr = member.condition.split(',');
for(var j in conditionArr) {
if(text.indexOf(conditionArr[j]) <= 0) {
} else {
selectMember.push(member.chatwork_id);
}
}
}
if(selectMember.length == 0) {
for(var i in allMembers) {
var member = allMembers[i];
if(member.guardian == true) {
selectMember.push(member.chatwork_id);
}
}
}
selectMember = selectMember.filter(function (x, i, self) {
return self.indexOf(x) === i;
});
return selectMember;
}
}

この辺ちょっと複雑、、、

要はチャットワークの文章を解析して、担当者のチャットワークIDを配列に入れてるだけ。

スーパーリファクタリング対象


Webhookの受け取り

function doPost(e) {

var json = JSON.parse(e.postData.contents);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues();
for(var i in headers[0]) {
i = Number(i)
sheet.getRange(2, i+1).setValue(json.webhook_event[headers[0][i]]);
}
doChatwork(json.webhook_event);
}

Webhookを受け取った後に、とりあえず logシートに内容を展開してます。(なんでこうしたか忘れたけど)


タスクの追加


TO_ROOM_ID = /*送りたい窓のID*/;

function doChatwork(details){
var headers = Common.getHeaders(LOG_SHEET);
var range = LOG_SHEET.getRange(2, 1, 1, LOG_SHEET.getLastColumn());
var detail = Common.getHashFromHeaders(headers, range.getValues()[0]);
if(detail.account_id == OPERATOR_ID) {
var memberIds = Member.selectIdList(detail.body).join(',');
Chatwork.addTask(TO_ROOM_ID, detail.body, memberIds);
}
}

Webhookを受け取った後に、これ実行すればタスクが追加されます。


全ソース


TO_ROOM_ID = /*送りたい窓のID*/;
LOG_SHEET = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("log");
MEMBER_SHEET = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Member");

var Chatwork = {
apiKey: "***** 適宜自分のAPI Key *******",
apiUrl: "https://api.chatwork.com/v2",

post: function(apiPath, payload){
var url = this.apiUrl + apiPath;
var options = {
headers: {
"X-ChatWorkToken": this.apiKey
},
'method': 'POST',
'payload': payload
}
var response = UrlFetchApp.fetch(url, options);
},

// タスクを追加
addTask: function(roomId, message, toIds) {
var apiPath = "/rooms/" + roomId + "/tasks";
var payload = {
body: message,
to_ids: toIds
}
this.post(apiPath, payload);
}
}

function doPost(e) {
var json = JSON.parse(e.postData.contents);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues();
for(var i in headers[0]) {
i = Number(i)
sheet.getRange(2, i+1).setValue(json.webhook_event[headers[0][i]]);
}
doChatwork(json.webhook_event);
}

function doChatwork(details){
var headers = Common.getHeaders(LOG_SHEET);
var range = LOG_SHEET.getRange(2, 1, 1, LOG_SHEET.getLastColumn());
var detail = Common.getHashFromHeaders(headers, range.getValues()[0]);
if(detail.account_id == OPERATOR_ID) {
var memberIds = Member.selectIdList(detail.body).join(',');
Chatwork.addTask(TO_ROOM_ID, detail.body, memberIds);
}
}

var Common = {
// ヘッダー情報をとってくる
getHeaders: function(sheet) {
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues();
return headers[0];
},
// ヘッダーと値を突合してハッシュを生成
getHashFromHeaders: function(headers, items) {
var hash = {};
for(var i in headers) {
hash[headers[i]] = items[i];
}
return hash;
}
}

var Member = {
// メンバー全員を配列で持ってくる
getAllList: function(memberSheet) {
var memberList = memberSheet.getRange(2, 1, memberSheet.getLastRow()-1, memberSheet.getLastColumn()).getValues();
var headers = Common.getHeaders(memberSheet);
var members = []
for(var i in memberList) {
var member = Common.getHashFromHeaders(headers, memberList[i])
members.push(member);
}
return members;
},
// 担当者を選択する
selectIdList: function(text) {
var allMembers = this.getAllList(MEMBER_SHEET);
var selectMember = [];
for(var i in allMembers) {
var member = allMembers[i];
var conditionArr = member.condition.split(',');
for(var j in conditionArr) {
if(text.indexOf(conditionArr[j]) <= 0) {
} else {
selectMember.push(member.chatwork_id);
}
}
}
if(selectMember.length == 0) {
for(var i in allMembers) {
var member = allMembers[i];
if(member.guardian == true) {
selectMember.push(member.chatwork_id);
}
}
}
selectMember = selectMember.filter(function (x, i, self) {
return self.indexOf(x) === i;
});
return selectMember;
}
}


まとめ

初めてGASでポストを受け取ってみてサッと作ったメソッドなので、スーパーリファクタリング対象が多いです。(最初からもっと綺麗に書けるようになりたい)

ただ、運用している際には特に何も影響がないので一旦これで運用しています。

CSは1次受付(対応できるものはする)、深い内容はビジネスサイドというように使い分けがきちんと出来ているので、


  • CSの時間短縮


    • 内容から担当者を考える必要なし

    • TOをいちいちつける必要なし



  • ビジネスサイドの対応漏れ削減


    • タスクが追加されるのでそれを完了させていけば、あら不思議対応漏れなし

    • タスクの数とかが可視化されているので、タスクが残っていたら相互にフォローする体制が自然とできた



あと、実は1日の終わりの18時にタスクが残っている人に、TOをつけて名前と未対応のタスク数をメッセージ送信するメソッドも作りました。

これによって、「やべえ対応漏れあるやん」「おい、お前対応漏れてるぞ」という感じになりました。

それも紹介しようと思いましたが、ここで力尽きました。

また余裕が出来たら書こうと思います。

他にもGASの便利機能を結構作っているので、それも今後紹介できたらしていきますー

ツイッターもやっているのでお友達になっていただけると嬉しいです。

以上です。