ランチに行く人を勝手に選出するGoogle App Scriptを作った

弊社では、コロナ禍以前からその日に最もパフォーマンスの出る場所で働くことをルールとして環境を整えているので、あまり事務所に人が集まることはありません。

自宅でも事務所でも、同じように仕事ができることは、これができない方々からすれば良いことのように思うかもしれませんが、各自宅に環境が整いすぎると出社する理由がなくなってしまい、万年リモートのようになってしまう人が一定数現れます。

リモートでは業務上の連絡はもちろん、わからないこと、知りたいこと、聞きたいことはチャットツールでもオンライン会議ツールでも、最適なものを使えば不自由なくコミュニケーションできますが、隣の席の人がどんな仕事をしているのかはわかりませんし、斜め後ろの仲良しの2人の雑談が耳に入ることもありません。

もし自分の仕事と無関係な情報だったとしても、自分と違う環境で生きている人間の話は、新たな興味を生むキッカケになり、仕事の幅や人生の歩みをわずかに変える可能性があります。

ある程度社会人経験を積んだオッサンだけの会社であれば事務所の重要性なんぞほとんどないと思うのですが、新卒を含む若年者にとっては、せっかく組織で働くのであればこういうメリットは大いに教授すべきだと思うわけです。

そんな万年リモート社員を多く抱える弊社でなんとなくアンケート調査を行ったところ、出社がイヤで万年リモートなのではなく、出社する理由がないだけであることがわかりました。

そこで、出社するキッカケづくりをするため、一緒にランチに行く人をGoogleカレンダーにランダムでアサインするGoogle App Scriptを作ってみることに。

要件は

  • このスクリプト専用のGoogleカレンダーを作成し、毎週月曜日にランチの予定を作る
  • 予定を作成するのは可能であればサービスアカウントを使いたい
  • ランダムで選出した社員3名を前述の予定に招待する
  • 毎月10日に来月の予定を自動作成するようトリガーをセットする

以下gs全文です。

こちらの記事を参考にさせて頂きました。

サービスアカウントの作り方なんかも説明するの面倒なので上記記事から辿ってください。

途中サービスアカウントを扱うためのGSAppというライブラリを使っていますが、メンテナンスされておらずライブラリとして読み込むことができなかったので元のソースを別のgsファイルとしてコピペしています。

const MEMBER_NUMBER = 3;
const USERS = [ // 社員のメールアドレス(Googleアカウント)を列挙しとく
	'staff001@nasbi.jp',
	'staff002@nasbi.jp',
	'staff003@nasbi.jp',
	'staff004@nasbi.jp',
	'staff005@nasbi.jp',
];
const SCOPE = [
  'https://www.googleapis.com/auth/calendar',
  'https://www.googleapis.com/auth/calendar.events', // たぶんいらない
  'https://www.googleapis.com/auth/admin.directory.resource.calendar', // ユーザー招待するのに必要?いらないかも
  'https://www.googleapis.com/auth/gmail.send' // 招待メールを飛ばすので必要かと思ったけど動いてない
];
const CALENDAR_ID = 'hogefugapiyo@group.calendar.google.com';

function main() {
  // カレンダーのスコープを指定
  const serverToken = new GSApp(jsonKey.private_key, SCOPE, jsonKey.client_email);

  // トークンの取得
  const tokens = serverToken.addUser(USERS[0]).requestToken().getTokens();

  const events = generateNextMonthEvents();
  events.forEach((event) => {
    insertCalendar(event, CALENDAR_ID, tokens[USERS[0]].token)
  });

  console.log('done');
}

function insertCalendar(eventData, CALENDAR_ID, token) {

  const date = Utilities.formatDate(eventData.date,'JST', 'yyyy-MM-dd');

  const attendees = eventData.members.map((email) => ({ email }))

  const payload = {
    start: { date },
    end: { date },
    summary: 'ランチ行くぞ ٩(ˊᗜˋ*)و',
    attendees,
  };

  // リクエストの設定 
  const fetchOptions = {
    method: 'post',
    payload: JSON.stringify(payload),
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${token}`,
    },
    muteHttpExceptions: true,
  };

  //リクエストURLを作成  
  const url = `https://www.googleapis.com/calendar/v3/calendars/${CALENDAR_ID}/events`;

  try {
    const res = UrlFetchApp.fetch(url, fetchOptions);
    Logger.log(res)
  } catch(e) {
    // 例外エラー処理
    Logger.log('Error:')
    Logger.log(e)
  }
}

function generateNextMonthEvents() {
  const events = [];

  const today = new Date();
  const firstDay = new Date();
  firstDay.setMonth(today.getMonth() + 1);
  firstDay.setDate(1);

  const thisMonth = firstDay.getMonth();
  const tmpDate = dateCopy(firstDay);
  while (tmpDate.getMonth() === thisMonth) {
    if (tmpDate.getDay() === 1) { // 月曜日は1
      events.push({
        date: dateCopy(tmpDate),
      });
    }
    tmpDate.setDate(tmpDate.getDate() + 1);
  }

  let tmpUsers = [];
  events.forEach((event) => {
    event.members = [];
    for (let i = 0; i < MEMBER_NUMBER; i++) {
      if (tmpUsers.length === 0) {
        tmpUsers = arrayShuffle([...USERS]);
      }
      while (event.members.includes(tmpUsers[0])) {
        tmpUsers = arrayShuffle(tmpUsers)
      }
      event.members.push(tmpUsers.shift());
    }
  })

  console.log(events);
  return events;
}

/**
 * 引数のDateを複製する
 * @param {Date} date
 * @return {Date}
 */
function dateCopy(date) {
	return new Date(date.getTime());
}

/**
 * 配列をシャッフルする
 * @param {Array} origin
 * @return {Array}
 */
function arrayShuffle(origin) {
	const array = [...origin];
	for (let i = array.length - 1; i > 0; i--) {
		const r = Math.floor(Math.random() * (i + 1));
		const tmp = array[i];
		array[i] = array[r];
		array[r] = tmp;
	}
	return array;
}

同じ人が毎週招待されたり、招待されない人がいないよう、単純なランダム選出ではなく満遍なく招待が届くようにしてみた。

GASの発火トリガーはこんな感じ


作ってみて困ったのは、2つ。

どうせならアカウントに依存しないようサービスアカウントを予定の作成者としたかったのだけど、他のアカウントを予定に招待する場合は実物のGoogleアカウントを偽装して予定の作成をする必要があったこと。

どうやらスパム対策でこのような仕様になったみたい。

偽装する必要があるなら最初からサービスアカウントなんて使う必要なかったなあと思ったり。

もうひとつは招待メールが飛ばないこと。

これは原因がわからなかったのだけど、もしかしたらこれもスパム対策なのかもしれない。

ともあれ最低限の要件は満たせたと思うので、来月から運用を開始します。

もちろんこれはあくまでキッカケづくりであり義務ではないので、気分じゃなかったら参加しなくてもいいし、ふたり以上集まればランチ代は領収書を切ってくれて構わないです

義務や個人の負担はチリツモでモチベーション低下に繋がりますからね。