macOS のサービスを用いた Notion タスク登録 : Notion 解説(40)

(1/9 追記) シングルクオートが入っている時にエラーになってしまっていたので、記事を修正しました。また、作成済みのバイナリを Github で配布しています。こちらもバグ修正しています。

github.com

はじめに

これまで Notion のタスク登録に、自作の Alfred workflow を使っていました。 ポモドーロの設定やタスクの終了は比較的楽なのですが、タスクの登録は文字列の入力やコピーがあり、少し手間がかかりました。 このため、ついタスク登録をサボってしまうことが何度か発生し、その度に締切などに追われたことがありました。 そこで、タスク登録に関してはさらに簡単な仕組みが必要だと感じました。

ほとんどの時間で macOS を使っているので、せっかくならサービスを作ってしまうといいのではと思いつきました。 名前は NotionQuickAction とします。 作業手順は以下のようになります。

  1. アプリケーションは問わず、タスクにしたい文字列を選択します。
  2. NotionQuickAction サービスに割り振ったショートカットキーを押します。
  3. ダイアログが出るので、日付・開始時間・終了時間を設定します。
    1. 全てを設定しなかった場合には、日付なしになります。
    2. 開始時間、終了時間を省略した場合には終日タスクになります。
    3. 終了時間を省略した場合には、開始時間から1時間のタスクになります。
  4. Notion にタスクが作成され、自動的に Notion アプリまたはブラウザで開きます。
  5. 日付ありのタスクであり、かつカレンダーへの登録機能をオンにした場合には、カレンダーにも登録されます。
  6. カレンダーの詳細には Notion のタスク ID が登録されています。Google カレンダーとの同期のために使うためです。

動作している様子の動画をここに示します。

作成方法

macOS のサービスを作るために、Automator を用います。

f:id:hkob:20220108142820p:plain
Automator

クイックアクションを選択します。

f:id:hkob:20220108143029p:plain
QuickAction

Automator 画面は以下のようになります。右上の項目は以下のように設定しました。

  • ワークフローが受け取る現在の項目: テキスト
  • 検索対象: 全てのアプリケーション
  • イメージ: 共有 (特になんでもよい)
  • カラー: ブラック (特になんでもよい)

アクションは以下の二つを連結しています。

  1. Javascript を実行
  2. Web ページを表示

f:id:hkob:20220108143544p:plain
Automator 編集画面

あとは JavaScript を実行の部分にスクリプトを以下のように記述しました。ユーザごとに異なる部分は上に定数として設定しています。この部分を自分のデータに書き換えてください。

// ########## 個人設定用定数 ##########
// Notion API Token を設定 (secret で始まる文字列)
const NOTION_API_TOKEN = "secret_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
// データベース ID を設定 (32桁16進数)
const DATABASE_ID = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
// タスクデータベースのタイトルのプロパティ名
const TASK_NAME = "タスク名"
// タスクデータベースの日付のプロパティ名
const TASK_DATE = "日付"
// タイムゾーン (see https://www.iana.org/time-zones)
const TIME_ZONE = "Asia/Tokyo"
// Notion タスクだけでなくカレンダーにも登録したい場合には true
const ADD_CALENDAR = true
// Notion のカレンダー名を設定
const NOTION_CALENDAR = "Notion"
// Notion で開く場合には true、ブラウザで開く時には false
const OPEN_BY_APP = true

function dialogText(app, input) {
  const message = "Input date and time:  (format) [[YYYY/]MM/DD [HH:MM [HH:MM]]]\n" +
    "Example: (no input) -> unsettled\n" +
    "\t 4/25 -> 25th, April\n" +
    "\t 4/25 10:00 -> 25th, April 10:00\n" +
    "\t 4/25 9:00 10:00 -> 25th, April 9:00 - 10:00\n" +
    "for Message : \n" + input
  return app.displayDialog(message, {
    defaultAnswer: "",
    withIcon: "note",
    buttons: ["Cancel", "Continue"],
    defaultButton: "Continue"
  })
}

function parseDateTime(dateTimeStr) {
  const app = Application.currentApplication()
  app.includeStandardAdditions = true

  var endTimeStr = null
  var startTimeStr = null

  // query を分割
  let words = dateTimeStr.split(" ")
  // 最後のパラメータが時間かどうか
  var timeStr = words.slice(-1)[0]
  if (timeStr && timeStr.match("[0-9]+:[0-9]+")) {
    endTimeStr = words.pop()
  }
  // 時間が設定されていたら開始時間もあるかどうか調べる。なければそれが開始時間
  if (endTimeStr) {
    timeStr = words.slice(-1)[0]
    if (timeStr && timeStr.match("[0-9]+:[0-9]+")) {
      startTimeStr = words.pop()
    } else {
      startTimeStr = endTimeStr
      endTimeStr = null
    }
  }
  var dateStr = words.slice(-1)[0]
  if (dateStr) {
    if (dateStr.match("^[0-9]+/[0-9]+$")) {
      dateStr = new Date().getFullYear() + "/" + words.pop()
    } else if (dateStr.match("^[0-9]+/[0-9]+/[0-9]+$")) {
      dateStr = words.pop()
    } else {
      dateStr = null
    }
  }
  if (dateStr == null && startTimeStr != null) {
    const timeNow = new Date()
    dateStr = timeNow.getFullYear() + "/" + (timeNow.getMonth() + 1) + "/" + timeNow.getDate()
  }

  if (startTimeStr) {
    // 開始時間が設定されていたら時間を含めた日付を設定
    const startTime = new Date(dateStr + " " + startTimeStr)
    var endTime
    if (endTimeStr) {
      // 終了時間が設定されていたら時間を含めた日付を設定
      endTime = new Date(dateStr + " " + endTimeStr)
    } else {
      // 終了時間が設定されていなかったら開始時間の1時間後を設定
      endTime = new Date(dateStr + " " + startTimeStr)
      endTime.setHours(endTime.getHours() + 1)
    }
    return {startTime: startTime, endTime: endTime}
  } else if (dateStr) {
    return {date: new Date(dateStr) }
  } else {
    return {noDate: true}
  }
}

