Yoshimaru's Blog

京都に住む院生が書いています

Google Tasksのタスクを自動的にSlackに投稿する

きっかけ

皆さんは毎日のタスク管理ってどうしてますか?

自分は研究室で毎日のタスクを報告する習慣があるのですが,その際ついつい忘れたり,書くことで満足しちゃってることがありました. なのでTodoサービスにあるタスクを毎日読み込んで自動でSlackに投稿する機能を作ってみました.

動作例は以下です. このようにたくさんタスクがあった時に.

このようなタスクがあるとして,

このようにSlackの特定のチャンネルにタスクリスト,タスク,サブタスクが綺麗に表示できます

動作例

自分はTodo系のアプリは長続きしなかったのですが,自動投稿されて誰かに見られるかも...とおもうと続く気がします..(まだ初めてまもないですが) Google TasksはPCだとなぜかブラウザ版がなかったりするので,以下のクライアントアプリが便利,無料 https://thetodo.net/ja/

thetodo.net

準備

使うのは以下.

  • Slack Webhook
  • App Script
  • Google SpreadSheet

流れとしては

  1. Google Tasksからその日のタスク一覧をGoogle SpreadSheet に記入する
  2. そのGoogle SpretSheetからApp Scriptで投稿テキストを生成する
  3. 生成されたテキストをSlack Webhookを通じて投稿する

以上の機能をApp Scriptのトリガー機能で毎朝や毎週などに設定して使えます.

1. Google Tasks -> Google SpreadSheet

SpreadSheetで以下のカラム名を1行目に作ります.

| リスト | タスク名 | メインサブ | 親タスク | 詳細 | 期限 | 完了・未完了 |

その後に画像にある拡張機能からApp Scriptを探す.

スプレットシートの前準備

サービスからTasksを追加しておく.

App Scriptには以下のコードを添付する.

function formatDueDate(dateString) {
  var date = new Date(dateString);
  var formattedDate = date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate();
    return formattedDate;
}

function syncTasksToSheet() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
    // スプレッドシートにデータが1行だけの場合は、以下の処理をスキップ
  if (sheet.getLastRow() > 1) {
    sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).clear();
  }
  var taskLists = Tasks.Tasklists.list();
  var parentTaskMap = {}; // 親タスクのIDと名前をマッピングするための辞書

  if (taskLists.items) {
    taskLists.items.forEach(function(taskList) {
      var tasks = Tasks.Tasks.list(taskList.id).items;
      if (tasks) {
        // 先に全てのメインタスクをマップに登録
        tasks.forEach(function(task) {
          if (!task.parent) {
            parentTaskMap[task.id] = task.title;
          }
        });

        // タスクの処理
        tasks.forEach(function(task) {
          var isSubtask = task.parent ? true : false;
          var parentTaskName = isSubtask ? parentTaskMap[task.parent] : 'main';
          var row = [
            taskList.title, // リスト名
            task.title, // タスク名
            isSubtask ? 'サブ' : 'メイン', // メインタスクかサブタスクか
            isSubtask ? parentTaskName : 'main', // 親タスク
            task.notes || '', // 詳細
            task.due ? formatDueDate(task.due) : '', // 期限
            task.status === 'completed' ? '完了' : '未完了', // 完了・未完了
          ];
          Logger.log(row);
          sheet.appendRow(row);
        });
      }
    });
  }
}

コードの処理としては, - まず最初にスプレットシートのデータを削除する.(2回目以降で)

syncTasksToSheet関数を実行して以下のようになっていたらOK

またさっき作ったSpreadSheetにも自動で追加されてると思います.

2. App Scriptで投稿文生成

今スプレットシートにタスク一覧が手に入ったので,そこからSlackに投稿する文を生成します. 以下の App Scriptは例でカスタマイズの余地があると思います

