この記事は ex-crowdworks Advent Calendar 2024の1日目の記事です。
はじめに
今年、株式会社クラウドワークスを退職した@nisyuuです。来年の大会に向けてジムで追い込む日々を送っています。
エンジニアとしてクラウドワークステック(旧クラウドテック)というフリーランスと企業をマッチングするエージェントサービスを開発していました。
クラウドワークスではGASを使った生産性向上を積極的に行っています。
本記事では、クラウドワークステック開発チームでも行っているファシリテーター当番のローテーション通知方法を紹介します。
毎回、当番探しで消耗している方々は、明日から簡単にできるためぜひ導入してください。
完成版を用意しているので、GASもSlackのウェブフックも分かってるよという方は、完成物をコピペしてさっさと生産性向上いただいて構いません。
仕組み
通知の仕組みはシンプルで、GASでスプレッドシートからユーザー情報を取得し、ファシリテーターをSlackへ通知させます。
ユーザーの取得
まずはスプレッドシートを用意します。
本記事で紹介するスプレッドシートには、予めこのようなデータを入れておきます。
name | slack_user_id | facilitator_flag |
---|---|---|
織田 | UAEUET2N4 | TRUE |
豊臣 | UAEUET2N4 | FALSE |
徳川 | UAEUET2N4 | FALSE |
nameはユーザーの名前です。
slack_user_idはSlackのユーザーIDです。Slackプロフィールから確認できます。
facilitator_flagはファシリテーターの当番管理に使い、TRUEのユーザーがファシリテーターとします。
ファシリテーターは定期的に通知させるため、現在のファシリテーターと次にファシリテーターになるユーザーデータを取得してfacilitator_flagを更新します。
/**
* シートを取得
* @param {string} spreadsheetId - スプレッドシートのID
* @param {string} sheetName - シート名
* @return {Object} - シートのオブジェクト
*/
function getSheet(spreadsheetId, sheetName) {
return SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
}
/**
* 現在の担当者と次の担当者を取得
* @param {Array} data - スプレッドシートのデータ
* @return {Object} - 現在の担当者と次の担当者の情報
*/
function getFacilitatorInfo(data) {
const headers = data[0];
const nameIndex = headers.indexOf('name');
const slackUserIdIndex = headers.indexOf('slack_user_id');
const facilitatorFlagIndex = headers.indexOf('facilitator_flag');
let current = null; // 現在のファシリ情報が入る
let next = null; // 次のファシリ情報が入る
for (let i = 1; i < data.length; i++) {
const row = {
rowIndex: i,
name: data[i][nameIndex],
slackUserId: data[i][slackUserIdIndex],
facilitatorFlag: data[i][facilitatorFlagIndex]
};
if (row.facilitatorFlag === true) {
current = row;
} else if (current && next === null) {
next = row;
}
}
// 最後のユーザーの後は最初に戻る
if (current && !next) {
next = {
rowIndex: 1,
name: data[1][nameIndex],
slackUserId: data[1][slackUserIdIndex],
facilitatorFlag: data[1][facilitatorFlagIndex]
};
}
return { current, next };
}
/**
* facilitator_flag を更新
* @param {Sheet} sheet - スプレッドシートのシート
* @param {Object} facilitatorInfo - 現在の担当者と次の担当者の情報
*/
function updateFacilitatorFlag(sheet, facilitatorInfo) {
const facilitatorFlagColIndex = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].indexOf('facilitator_flag') + 1;
// 現在の担当者フラグを false にする
if (facilitatorInfo.current) {
sheet.getRange(facilitatorInfo.current.rowIndex + 1, facilitatorFlagColIndex).setValue(false);
}
// 次の担当者フラグを true にする
if (facilitatorInfo.next) {
sheet.getRange(facilitatorInfo.next.rowIndex + 1, facilitatorFlagColIndex).setValue(true);
}
}
Slackにファシリテーターを通知
送信したメッセージをSlackで受け取れるようにするには、ウェブフックを使用する必要があります。
ウェブフックのURLはワークフローから生成する方法とIncoming Webhookアプリから生成する方法があり、方法に応じてリクエストするパラメーターも変わります。
ワークフローから生成する方法
Slackの自動化から新しいワークフローを作成します。ウェブフックの設定からURLを発行してください。
設定時にリクエスト元から受け取るキーを自由に指定できます。
Incoming Webhookアプリを使った方法
SlackのマーケットプレイスからIncoming Webhookアプリを選択してURLを発行します。
payloadプロパティ内のtextプロパティに設定した値を、Slackに通知することができます。
実装はこのようになります。
/**
* Slackメッセージを送信
* @param {string} webhookUrl - SlackのWebhook URL
* @param {string} name - メンション対象のユーザー名
* @param {string} slackUserId - メンション対象のSlackユーザーID
*/
function sendSlackMessage(webhookUrl, name, slackUserId) {
const payload = {
text: `今週のファシリは${name}<@${slackUserId}>さんです。\nよろしくお願いします🎤`
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(webhookUrl, options);
}
各関数を呼び出す関数
処理を操作するためのメインとなる関数です。
function sendSlackMessageToFacilitator() {
const spreadsheetId = 'xxxxxxxxxxxxxxxxxxxx'; // スプレッドシートのID
const sheetName = 'xxxxxxxxx'; // シート名
const sheet = getSheet(spreadsheetId, sheetName);
const data = sheet.getDataRange().getValues();
const facilitatorInfo = getFacilitatorInfo(data);
const webhookUrl = 'https://hooks.slack.com/triggers/xxxxx/xxxxx/xxxxxxxxxxxxxxxxxxxxxxxxx'; // ウェブフックのURL
sendSlackMessage(
webhookUrl,
facilitatorInfo.next.name,
facilitatorInfo.next.slackUserId
);
updateFacilitatorFlag(sheet, facilitatorInfo);
}
sendSlackMessageToFacilitator
関数を実行し、Slackに通知されれば完成です。
定期実行設定
当番通知を定期実行するため、GASのトリガー設定で実行タイミングの指定をします。
完成物
function sendSlackMessageToFacilitator() {
const spreadsheetId = 'xxxxxxxxxxxxxxxxxxxx'; // スプレッドシートのID
const sheetName = 'xxxxxxxxx'; // シート名
const sheet = getSheet(spreadsheetId, sheetName);
const data = sheet.getDataRange().getValues();
const facilitatorInfo = getFacilitatorInfo(data);
const webhookUrl = 'https://hooks.slack.com/triggers/xxxxx/xxxxx/xxxxxxxxxxxxxxxxxxxxxxxxx'; // ウェブフックのURL
sendSlackMessage(
webhookUrl,
facilitatorInfo.next.name,
facilitatorInfo.next.slackUserId
);
updateFacilitatorFlag(sheet, facilitatorInfo);
}
/**
* シートを取得
* @param {string} spreadsheetId - スプレッドシートのID
* @param {string} sheetName - シート名
* @return {Object} - シートのオブジェクト
*/
function getSheet(spreadsheetId, sheetName) {
return SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
}
/**
* 現在の担当者と次の担当者を取得
* @param {Array} data - スプレッドシートのデータ
* @return {Object} - 現在の担当者と次の担当者の情報
*/
function getFacilitatorInfo(data) {
const headers = data[0];
const nameIndex = headers.indexOf('name');
const slackUserIdIndex = headers.indexOf('slack_user_id');
const facilitatorFlagIndex = headers.indexOf('facilitator_flag');
let current = null; // 現在のファシリ情報が入る
let next = null; // 次のファシリ情報が入る
for (let i = 1; i < data.length; i++) {
const row = {
rowIndex: i,
name: data[i][nameIndex],
slackUserId: data[i][slackUserIdIndex],
facilitatorFlag: data[i][facilitatorFlagIndex]
};
if (row.facilitatorFlag === true) {
current = row;
} else if (current && next === null) {
next = row;
}
}
// 最後のユーザーの後は最初に戻る
if (current && !next) {
next = {
rowIndex: 1,
name: data[1][nameIndex],
slackUserId: data[1][slackUserIdIndex],
facilitatorFlag: data[1][facilitatorFlagIndex]
};
}
return { current, next };
}
/**
* Slackメッセージを送信
* @param {string} webhookUrl - SlackのWebhook URL
* @param {string} name - メンション対象のユーザー名
* @param {string} slackUserId - メンション対象のSlackユーザーID
*/
function sendSlackMessage(webhookUrl, name, slackUserId) {
const payload = {
text: `今週のファシリは${name}<@${slackUserId}>さんです。\nよろしくお願いします🎤`
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(webhookUrl, options);
}
/**
* facilitator_flag を更新
* @param {Sheet} sheet - スプレッドシートのシート
* @param {Object} facilitatorInfo - 現在の担当者と次の担当者の情報
*/
function updateFacilitatorFlag(sheet, facilitatorInfo) {
const facilitatorFlagColIndex = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].indexOf('facilitator_flag') + 1;
// 現在の担当者フラグを false にする
if (facilitatorInfo.current) {
sheet.getRange(facilitatorInfo.current.rowIndex + 1, facilitatorFlagColIndex).setValue(false);
}
// 次の担当者フラグを true にする
if (facilitatorInfo.next) {
sheet.getRange(facilitatorInfo.next.rowIndex + 1, facilitatorFlagColIndex).setValue(true);
}
}
おわりに
ex-crowdworks Advent Calendar 2018は大盛況で生々しい話もあったようでしたが、今回ネガティブな退職理由はなく皆様がご期待されているかもしれない闇の話はありません。
むしろ、まだまだクラウドワークスで個のためのインフラを作っていきたかったです。
個人的な見解ですが、伝達は比較的なされており、レビューはテック開発チームに至っては積極的に行われ、朝会は前向きな参加者が多く、インターン生が激詰めされてる声を聞いたことはなく(たまーに角の部屋から机を叩く音が聞こえるくらい)、キャリアチェンジを検討していた人もクラウドワークスで活躍されているようです。