Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

Sansan の ランチを支える技術 ~ランチボット開発記~

Sansan 事業部 プロダクト開発部の寺田です。 スマホアプリのAPIの開発を担当しています。

今日は、プロダクト開発部と、アプリチームそれぞれで動いている ランチボット について紹介したいと思います。

ランチボット とは

部門用ランチボット

現在、プロダクト開発部には協力会社の方を含め約100人ほど在籍しており、毎月新しいメンバーが増えています。 所属チームが異なったりすると、なかなかコミュニケーションが行われず Slack上でしか見たことがない人が増えてきます。

こういった問題への取り組みのひとつとして、部のランチボットは誕生しました。

最初は人力ボットで丁寧にチーム分けを行なっていましたが、 2回目くらいから面倒になったので、 GAS で自動化させました。

f:id:satouryou1205:20190723183941p:plain

人力ボットで丁寧にチーム分けをしていた時のSlack

部内ランチが行われる流れは、以下のようになっています。

  1. 毎月第1営業日に、random チャンネルにランチ開催が宣言される

  2. 第2営業日に、ランチ用チャンネルにランチのグループ分けが投稿される

  3. 2 ~ 3 人のグループ内で、リーダーに任命された人はランチのスケジュールを立てる

  4. ランチ当日になったら外に食べに行ったり、リモート拠点の方がいる場合はお弁当を買ってきてリモートランチをする

このうち、運営側のコストとなっていた、 1, 2 を自動化しています。

チーム用ランチボット

チーム用のボットも同様の理由で誕生しました。 もともと、スマホアプリチームで毎週ランチに行く習慣があったのですが、 チームメンバーが増加するに従い、入れるお店がなくなりランチ難民となりました。

ですので、こちらはチームを 2 ~ 3 グループに分割して投稿してくれるように、部門用のボットを参考にして作成しました。

材料

  • Slack
  • Google Apps Script (GAS)

共通機能

特定の Slack チャンネル or グループにいる人の組み合わせを作成する

使用するAPIは以下となります。

組み合わせを決定する方式としては、 アンケート等で参加者を募集するのではなく、 チャンネルに参加している人を対象にして以下の処理を行なっています。

  1. チャンネルに存在するユーザーを取得

  2. 実態があるユーザーにフィルタリング

  3. ユーザーをシャッフル

  4. 3 ~ 4人 の組み合わせに分割する

実態があるユーザーにフィルタリング

上記を行なっている理由は、チャンネルに参加しているメンバーを取得するだけですと、 削除されたユーザーのIDや、別のSlack bot がランチに参加することになってしまうためです。

f:id:satouryou1205:20190723184649p:plain 実体のないユーザーも含めてチーム分けしてしまった時のSlack

以下のフォーマットで取得できるので、 deleted と is_bot をチェックすると判定できます。

{
    "ok": true,
    "user": {
        "id": "W012A3CDE",
        "team_id": "T012AB3C4",
        "name": "spengler",
        "deleted": false,
        "color": "9f69e7",
        "real_name": "Egon Spengler",
        "tz": "America/Los_Angeles",
        "tz_label": "Pacific Daylight Time",
        "tz_offset": -25200,
        "profile": {
            "avatar_hash": "ge3b51ca72de",
            "status_text": "Print is dead",
            "status_emoji": ":books:",
            "real_name": "Egon Spengler",
            "display_name": "spengler",
            "real_name_normalized": "Egon Spengler",
            "display_name_normalized": "spengler",
            "email": "spengler@ghostbusters.example.com",
            "image_original": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_24": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_32": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_48": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_72": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_192": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_512": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "team": "T012AB3C4"
        },
        "is_admin": true,
        "is_owner": false,
        "is_primary_owner": false,
        "is_restricted": false,
        "is_ultra_restricted": false,
        "is_bot": false,
        "updated": 1502138686,
        "is_app_user": false,
        "has_2fa": false
    }
}

部門用ランチボット機能

休日判定

部門用に関しては、組み合わせが投稿されたタイミングでメンションすることで、本人が気づけるようにしています。 しかし、これを土日にやってしまうと業務関係で何かあったのかと驚かせてしまうので、休日判定を行なって、土日や祝日はランチボットが動かないようにしています。 コードは以下です。

function isBusinessDay(date){
  if (date.getDay() == 0 || date.getDay() == 6) {
    return false;
  }
  var calJa = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com');
  if(calJa.getEventsForDay(date).length > 0){
    return false;
  }
  return true;
}

random チャンネルでランチボットの起動を周知する

f:id:satouryou1205:20190722203944p:plain

機能でもなんでもない感じですが、じつは手間を省く大事な機能です。 もともと手動でやっていた頃は、新しく入社した方に DM でこういったランチ企画があるのですが、いかがですかと アナウンスをしていました。

また、チャンネルに参加しているメンバーでも、今月は忙しくランチに参加できなそうだという場合はチャンネルから抜けてもらう必要があるので、その役割も担ってもらっています。

