見出し画像

ツイ🐙くん

こんにちは,ふなち(@_hunachi)です!

ありえないくらい遅刻なんですが,アドカレ20日目の記事です.(もうクリスマス過ぎたのにごめんなさい🙇‍♀️)

普段は内定者インターン(Androidエンジニア)としてAndroidのコードをバリバリ書く仕事をさせていただいているのですが,今回は仕事の話でもAndroidの話でもはないです..🙇‍♀️

趣味で自分用の簡単な打刻システムを作ったのでその話を書きます!
(本当は電子工作で色々頑張ってたのですが終わりそうにないのでこの話になりました😭)

作った動機

みなさんは,自分の1日のバイトや研究をした合計時間を知りたい..という気持ちになったことありませんか?

また業務委託的な働き方をしているバイトやお仕事で1日に働いた時間を知る必要があるのに今日何時間働いたか分からない....🙄 時間測り忘れちゃったわ😃となることはありませんか?

私はバイト(ヤプリさんではない別の会社さん)で自分で勤務時間を管理する必要がありよくやらかしています.
ちなみにヤプリさんではslackで出勤/退勤するためのコマンドを打つと自動で勤怠管理システムに出勤/退勤をしてくれるシステムがあります.仕事での連絡手段もslackなので出勤/退勤のし忘れがほぼなく,めちゃくちゃ便利です!このシステムが羨ましいと思ったのも作った動機です.

他には頑張ったのを可視化してSNSで友人に褒めてもらいたい!!ってことありませんか?

私は日々の頑張りをTwitterで繋がっている人や友人に褒めて欲しいです!!(めちゃくちゃ不純な動機!!)

そこで作ったのが,

ツイ🐙くん

です.読みは「ツイダコクン」です.

その名の通りツイートで打刻できちゃうくんです!(激寒親父ギャグ)
ツイートで打刻できるようにした理由は,私は普段から日常の出来事をTwitterで垂れ流しておりツイートで打刻することにより追加でかかる労力がほぼ0な為です🦤(※@_hunachiではなく日常垢の@huna_nekoでよくツイートしてます.)

ツイ🐙くんの主な機能

1.ツイートを拾ってくれます
「〇〇始める〜〜」,「〇〇終えるぞい!」などのツイートが対象になります.1日に何度も始める&終わるツイートをしても大丈夫です.後で合計を計算してくれるため何も考えずに今からすることをツイートするだけでOKです!

スクリーンショット 2021-12-29 2.48.45

2.毎日日が変わった数時間後に前日の頑張りをTwitterとDiscordに自動で共有してくれます.

スクリーンショット 2021-12-29 2.45.24
スクリーンショット 2021-12-29 3.02.50

共有することでみんなに褒めてもらえます!!やったね!!

使い方

・事前に何の時間を集計したいかはスクリプト上で設定しておきます(複数設定可能).
・ログを取るためのシートも集計したいことの種類ごとに用意してあげます.管理やデバッグしやすいように種類ごとに作るような仕様にしました.
・ツイートします.現状は「〇〇始める」と「〇〇終わる」から始まるツイートを使うようにしています.〇〇の部分には集計したいものの名前が入ります.

使用した技術

・Google Spread Sheet
・AppScript
・IFTTT
・Discord Bot

システム構成

めちゃくちゃ簡易的に書いたシステム構成は以下の通りです.

スクリーンショット 2021-12-29 6.29.24

作り方

ちょっと工程が多いのでざっくり説明します.詳細は調べると分かるはずです!また,もっといい作り方あるよ!という場合は教えて頂けると嬉しいです!

1.Spread Sheetを適当に作ります
  今回は,このシートに集計用のシートの情報など記載するので何も書かれていない状態のシートがベストです

2.作ったSpread SheetでAppScriptを作ります

スクリーンショット 2021-12-29 3.22.57

3.AppScriptでappscript.json ファイルを作ってタイムゾーンの設定をします
プロジェクトの設定→「appscript.json」マニフェスト ファイル...に☑️を入れれば作られます.

スクリーンショット 2021-12-29 3.23.45
スクリーンショット 2021-12-29 3.23.53

エディタを開き直すと作られているのがわかります.

