【GAS】Toggl→Slack&スプレッドシートに転送する・プロジェクト指定可能

悩む男性Togglでの記録を、Slackに自動で飛ばせたら便利そうだな〜
上司に稼働報告をするとき、Togglの記録をSlackに自動で転送できたら便利そうですよね。
また、スプレッドシートにも転記されたら、時間の集計もできて、うれしいと思います。
このページでは、TogglからSlackとスプレッドシートに転送するGASを紹介します。
コピペして使ってみてくださいね。
\ まずは無料で相談してみる /
目次
仕様
仕様
- 前日のTogglの記録をSlackとスプレッドシートに転送する
 - 転送するのは、指定したプロジェクトのみ
 - Slackの転送先のスレッドを指定できる
 - 転送先スプレッドシートのテンプレあり
 
コード
スクリプトファイルごとに、書いています。
GASのスクリプトエディター上で、スクリプトファイルを6つ作って、それぞれにコピペして使ってください。
global
/**
 * スプレッドシートの設定
 */
const SPRREADSHEET_URL = '***スプレッドシートのURL***';
const SHEET_NAME = '***シートの名前***';
/**
 * Toggl APIの設定
 */
//  TogglのAPIキーをスクリプトプロパティから取得する
const TOGGL_API_KEY = PropertiesService.getScriptProperties().getProperty('toggl_api_key');
// 抽出するプロジェクト名を指定する
const PROJECT_NAME = '***抽出するプロジェクト名***';
// Toggl APIのURL
const TOGGL_API_URL = 'https://api.track.toggl.com/api/v8/time_entries';
/**
 * Slackの設定
 */
// 稼働時間を報告するチャンネルのチャンネルID
const CHANNEL_ID_FOR_REPORT = '***稼働時間を報告するチャンネルのチャンネルID***';
// 稼働時間を報告するチャンネルのWebhook URL
const REPORT_CHANNEL_WEBHOOK_URL = '***Webhook URL***';
// 稼働時間を報告するスレッドの親投稿のURL
const THREAD_URL = '***スレッドのURL***';
// 稼働時間を報告するスレッドのタイムスタンプ
const THREAD_TS = getThreadTimestampFromSlackURL(THREAD_URL);
/**
 * 前日の日付を取得する
 */
const YESTERDAY_DATE = getYesterdayDate();
function getYesterdayDate() {
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  return yesterday.toISOString().split('T')[0];
}
/**
 * スレッドのタイムスタンプを取得する
 */
function getThreadTimestampFromSlackURL(url) {
  const match = url.match(/p(\d{10})(\d{6})$/);
  if (!match) {
    throw new Error('Invalid Slack URL format.');
  }
  console.log(`${match[1]}.${match[2]}`);
  return `${match[1]}.${match[2]}`;
}
01_main
/**
 * メイン
 */
function main() {
  // 指定したプロジェクトの記録をTogglから取得する
  const entries = getTogglEntriesForProject();
  // スプレッドシートに記録を入力する
  inputEntriesOnSpreadSheet(entries);
  // Slackに転送するメッセージを作る
  const formattedMessage = formatEntriesForSlack(entries);
  // Slackに投稿する
  postToSlack(formattedMessage);
}02_getTogglEntriesForProject
/**
 * タイムエントリーの中から、指定してプロジェクトのみを抽出する
 */
// 指定したプロジェクト名 かつ 前日のタイムエントリーを抽出する
function getTogglEntriesForProject() {
  // プロジェクトIDを取得する
  const projectId = getProjectIdForName();
  // 抽出条件文をつくる
  const startDate = `${YESTERDAY_DATE}T00:00:00Z`;
  const endDate = `${YESTERDAY_DATE}T23:59:59Z`;
  const filteredURL = `${TOGGL_API_URL}?start_date=${startDate}&end_date=${endDate}`;
  // 前日のタイムエントリーを取得する
  const entries = fetchFromTogglAPI(filteredURL);
  console.log(entries);
  Logger.log(entries);
  // 指定したプロジェクトIDのエントリーのみを抽出する
  return entries.filter(entry => entry.pid === projectId);
}
// プロジェクト名からプロジェクトIDを取得する
function getProjectIdForName() {
  const workspaceId = getUserWorkspaceId();
  const PROJECTS_API_URL = `https://api.track.toggl.com/api/v8/workspaces/${workspaceId}/projects`;
  const projects = fetchFromTogglAPI(PROJECTS_API_URL);
  const project = projects.find(p => p.name === PROJECT_NAME);
  if (project) {
    return project.id;
  } else {
    throw new Error(`Project with name ${PROJECT_NAME} not found.`);
  }
}
// Toggl APIからユーザーのワークスペースを取得する
function getUserWorkspaceId() {
  const WORKSPACES_API_URL = 'https://api.track.toggl.com/api/v8/workspaces';
  const workspaces = fetchFromTogglAPI(WORKSPACES_API_URL);
  if (workspaces && workspaces.length > 0) {
    // 最初のワークスペースのIDを返す
    return workspaces[0].id;
  } else {
    throw new Error('No workspaces found for the user.');
  }
}
// Toggl APIへのリクエストを行う共通関数
function fetchFromTogglAPI(url) {
  const headers = {
    'Authorization': 'Basic ' + Utilities.base64Encode(TOGGL_API_KEY + ':api_token'),
    'Content-Type': 'application/json'
  };
  const options = {
    'method': 'GET',
    'headers': headers
  };
  const response = UrlFetchApp.fetch(url, options);
  return JSON.parse(response.getContentText());
}03_inputEntriesOnSpreadSheet
/**
 * Togglのエントリーをスプレッドシートに出力する
 * 出力する項目
 * ・開始日:entry.start
 * ・開始時刻:entry.start
 * ・終了日:entry.stop
 * ・終了時刻:entry.stop
 * ・経過時間:entry.duration
 * ・内容:entry.description
 */
