16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Develop fun!」を体現する! Works Human IntelligenceAdvent Calendar 2022

Day 22

GAS × Slack Bot で全社ウォーキング大会 444 名のふりかえりをパーソナライズした分析情報で応援した話(実装サンプル付き)

Last updated at Posted at 2022-12-23

本記事は株式会社 Works Human Intelligenceアドベントカレンダー の 22 日目の記事となります。

昨日は @shishi_doooo さんの記事「そうだ、100 万件データ作ろう」でした。
よろしければ、ぜひ他の記事もご覧になっていってください。

【2024年7月 追記】登壇しました

本記事の内容を、デブサミ(Developers Summit 2024 Summer)で発表しました。

スライドを公開したので、よかったらこちらも見てみてください。

(以下はスライドの一部です)

image.png

image.png

はじめに

アドカレのテーマが 「Develop fun!」を体現する! Works Human Intelligence Advent Calendar 2022 なのですが、当社には Value には Work fun! という行動理念があります。(参考:企業理念ページ
この記事は、社員が楽しくウォーキングする(Walk fun)ことで健康第一に楽しく働く(Work fun)ことができるように、楽しく開発した(Develop fun)経験とその方法を書きます(こじつけ

まとめ

  • 全社ウォーキング大会やったら社員 444 人(約 1/4)が参加してくれた
  • 少人数で運営を回すのに GAS × Slack Bot で各種 Bot を作った
    • ふりかえり実施時にパーソナライズした分析情報を送ってくれるおつかれさま Bot がモチベーション維持に貢献した
  • 実装サンプルつくったので使ってみてください oOo

image.png


最初に全社ウォーキング大会を開催した有志団体 "Works Healthy Project" とウォーキング大会 "Connected Walking" について紹介します。
「んなこたいいから実装教えろ」という方は おつかれさま Bot の実装 のセクションにスキップしてください。

Works Healthy Project (WHP)

「健康」をキーワードに社員の「『はたらく』を楽しく」を実現する有志プロジェクト(スカンクワーク)です。
ミッションは 「社員目線の健康づくりを通じて、会社と社会に貢献する」「健康な社内風土を醸成することで、 個々の多様な健康を「楽しく」実現する」 です。
発足は 2009 年と 10 年以上の歴史があるプロジェクトで、現在は約 30 名で運営しています。
2018 年頃から私が代表を務めています。

image.png

Connected Walking とは

そんな WHP は、在宅勤務で運動量や社会的つながりが減少することを課題と感じ、全社チーム対抗ウォーキング大会を開催しました。
「リモートワークでも歩いて繋がろう!」をテーマに、4 週間でチームで何歩歩いたか、各種ミッションやふりかえりなど、ゲーミフィケーションの要素を取り入れて参加者が継続しやすいような仕組みを取り入れました。
企画の目的は以下として、人事や健康保険組合へ企画説明をしました。

image.png

仕組み

特徴 ① みんなでなら、できる。チーム制でお互いに刺激を!
一人では続かないという人でも、誰かと一緒であれば不思議と頑張れるものです。
私は継続の一番の秘訣はコミュニティだと考えているため、コミュニティができやすい環境を作ろうとチーム制を導入しました。

特徴 ② PCDA を回す。毎週の振り返りで着実な改善へ!
単に 1 ヶ月のウォーキングイベントに参加するだけだと途中で飽きたり、改善の気づきを得られないまま終了してしまいます。
スクラムのスプリントレトロスペクティブのように、KPT をつかって毎週ふりかえりをし、自己修正をする機会をつくりしました
参加者の中にはこの仕組みをうまく活用してくれる人もいました。(以下は去年のイベント時に当社社員が投稿してくれた記事です :tada:

結果

イベント自体の紹介は別途いずれかの機会で書くとして、この大会は以下のような結果をもたらしました。

  • 参加者数:444 名
    • 社員の約 1/4!
  • 平均歩数:7,385 歩、中央歩数 6,756 歩
    • 企画開始時の平均歩数は 4,675 歩。163% に UP!
  • 満足度:4.22 / 5.0
    • 参加者の 45 % が最高評価の 5!
  • Slack へのミッション報告投稿数:約 650 件
    • 1 日あたり 23 件の投稿!
    • ※ 「チーム MTG 開催報告を投稿」「ウォーキング実施報告を投稿」してもらい、特定のスタンプで Reacji Channeler によって参加者が全員参加しているチャンネルに共有するミッション

ミッション報告チャンネルには、多くのウォーキングやチーム MTG 報告が投稿されました。

image.png

image.png

image.png

また、イベント終了後にも「楽しかった!」いう声を少なからず観測することができました。

image.png

image.png

これらの結果から、本イベントが社員の健康増進および参加者のコミュニケーション活性化に貢献したといっていいんじゃないかなと思います。
Work fun! を支えるための Walk fun! が達成できたのではないでしょうか。

活躍した Bot たち

おつかれさま Bot

毎週のふりかえりフォーム提出時に、フォームの回答内容や歩数記録表の入力内容をベースに、パーソナライズした歩数分析とともに応援メッセージを Slack に送ってくれる Bot です。

image.png

とても好評でした!
人事の方には「研修のアンケートとかにも応用したい」というような声もいただきました。

image.png

image.png

image.png

リマインド Bot

歩数入力とふりかえりフォーム提出は毎週締切りを設けていました。
それらの締切りを未達成者へ Slack でリマインドする Bot です。

image.png

参加者が多いこともあって、1 人 1 人にリマインドするよりもだいぶ工数を抑えることができました。
また、イベント全体チャンネルで @channel を付けてアナウンスするよりもリマインド率は高かったです。
(体感ですが、未達成者のうち 50~70% はこの Bot を実行後に行動してくれました!)

ミッション集計 Bot

「チーム MTG 開催報告を投稿」「ウォーキング実施報告を投稿」してもらい、特定のスタンプで Reacji Channeler によって参加者が全員参加しているチャンネルに共有するミッションがあるのですが、そのミッション集計・ポイント付与をする Bot です。
役割は以下となっています。

  1. Slack の Reacji 転送先チャンネルの投稿を取得してミッションポイント集計シート(スプレッドシート)へ転送し、
  2. 投稿内容を解析してミッションの達成可否やポイント計算を行い、
  3. ポイントを記録している歩数記録表(スプレッドシート)へ反映します。

image.png

この Bot の実装が一番複雑であるため、GAS の開発環境を Clasp × Slack API × TypeScript × Jest でローカル開発できるようにしました。
(WHP メンバーの @suzuSho さんが実装してくれました!)
Apps Script エディタの 1 ファイル上で生の JavaScript を書くより格段にいい開発体験を得られました。
(ファイル分割、型サポート、静的解析、自動テストなど)
これについても別の機会に紹介できたらと思います。

おつかれさま Bot の実装

少人数運営を実現すべくいろいろな Bot が活躍しましたが、今回は最も Walk fun! に貢献した「おつかれさま Bot」の実装方法を紹介します。
その他の Bot についてはまた別記事で紹介できればと思います。

Slack App の作成

※ Slack ワークスペースで Bot を作成・インストールできる権限が必要です。
企業によってはワークスペースにインストールできる Bot アプリを承認制にしているため、システムを管理している部署のレビュー・承認が必要になる可能性があります。

Slack API の Your Apps ページ にアクセスします。

image.png

[Create New App] ボタンをクリックします。
From an app manifest を選択します。

image.png

Pick a workspace to develop your app でアプリを開発したいワークスペースを選択します。

image.png

Enter app manifest below で以下の YAML を貼り付けます。
scopes は Bot が利用したい Slack API に応じて権限を修正してください。(下記は今回利用しない権限も記載しています。本記事では解説していないミッション集計などの機能も実装していたため)

display_information:
  name: ウォーキング大会運営Bot
  description: ウォーキング大会のアシスタント Bot です
  background_color: "#2d7541"
features:
  bot_user:
    display_name: Walking Assistant Bot
    always_online: false
oauth_config:
  scopes: # Bot が利用したい Slack API に応じて権限を修正してください。
    bot:
      - chat:write
      - chat:write.customize
      - im:write
      - users:read
      - users:read.email
      - channels:history
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

image.png

Review summary & create your app で内容を確認します。
問題なければ [Create] ボタンをクリックしてください。

image.png

image.png

作成後、Basic Information ページの Install your app[Install to Workspace] をクリックします。

image.png

OAuth の認可同意画面が表示されるため、[許可する] をクリックします。

image.png

これで作成したアプリがワークスペースにインストールされました。
必要な方は、Basic Information ページの下部 Display Information でアイコンを設定します。(任意)

image.png

最後に、OAuth & Permissions ページを開き、OAuth Tokens for Your Workspace にある Bot User OAuth Token をコピーします。
ふりかえりフォーム提出時のスクリプト で利用します。

image.png

歩数記録表の用意

歩数記録表を作成します。
イベントのルールや仕組みに応じて、作成してください。
ここではサンプルとして、歩数記録表【サンプル】 を利用します。

image.png

メッセージを送りたい Slack ユーザーのメールアドレスを歩数記録表のメールアドレス列に入力するようにしてください。
メールアドレスをキーに Google Form の回答者、スプレッドシートの回答者の行、Slack ユーザーの特定を行っています。
(サンプルでは org シートに記載のメールアドレスを record シートで参照するようにしています)

おつかれさまメッセージテンプレートの用意

ふりかえりフォーム提出時に Bot が回答者送るメッセージを用意しましょう。
ここではサンプルとして、今週もおつかれさまでしたメッセージ 【サンプル】 を利用します。

image.png

ここで <hoge> は変数であり、Apps Script 内で決定するように実装しています。
変数を追加した場合は Apps Script も合わせて修正する必要があります。

第 <week> 週のふりかえりフォームの提出ありがとうございます。先週もおつかれさまでした。
先週の目標は <oneWeekAgoTargetSteps> 歩に対し、
先週の歩数は <oneWeekAgoSteps> 歩(1 日平均 <oneWeekAgoOneDayAverageSteps> 歩)でした。
<hitTargetStatusMessage>
(先々週の歩数は <twoWeekAgoSteps> 歩。<stepsDiffBetweenWeeksMessage>)

*<week> 週目が終わった時点の累計歩数は <totalSteps> 歩(1 日平均 <totalAverageSteps> 歩)です!*
*距離にすると約 <totalDistance> km、東海道新幹線 :bullettrain_front: にたとえると 東京駅 から <reachedStation>駅 まで歩いたことになります! :tada:*

*今週の目標は <thisWeekTargetSteps> 歩です。*
*今週の Try は以下となっています。*

<thisWeekTryAction>

今週も楽しく歩きましょう! :muscle:

ふりかえりフォームの用意

週次で提出してもらうふりかえりフォームを用意します。
スクラムのスプリントレトロスペクティブ的な役割を果たします。
ここではサンプルとして、【第 1 週】ウォーキング大会 ふりかえりフォーム【サンプル】 を利用します。

image.png

image.png

現在のサンプルは毎週フォームを用意する仕様にしています。
理由は、Google Form 上でアンケート結果を週次ごとに分析しやすいからです。
フォームの設定や選択肢の内容によって、1 つのフォームだけで運用することも可能です。

ふりかえりフォーム提出時のスクリプト

フォームのスクリプトを実装します。
ふりかえりフォームの右上にある三点リーダーより <> スクリプト エディタ をクリックします。
Apps Script のエディタが表示されたら、以下のスクリプトを貼り付けます。
(サンプルを利用した場合は、既にスクリプトが記載されています)
//★要編集★ コメントを中心に、各種設定値を変更しましょう。

<編集必須の設定値>

  • ふりかえり対象週
  • 「今週もおつかれさまでしたメッセージ」のスプレッドシート ID(URL 内のランダム英数字部分)
  • 上記の対象シート名
  • 「歩数記録表」のスプレッドシート ID(URL 内のランダム英数字部分)
  • 上記の対象シート名
  • 作成した Slack Bot の Bot User OAuth Access Token(xoxb~~~
  • 歩数記録表の基準となる各指標の列番号

<編集任意の設定値>

  • おつかれさまメッセージテンプレートの変数
    • デフォルトより増減があった場合に修正

image.png

※ サンプルコード ※
// (前提)各種設定値をセット
const weekNum = 2; //★要編集★ 以下の week の値(右側の数字)よりスクリプト実行対象週のキー(左側の数字)を入力。
const totalWeekCounts = 4; // ★要編集★ イベント実施週数。

//★要編集★ ふりかえりを実施する週を定義
const week = {
  1: "1",
  2: "2",
  3: "3",
  4: "4",
};

//★要編集★ ふりかえりフォームのうち歩数記録表に連携する質問を定義
const thisWeekTargetStepsQuestion = "今週の通常目標を設定しましょう!";
const thisWeekTryActionQuestion =
  "今週新たに挑戦することについて教えてください!(Try|新たに挑戦すること。うまくいったことを継続するためにやること。または問題や課題の解決策)";

//★要編集★ フォーム回答時に Bot が送信するメッセージスプレッドシート「今週もおつかれさまでしたメッセージ」のID(URL内のランダム英数字部分)とシート名を定義
const messageGssId = "XXXXXXXX";
const messageSheetName = "振り返りフォーム回答時メッセージ";

//★要編集★ 連携先の歩数記録表スプレッドシートのID(URL内のランダム英数字部分)とシート名を定義
const targetGssId = "YYYYYYYY";
const targetSheetName = "record";

const emailColumn = 8; //★要編集★ 歩数記録表の「メールアドレス」列
const oneWeekAgoTargetStepsColumn = 10 + (weekNum - 1); //★要編集★ 歩数記録表の ※先週(ふりかえり対象週)※ の「週次通常目標歩数」列
const thisWeekTargetStepsColumn = oneWeekAgoTargetStepsColumn + 1; //★要編集★ 歩数記録表の ※今週(ふりかえり対象週の翌週)※ の「週次通常目標歩数」列)
const totalStepsColumn = 14; //★要編集★ 歩数記録表の「歩数累計」列
const twoWeekAgoStepsColumn = totalStepsColumn + (weekNum - 1); //★要編集★ 歩数記録表の ※先々週(ふりかえり対象週の先週)※ の「歩数」列
const oneWeekAgoStepsColumn = twoWeekAgoStepsColumn + 1; //★要編集★ 歩数記録表の ※先週(ふりかえり対象週)※ の「歩数」列
const totalDistanceColumn = totalStepsColumn + totalWeekCounts + 1; //★要編集★ 歩数記録表の「距離累計」列
const reachedStationColumn = totalStepsColumn + totalWeekCounts + 2; //★要編集★ 歩数記録表の「到達駅」列

//Slack APIに関するデータを定義
const token = "xoxb-ZZZZZZZZ"; //★要編集★ 作成した Slack Bot の Bot User OAuth Access Token(`xoxb~~~`)を記載
const lookupByEmail = "https://slack.com/api/users.lookupByEmail"; //メールアドレスからユーザー情報を取得するAPI
const postMessage = "https://slack.com/api/chat.postMessage"; //メッセージを送るAPI

// フォーム提出時にトリガー実行するメイン関数。
// ふりかえりフォーム回答を歩数記録表 GSS に反映し、おつかれさまでしたメッセージを Slack で送信。
function onSubmitHandler(e) {
  // ふりかえりフォーム回答を歩数記録表に反映・歩数データ取得
  const {
    email,
    oneWeekAgoTargetSteps,
    oneWeekAgoSteps,
    oneWeekAgoOneDayAverageSteps, 
    twoWeekAgoSteps,
    totalSteps, 
    totalAverageSteps, 
    totalDistance, 
    reachedStation, 
    thisWeekTargetSteps, 
    thisWeekTryAction, 
  } = refrectAnswersToStepsSheet(e);

  // デバッグ用のログ出力。うまく動いているようなら削除して OK
  console.log("email: ", email);
  console.log("oneWeekAgoTargetSteps: ", oneWeekAgoTargetSteps);
  console.log("oneWeekAgoSteps: ", oneWeekAgoSteps);
  console.log("oneWeekAgoOneDayAverageSteps: ", oneWeekAgoOneDayAverageSteps);
  console.log("twoWeekAgoSteps: ", twoWeekAgoSteps);
  console.log("totalSteps: ", totalSteps);
  console.log("totalAverageSteps: ", totalAverageSteps);
  console.log("totalDistance: ", totalDistance);
  console.log("reachedStation: ", reachedStation);
  console.log("thisWeekTargetSteps: ", thisWeekTargetSteps);
  console.log("thisWeekTryAction: ", thisWeekTryAction);


  // Slack Bot が DM でおつかれさまでしたメッセージを送信
  sendEncouragementMessage(
    email,
    oneWeekAgoTargetSteps,
    oneWeekAgoSteps,
    oneWeekAgoOneDayAverageSteps, 
    twoWeekAgoSteps,
    totalSteps, 
    totalAverageSteps, 
    totalDistance, 
    reachedStation, 
    thisWeekTargetSteps, 
    thisWeekTryAction, 
  );
}

function refrectAnswersToStepsSheet(e) {
  // ふりかえりフォームの回答データを取得
  const email = e.response.getRespondentEmail();
  const itemResponses = e.response.getItemResponses();
  let thisWeekTargetSteps = "";
  let thisWeekTryAction = "";

  // 歩数記録表スプレッドシートのデータを取得
  for (let i = 0; i < itemResponses.length; i++) { // 設問の数分、操作を繰り返す
    const itemResponse = itemResponses[i]; // 設問の配列のi番目を指定
    const question = itemResponse.getItem().getTitle(); // 設問のタイトルを取得
    const answer = itemResponse.getResponse(); // 設問の回答を取得
    // もし、設問のタイトルが条件に当てはまったら、その回答を変数に収納する
    console.log(i, ": question: ", question,answer);
    if (question === thisWeekTargetStepsQuestion){
      thisWeekTargetSteps = answer;
    }
    if (question === thisWeekTryActionQuestion){
      thisWeekTryAction = answer;
    }
  }

  // 歩数記録表スプレッドシートを定義
  const ssid = targetGssId; // スプレッドシートのIDを定義
  const ss = SpreadsheetApp.openById(ssid); // スプレッドシートを開く
  const datass = ss.getSheetByName(targetSheetName); // スプレッドシートの対象シートを開く
  const lastRow = datass.getLastRow(); // 対象シートの最終行を取得する
  const emailValues = datass.getRange(1,emailColumn,lastRow,1).getValues(); // 対象シートの1行目1列目(A列)~メールアドレス列の最終行1列分までの値を取得する(2次元配列で取得されます)
  let oneWeekAgoTargetSteps;
  let twoWeekAgoSteps;
  let oneWeekAgoSteps;
  let oneWeekAgoOneDayAverageSteps;
  let totalSteps;
  let totalAverageSteps;
  let totalDistance;
  let reachedStation;

  // メールアドレスをキーに歩数記録表における回答者の行を特定
  for (let i = 0; i < emailValues.length; i++) { // 配列の数だけ処理を繰り返す
    const target = emailValues[i][0]; // 取得した配列の、i番目、0番目(A列)の値を取得する
    if (target === email) { // もしemailと一致したら、i+1 が該当のメールアドレス」がある行なので、その行に対して処理をする
      // ふりかえりフォーム回答データを歩数記録表に反映
      if (weekNum !== totalWeekCounts) {
        // 今週の目標を歩数記録表の対象列にセットする(最終週に対するふりかえりではスキップ)
        datass.getRange(i+1, thisWeekTargetStepsColumn).setValue(thisWeekTargetSteps);
      }

      // おつかれさまメッセージに利用する歩数記録表のデータを取得
      oneWeekAgoTargetSteps = datass.getRange(i+1, oneWeekAgoTargetStepsColumn).getValue();
      twoWeekAgoSteps = datass.getRange(i+1, twoWeekAgoStepsColumn).getValue();
      oneWeekAgoSteps = datass.getRange(i+1, oneWeekAgoStepsColumn).getValue();
      totalSteps = datass.getRange(i+1, totalStepsColumn).getValue();
      totalDistance = datass.getRange(i+1, totalDistanceColumn).getValue();
      reachedStation = datass.getRange(i+1, reachedStationColumn).getValue();
      oneWeekAgoOneDayAverageSteps = (oneWeekAgoSteps > 0) ? Math.round(oneWeekAgoSteps / 7) : 0;
      totalAverageSteps = (totalSteps > 0) ? Math.round(totalSteps / (7 * weekNum)) : 0;
    }
  }

  // おつかれさまでしたメッセージ送信に利用する変数を返す
  return {
    email,
    oneWeekAgoTargetSteps,
    oneWeekAgoSteps,
    oneWeekAgoOneDayAverageSteps, 
    twoWeekAgoSteps,
    totalSteps, 
    totalAverageSteps, 
    totalDistance, 
    reachedStation, 
    thisWeekTargetSteps, 
    thisWeekTryAction, 
  };
}

// おつかれさまでしたメッセージの送信
function sendEncouragementMessage(
  email,
  oneWeekAgoTargetSteps,
  oneWeekAgoSteps,
  oneWeekAgoOneDayAverageSteps, 
  twoWeekAgoSteps,
  totalSteps, 
  totalAverageSteps, 
  totalDistance, 
  reachedStation, 
  thisWeekTargetSteps, 
  thisWeekTryAction, 
) {
  // おつかれさまメッセージテンプレートのデータを取得
  const ssid = messageGssId; // スプレッドシートのIDを記載。URLにある英数字の羅列の部分を記載。
  const ss = SpreadsheetApp.openById(ssid); // スプレッドシートを定義
  const messagedatass = ss.getSheetByName(messageSheetName); // シートを定義。振り返りフォーム回答時メッセージ文が載っているシート名を記載。
  const messageRaw = messagedatass.getRange(2,weekNum).getValue(); // メッセージが載っているセルを指定。カッコの中は、行、列の順番で指定する。この場合は1行目、1列目という意味。

  /* おつかれさまでしたメッセージの送信 */
  // ユーザー情報取得に必要な情報を定義する
  const lookupHeaders = { // ヘッダーを設定する
    "Content-Type": "application/x-www-form-urlencoded"
  };
  const lookupPayload = { // ペイロードを設定する
    "token": token,
    "email": email
  };
  const lookupParams = { // パラメーターを設定する
    "headers": lookupHeaders,
    "method": "post",
    "payload": lookupPayload
  };

  // メールアドレスから Slack のユーザー情報を取得
  const lookup = UrlFetchApp.fetch(lookupByEmail,lookupParams); // 「lookupByEmail」APIを実行する
  const lookupOk = JSON.parse(lookup).ok; // ユーザー情報が取得できたかどうかを判定する
  if (lookupOk === false) { // もし、ユーザー情報が取得できていない場合
    const lookupError = JSON.parse(lookup).error; // エラー内容を取得する
    console.error(lookupError);
  } else { // もし、ユーザー情報が取得できた場合

    // 投稿に必要な情報を定義する
    const id = JSON.parse(lookup).user.id; // DMに送る際に必要になるIDを取得する
    const username = email.split('@')[0]; // メンションをする際に必要になるusernameを取得する

    // 歩数分析情報を算出して送信メッセージをパーソナライズ
    const stepsDiffBetweenWeeksMessage = resolveStepsDiffMessage(oneWeekAgoSteps, twoWeekAgoSteps);  // 先週と先々週の歩数差分メッセージを作成
    const hitTargetStatusMessage = resolveHitTargetStatusMessage(oneWeekAgoSteps, oneWeekAgoTargetSteps); // 先週の目標達成状況メッセージを作成

    // メッセージテンプレートの変数部分の解決
    const messageResolved = 
      resolveMessageVariables(
        messageRaw, 
        week[weekNum], 
        oneWeekAgoTargetSteps, 
        oneWeekAgoSteps, 
        oneWeekAgoOneDayAverageSteps, 
        twoWeekAgoSteps,
        stepsDiffBetweenWeeksMessage, 
        hitTargetStatusMessage, 
        totalSteps, 
        totalAverageSteps, 
        totalDistance, 
        reachedStation, 
        thisWeekTargetSteps, 
        thisWeekTryAction, 
      );
    const message = "<@" + username + ">\n\n" + messageResolved; // Slack のユーザーメンションを冒頭に付ける

    // Bot がおつかれさまメッセージを Slack で DM 送信
    const postPayload = { // ペイロードを設定する
      "token": token,
      "channel": id,
      "text": message,
    };
    const postParams = { // パラメーターを設定する
      "method" : "post",
      "payload" : postPayload
    };
    UrlFetchApp.fetch(postMessage, postParams); // Slackに投稿する
    console.log("Send message to ", email);
  }
}

// 歩数分析情報を算出して送信メッセージをパーソナライズ
// 目標達成状況メッセージの解決
function resolveHitTargetStatusMessage(oneWeekAgoSteps, oneWeekAgoTargetSteps) {
  if ((typeof oneWeekAgoSteps !== 'number' || oneWeekAgoSteps < 0) || (typeof oneWeekAgoTargetSteps !== 'number' || oneWeekAgoTargetSteps < 0)) {
    return '目標達成できましたか?';
  }

  if (oneWeekAgoSteps >= oneWeekAgoTargetSteps) {
    return `目標達成おめでとうございます! :clap:`
  }
  return `残念ながら目標達成とはなりませんでした。自己修正して頑張りましょう!`
}

// 週間歩数差分メッセージの解決
function resolveStepsDiffMessage(oneWeekAgoSteps, twoWeekAgoSteps) {
  // エラーチェック
  if ((typeof oneWeekAgoSteps !== 'number' || oneWeekAgoSteps < 0) || (typeof twoWeekAgoSteps !== 'number' || twoWeekAgoSteps < 0)) {
    return '';
  }

  // 第1週の場合は前々週との差分を算出できないため 0 とする
  if (week < 2) {
    return 0;
  }

  const stepsDiffBetweenWeeks = oneWeekAgoSteps - twoWeekAgoSteps;  // 先週と先々週の歩数の差分を算出
  
  if (stepsDiffBetweenWeeks > 0) {
    return `${Math.abs(stepsDiffBetweenWeeks).toLocaleString()} 歩 UP! :arrow_upper_right:`
  }
  if (stepsDiffBetweenWeeks < 0) {
    return `${Math.abs(stepsDiffBetweenWeeks).toLocaleString()} 歩 DOWN :arrow_lower_right:`
  }
  return `±0 歩 :arrow_right:`
}

// ★要修正★ テンプレートの変数を修正したらこここも修正する
// メッセージテンプレートの変数部分の解決
function resolveMessageVariables(
  messageRaw, 
  week,
  oneWeekAgoTargetSteps, 
  oneWeekAgoSteps, 
  oneWeekAgoOneDayAverageSteps, 
  twoWeekAgoSteps, 
  stepsDiffBetweenWeeksMessage, 
  hitTargetStatusMessage, 
  totalSteps,
  totalAverageSteps,
  totalDistance,
  reachedStation,
  thisWeekTargetSteps, 
  thisWeekTryAction, 
) {
  const regexWeek = /\<week\>/g;
  const regexOneWeekAgoTargetSteps = /\<oneWeekAgoTargetSteps\>/g;
  const regexOneWeekAgoSteps = /\<oneWeekAgoSteps\>/g;
  const regexOneWeekAgoOneDayAverageSteps = /\<oneWeekAgoOneDayAverageSteps\>/g;
  const regexTwoWeekAgoSteps = /\<twoWeekAgoSteps\>/g;
  const regexStepsDiffBetweenWeeksMessage = /\<stepsDiffBetweenWeeksMessage\>/g;
  const regexHitTargetStatusMessage = /\<hitTargetStatusMessage\>/g;
  const regexTotalSteps = /\<totalSteps\>/g;
  const regexTotalAverageSteps = /\<totalAverageSteps\>/g;
  const regexTotalDistance = /\<totalDistance\>/g;
  const regexReachedStation = /\<reachedStation\>/g;
  const regexThisWeekTargetSteps = /\<thisWeekTargetSteps\>/g;
  const regexthisWeekTryAction = /\<thisWeekTryAction\>/g;

  const messageResolved = messageRaw
    .replace(regexWeek, week)
    .replace(regexOneWeekAgoTargetSteps, oneWeekAgoTargetSteps.toLocaleString())
    .replace(regexOneWeekAgoSteps, oneWeekAgoSteps.toLocaleString())
    .replace(regexOneWeekAgoOneDayAverageSteps, oneWeekAgoOneDayAverageSteps.toLocaleString())
    .replace(regexTwoWeekAgoSteps, twoWeekAgoSteps.toLocaleString())
    .replace(regexStepsDiffBetweenWeeksMessage, stepsDiffBetweenWeeksMessage)
    .replace(regexHitTargetStatusMessage, hitTargetStatusMessage)
    .replace(regexTotalSteps, totalSteps.toLocaleString())
    .replace(regexTotalAverageSteps, totalAverageSteps.toLocaleString())
    .replace(regexTotalDistance, Math.round(totalDistance).toLocaleString())
    .replace(regexReachedStation, reachedStation)
    .replace(regexThisWeekTargetSteps, thisWeekTargetSteps.toLocaleString())
    .replace(regexthisWeekTryAction, thisWeekTryAction)

  console.log("messageResolved: ", messageResolved);
  return messageResolved;
}

大まかなコードの流れは以下です。

  1. (前提)各種設定値をセット
  2. onSubmitHandler() でフォーム提出時にトリガー発動
    1. refrectAnswersToStepsSheet() でふりかえりフォーム回答を歩数記録表に反映・歩数データ取得
      1. ふりかえりフォームの回答データを取得
      2. 歩数記録表スプレッドシートのデータを取得
      3. メールアドレスをキーに歩数記録表における回答者の行を特定
      4. ふりかえりフォーム回答データを歩数記録表に反映
      5. おつかれさまメッセージに利用する歩数記録表のデータを取得
      6. おつかれさまでしたメッセージ送信に利用する変数を返す
    2. sendEncouragementMessage() で Slack Bot が DM でおつかれさまでしたメッセージを送信
      1. おつかれさまメッセージテンプレートのデータを取得
      2. メールアドレスから Slack のユーザー情報を取得
      3. 歩数分析情報を算出して送信メッセージをパーソナライズ
      4. メッセージテンプレートの変数部分の解決
      5. Bot がおつかれさまメッセージを Slack で DM 送信

実装のポイント

簡単にポイントとなる実装箇所を解説します。

メールアドレスをキーに歩数記録表における回答者の行を特定 ~ おつかれさまメッセージに利用する歩数記録表のデータを取得

ふりかえりフォームで取得した回答者のメールアドレスをキーに、歩数記録表スプレッドシートのメールアドレス列を上から for 文でグルグルと回答者の該当行を探していきます。
該当行を見つけたら、getRange で事前に設定した目的の列を指定してセルを取得します。
取得したセルに対し、「反映」なら setValue、「取得」なら getValue をします。

  // メールアドレスをキーに歩数記録表における回答者の行を特定
  for (let i = 0; i < emailValues.length; i++) { // 配列の数だけ処理を繰り返す
    const target = emailValues[i][0]; // 取得した配列の、i番目、0番目(A列)の値を取得する
    if (target === email) { // もしemailと一致したら、i+1 が該当のメールアドレス」がある行なので、その行に対して処理をする
      // ふりかえりフォーム回答データを歩数記録表に反映
      datass.getRange(i+1, thisWeekTargetStepsColumn).setValue(thisWeekTargetSteps);

      // おつかれさまメッセージに利用する歩数記録表のデータを取得
      oneWeekAgoTargetSteps = datass.getRange(i+1, oneWeekAgoTargetStepsColumn).getValue();
      ...
    }
  }

メールアドレスから Slack のユーザー情報を取得

Slack の lookupByEmail API を利用して、メールアドレスから回答者の Slack のユーザー情報を取得します。
GAS で外部 WebAPI を実行するには URL Fetch Service を利用します。

API の結果は JSON (String) なので、扱いやすいように JSON.parse(hoge) でオブジェクトに変換します。(型サポートがないの辛い・・・)

  /* おつかれさまでしたメッセージの送信 */
  // ユーザー情報取得に必要な情報を定義する
  const lookupHeaders = { // ヘッダーを設定する
    "Content-Type": "application/x-www-form-urlencoded"
  };
  const lookupPayload = { // ペイロードを設定する
    "token": token,
    "email": email
  };
  const lookupParams = { // パラメーターを設定する
    "headers": lookupHeaders,
    "method": "post",
    "payload": lookupPayload
  };

  // メールアドレスから Slack のユーザー情報を取得
  const lookupResultRaw = UrlFetchApp.fetch(lookupByEmail, lookupParams); // lookupByEmail APIを実行する
  const lookupResult = JSON.parse(lookupResultRaw)
  const lookupOk = lookupResult.ok; // ユーザー情報が取得できたかどうかを判定する
  // もし、ユーザー情報が取得できなかった場合
  if (lookupOk === false) {
    const lookupError = lookupResult.error; // エラー内容を取得する
    console.error(lookupError);
    return;
  } 
  // もし、ユーザー情報が取得できた場合
  // 投稿に必要な情報を定義する
  const id = lookupResult.user.id; // DMに送る際に必要になるIDを取得する
  const username = email.split('@')[0]; // メンションをする際に必要になるusernameを取得する

歩数分析情報を算出して送信メッセージをパーソナライズ

Bot が愛されるかどうかの肝の部分です。
「パーソナライズ」などと洒落た横文字を使っておりますが、要は条件分岐です。
どれだけ細かい分析ができるか、送信パターンを用意するかによって、参加者に与えられる気づきの質が変わってきます。

この例では「先週と先々週の歩数差分」と「先週の目標達成状況」を分析の指標とし、達成できたかどうかによってメッセージの送信パターンを変えるようにしています。

▼ 呼び出し側

// 歩数分析情報を算出して送信メッセージをパーソナライズ
const stepsDiffBetweenWeeksMessage = resolveStepsDiffMessage(
  oneWeekAgoSteps,
  twoWeekAgoSteps
); // 先週と先々週の歩数差分メッセージを作成
const hitTargetStatusMessage = resolveHitTargetStatusMessage(
  oneWeekAgoSteps,
  oneWeekAgoTargetSteps
); // 先週の目標達成状況メッセージを作成

▼ 関数側

// 目標達成状況メッセージの解決
function resolveHitTargetStatusMessage(oneWeekAgoSteps, oneWeekAgoTargetSteps) {
  ...

  if (oneWeekAgoSteps >= oneWeekAgoTargetSteps) {
    return `目標達成おめでとうございます! :clap:`;
  }
  return `残念ながら目標達成とはなりませんでした。自己修正して頑張りましょう!`;
}

// 週間歩数差分メッセージの解決
function resolveStepsDiffMessage(oneWeekAgoSteps, twoWeekAgoSteps) {
  ...

  const stepsDiffBetweenWeeks = oneWeekAgoSteps - twoWeekAgoSteps; // 先週と先々週の歩数の差分を算出

  if (stepsDiffBetweenWeeks > 0) {
    return `${Math.abs(
      stepsDiffBetweenWeeks
    ).toLocaleString()} 歩 UP! :arrow_upper_right:`;
  }
  if (stepsDiffBetweenWeeks < 0) {
    return `${Math.abs(
      stepsDiffBetweenWeeks
    ).toLocaleString()} 歩 DOWN :arrow_lower_right:`;
  }
  return `±0 歩 :arrow_right:`;
}

メッセージテンプレートの変数部分の解決

テンプレート内の変数を歩数等のデータ入力状況に応じて動的に解決しています。
スプレッドシート内の文字列を動的に変えるスマートな方法を見つけることはできなかったため、文字列置換という力技で解決しています。
(もっとスマートな解決方法があったら教えてください)

▼ 呼び出し側

    // メッセージテンプレートの変数部分の解決
    const messageResolved =
      resolveMessageVariables(
        messageRaw,
        week[weekNum],
        oneWeekAgoTargetSteps,
        oneWeekAgoSteps,
        ...
      );

▼ 関数側

// ★要修正★ テンプレートの変数を修正したらこここも修正する
// メッセージテンプレートの変数部分の解決
function resolveMessageVariables(
  messageRaw,
  week,
  oneWeekAgoTargetSteps,
  oneWeekAgoSteps,
  ...
) {
  const regexWeek = /\<week\>/g;
  const regexOneWeekAgoTargetSteps = /\<oneWeekAgoTargetSteps\>/g;
  const regexOneWeekAgoSteps = /\<oneWeekAgoSteps\>/g;
  ...

  const messageResolved = messageRaw
    .replace(regexWeek, week)
    .replace(regexOneWeekAgoTargetSteps, oneWeekAgoTargetSteps.toLocaleString())
    .replace(regexOneWeekAgoSteps, oneWeekAgoSteps.toLocaleString())
  ...

  console.log("messageResolved: ", messageResolved);
  return messageResolved;
}

フォーム提出時のトリガー

スクリプトを修正後、左サイドメニューの時計アイコン トリガー をクリックします
右下の [+ トリガーを追加] ボタンをクリックします。

image.png

トリガー条件を以下の通り修正します。

  • イベントの種類を選択: フォーム送信時
  • エラー通知設定: 今すぐ通知を受け取る

image.png

[保存] をクリックします。
初回は OAuth 認可同意画面が表示されるので、[Allow] をクリックします。

image.png

完了後、トリガー画面に設定したトリガー条件が表示されます。

image.png

テスト

実際にフォーム提出時に Bot から Slack へ DM が送信されるかテストしてみましょう。
作成したふりかえりフォームを回答モードで開き、回答してください。
もし届かなかった場合は、Apps Script の左サイドメニューの「実行数」を開きます。
ステータスが 失敗しました となっているログを確認し、デバッグしましょう。

image.png

成功すると、以下のようなメッセージが Slack Bot からフォーム回答者へ送信されます。

image.png

上記は歩数目標を達成している場合ですが、達成できていない場合は以下のようなメッセージが送信されます。

image.png

これで、歩数記録表やふりかえりフォームの入力内容からパーソナライズされたメッセージを送信することができるようになりました! :tada:

注意

送信する前に動作確認テストしましょう!
使い方資料をちゃんと作りましょう!

慣れてきたり、使い方がきちんと引継ぎされていないと、GAS が付いているリソース(スプレッドシート、フォーム等)をコピーし、スクリプトを編集しないでそのまま使ってしまいます。
参加者に公開する前にちゃんとメッセージが送信できるかテストしましょう!

<過去にやらかした例>

  • 違うワークスペース / Bot の Slack トークンを設定してしまい、意図しないターゲットにメッセージを送信してしまった
  • メッセージテンプレートのスプレッドシート ID を現在のものに修正しなかったため、意図しないメッセージをターゲットに送ってしまった
  • テンプレートを修正したが動作確認しなかったため、メッセージが意図しない表現になってしまった

サンプル

本記事で利用した「おつかれさま Bot」を実装するための各種サンプルは以下です。
ぜひご利用ください。

まとめ(再掲)

  • 全社ウォーキング大会やったら社員 444 人(約 1/4)が参加してくれた
  • 少人数で運営を回すのに GAS × Slack Bot で各種 Bot を作った
    • ふりかえり実施時にパーソナライズした分析情報を送ってくれるおつかれさま Bot がモチベーション維持に貢献した
  • 実装サンプルつくったので使ってみてください oOo

おわりに

いかがでしたでしょうか。
今回は「ウォーキング大会のふりかえり」をテーマにしましたが、この仕組み自体はどんなテーマでも転用が可能だと思います。
(ダイエット企画、新卒オンボーディングのアンケート、スクラムでのふりかえり、など)
この記事がどなたかの参考になれば幸いです。

本記事は株式会社 Works Human Intelligenceアドベントカレンダー の 22 日目の記事でした。

明日は @satomihoya さんの記事「日々息をするようにアウトプットしている人は何を意識しているのか?」です。
元々同じチームで席が隣だったり、この記事で紹介した WHP のメンバーだったりと共通点が多い方です。
Developers Summit 2022 にも登壇したりと様々な方面で活躍している方です。
ぜひご覧ください。

16
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?