スクリーンショット 2021-12-29 3.26.24

"timeZone"を"Asia/Tokyo"に変更or以下を追加します.

{
 "timeZone": "Asia/Tokyo", // これを書きます
 ....
}

4.とりあえずAppScriptをデプロイします

スクリーンショット 2021-12-25 5.38.57

5.IFTTTを使ってAppletsを作ります

Twitter→AppScript

スクリーンショット 2021-12-29 3.08.49

Twitterは認証してれば設定は何も要りません.
WebHook(Make a web request)の設定については以下のようにしました.
・URLにはAppScriptのデプロイした時に表示されるURLを貼り付けます
・MethodはPOST
・Content Typeはapplication/json
・Bodyは以下のように書きました(tokenの部分は適宜書き換えて下さい)

{
    "token":"何か好きなトークンなどを書いてね",
    "type": "tweet",
    "user": "<<<{{UserName}}>>>",
    "contents": "<<<{{Text}}>>>",
    "created_at":"<<<{{CreatedAt}}>>>"
}

AppScript→Twitter

スクリーンショット 2021-12-29 3.17.47

WebHook(Receive a web request)のEvent Nameは使わないので適当に daily_result と設定しました.
Twitter(Post a tweet)の方もWebHook側に投げるデータそのままをツイートさせたいのでシンプルに以下のようにしました.

スクリーンショット 2021-12-29 3.19.26

WebHook側の情報は,以下のサイトのDocumentationを見ると分かります.

以下のような今回叩きたいURLを知ることができます.⚠️daily_resultは自分で埋めてください.

スクリーンショット 2021-12-29 3.42.19

6.AppScript上にコードを書く
次の章で詳しく説明します.
 ⚠️ App Scriptでコードを書いて実行しているとGoogleアカウントでのログインやら権限を承諾しなきゃいけない場面がちょこちょこ出てきますが,内容が大丈夫か確認した上でOKを押すようにしてください!機密情報扱ってるGoogleアカウントは使わない方がいいと思います!

AppScript上のコード

普段全くJavaScriptを書かない&特に綺麗なコードを書こうと思ってないのでまずかったり読み辛いコードが含まれるかもです😞
また,実際はテストコードやログを出力するコードも書いているのですが長くなるためここでは紹介していません.

1.集計したいことを設定します

まず以下のコードを書きます.集計したいものを今後 dakokuType と呼ぶことにします.

const dakokuTypes = ["研究", "勉強", "インターン", "バイト"];

今回,私は研究,勉強,インターン,バイトを頑張った時間が知りたかったのでこの4つをdakokuTypesに入れました.適宜自分の好きな単語に置き換えてください👩‍💻

次にそれぞれに対するシートを作ります.

スクリーンショット 2021-12-29 4.15.26

そしてそれぞれのSpread SheetのIDをとってきて,AppScriptを書いているSpread Sheetの適当なところにまとめます.

スクリーンショット 2021-12-29 4.20.08

Sheet IDはURLからゲットできます.以下の写真でいう黒く隠されている文字列です.

スクリーンショット 2021-12-29 4.15.53

次に,Sheet IDをdakokuType名からとってこれるような関数を書きます.

function getSheetId(typeName) {
 const sheet = SpreadsheetApp.getActiveSheet();
 let typeIndex = -1;
 for (let i = 0; i < dakokuTypes.length; i++) {
   if (dakokuTypes[i] == typeName) {
     typeIndex = i;
     break;
   }
 }
 if (typeIndex < 0) return null;
 const sheetId = sheet.getRange("I" + (typeIndex + 2)).getValue();
 return sheetId;
}

2.ツイートやディスコードからのコマンド(POSTされるデータ)を受け付けるようにします

コードは以下になります.今までに説明していない関数を呼んでますがそれらは後で説明します.

