TickTickで完了したタスクをNotionに自動登録する

eyecatch-TickTickで完了したタスクをNotionに自動登録する

TickTickで完了したTaskを自動的にNotionのデータベースに登録する仕組みを構築します。

概要

Notionはノートの作成やカンバンによる管理、データベースの構築などオールインワンに情報を管理できます。データベースを使ったGUIでフローを構築したり、情報をストックできたりする点にメリットがある一方で、特定の目的に特化したツールを使うほうが時には高い利便性を得られることもあります。
今回はToDoの管理をTickTickを用いつつ、完了したTaskをNotionに蓄積させて集計で振り返りができるように機能をつくります。

やること

TickTickで完了したタスクをNotion上のデータベースに蓄積します。大まかな流れは以下の通りです。

  • TickTickでタスクの完了をする
  • IFTTTでタスクの完了を検知してGASへWebhook(POST)
  • GASでNotionの認証と値の変換をしてWebhook(POST)
  • Notionのデータベースに完了したタスクを挿入する

イベントの発火にはIFTTTのようなWebサービスをつなぐツールが便利なので、これを使います。IFTTTだけではWebhookのHeaderに認証情報を加えたり値を変換したりが難しいので、このギャップをGASで埋めます。

Notion APIを使う準備

Integrations>手順を参考に、Notion側でAPIを使えるように準備します。

データベースの構築

TickTickからIFTTTでWebhookにのせられる情報は上の通りです。すべてのデータを保持することに現状ではデメリットはないので、上の情報をストックできるデータベース(テーブル)をNotion上につくります。

日付系の列はDate型、TagはMultiSelect型、それ以外はString型に設定します。データベースの構築が完了したら、ページ右上のShareから作成したIntegrationsをInviteしてAPIを使えるようにしてください。

[FYI]https://www.notion.so/mikohei/TicktickToNotion-025088cb072748fbba1a1586346d108d#4377d6f1fc2b4efbad100d0b54c4a06f

GASの構築

HTTP-POSTのテスト

[FYI]IFTTTのWebhookをGASのPOSTで受け取るときのパラメータ覚書

上を参考にIFTTTからWebhookを受け取るコードをためしに書きます。Google Apps ScriptのEditorを開き、以下のコードを入力します。

function doPost(e) {
  var jsonString = e.postData.getDataAsString()
  const options = {name: "webhookTest"}
  GmailApp.sendEmail("yourAlias@gmail.com", "webhookTest", jsonString, options)
}

yourAlias@gmail.comyourAliasは使っているGoogleアカウントのIDなど、任意のアドレスに書き換えます。doPost関数でPOSTをためしに受け取るため、APIをデプロイします。

  • [デプロイ]をクリック
  • [新しいデプロイ]をクリック
  • [種類の選択]をクリック
  • [ウェブアプリ]をクリック
  • フォームに入力する
    • 説明文を書く
      • e.g. Receive a webhook from IFTTT
    • 次のユーザーとして実行で自分を選ぶ
    • アクセスできるユーザーを「全員」にする
  • [デプロイ]をクリック
  • ウェブアプリのURLをコピーしてメモ帳などにペースト

手順が完了したら、ARCやIFTTTなどを使い、コピーしたURL宛にHTTP-POSTを投げてみてください。POSTのRequest内容が上記で設定したメールアドレスに送信されます。うまくいかない場合には、デプロイの設定やコードの誤りなどご確認ください。

ライブラリの導入

Notion APIを操作するライブラリを導入します。ソースコードは以下の通りです。

Error処理などは省きましたが、notion-sdk-py公式ドキュメントを参考に書いたものです。

公式のライブラリをGAS用に作り直してくださっている方もいるかもなので、notion-sdk-gsを使う前に再利用できるリソースがないかご確認ください。また、APIのアップデートで機能が充実する可能性もあるので、公式ドキュメントもなるべくご覧ください。

以下、notion-sdk-gsの導入手順です。

  • GAS Editorの左側のサイドメニューから[ライブラリ]をクリック
  • スクリプトIDに以下を入力
    • 1C6kLuU1Ugclg-8C7hsbK221IgepJoGTrbh2VI4itdExvyFDCzc4adK8h
  • [検索]をクリックする
  • IDに「Notion」と入力する
  • [追加]をクリックする

追加が完了するとサイドメニューのライブラリに「Notion」が追加されます。「Notion」以外の表示がされているときは、それをクリックしてIDを「Notion」に修正してください。

NotionのTokenの登録

以下のコードを実行して、NotionのTokenをGASのプロジェクト上に登録します。{{yourToken}}を「Notion APIを使う準備」で取得したTokenに書き換えて、setNotionTokenを実行してください。実行後、readPropを実行して Tokenが表示されることを確認します。