function postToSlackmain() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var tasks = {};
  var formattedText = '';

  data.forEach(function(row, index) {
    if (index === 0) return; // ヘッダー行をスキップ

    var [list, task, mainSub, parentTask, detail, deadline, status] = row;

    if (mainSub === 'メイン') {
      if (!tasks[list]) {
        tasks[list] = {};
      }
      tasks[list][task] = {
        'detail': detail,
        'deadline': deadline ? Utilities.formatDate(new Date(deadline), Session.getScriptTimeZone(), 'yyyy/MM/dd') : '期限なし',
        'subtasks': tasks[list][task] ? tasks[list][task]['subtasks'] : [] // 既存のサブタスクを保持
      };
    } else if (mainSub === 'サブ') {
      if (!tasks[list]) {
        tasks[list] = {};
      }
      if (!tasks[list][parentTask]) {
        tasks[list][parentTask] = { 'subtasks': [] }; // 仮のメインタスクを作成
      }
      tasks[list][parentTask]['subtasks'].push({
        'name': task,
        'status': status,
        'detail': detail,
        'deadline': deadline ? Utilities.formatDate(new Date(deadline), Session.getScriptTimeZone(), 'yyyy/MM/dd') : '期限なし',
      });
    }
  });
  var formattedText = '';
  // フォーマットされたテキストを生成
  for (var listName in tasks) {
    formattedText += `====== 【${listName}】 ============================================= \n`;
    for (var taskName in tasks[listName]) {
      var task = tasks[listName][taskName];
      formattedText += `*${taskName}* (締切:${task.deadline}) \n`;
      if (task.detail) {
        formattedText += `>${task.detail.replace(/\n/g, '\n>').replace(/\r/g, '\n>')} \n`;
      }
      task.subtasks.forEach(function(subtask) {
        formattedText += `   • ${subtask.name} :${subtask.status}: (締切:${subtask.deadline}) \n`;
      });
    }
  }
  postToSlack(formattedText);
}

function postToSlack(message) {
  var webhookUrl = '******************************************'; // ここにSlackのWebhook URLを設定
  var payload = {
    'text': message // 送信するメッセージ
  };

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

  UrlFetchApp.fetch(webhookUrl, options);
}

機能としてはスプレットシートからいい感じに文を生成して,PostToSlack関数に投げます. ここでWebhookURLが必要ですが,3で手に入れます.

その後この関数を毎日(もしは毎週など)実行されるようにトリガーを設定する. この時注意して欲しいのが, syncTasksToSheet→postToSlackmain の順で実行できるように時間をずらさないと最新の情報を取れないです.

トリガー設定

3. Slack Webhookを有効にして投稿

以下のURLからWebhookを作る. https://dumil.slack.com/apps/new/A0F7XDUAZ--incoming-webhook-

参考サイト https://documents.trocco.io/docs/how-to-generate-slack-webhook-url

投稿するチャンネルを指定して,Incoming Webhookを作成する.

Webhookの設定,アイコンや説明などはここで追加する

これで,2で指定したトリガーをもとに

追加機能

自分は追加で日毎に差分を調査して,完了したタスクを毎日投稿する設定にしてます. その場合のコードは以下

function getSpreadsheetData() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  var range = sheet.getDataRange();
  var values = range.getValues();
  var tasks = [];

  for (var i = 1; i < values.length; i++) {
    var row = values[i];
    tasks.push({ title: row[1], status: row[6] }); // タスク名と状態(完了・未完了)を取得
  }
  return tasks;
}

function notifyCompletedTasks() {
  var spreadsheetTasks = getSpreadsheetData();
  var taskLists = Tasks.Tasklists.list();
  var message = "今日完了したタスク \n";
  if (taskLists.items) {
    taskLists.items.forEach(function(taskList) {
      var tasks = Tasks.Tasks.list(taskList.id).items;
      var parentTaskMap = {};

      // 全てのメインタスクをマップに登録
      tasks.forEach(function(task) {
        if (!task.parent) {
          parentTaskMap[task.id] = task.title;
        }
      });
      
      // タスクの処理
      tasks.forEach(function(task) {
        if (task.status === 'completed') {
          var isSubtask = task.parent ? true : false;
          var parentTaskName = isSubtask ? parentTaskMap[task.parent] : '';
          var spreadsheetTask = spreadsheetTasks.find(t => t.title === task.title);

          // スプレッドシートにないが完了した新たなタスク、または状態が変わったタスクを特定
          if (!spreadsheetTask || (spreadsheetTask && spreadsheetTask.status !== '完了')) {
            message += isSubtask ? ` • ${task.title}(メインタスク:${parentTaskName}\n` : task.title;
            
          }
        }
      });
      
    });
  }
  postToSlack(message);
}

これを同じように毎日トリガーを設定すれば以下のようにその日の完了タスクを投稿してくれます.

またここにある完了だったり未完了というのはスタンプを作っているので適応できています.

このサイトで簡単に作れるので便利

emoji-gen.ninja