// Notion に payload を send する
function sendNotion(app, url, payload, method) {
  const header = " --header 'Authorization: Bearer '" + NOTION_API_TOKEN + "'' "  +
    "--header 'Content-Type: application/json' " +
    "--header 'Notion-Version: 2021-08-16' " +
    "--data '"
  const script = "curl -X " + method + " " + url + header + JSON.stringify(payload).replaceAll("'", '\'"\'"\'')  + "'"
  return JSON.parse(app.doShellScript(script))
}

function createPage(app, payload) {
  return sendNotion(app, "https://api.notion.com/v1/pages", payload, "POST")
}

function dateTime2str(dt) {
  return dt.toLocaleTimeString([], {year: "numeric", month: "2-digit",  day: "2-digit", hour: "2-digit", minute: "2-digit"}).replaceAll("/", "-")
}

function date2str(dt) {
  return dt.toLocaleDateString([], {year: "numeric", month: "2-digit",  day: "2-digit"}).replaceAll("/", "-")
}

// カレンダーイベントから日付の hash  を作成 (削除時にも呼ばれるので、イベントに日付が設定されていなければ null を返却)
function notionDateHash(hash) {
  if (hash.startTime) {
    return {
      start: dateTime2str(hash["startTime"]),
      end: dateTime2str(hash["endTime"]),
      time_zone: TIME_ZONE
    }
  } else if (hash["date"]) {
    return {
      start: date2str(hash["date"])
    }
  } else {
    return {
      start: "",
      end: ""
    }
  }
}

// title と date_hash から create payload を作成
function createPayload(title, date_hash) {
  return {
    parent: {
      database_id: DATABASE_ID,
    },
    properties: {
      [TASK_NAME]: {
        title: [
          {
            text: {
              content: title
            }
          }
        ]
      },
      [TASK_DATE]: {
        type: "date",
        date: date_hash
      },
    }
  }
}

ObjC.import("stdlib");

function createCalendar(task_id, title, dateTimeHash) {
  let Calendar = Application("Calendar")

  let notionCal = Calendar.calendars[NOTION_CALENDAR]
  var event = { summary: title }
  // Notion のタスク ID をカレンダーの description に入れます。
  // Google カレンダーの変更で Notion タスクを連携するためです。
  // 私の場合には、description の最終行に「id:」で始まる行があるかどうかで判断しています。
  // automate.io などで タスクID だけを入れている場合にはここを修正してください。
  if (task_id != "") {
    event.description = "id:" + task_id
    // automate.io などで task_id だけを入れている場合にはこちら
    // event["description"] = task_id
  }
  if (dateTimeHash.startTime) {
    // 開始時間が設定されていたら時間を含めた日付を設定
    event.startDate = dateTimeHash.startTime
    event.endDate = dateTimeHash.endTime
    let newEvent = Calendar.Event(event)
    notionCal.events.push(newEvent)
  } else {
    event.startDate = dateTimeHash.date
    event.endDate = dateTimeHash.date
    event.alldayEvent = true
    let newEvent = Calendar.Event(event)
    notionCal.events.push(newEvent)
  }
}

function run(input, parameters) {
  const app = Application.currentApplication()
  app.includeStandardAdditions = true

  if (input.length > 0) {
    const response = dialogText(app, input)
    if (response.buttonReturned == "Continue") {
      const parsedTime = parseDateTime(response.textReturned)
      // データベースにタスクを作成
      const payload = createPayload(String(input), notionDateHash(parsedTime))
      const taskCreateResult = createPage(app, payload)
      // 日付が設定されていた時にはカレンダーも登録
      const task_id = taskCreateResult.id
      if (ADD_CALENDAR && !parsedTime.noDate) {
        createCalendar(task_id, input, parsedTime)
      }
      const tmp_url = taskCreateResult.url
      var url
      if (OPEN_BY_APP == true) {
        url = tmp_url.replace("https", "notion")
        let notion = Application("Notion")
        notion.activate()
      } else {
        url = tmp_url
      }
    }
    return url
  } else {
    return ""
  }
}

記述が終わったら保存します。私は NotionQuickAction としています。保存すると、自分のライブラリフォルダの下の Services に保存されるはずです。

ショートカットの設定

設定された NotionQuickAction は環境設定のキーボード - ショートカットに表示されます。チェックマークをつけると有効になります。またここで、ショートカットを適当な空いているものに設定します。ここでは「option - command - N」に設定してみました。これは自分の使いやすいものに設定してもらっていいです。

f:id:hkob:20220108145423p:plain
環境設定 (キーボード - ショートカット)

おわりに

Alfred と違って、macOS 使っている人なら誰でも使えるので是非試してみてください。


hkob.notion.site