function doPost(e) {

 if (e == null || e.postData == null || e.postData.contents == null) {
   console.log("data is Null!")
   return;
 }
 const requestJSON = e.postData.contents;
 console.log(requestJSON);
 const requestObj = JSON.parse(requestJSON);

 // https://blog.ohheybrian.com/2021/09/using-google-apps-script-as-a-webhook/
 if (requestObj["token"] != "任意のトークン") {
   return;
 }

 const content = requestObj["contents"];

 // タイプそれぞれに対してチェックする
 for (let i = 0; i < dakokuTypes.length; i++) {
   const type = dakokuTypes[i];
   const sheetId = getSheetId(type);

   if (content.substring(0, type.length) == type) {
     const element = new Object();
     element.collect_date = new Date();
     element.created_at = strToDate(requestObj["created_at"]);
   
     if (content.substring(type.length, type.length + 3) == "始める") {
       element.content = content.substring(type.length + 3);
       element.type = type + "開始";
       element.message = type + "を開始した";
       recordState(element, sheetId); // ステータスを保存するための関数
     } else if (content.substring(type.length, type.length + 3) == "終わる") { 
       element.content = content.substring(type.length + 3);
       element.type = type + "終了";
       element.message = type + "を終了した";
       recordState(element, sheetId);  // ステータスを保存するための関数
     }
   }
 }
}

// Twitterから得られた日付のフォーマットがnew Dateで変換できなかったので強引に変換するための関数
function strToDate(e) {
 const dateArray = e.split(" ");
 const monthName = dateArray[0];
 const day = dateArray[1].substring(0, 2);
 const year = dateArray[2];
 let hour = Number(dateArray[4].substring(0, 2));
 const min = dateArray[4].substring(3, 5);
 const ampm = dateArray[4].substring(5);
 if (ampm == "PM") {
   hour += 12;
 } 
 if (ampm == "AM" && hour >= 12) {
   hour -= 12;
 }
 const date = new Date(monthName + " " + day + ", " + year + " " + hour + ":" + min + ":00");
 return date;
}

3.ステータスを保存します

ステータスを保存するための関数を作ります.

function recordState(e, sheetId) {
 const createdDate = e["created_at"];
 const sheet = SpreadsheetApp.openById(sheetId);
 const type = e["type"];
 const message = e["message"];
 const values = [type, createdDate, e["content"]];
 sheet.appendRow(values); // データの保存
}

4.集計機能をつけます

日付を跨ぐ場合も含め,00:00~23:59の間での時間を集計するようにしました.(例:12/18の23:50~24:10で何かした場合は,12/18で10分,12/19で10分行ったとする)
コードは長いですが以下になります.
一応デバッグもしてる&うまく動いているのですが,気づいていないバグがあるかもです..

function checkWorksTime(type, year, month, day) {
 const sheet = SpreadsheetApp.openById(getSheetId(type));
 if (sheet == null) {
   return { "hourSum": 0, "minSum": 0 };
 }

 const lastRow = sheet.getLastRow();
 const beginDate = new Date(year, month, day, 0, 0, 0, 0);
 const endDate = new Date(year, month, day, 23, 59, 59, 999);

 let preState = null;
 let endTime = null;
 let hourSum = 0;
 let minSum = 0;
 spLoop: for (let i = lastRow; i >= 1; i--) {
   const rangeName = "A" + i.toString() + ":C" + i.toString();
   const dakokuData = sheet.getRange(rangeName).getValues();
   const dakokuDate = new Date(dakokuData[0][1]);
   if (dakokuData == null) break spLoop;
   const dakokuType = dakokuData[0][0];

   switch (dakokuType) {
     case type + "開始":
       if (preState == "開始") {
         break;
       }
       preState = "開始";
       if (endTime == null) {
         // 前日~終了が当日中にない場合は無視して終了する. 
         if (dakokuDate.getTime() < beginDate.getTime()) {
           break spLoop;
         }
         // 次の日の分なので読み込まない
         if (dakokuDate.getTime() > endDate.getTime()) {
           break;
         }
         // 当日
         const result = calculateGap(dakokuDate, endDate, hourSum, minSum);
         hourSum = result.hourSum;
         minSum = result.minSum;
         break;
       }
       // 前日〜当日までの間
       if (dakokuDate.getTime() < beginDate.getTime()) {
         const result = calculateGap(beginDate, endTime, hourSum, minSum);
         hourSum = result.hourSum;
         minSum = result.minSum;
         break spLoop;
       }
       const result = calculateGap(dakokuDate, endTime, hourSum, minSum);
       hourSum = result.hourSum;
       minSum = result.minSum;
       break;
     case type + "終了":
       if (preState == "終了") {
         break;
       }
       preState = "終了";
       // 当日以外
       if (dakokuDate.getTime() > endDate.getTime()) {
         break;
       }
       if (dakokuDate.getTime() < beginDate.getTime()) {
         break spLoop;
       }
       endTime = dakokuDate;
       break;
     default:
       break;
   }
 }

 const result = new Object();
 result.hourSum = hourSum;
 result.minSum = minSum;
 return result;
}