function setNotionToken() {
  PropertiesService.getScriptProperties().setProperty("NOTION_TOKEN", "{{yourToken}}")
}
function readProp() {
  var props = PropertiesService.getScriptProperties()
  console.log(props.getProperty("NOTION_TOKEN"))
}

Tokenが登録され、正しく出力できたら上のコードは削除してOKです。

データベースを解析する

https://www.notion.so/1349f9e927674a03a87d772483dd5b1b?v=05282e02290749fd95decb0df0dc3f5c&p=de3635356a224c569f103a2038dc3521
                      |--------- DatabaseID ---------|                                      |--------- Page ID ------------|

データベースのレコード(ページ)を開くとPageIDを取得できます。PageIDを使ってデータベースの構造を取得します。

function dispoGetDbPgStructure(client, pgId){
  const ret = client.pages.retrieve(pgId)
  console.log(JSON.stringify(ret))
}
function testMain() {
  var props = PropertiesService.getScriptProperties()
  const notion = new Notion.client(props.getProperty("NOTION_TOKEN"))
  dispoGetDbPgStructure(notion, "8aa1faecaba847f488f277a4b711a42e")
}

正しくPageIDが設定されていると、JSONでデータベースの行の情報を得られます。Responseのpropertiesに各列名があります。列名の配下にはテキストの内容や色、文字の装飾などの情報があります。
単にデータの挿入をするだけであれば、テキストなど必要な情報だけ使うだけで更新できるので、JSONから必要な箇所のみを抜き出します。抜き出した情報を参考にして、TickTickのJSONをNotion用のフォーマットに変換する作業をします。

日付フォーマットの変換

TickTickでは日時情報はJuly 4 2021 at 01:13PMの形式で、日付情報はJuly 4 2021で表現しています。Notionではそれぞれ、2021-07-04T13:13:00.000+09:002021-07-04と表現されます。これにあわせてTickTick→Notionへ日付の書式を変換します。

function  _isDatetime(dtString) {
  return (dtString.indexOf("AM") >= 0 || dtString.indexOf("PM") >= 0)
}
function  _rmDtNoise(dtString) {
  var  rmAt = dtString.replace(" at ", " ")
  var  blAM = rmAt.replace("AM", " AM")
  var  blPM = blAM.replace("PM", " PM")
  return  blPM
}
function  _formatDate(dt, isDt, timezone="+09:00", sep="-") {
  var  year = dt.getFullYear()
  var  month = ("00" + (dt.getMonth()+1)).slice(-2)
  var  day = ("00" + dt.getDate()).slice(-2)
  var  date = `${year}${sep}${month}${sep}${day}`
  if (!isDt) {
    return  date;
  }
  var  hour = ("00" + (dt.getHours())).slice(-2)
  var  min = ("00" + (dt.getMinutes())).slice(-2)
  var  datetime = `${date}T${hour}:${min}:00.000${timezone}`
  return  datetime
}
function  dtFormatter(dtString) {
  var  fmtDtString = _rmDtNoise(dtString)
  var  isDT = _isDatetime(fmtDtString)
  var  dtParse = Date.parse(fmtDtString)
  var  dt = new  Date(dtParse)
  return _formatDate(dt, isDT) 
}

時間情報を含むか含まないかで処理を分岐させるため、_isDatetime関数で日時情報を含むか判定します。_rmDtNoise関数ではatなどを取り除きつつ、時間情報があればAM/PMの前に半角スペースをreplaceを使って挿入します。

Data.paraseで日付の数値情報を取得して、Dataオブジェクトに数値を渡してDate型のオブジェクトをつくります。_formatDate関数でDate型→String型に変換してNotion用のフォーマットにします。

上記の一連の処理をdtFormatter関数で実行します。以下はdtFormatterで日付の書式を変換するコードです。テスト用なので実行後に削除いただいて大丈夫です。

function dispoTest(){
  var ret1 = dtFormatter("July 4 2021 at 01:13PM")
  console.log(ret1) // 2021-07-04T13:13:00.000+09:00
  var ret2 = dtFormatter("July 4 2021")
  console.log(ret2) // 2021-07-04
}

Tag(MultiSelect)の変換

NotionのMultiSelect型の構造を単純に表現すると以下の通りになります。

{"Tag": {
  "multi_select": [
      {"name": "Private",},
      {"name": "Work", }
    ]
  }
}

TickTick側のTag情報は#{tagName}の書式です。複数のタグがある場合には半角スペース区切りでタグを複数表記する書き方となっています。この仕様のため、{tagName}#や半角スペースを含めることができない仕様です。この仕様を使ってmulti_selectの値をつくる関数を書きます。

function genMultiSel(multiSelStr) {
  var ret = []
  var selList = multiSelStr.split(" ")
  selList.forEach((tag) => {
    if (tag.length > 0) {
      var rmSharp = tag.replace("#", "")
      ret.push({name: rmSharp})
    }
  })
  return ret
}

