こんにちは、サーバーサイドエンジニアのいいだです。
残念ながら夏になりました。夏になるとそうめんのことばかり考えていますが、今年はそうめんサラダが大流行しています。いかにも上品でヘルシーな食べ物という見た目をしておいてしっかり小麦の塊であり、さらにマヨの塊であるところなんかが気に入っています。ハムときゅうりをこれでもかと入れて、ちょっと食べにくいなぁと思いながら食べています。
今回は、そうめんサラダとは特に関係がないのですが、NotionのAPIを使ってタスクを定期的に作るということを最近やったので、これについて書きます。
NotionのRepeatを隙間産業したい
今や地球人類あまねくよろずのことに使いけりNotionですが、私は主に日常の家事タスク管理に使っています。毎週のゴミの日や掃除、洗濯の予定、細かいところではサプリを飲むことまでタスクになっています。書かないと忘れますし、書けば忘れてもよいからです。
このタスクを作るにあたって、これまではデータベースのテンプレートが持つ機能であるRepeatを使っていました。日次や週次、曜日指定もでき、隔週なんてのも簡単にできます。必要十分で便利な機能です。ありがたいですね。
しかし、その便利極まりないRepeatにも少なからず不満がありました。その主たるものとしては、タスクを作るタイミングと期限を分けられないこと、時間や期間をテンプレートに含められないこと。そして「月ごとの何番目の何曜日」という指定ができないことです。そう、燃えないゴミの日です。

幸い、人々が燃えないゴミの日を忘れないために、NotionはAPIを提供しています。これを活用して、どうにか燃えないゴミの日のタスクを定期的に作ろうというのが今回の目標になります。
rubyでNotion APIを使う
ユニファのサーバーサイドは主にrubyで書かれていますので、個人的な日曜開発でもrubyを使うことが多いです。ruby向けに作られたnotion apiのクライアントはいくつかありますが、今回は notion-ruby-client を使わせてもらいました。
環境は、この手のちょっとした用事のために転がしているなんでもサーバーがいますので、これを使います。バージョンは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-client の create_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を利用しています。