// 時間の差を計算するための関数
function calculateGap(startDate, endDate, hourSum, minSum) {
 let hourGap = endDate.getHours() - startDate.getHours();
 if (hourGap < 0) {
   hourGap = 24 + hourGap;
 }
 let minGap = endDate.getMinutes() - startDate.getMinutes();
 if (minGap < 0) {
   hourSum--;
   minGap = 60 + minGap;
 }
 minSum = minSum + minGap;
 if (minSum >= 60) {
   hourSum++;
   minSum -= 60;
 }
 hourSum = hourSum + hourGap;
 const result = new Object();
 result.hourSum = hourSum;
 result.minSum = minSum;
 return result;
}

5.ツイートやDiscordで定期的に投稿させます

webhookURLには,Discordの場合は特定のサーバーのチャンネルにメッセージを投げる際のWebHookURLを使い(ググると方法を書いたブログが沢山出てきます),Twitterの方には,作り方の章の5で説明したものを使います.

// 定期的に実行する君
function checkTodayWorks() {
 const today = new Date();
 today.setDate(today.getDate() - 1); // 前日分の結果を知りたいため
 console.log(today);
 let text = "☆ふなちの昨日(" + (today.getMonth() + 1).toString() + "月" + today.getDate().toString() + "日" + ")の頑張り☆\n"
 for (let i = 0; i < dakokuTypes.length; i++) {
   const type = dakokuTypes[i];
   const result = checkWorksTime(type, today.getFullYear(), today.getMonth(), today.getDate());
   text += (type + ":" + result.hourSum + "時間" + result.minSum + "分\n");
 }
 sendDiscord(text);
 sendTwitter(text);
}

function sendDiscord(text) {
 const webhookUrl = "https://discord.com/api/webhooks/*******"
 const data = {
   'content': text,
 };

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

 UrlFetchApp.fetch(webhookUrl, options);
}

function sendTwitter(text){
 const webhookUrl = "https://maker.ifttt.com/trigger/daily_result/with/key/******";
 
 const data = {
   'value1': text,
 };

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

 UrlFetchApp.fetch(webhookUrl, options);
}

この checkTodayWorks を定期的に呼ぶような設定をします.
AppScriptのトリガーを作成してあげます.

スクリーンショット 2021-12-29 5.35.30

+トリガーを追加 というボタンを押します.
設定は,時刻主導型→日付ベースのタイマー→午前1時~2時(または午前2時~3時)とします.
午前1時以降に設定するのは,IFTTTがTwitterをポーリングするスパンが1時間なので午前1時前だと前日のツイートが全部拾われる前に checkTodayWorks が呼ばれてしまう可能性があるからです.

スクリーンショット 2021-12-29 5.40.01

6.コードの保存&デプロイします

7.完成!

今回紹介しなかったけど作成済みの機能

・ステータスの更新があった時にDiscordに通知
・シートの更新
・バイトの出勤表に自動で働いた時間を入力してくれる

これらに関してはまた別の記事で書くかもしれないです!

まとめ

ツイ🐙くんにより,既にハッピーな毎日がさらにハッピーになりました.
今後はツイ🐙くんをもっと生かし毎日コツコツ進捗を生むことで,今回のような遅刻を減らしていきたいです😢 瀕死状態の研究も頑張ります😨

おまけ

これは数日前ガチャガチャをしたら出てきたハンコです.

画像14

私にぴったりのハンコでビックリです!
戒めに手に押してみましたがうまく押せなかったので写真は無いです.今後はこれを使わなくていいように頑張ります🍰

参考にした記事など

https://help.ifttt.com/hc/en-us/articles/1260803042229 

各種公式ドキュメント


この記事が気に入ったらサポートをしてみませんか?