ですので、必ず random で通知されて翌営業日にランチボットが起動するようにしています(ただし休日以外)。

  date = getBusinessDate(date);
  ScriptApp.newTrigger('randomに通知する君').timeBased().at(date).create();
  
  date.setDate(date.getDate() + 1);
  date = getBusinessDate(date);
  ScriptApp.newTrigger('ランチボット君').timeBased().at(date).create();

チーム用ランチボット機能

欠席機能

使用するAPIは以下となります。

チーム用のランチボットは問答無用で、毎週組み合わせを発表していきます。 しかし、チームの人数が増えてくると、さまざな要因でランチを欠席する人が出てきます。 (Sansan ではランチ勉強会が積極的に行われていたり、部活動や他部署とのランチなど、ランチイベントがいろいろあるのが原因です)

チーム用の方は、チームのrandom用チャンネルに実装したので、 ランチボットが動くタイミングで離脱してもらうのは大変なため、このような機能を実装しました。

(ブログを書きながら、欠席機能ではなくて lunch-train 入れればよかったのではとか思っています)

lunchtrain.builtbyslack.com

以下の手順で処理を行なっています。

  • チャンネルに存在するユーザーを取得
  • チャンネルで、ボット起動日に 欠席 と書き込んだユーザーを取得
  • 実態があるユーザー もしくは、 欠席 と投稿していないユーザーにフィルタリング
  • ユーザーをシャッフル
  • 人数に合わせて、1 ~ 3 チームになるようにシャッフル

注意する点としては、ts には秒単位のUNIX形式のタイムスタンプが入っているので、 ボット起動日内に投稿したものかどうかのチェックには ts を Dateに変換するか、 起動日を 秒単位のUNIX形式 に変換する必要があります。

{
    "ok": true,
    "messages": [
        {
            "type": "message",
            "ts": "1358546515.000008",
            "user": "U2147483896",
            "text": "Hello"
        },
        {
            "type": "message",
            "ts": "1358546515.000007",
            "user": "U2147483896",
            "text": "World",
            "is_starred": true,
            "reactions": [
                {
                    "name": "space_invader",
                    "count": 3,
                    "users": [
                        "U1",
                        "U2",
                        "U3"
                    ]
                },
                {
                    "name": "sweet_potato",
                    "count": 5,
                    "users": [
                        "U1",
                        "U2",
                        "U3",
                        "U4",
                        "U5"
                    ]
                }
            ]
        },
        {
            "type": "something_else",
            "ts": "1358546515.000007"
        },
        {
            "text": "Containment unit is 98% full",
            "username": "ecto1138",
            "bot_id": "B19LU7CSY",
            "attachments": [
                {
                    "text": "Don't get too attached",
                    "id": 1,
                    "fallback": "This is an attachment fallback"
                }
            ],
            "type": "message",
            "subtype": "bot_message",
            "ts": "1503435956.000247"
        }
    ],
    "has_more": false
}

今後欲しい(と言われている)機能

組み合わせのダブり防止機能

  • 人力から自動化したことによりデグレした機能
  • 完全にランダムでシャッフルしているため、稀にかぶることがある
  • 組み合わせ結果をそのままスプレッドシートに書き込んで、組み合わせを行うときに先月分と重複が無いかチェックすれば可能
  • 気が向いたら実装する

同じチームが被らないようにする機能

  • 人力から自動化したことによりデグレした機能
  • 完全にランダムでシャッフルしているため、稀にかぶることがある
  • 以下の2パターンのどちらかで考えている
    • 同じユーザーグループのユーザーは関係者だろうという仮定のもと、同じユーザーグループに所属している人同時を同じ組み合わせに入れない
    • メンバー一覧のスプレッドシートがあるので、アクセス権限をもらって同じチームのメンバーが同じ組み合わせにならないようにする
  • 気が向いたら実装する

ランチボットの評価

先月のランチは、34人の方々が参加していました。

会話内容ですが、実体験も含めて以下の感じでした。

  • 自己紹介 (リアルでは初対面というケースがあるので)
  • 現在対応中の案件
  • 世間話
  • 過去の思い出話

この取り組みの良い点として、以下のような声が上がっています。

  • 地方拠点勤務で名前しか知らない人もいるが、顔がわかったりどんな仕事をしているか知れること
  • Devランチで話したことがキッカケで、その後も付き合いができたこと
  • ランチがキッカケで他のチームでやっている取り組みを知れること
  • 自分からランチに行くために声をかける必要がないこと
  • 始めからランチに参加したい人が集まっているので、断られるという懸念が排除されていること

終わりに

ランチボットの作成は、GASを利用して作成していますが、他にもLambdaを利用して簡単に構築することができます。

ランダムに設定されるランチで新しいコミュニケーションが生まれるので、皆さんもランチボットを使って試されてはいかがでしょうか。

© Sansan, Inc.