以下はテスト用のコードです。

function test() {
  var ret = genMultiSel("#test1 #test2")
  console.log(ret)
  // [ { name: 'test1' }, { name: 'test2' } ]
}

Body(JSON)の生成

NotionにPOSTするためのJSONをつくります。

function _addDate(params, dateStrings, key, value) {
  if (dateStrings.length > 0) {
    params["properties"][key] = { "date": { "start": value } }
  }
  return params
}
function genParams(data, databaseId) {
  var params = {
    "parent": { "database_id": databaseId },
    "properties": {
        "TaskName": {
            "title": [{ "text": { "content": data.TaskName } }]
        },
        "TaskContent": {
            "rich_text": [{ "text": { "content": data.TaskContent } }]
        },
        "List": {
            "rich_text": [{ "text": { "content": data.List } }]
        },
        "Priority": {
            "rich_text": [{ "text": { "content": data.Priority } }]
        },
        "LinkToTask": {
            "rich_text": [{ "text": { "content": data.LinkToTask } }]
        },
        "CreatedAt": { "date": { "start": dtFormatter(data.CreatedAt) } }
    }
  }
  var tags = genMultiSel(data.Tag)
  if (tags.length > 0) {
    params["properties"]["Tag"] = {"multi_select": tags}
  }
  params = _addDate(params, data.StartDate.length, "StartDate", dtFormatter(data.StartDate))
  params = _addDate(params, data.EndDate.length, "EndDate", dtFormatter(data.EndDate))
  return params
}

DatabaseIDをつかってデータを流し込む先のデータベースを特定します。データベースの型にあわせてpropertiesを設定してあげます。MultiSelectは空文字のケースもあるので、タグの有無にあわせて後から値を追加する処理をしています。これでTickTick→Notionの変換処理は完了です。

doPostをデプロイする

上記の処理をdoPostに反映されます。

function doPost(e) {
  // Init
  var props = PropertiesService.getScriptProperties();
  const notion = new Notion.client(props.getProperty("NOTION_TOKEN"));
  var jsonString = e.postData.getDataAsString();
  var data = JSON.parse(jsonString);

  // Generate a Parameter
  const params = genParams(data, "cf0bc59e33734173838e0370c210fa3d")

  // Create a record
  var ret = notion.pages.create(params)
  if (ret["status"] >= 300 && ret["status"] < 200 ) {
    const options = {name: "IFTTT Webhooks Error"};
    GmailApp.sendEmail("yourAlias@gmail.com", "webhookTest", JSON.stringify(ret), options);
  }
}

変更を反映させるためもう一度デプロイする作業をしてください。作業後、新しくURLが発行されるため、そのURLを使ってNotionのデータベースにレコードが追加されるか確認します。エラーメールやエラーメッセージがある場合にはメッセージに従ってデバッグしてください。

IFTTTの設定

最後に、IFTTTからGASのAPIを叩くように設定します。TriggerにはTickTickのNew completed taskを選択します。New completed taskでは対象のListやTag、Priorityを選択できます。Notionのデータベースで集計やフィルターができるので、ここでは以下の通りに設定してすべてのタスクをストックする設定とします。

Item

Value

List

All Lists

Tag

Please Select(Default)

Priority

Please Select(Default)

設定した[Create trigger]をクリックします。Then ThatにはWebhookを選び以下の設定をします。

Item

Value

URL

デプロイしたGAS APIのURL

Method

POST

Content Type

application/json

Body

以下参照

[Body]

{
    "TaskName": "{{TaskName}}",
    "TaskContent": "{{TaskContent}}",
    "CompleteDate": "{{CompleteDate}}",
    "StartDate": "{{StartDate}}",
    "EndDate": "{{EndDate}}",
    "List": "{{List}}",
    "Priority": "{{Priority}}",
    "Tag": "{{Tag}}",
    "LinkToTask": "{{LinkToTask}}",
    "CreatedAt": "{{CreatedAt}"
}

設定が完了したら、[Create action]をクリックします。

動作確認

TickTickでタスクをつくり、タスクを完了の状態とします。タスクの完了にあわせてNotionに完了したタスクが登録されたら正常動作となります。なお、IFTTTでイベントが発火するまで数分のラグがあるため、IFTTTの「View activity」やエラーメール、Notionのデータベースなどで動作状況をご確認ください。

まとめ

GASやIFTTTからNotion APIが叩けるようになりました。これにより、タイムトリガーやイベントトリガーでNotionを使えるようになり、より柔軟にデータをストックできるようになります。タスクや習慣・健康管理・分析など幅広く活用できるので、ご自身の用途に合わせて自由に使っていくきっかけになりましたら幸いです。