DMM.comの、一番深くておもしろいトコロ。

GASとLINE Messaging APIで同棲生活を便利にした話

GASとLINE Messaging APIで同棲生活を便利にした話

f:id:dmmlabotech:20171219103743p:plain

これはDMM.com #2 Advent Calendar 2017 - Qiitaの19日目の記事です。

カレンダーのURLはこちら


こんにちは、電子書籍サービスの保守・運用をしている いのもえ です。 今回は、私が家で取り組んでいるおうちハックについて紹介したいと思います。

ことのきっかけ

今年の2月に会社の場所が恵比寿から六本木に移転したことをきっかけに、同棲生活を始めました。

同棲生活を始めるにあたって心配だったのが、「どうでもいいことで喧嘩すること」でした。

特に原因になりそうだなと思っていたのが、家事の分担です。

「気になった人がやる」では、おそらく私が多く担当することになるのでは…。さらに、「なんで私ばっかりやらないといけないの」と喧嘩になるのでは…と思っていました。  

喧嘩にならない家事分担を考案

家事分担の偏りをなくすために、「家事をやったら、設定された金額と実施した回数に応じてお互いにお金を払う」という仕組みを提案しました。

具体的な1回の設定金額はこんな感じです。 話し合って決めました。

提案時、こんなことを思ってました。

  • 家事をきっちり半分に分担するのは無謀だから、せめて多くやったぶん嬉しいことがあると良い
  • お互い協力しあってほとんど半々にもっていき、プラマイゼロみたいな状況になると良い
  • やった数や貢献度を数値化できると良い
  • 相手が多く担当してくれた時に、「次は自分もちゃんとやらなきゃな」と思えるような仕組みになると良い

提案内容を要約すると「やってもらったぶんだけお金を払う」というちょっと生々しい感じになってしまいましたが、コンセプトはそんなところだと思います。

金額設定の時には、「1ヶ月にどのくらい家事があるか」を試算して、お互いの担当しそうな家事をちゃんと1ヶ月やりきったら払うお金が同じになるようにしました。 この時に書いた資料は我が家では、「要件定義書みたいなやつ」と呼ばれています(笑)。

運用し始めてから

うちにはホワイトボードが冷蔵庫に貼ってあって、こんな感じで「支払うべきお金」を可視化するようにしていました。

このボードに書くうえでのルールはこんな感じでした。

  • 書く時に「xxxをやったからyyy円分追記するよ」と声をかける(事後でもOK)
  • お金を払う時に「(家事をやってくれて)ありがとう」と言う

初めは「とりあえず家事をやるとお金をもらえるという習慣を付けなきゃ!」と意気込んでいました。

問題発生と解決

ホワイトボードでの運用を始めて、「家事をやる=お金をもらえる」の意識が定着しだした頃、段々と問題が明らかになってきました。

  • 「この家事のぶんは渡したっけ?」「この家事、ホワイトボードに書いたっけ?」とわからなくなってしまう
  • 少額のお金をほぼ毎日やりとりするので、少しずつ険悪なムードが…(請求するのも嫌だし、されるのも嫌だし、でもお金は欲しい、という気持ちからだと思います)
  • 支払ったら消すので、「どちらが何の家事をどのくらいやったのか」がわからない(家事の偏りをなくしたい、という目的が果たせない)

自分で提案した仕組みということもあるのですが、私自身は「仕組みが悪いんじゃなくて、ツールと運用が悪いんだ!」と強く感じました。

とはいえ、お金のやりとりは人間同士でやったら喧嘩になりやすいとわかり…。

今度はこの仕組みをシステム化することを提案しました。

システムの提案

システム化することを提案した時、「今のままでも十分だよ。そのシステムを作るのだって大変なんじゃない?」と言われました。

確かにもっともな指摘でした。

でも、「これ以上このやりとりで険悪な雰囲気になるのはイヤだ。ホワイトボードに書くのもめんどくさい、だいたい人間に計算作業をさせるほうがおかしい」という気持ちのほうが大きく、「いったん作るから、それを使わせてほしい。要望は受け付けるから!!」と半ば強引に押し切りました。

