ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

NotionのRepeatをもうちょっと便利にしたいなぁ

 こんにちは、サーバーサイドエンジニアのいいだです。

 残念ながら夏になりました。夏になるとそうめんのことばかり考えていますが、今年はそうめんサラダが大流行しています。いかにも上品でヘルシーな食べ物という見た目をしておいてしっかり小麦の塊であり、さらにマヨの塊であるところなんかが気に入っています。ハムときゅうりをこれでもかと入れて、ちょっと食べにくいなぁと思いながら食べています。

 今回は、そうめんサラダとは特に関係がないのですが、NotionのAPIを使ってタスクを定期的に作るということを最近やったので、これについて書きます。

NotionのRepeatを隙間産業したい

 今や地球人類あまねくよろずのことに使いけりNotionですが、私は主に日常の家事タスク管理に使っています。毎週のゴミの日や掃除、洗濯の予定、細かいところではサプリを飲むことまでタスクになっています。書かないと忘れますし、書けば忘れてもよいからです。

 このタスクを作るにあたって、これまではデータベースのテンプレートが持つ機能であるRepeatを使っていました。日次や週次、曜日指定もでき、隔週なんてのも簡単にできます。必要十分で便利な機能です。ありがたいですね。

 しかし、その便利極まりないRepeatにも少なからず不満がありました。その主たるものとしては、タスクを作るタイミングと期限を分けられないこと、時間や期間をテンプレートに含められないこと。そして「月ごとの何番目の何曜日」という指定ができないことです。そう、燃えないゴミの日です。

作った日しか選べないため、ある日突然今日がゴミの日であることを知らされます

 幸い、人々が燃えないゴミの日を忘れないために、NotionはAPIを提供しています。これを活用して、どうにか燃えないゴミの日のタスクを定期的に作ろうというのが今回の目標になります。

rubyでNotion APIを使う

 ユニファのサーバーサイドは主にrubyで書かれていますので、個人的な日曜開発でもrubyを使うことが多いです。ruby向けに作られたnotion apiのクライアントはいくつかありますが、今回は notion-ruby-client を使わせてもらいました。

rubygems.org

 環境は、この手のちょっとした用事のために転がしているなんでもサーバーがいますので、これを使います。バージョンはruby 3.2.1、rails 7.1.3です。まずはREADME.mdを見ながらbundleします。

gem 'notion-ruby-client'

 その後、 config/initializers あたりでよしなに設定しておきます。

Notion.configure do |config|
  config.token = Settings.notion.secret
end

 誤ってtokenをインターネットに公開すると地元のゴミの日などの機密情報が誰でも閲覧可能になってしまいますので、一応環境変数などにして若干厳重にいい感じにしておきます。もちろんもっと厳重にしたい人はもっと厳重にしてもいいと思います。

notion:
  secret: <%= ENV['NOTION_SECRET'] %>

 あとは、Notionを覗きたい時に Notion::Client.new すればよいだけです。

def client
  @client ||= Notion::Client.new
end

条件に合致したページを取得する

 今回、タスクを作るにあたっては「Notion側でテンプレートとなるタスクを用意し、その中身をサーバー側にとっておいて新規作成のときに使う」という形を取るので、まずは対象のデータベースに対して「Doneプロパティのチェックがついていない」かつ「タイトルが###から始まる」ページを取得するというクエリを投げます。

tasks = []
filter = {
  and: [
    {
      property: :Done,
      checkbox: {
        equals: false,
      },
    },
    {
      property: :Name,
      title: {
        starts_with: template_prefix,
      },
    },
  ],
}
client.database_query(database_id: @database_id, filter:, sleep_interval: 0.5,
                      page_size: 100) do |page|
  tasks.concat(page.results)
end

 sleep_interval を指定しているのはNotionのAPIが連打を嫌がるためで、これを指定するとページングされている場合にclient側でよしなにsleepを入れてくれるため大変安心です。是非とも指定しましょう。

 その後、レスポンスから我が家で使いやすいよう必要なプロパティを取り出して終わりです。レスポンスの構造は notion-ruby-client公式のAPIリファレンス通りに作ってくれていますので、それを見るのがわかりやすいです。

  tasks.map do |task|
    ::Notion::Task.from_api_response(task)
  end
def from_api_response(task)
  properties = task.properties
  self.new(
    name: self.extract_name(properties.Name),
    tags: self.extract_tags(properties.Tags),
    icon: self.extract_icon(task.icon),
    parent: self.extract_parent(task.parent),
    memo: self.extract_memo(properties.Memo),
    url: self.extract_url(properties.URL),
    deadline_time_start: self.extract_date_start(properties.Deadline),
    deadline_time_end: self.extract_date_end(properties.Deadline)
  )
end

def extract_name(name)
  self.remove_prefix(name.title[0].text.content)
end

def extract_memo(memo)
  memo.rich_text[0] ? memo.rich_text[0].plain_text : nil
end

def extract_tags(tags)
  tags.multi_select.map(&:name).join(",")
end

def extract_icon(icon)
  icon ? icon.external.url : nil
end

def extract_parent(parent)
  parent.database_id
end

def extract_url(url)
  url.url
end

画面も適当に作って選べるようにしておきます

好きなときにNotionにページを作る

 Notionに作るページの中身が用意できたら、次はページを作ります。これも簡単で、 notion-ruby-clientcreate_page に適切な値を指定するだけです。

client.create_page(
  properties: form.properties,
  icon: form.icon_object,
  parent: { database_id: form.parent_id }
)

 properties の中身にはNotion APIがレスポンスで返してくるのをだいたい同じ形で突っ込んでやります。

def properties
  {
    Name: {
      title: [
        {
          text: {
            content: @name,
          },
        },
      ],
    },
    Tags: {
      multi_select: @tags.split(",").map { |tag| { name: tag } },
    },
    Memo: {
      rich_text: [
        {
          type: "text",
          text: {
            content: @memo,
          },
        },
      ],
    },
    URL: {
      url: @url,
    },
    Deadline: {
      date: deadline_object,
    },
  }
end

今回わざわざテンプレートを取る形にしたのは主にページのアイコンを維持したかったためですが、これももらってきたURLを返すだけでよく、わかりやすいです。

def icon_object
  { type: "external", external: { url: @icon } } if @icon
end

 日付のプロパティは start に日付を指定するか日時を指定するか、さらに end も指定するかで仕上がりが変わってくるので少々面倒です。ここでは別途設定画面を作って保存しておいたタスクの開始時間と終了時間をくっつけてiso8601形式にしています。終日のタスクにしたい場合、誤ってiso8601形式にすると0時にそのタスクをやる羽目になってつらいのでそれは避けるようにします。

def deadline_object
  deadline_time_start = if time_start
                          (date_with_time(deadline, time_start) - 9.hours).iso8601
                        elsif deadline
                          deadline.strftime("%Y-%m-%d")
                        else
                          nil
                        end
  deadline_time_end = if time_end
                        (date_with_time(deadline, time_end) - 9.hours).iso8601
                      else
                        nil
                      end
  {
    start: deadline_time_start,
    end: deadline_time_end,
  }
end

手作り感あふれる設定ページも用意しましょう

 rubyから任意のページをNotionに作ることができるようになれば、あとは適当に定期実行する仕組みを用意してやるだけで繰り返しタスク作成機能ができますね。これで燃えないゴミの日を忘れることはもうないでしょう。

 ユニファではNotionをよろずのことに使いけりエンジニアを募集しています。なお、業務ではJiraを利用しています。

unifa-e.com