Part1🤘🏻コピペOK🤘🏻GASを使ってSlackに当番通知する生産性向上をこっそり紹介します😶‍🌫️

この記事は [ex-crowdworks Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ex-cw)の1日目の記事です。

# はじめに
今年、株式会社クラウドワークスを退職した@nisyuuです。来年の大会に向けてジムで追い込む日々を送っています。
エンジニアとしてクラウドワークステック（旧クラウドテック）というフリーランスと企業をマッチングするエージェントサービスを開発していました。

クラウドワークスではGASを使った生産性向上を積極的に行っています。
本記事では、クラウドワークステック開発チームでも行っている担当者当番のローテーション通知方法を紹介します。
毎回、当番探しで消耗している方々は、明日から簡単にできるためぜひ導入してください。

完成版を用意しているので、GASもSlackのウェブフックも分かってるよという方は、[完成物](#完成物)をコピペしてさっさと生産性向上いただいて構いません。

# 仕組み
通知の仕組みはシンプルで、GASでスプレッドシートからユーザー情報を取得し、担当者をSlackへ通知させます。

# ユーザーの取得

まずはスプレッドシートを用意します。
本記事で紹介するスプレッドシートには、予めこのようなデータを入れておきます。

| name | slack_user_id | person_in_charge_flag |
|:-----------:|:------------:|:------------:|
| 織田 | UAEUET2N4 | TRUE |
| 豊臣 | UAEUET2N4 | FALSE |
| 徳川 | UAEUET2N4 | FALSE |

nameはユーザーの名前です。
slack_user_idはSlackのユーザーIDです。Slackプロフィールから確認できます。
person_in_charge_flagは担当者の当番管理に使い、TRUEのユーザーが担当者とします。

担当者は定期的に通知させるため、現在の担当者と次に担当者になるユーザーデータを取得してperson_in_charge_flagを更新します。

```javascript
/**
 * シートを取得
 * @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 getPersonInChargeInfo(data) {
  const headers = data[0];
  const nameIndex = headers.indexOf('name');
  const slackUserIdIndex = headers.indexOf('slack_user_id');
  const personInChargeFlagIndex = headers.indexOf('person_in_charge_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],
      personInChargeFlag: data[i][personInChargeFlagIndex]
    };

    if (row.personInChargeFlag === true) {
      current = row;
    } else if (current && next === null) {
      next = row;
    }
  }

  // 最後のユーザーの後は最初に戻る
  if (current && !next) {
    next = {
      rowIndex: 1,
      name: data[1][nameIndex],
      slackUserId: data[1][slackUserIdIndex],
      personInChargeFlag: data[1][personInChargeFlagIndex]
    };
  }

  return { current, next };
}

/**
 * person_in_charge_flag を更新
 * @param {Sheet} sheet - スプレッドシートのシート
 * @param {Object} personInChargeInfo - 現在の担当者と次の担当者の情報
 */
function updatePersonInChargeFlag(sheet, personInChargeInfo) {
  const personInChargeFlagColIndex = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].indexOf('person_in_charge_flag') + 1;

  // 現在の担当者フラグを false にする
  if (personInChargeInfo.current) {
    sheet.getRange(personInChargeInfo.current.rowIndex + 1, personInChargeFlagColIndex).setValue(false);
  }

  // 次の担当者フラグを true にする
  if (personInChargeInfo.next) {
    sheet.getRange(personInChargeInfo.next.rowIndex + 1, personInChargeFlagColIndex).setValue(true);
  }
}
```

# Slackに担当者を通知

送信したメッセージをSlackで受け取れるようにするには、ウェブフックを使用する必要があります。
ウェブフックのURLはワークフローから生成する方法とIncoming Webhookアプリから生成する方法があり、方法に応じてリクエストするパラメーターも変わります。

## ワークフローから生成する方法
[Slackの自動化](https://slack.com/intl/ja-jp/features/workflow-automation)から新しいワークフローを作成します。ウェブフックの設定からURLを発行してください。
設定時にリクエスト元から受け取るキーを自由に指定できます。

## Incoming Webhookアプリを使った方法
SlackのマーケットプレイスからIncoming Webhookアプリを選択してURLを発行します。
payloadプロパティ内のtextプロパティに設定した値を、Slackに通知することができます。

実装はこのようになります。

```javascript
/**
 * 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}>さんです。
よろしくお願いします🎤`
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch(webhookUrl, options);
}
```

# 各関数を呼び出す関数
処理を操作するためのメインとなる関数です。

```javascript
function sendSlackMessageToPersonInCharge() {
  const spreadsheetId = 'xxxxxxxxxxxxxxxxxxxx'; // スプレッドシートのID
  const sheetName = 'xxxxxxxxx'; // シート名
  const sheet = getSheet(spreadsheetId, sheetName);
  const data = sheet.getDataRange().getValues();
  const personInChargeInfo = getPersonInChargeInfo(data);
  const webhookUrl = 'https://hooks.slack.com/triggers/xxxxx/xxxxx/xxxxxxxxxxxxxxxxxxxxxxxxx'; // ウェブフックのURL

  sendSlackMessage(
    webhookUrl,
    personInChargeInfo.next.name,
    personInChargeInfo.next.slackUserId
  );

  updatePersonInChargeFlag(sheet, personInChargeInfo);
}
```

`sendSlackMessageToPersonInCharge`関数を実行し、Slackに通知されれば完成です。

# 定期実行設定

当番通知を定期実行するため、GASのトリガー設定で実行タイミングの指定をします。


# 完成物

```javascript
function sendSlackMessageToPersonInCharge() {
  const spreadsheetId = 'xxxxxxxxxxxxxxxxxxxx'; // スプレッドシートのID
  const sheetName = 'xxxxxxxxx'; // シート名
  const sheet = getSheet(spreadsheetId, sheetName);
  const data = sheet.getDataRange().getValues();
  const personInChargeInfo = getPersonInChargeInfo(data);
  const webhookUrl = 'https://hooks.slack.com/triggers/xxxxx/xxxxx/xxxxxxxxxxxxxxxxxxxxxxxxx'; // ウェブフックのURL

  sendSlackMessage(
    webhookUrl,
    personInChargeInfo.next.name,
    personInChargeInfo.next.slackUserId
  );

  updatePersonInChargeFlag(sheet, personInChargeInfo);
}

/**
 * シートを取得
 * @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 getPersonInChargeInfo(data) {
  const headers = data[0];
  const nameIndex = headers.indexOf('name');
  const slackUserIdIndex = headers.indexOf('slack_user_id');
  const personInChargeFlagIndex = headers.indexOf('person_in_charge_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],
      personInChargeFlag: data[i][personInChargeFlagIndex]
    };

    if (row.personInChargeFlag === true) {
      current = row;
    } else if (current && next === null) {
      next = row;
    }
  }

  // 最後のユーザーの後は最初に戻る
  if (current && !next) {
    next = {
      rowIndex: 1,
      name: data[1][nameIndex],
      slackUserId: data[1][slackUserIdIndex],
      personInChargeFlag: data[1][personInChargeFlagIndex]
    };
  }

  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}>さんです。
よろしくお願いします🎤`
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch(webhookUrl, options);
}

/**
 * person_in_charge_flag を更新
 * @param {Sheet} sheet - スプレッドシートのシート
 * @param {Object} personInChargeInfo - 現在の担当者と次の担当者の情報
 */
function updatePersonInChargeFlag(sheet, personInChargeInfo) {
  const personInChargeFlagColIndex = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].indexOf('person_in_charge_flag') + 1;

  // 現在の担当者フラグを false にする
  if (personInChargeInfo.current) {
    sheet.getRange(personInChargeInfo.current.rowIndex + 1, personInChargeFlagColIndex).setValue(false);
  }

  // 次の担当者フラグを true にする
  if (personInChargeInfo.next) {
    sheet.getRange(personInChargeInfo.next.rowIndex + 1, personInChargeFlagColIndex).setValue(true);
  }
}
```

# おわりに
[ex-crowdworks Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ex-cw)は大盛況で生々しい話もあったようでしたが、今回ネガティブな退職理由はなく皆様がご期待されているかもしれない闇の話はありません。
むしろ、まだまだクラウドワークスで個のためのインフラを作っていきたかったです。

個人的な見解ですが、伝達は比較的なされており、レビューはテック開発チームに至っては積極的に行われ、朝会は前向きな参加者が多く、インターン生が激詰めされてる声を聞いたことはなく（たまーに角の部屋から机を叩く音が聞こえるくらい）、キャリアチェンジを検討していた人もクラウドワークスで活躍されているようです。