システム構成

技術選定をするに当たって、自分なりに重視するところを決めました。

  • 会社の業務で頻繁に使っているのかどうかはさておき、速く実装できること
  • ゼロ円で運用できること
  • 形骸化を防ぐため、入力しやすい形式が提供できること

色々考えて、以下の構成になりました。

役割 使うことにしたもの
データ蓄積 Googleスプレッドシート
データの処理 GoogleAppScript
データの入出力 Googleフォーム&LINE MessagingAPI

実装したもの

LINE@を使ってbotの「うさこ」を作りました。

同棲するうえでの「人間同士でやるとギスギスしがちなもの」は、うさこが代わりにやってくれます。

うさこが代わりにやってくれることは、こんな感じです。

  • 実施した家事のサマリ通知
  • 報告済家事の確認
  • ゴミ出し日のリマインド
  • 家事報告のリマインド
  • 家計の締め日をリマインド
  • 報告データの整理
  • お菓子のリクエスト(する/される)
  • 買い出しメモ(登録/確認/削除)
  • うさことおしゃべり

「催促(リマインド)」「1度言ったことの確認」「お金周り」はうさこを通してやりとりしたかったので、役割が色々増えました。

家事管理の機能

実施家事の報告

データの入力は上の図のような感じになっています。

入力フォームの呼び出し

doPost()関数を使って、「フォーム」という言葉に反応するようにしています。

/**
 * LINEのwebhookに反応する。
 */
function doPost(e) {
  var token = "LINE Messaging APIに使うトークン";
  var posted_json = JSON.parse(e.postData.contents);
  var events = posted_json.events;
  // デフォルトの返信メッセージを取得
  var text = get_usako_message();
  events.forEach(function(event) {
    // 各コマンドの呼び出し
    if ((event.message.text).match(/フォーム/)) 
    {
      var dt = new Date();
      var date = String(dt.getFullYear()) + '-' + String(("0"+(dt.getMonth() + 1)).slice(-2)) + '-' + String(("0"+(dt.getDate())).slice(-2));
      text = '家事報告だね!\nhttps://docs.google.com/forms/d/e/フォーム固有ID/viewform?usp=pp_url&entry.1025033203=' + date;
    }

    ... 省略...
    
    line_obj.reply(token, event.replyToken, text);
  });
};

入力するのは「家事をやった直後」と仮定して、「いつやったのか?」という回答欄に予め報告当日の日付が入力されるようなURLを生成しています。 

家事のサマリを通知

毎週日曜日の夜中に、「先週、誰が何の家事をどのくらいやったか。その実績でいくら払わなければならないのか」を通知します。

こんなフローで整形しています。

報告済家事の確認

うさこに「いつもの」と話しかけると、サマリとして未通知な報告済家事をおしえてくれます。

「あれ、この家事報告したっけ?」を確認するのに便利です。

お菓子リクエスト・買い出しメモの管理

決まった形式で話しかけると、うさこを通じてスプレッドシートの内容を操作できます。

入力 出力

実際に実行すると、こんなかんじです。

指定した形式に反応するのは、家事報告フォームと同様です。

続く言葉に応じて、処理内容を切り替えます。

/**
 * 買い出し関連のコマンド呼び出し
 *
 * @param string com リクエスト本文
 * @param obj    ss  スプレッドシートのオブジェクト
 * @return string うさこの返事
 */
function command_purchase(com, ss)
{
  // コマンド本文を空白で区切る
  var command = com.split(' ');

  // コマンドが入力されていない場合はエラーの返事を返す
  if (command.length < 2) return 'フォーマットエラーだよ!\n「買い出し リスト」「買い出し xx yyy 買ったよ」「買い出し xx yyy 欲しい」で書いてね!\n※xxx,yyyは欲しいモノを表しているよ!半角スペースで区切ってね。'; 
  
  // コマンドを識別し、返事をする
  command.shift();
  if (String(command[0]) == 'リスト') return get_purchase_list_(ss);
  if (String(command[command.length - 1]) == '買ったよ') 
  {
    command.pop();
    return set_item_purchased_(command, ss);
  }
  if (String(command[command.length - 1]) == '欲しい')
  {
    command.pop();
    return set_item_purchase_list_(command, ss);
  }
  return '「リスト」「買ったよ」「欲しい」のどれかで話しかけて…!!!';
}