function inputEntriesOnSpreadSheet(entries) {
  // スプレッドシートを取得する
  let spreadSheet = SpreadsheetApp.openByUrl(SPRREADSHEET_URL);
  // シートを取得する
  let logSheet = spreadSheet.getSheetByName(SHEET_NAME);
  // データのある最終行の行番号を取得する
  let lastRow = getLastRow(logSheet);
  // Togglのエントリーズから、エントリーを取り出して、各項目に分けていく
  let data = entries.map(entry => {
    // UTC時間の開始日と開始時刻を日本時間になおす
    let [jpnStartDate, jpnStartTime] = formatDateTimeToJPN(entry.start);
    // UTC時間の終了日と終了時刻を日本時間になおす
    let [jpnStopDate, jpnStopTime] = formatDateTimeToJPN(entry.stop);
    // 経過時間(秒)を時分秒になおす
    const formattedDuration = formatDuration(entry.duration);
    // スプレッドシートの列の順番でdataに格納する
    // 開始日、開始時刻、終了日、終了時刻、内容、空欄(スプシ上の分類単語が入る)、経過時間
    return [jpnStartDate, jpnStartTime, jpnStopDate, jpnStopTime, entry.description, '', formattedDuration];
  });
  // シートにdataを出力する
  logSheet.getRange(lastRow + 1, 1, data.length, 7).setValues(data);
}
// UTC時間を日本時間になおす
function formatDateTimeToJPN(utcDateOfString) {
  // string形の日時を時間型になおす
  let utcDateOfTime = new Date(utcDateOfString);
  // UTC時間を日本時間になおす(9時間を足す)
  utcDateOfTime.setHours(utcDateOfTime.getHours() + 9);
  // 日付と時刻を配列で返す
  return [
    utcDateOfTime.toISOString().split('T')[0], utcDateOfTime.toISOString().slice(11, 19)
  ];
}
// データの入ってる最終行の行番号を取得する
function getLastRow(logSheet) {
  // シートの最終行を取得する
  let maxRow = logSheet.getMaxRows();
  // シートの最終行から、上向きにサーチして、データが入っている行を探す。
  let lastRow = logSheet.getRange(maxRow, 1).getNextDataCell(SpreadsheetApp.Direction.UP).getRowIndex();
  // その行番号を返す
  return lastRow;
}
// 秒数をhh:mm:ssに変換する
function formatDuration(durationInSeconds) {
  // 秒→時間
  const hours = Math.floor(durationInSeconds / 3600);
  // 秒→分
  const minutes = Math.floor((durationInSeconds % 3600) / 60);
  // 秒
  const seconds = durationInSeconds % 60;
  // hh:mm:ssの形式に変換する
  return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}04_formatEntriesForSlack
function formatEntriesForSlack(entries) {
  return entries.map(entry => {
    return `・ ${entry.description}:${secondsToHMS(entry.duration)}`;
  }).join('\n');
}
// 秒数を hh時間mm分ss秒 の形式に変換する関数
function secondsToHMS(seconds) {
  const hours = Math.floor(seconds / 3600);
  seconds %= 3600;
  const minutes = Math.floor(seconds / 60);
  seconds %= 60;
  return `${hours}時間${minutes}分${seconds}秒`;
}05_postToSlack
function postToSlack(message) {
  const payload = {
    'channel': CHANNEL_ID_FOR_REPORT,
    'username': 'Toggl_昨日の稼働報告',
    'icon_emoji' : ':alphabet-yellow-t:',
    'text': formatDateToMMDD(YESTERDAY_DATE) + '\n' + message
  };
  payload.thread_ts = THREAD_TS; // thread_tsをpayloadに追加
  const options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  };
  UrlFetchApp.fetch(REPORT_CHANNEL_WEBHOOK_URL, options);
}
// ISO形式の日付を MM/dd の形式に変換する関数
function formatDateToMMDD(isoDate) {
  const date = new Date(isoDate);
  const month = date.getMonth() + 1; // 月は0から始まるため、+1が必要
  const day = date.getDate();
  return `${month}/${day}`;
}さいごに
私がテレワーク中に、稼働報告をするために作りました。
もし、自分じゃ実装できないという場合は、以下のページからお気軽にご連絡ください。
あなたの状況に合わせて、GASを実装します。
\ 稟議にかける資料に使える /