/**
 * 買い物リストに欲しいモノを登録する
 *
 * @param array items 欲しいモノリスト
 * @param obj   ss    スプレッドシートのオブジェクト
 * @return string うさこの返事
 */
function set_item_purchase_list_(items, ss)
{
  var last_row = 1;
  // スプレッドシートに記入
  items.forEach(function(item) {
    last_row = ss.getLastRow() + 1;
    ss.setActiveCell('A' + last_row).setValue(item);
  });
   return '買い出しリストに追加しておいたよ!\nリストの内容を見るには「買い出し リスト」って言ってね';
}

/**
 * 買い出しリストを返却
 *
 * @param obj ss スプレッドシートのオブジェクト
 * @return string うさこの返事
 */
function get_purchase_list_(ss)
{
  var items = ss.getRange(2, 1, ss.getLastRow()-1, 2).getValues();
  // 買い出しリストに登録がなければ後続処理を実行しない
  if (items.length < 1) return 'いま登録されている品目はないよ!\n欲しいものがあったら「買い出し xxx yyy 欲しい」で教えてね。';
  
  var text = '買い出しリストには、いま以下の品目が登録されてるよ!\n';
  var item_not_exist_flg = true;
  items.forEach(function(item){
    if (item[1] != '済')
    {
      item_not_exist_flg = false;
      text = text + String(item) + '\n';
    }
  });
  // 全て購入済ならリストに記載項目がない旨を返却
  if (item_not_exist_flg) return 'いま登録されている品目はないよ!\n欲しいものがあったら「買い出し xxx yyy 欲しい」で教えてね。';
  text = text + '\n買い出しが終わったら「買い出し xxx yyy 買ったよ」でリストから削除できるよ!';
 
  return text;
}

/**
 * 買い出し品目を「買い出し済」にする
 *
 * @param array purchased_items 買ったものリスト
 * @param obj   ss              スプレッドシートのオブジェクト
 * @return string うさこの返事
 */
function set_item_purchased_(purchased_items, ss)
{
  var items = ss.getRange(2, 1, (ss.getLastRow() - 1), 2).getValues();
  if (purchased_items.length < 1 || items.length < 1) return '教えてもらった品目がリストに無いよ!\n「買い出し リスト」でリストにある品目を確認してね';
  var item_not_exist_flg = true;
  purchased_items.forEach(function(purchased_item){
    Object.keys(items).forEach(function (key) {
      if (items[key][1] != '済' && String(purchased_item) == items[key][0])
      {
        item_not_exist_flg = false;
        ss.getRange((Number(key)+2), 2).setValue('済');
      }
    });
  });
   if (item_not_exist_flg) return '教えてもらった品目がリストに無いよ!\n「買い出し リスト」でリストにある品目を確認してね';
  
  return 'リストから品目を消しておいたよ〜';
}

言葉が変わるのみで、お菓子リクエストについても同様の処理を行っています。

お菓子のリクエスト機能は、確認機能がなく、毎週木曜日に「こういうリクエストがあったよ」と通知されます。

「作って欲しいお菓子をリクエストする」ので、よく話のきっかけになります。

 

リマインド関連

ごみの日や家計の締め日などは、時間指定でスクリプトを実行して通知しています。

/**
 * ゴミ捨て通知
 */
function notice_trash_day()
{
  var channel_access_token = "トークン";
  var dt = new Date();
  dt.setDate(dt.getDate() + 1);
  var comment = [];
  
  // 翌日が第何週目かを求める
  var is_the_what_weekly = Math.floor((dt.getDate() - 1 ) / 7) + 1;
  // ゴミ出しの前日曜日だったら通知対象とする
  switch(dt.getDay())
  {
    // 月曜日のごみ
    case 1:
      if (is_the_what_weekly == 1 || is_the_what_weekly == 3)
      {
        comment.push('金属類・紙類');
        comment.push('ペットボトル・繊維類');
      }
      break;
    // 火曜日のごみ
    case 2:
      comment.push('一般ごみ・有害ごみ');
      break;
    // 水曜日のごみ
    case 3:
      comment.push('プラスチック製容器包装');
      break;
    // 金曜日のごみ
    case 5:
      comment.push('一般ごみ・有害ごみ');
      if (is_the_what_weekly == 2 || is_the_what_weekly == 4)
      {
        comment.push('びん・飲料かん');
      }
      break;
  }
  
  // ごみの日じゃない場合は何もしない
  if (comment.length < 1) return;
  var text = '明日は ' + comment.join('、') + ' のゴミの日だよ!\n準備忘れずに!';
  
  // LINEに通知
  line_obj.push(channel_access_token, text, 'チャンネル識別子');
}

こんな感じのスクリプトを毎日回して、前日になるとうさこから通知されます。

私が住んでいる地域では隔週で収集対象になるものがあり、よく間違えて通知されていたのですが、最近またそれを直しました。

次の収集日が楽しみです!

データの整理

サマリ通知に影響がないよう、報告データは毎月アーカイブしています。

/**
 * 記入されたシートをアーカイブし、記入シートをきれいにする
 */
function create_archive()
{
  var dt = new Date();
  
  // その月の第一日曜日ならアーカイブを実行
  if (dt.getDate() > 7) return;
  
  // フォームと連携しているシートをアクティブにする
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('今月');
  SpreadsheetApp.setActiveSheet(sheet);
  
  // 年月(YYYYMM)の名前でシートをアーカイブ
   var sheet_name = String(dt.getFullYear()) + String(("0"+(dt.getMonth())).slice(-2))
   SpreadsheetApp.getActiveSpreadsheet().duplicateActiveSheet().setName(sheet_name);
  
  // シートをクリア
  var last_row    = sheet.getLastRow() - 1;
  sheet.deleteRows(2, last_row);
}

買い出しリストなども、完了したものは削除されます

/**
 * 買い出しリストから購入済データを削除する。
 *
 * @param obj スプレッドシートオブジェクト
 */
function delete_purchased_data(ss)
{
   var items = ss.getRange(2, 1, (ss.getLastRow() - 1), 2).getValues();
    // 買い出しリストに登録がなければ後続処理を実行しない
    if (items.length < 1) return;
    var delete_rows = [];
    Object.keys(items).forEach(function (key) {
      if (items[key][1] == '済')  delete_rows.push(parseInt(key, 10) + 2);
    });
  
  // 別の行を消さないようにするため、検索とは逆順で行を削除
  for (var i = delete_rows.length-1; i > -1; i--)
  {
    ss.deleteRow(delete_rows[i]);
  }
}

導入してみて

多少苦戦したところもあったのですが、結果として作ってみて良かったと思っています。

手で運用していた時の問題がほとんど解決できただけでなく、嬉しい誤算もありました。

  • botがかわいい。通知バグすら「ちょっと間抜けでかわいい」(うるさい、じゃない)
  • 相手もSEなので、協力して修正できる。
  • 思った以上にGASが楽しい
  • 自分で構成を考えて実装し、運用保守していくのが楽しい
  • 自動化すること自体が楽しくなってしまい、他にもできないか探してしまう  

GASの知識を会社でも有用

もともと、弊社ではスプレッドシートやGmailを使っていることが多く、GASを利用して「ちょっとした自動化」を出来る機会は多くありました。

社内で活用した大きな例だと、書籍貸出シートの延滞者抽出を自動化したものがあります。 細かな例だと、勤怠管理の自動化や、gmailに来た情報をslackに流す、などの相談を受けたり実際に作ったり…といったものがありました。

思い立った時にさくっと自動化できるし、拡張する時にも似たような発想でシステム化できるので、 エンジニアとしての経験が浅い人にもとっつきやすいと思います。非エンジニアの皆様も困っていることがあればぜひチャレンジしてみてください!