- 出張や旅行が多く、複数のタイムゾーンにまたがる人間関係の調整も重なったため、Big Tech への依存を減らし、予定データを自分で直接管理できる独自のカレンダーシステムが必要になった
- 新しい構成では Baïkal CalDAV サーバーを中心に据え、Airtrail、IMAPメール、語学学校の ICS を集約して CalDAV に投入し、その後共有用の
.ics ファイルとして再公開する
- YAML で予定を書き、CI/CD で ICS に変換していた方式はプロトタイプとしては十分だったが、手作業ミスと保守負担が増えたため、静的ファイル方式を置き換えることになった
- Python スクリプトは15分ごとにイベントを収集・分類し、家族向けと業務向けの ICS を別々に作成し、業務カレンダーでは一部イベントを PRIVATE として扱う
- Google Calendar と Google Script は業務カレンダー同期のためだけに使用し、Airtrail API 連携後はフライトを一度入力するだけで、15分以内に個人カレンダー、1時間以内に業務カレンダーへ反映される
カレンダー管理が複雑になった理由
- 移動が多いため、家族、友人、同僚が現在のタイムゾーンや搭乗中かどうかを把握しづらく、複数タイムゾーンの約束を合わせるには常に時差計算が必要だった
- フライト、列車、搭乗前のブロック時間、空港への移動時間を複数の場所に重複入力する必要があり、予定管理の負担が増大した
- 既存のカレンダーエコシステムの不便さは、標準、フロントエンドアプリ、ユーザー体験全般に及んでいた
- メールプロバイダーがフライト用のカレンダー項目を作成しても、乗り継ぎを落としたり、タイムゾーンを誤ったりする場合がある
- 生成された項目の organizer がユーザー本人ではないため、共有や編集が難しいことがある
- 必要条件は単なる予定保存にとどまらず、共有とコントロールまで含んでいた
- イベントが業務カレンダーで blocker として表示される必要がある
- 配偶者がカレンダーを購読できる必要がある
- イベント入力は最大でも1回にしたい
- 複数デバイスから修正できる必要がある
- データは自分で管理したい
- 業務カレンダーを配偶者と共有する方法は使えない
- 追加要件には、
.ics メール添付の取り込み、語学学校カレンダーの HTTP ICS 取り込み、セルフホスト型フライトトラッカー Airtrail 連携、色分け、業務カレンダー向けの非公開フラグ、高頻度リフレッシュ、任意のフロントエンド利用が含まれていた
YAML・ICS 方式が限界に達した点
- 既存の共有方式は、共通プラットフォームや同一アカウント環境がないと完全な機能を提供できないことが多かった
- Gmail または Outlook.com のエコシステム
- Exchange のような同一環境内でのアカウント共有
- 一般的な回避策は2つあった
- iCal データを HTTP で提供し、読み取り専用カレンダーとして公開する
- iCal の
.ics ファイルをメール送信する
- 初期バージョンでは、Webサイト上の公開されているが推測しにくい URL に
.ics ファイルを置き、用途別に複数の URL を作る方式を使っていた
- イベントデータは YAML で記述し、CI/CD パイプライン内の小さなスクリプトで ICS ファイルへ再シリアライズしていた
- プロトタイプとしては十分だったが、規模が大きくなると YAML の手書きでミスが増え、本来は低コストで済むべき作業が負担になった
CalDAV と Baïkal に切り替えた構成
- 新しい構成では静的ファイルを離れ、ホスティングされた CalDAV ベースのシステムへ移行した
- CalDAV は WebDAV 分散オーサリング仕様の拡張であり、カレンダーアプリケーションに必要な機能を提供する
- CalDAV サーバーを置けば、選んだフロントエンドアプリでノートPCやスマートフォンなど複数デバイスからイベントを閲覧・管理できる
- 認証なしで簡単に購読できる CalDAV サーバーは多くないため、サーバーを定期的にポーリングしてイベントを抽出し、Webサイト上の iCal ファイルとして公開するスクリプトが必要だった
- 全体の流れは次の通り
- データソースからイベントをポーリングする
- イベントをプログラムで CalDAV に公開する
- CalDAV から全イベントを取得して
.ics ファイルに書き出す
.ics ファイルを HTTP で提供する
Baïkal の導入とクライアント接続
- 選んだツールは Baïkal で、カレンダーと連絡先を管理する軽量なセルフホスト型 CalDAV/CardDAV サーバーである
- Docker Compose で
ckulka/baikal:0.9.5 イメージを実行し、ポートとローカルボリュームを指定して docker compose up -d で起動した
- Baïkal は MySQL も使えるが SQLite でも動作し、SQLite の方が管理は簡単である
- Web公開には nginx のリバースプロキシを使用した
proxy_pass でローカルの Baïkal ポートへ接続する
/.well-known/caldav を dav.php へ 301 リダイレクトする
- このリダイレクトは iPhone や Mac のカレンダーアプリに追加する際に必要である
- TLS は Let’s Encrypt と
certbot で構成し、certbot が nginx 設定ファイルを自動更新する
- ブラウザでドメインにアクセスして管理者アカウントを作成し、その後ユーザーとカレンダーを作成した
- macOS と iOS の標準カレンダーアプリに接続し、macOS/iOS の設定では automatic や advanced ではなく manual settings を使用した
イベント分類と業務カレンダーの公開範囲
- iCalendar 仕様には
EVENT コンポーネントの任意属性 CATEGORIES があるが、macOS Calendar、iOS Calendar、Google Calendar など使用したツールはいずれもこのフィールドを実装していない
- 自由テキスト分類では一貫性の維持が難しいため、Python の enum でイベント種別をモデル化した
- 例:
TerminType, CultureType, SocialType, AwayType, TransportType
MEETUP, CONFERENCE, CLASS, APPOINTMENT, MEETING, EXAM といった値を含む
- enum を複数グループに分けたのは、概念的にイベント種別を整理するためである
- この分類体系には、業務カレンダーでの公開範囲を決めるロジックも含まれている
- 2桁の enum 値は業務カレンダーでデフォルトで private として扱われる
- 診察のようなイベントは別の業務カレンダーファイルに非公開でシリアライズされる
- この分類は、イベントの検索性と視覚的な識別性を高め、色分け対応のフロントエンドで一目で把握しやすくするために使われる
Python 同期スクリプトと ICS 公開
- 自動化のために Python スクリプトを cron job に接続した
- スクリプトは複数の入力を集約して Baïkal に入れ、再び共有可能な ICS として出力する
- IMAP でメールからイベントを取り込む
- Airtrail API からフライトイベントを抽出する
- 語学学校がホストする ICS ファイルを取得する
- 収集したイベントを Baïkal に push する
- Baïkal から全イベントを取得し、1つ以上の共有可能な ICS ファイルとして再シリアライズする
- IMAP 連携は、メールで受け取ったカレンダー招待を自動で取り込んでカレンダーに追加するため、Google Calendar に近い機能を提供する
- cron のエントリーポイントスクリプトは家族用と業務用の
Calendar をそれぞれ作成し、Baïkal から取得したイベントを分岐処理する
- 家族用 ICS にはイベントを追加する
- 業務用メールアドレスがイベントのシリアライズ結果に含まれていない場合にのみ、業務用 ICS に追加する
is_work_public の結果に応じて classification を PUBLIC または PRIVATE に設定する
- Baïkal 連携スクリプトは
PROPFIND リクエストと HTTP Digest 認証を使ってリモートイベントを取得する
- イベント説明に
CATEGORIES: パターンがある場合は、説明フィールドからカテゴリを抽出して event.categories に入れ、説明からはその文字列を削除する
- 共有用 ICS ファイルは nginx 上で推測しにくい URL から提供され、URL はランダムかつ salt を加えたハッシュ文字列から生成される
- この cron job は 15分ごと に実行される
業務用 Google Calendar 同期
- 個人カレンダーでイベントを見るだけでなく、業務カレンダー上でも移動やフライト予定がブロック時間として見え、同僚が状況を把握できる必要があった
- 業務カレンダーへのコピーには Google Script Engine を使用した
- ベースとなるスクリプトは GAS-ICS-Sync の改変版である
- 改変版はカレンダーの
CATEGORIES 属性を読み取り、業務カレンダーに色分けを適用する
- Google Script は 30分間隔 で実行される
- Google Calendar の利用は Big Tech 依存を減らすという目標と矛盾するが、これは業務環境上の選択であり、個人用途での依存とは区別している
運用結果とコスト
- このシステムは約6か月にわたり、小さな修正や追加を加えながら運用されている
- 最新の変更は Airtrail API 統合である
- フライトを予約したら、フライトトラッカーにデータを入力する
- 15分以内に個人カレンダーへ追加される
- 1時間以内に業務カレンダーへ自動コピーされる
- 複雑な移動要件の中で予定管理にかかる時間が大きく減り、生活の質の向上につながった
- 全体のコストは最小限に抑えられ、家庭用 NAS でも容易に運用できる
- 現在は Webサイトと統合を動かすためにスイスにデータを置き、月約 100ドル のサーバー時間を契約している
- このコストはやや過剰な構成によるもので、今後時間をかけて最適化する予定である
- VM インスタンスの Docker ホスト上で素早く構成できた点は追加コストを払う価値があり、予定管理が容易になったことで月100ドル分に相当する時間を節約できている
1件のコメント
Hacker News のコメント
反発はあると思うが、CalDAV はいまひとつだと思う
本当に簡単で優れたものなら、セルフホストの CalDAV ソリューションはもっと多く、使い勝手ももっと良かったはずだが、Radicale も印象的ではなかった
実質的に自分に提供したいカレンダーファイルが 1 つだけだったので、自分で CalDAV サーバーを実装してみようとしたが、単一ファイルを公開するだけでも直感的でなく複雑すぎて諦めた
他人と CalDAV 対応アプリでカレンダーを共有する必要があるなら必要性は理解できるが、1人で使うカレンダーをホストする用途としては、時間の無駄に近いと感じる
代わりに最初は、動的に更新される iCal ファイルを S3 バケットに置き、Android の ICSx5 で同期していた。CalDAV なしで HTTPS だけで十分だった
ただし Android 向けの自由なオープンソースのカレンダーアプリは依然として不足しており、GrapheneOS では ICSx5 がうまく動かないため、今は iCal ファイルの直接リンクをサポートする Proton Calendar をほぼ同じ方式で使っている
iCalendar 形式を使うアプローチは良いが、カレンダーソフトごとに iCalendar 機能の対応範囲が大きく異なるため、結局ほとんどのメタデータを専用プロパティではなく説明欄に押し込むことになる
自分の用途は、Eventbrite、Meetup、パブクイズ業者、公開イベントカレンダーなど複数のサイトからデータを取り込み、興味のある交流イベントだけを絞り込んで、普段使っているカレンダーアプリで見られる単一のカレンダーにまとめるソフトウェアだ
そのうえ、Exchange と Google の仕様非準拠な CalDAV サーバーへの対応でひどく苦労し、ユーザーはその問題をすべてこちらのせいにした
今 Google で働いてみると、gCal アプリ群はまったく別のプロトコルを使っている
軽量な形式ではなく、一度にあまりにも多くのことをしようとして過剰設計になり、その結果、異なるアイデアが絡み合った毛玉のようなプロトコルになった
そのため、多くのサービスにとって実際に望む範囲を超えてしまい、仕様全体に完全準拠しようとしない状況が生まれている
W3C のデータ形式仕様はいつもこういう結末になるように思えるし、低レベルのプロトコル仕様では相対的にそこまでではないように見える
読んだのは 15 年前くらいだが、こうしたプロトコルが設計過程で委員会の中でどう迷子になるのかについても一部扱っていたと記憶している
このプロジェクトが 2001 年ごろに始まったという事実だけを見ても、カレンダー問題がどれほど長年の厄介ごとかが分かる
なぜいまひとつなのかを知りたいならおすすめだし、スタートアップとオープンソースの衝突、プロダクト作り、ソフトウェア開発全般についての話としても良い
http://www.dreamingincode.com/ ; https://en.m.wikipedia.org/wiki/Dreaming_in_Code
Android の同期には、バックグラウンド同期サービスを動かすオープンソースの非 root アプリ DAVX5 を使っているが、設定に問題が多く、CalDAV 実装の差が原因かもしれない。DAVX5 から ZOHO カレンダーへ同期するのにも時間がかかる
Ubuntu の Evolution は CalDAV 対応がかなり良いが、メール表示とカレンダー表示を切り替えるたびにカレンダー全体を再読み込みする。ZOHO カレンダーの再読み込みには時間がかかり、ZOHO のレート制限も厳しいため、実質的にソフトブロックされることになる
最新の Thunderbird には、別の HTTP 成功コードを期待する CalDAV 非互換がある。ZOHO は 200 を返すが、Thunderbird は別のコードを期待している。ちなみに Thunderbird の CalDAV コードは JS で書かれている
この問題は 2 つ目のカレンダーでだけ発生し、2 つ目を追加した瞬間に失敗し始める。少なくとも ZOHO ではかなりつらい
おすすめできる別の CalDAV サーバーとして Radicale がある
Python で書かれており、かなりモジュール式に設計されていて、認証、認可などのプラグインを付けられる
データは通常のテキストファイルとして保存され、git で追跡してバックアップしている
時間がたつにつれ、いくつか興味深い修正を積み重ねてきた。認証はホストシステムの pam で処理し、カレンダー共有のために、許可されたすべてのユーザーへカレンダーをシンボリックリンクする定期スクリプトを回している。ただし組み合わせ爆発の問題がある
カスタム CalDAV プロパティを使う認可プラグインと、改造した Web プラグインでアクセス制御リストも追加した
公開カレンダーは ACL で
publicユーザーに読み取り権限を与え、nginx のハックで読み取り操作に包括的なアクセスを許可しているこの公開カレンダーは Web カレンダーで見られる: https://gitlab.nomagic.uk/popi/js_calendar_from_ics
この設定についてブログ記事を書くべき気がする
まさに必要としていた記事だ
個人サーバーでは Mailcow の SoGO カレンダーを使い、仕事用カレンダーは Fastmail を使っているが、互いにうまく合わない
Mailcow カレンダーを他のツールときちんと連携させる方法が見つからず、半手動で同期するのに時間を使っている
しばらくカレンダーを分離したいと思っていたが、知っていた代替案は Nextcloud だけで、これは少し重いと思っている
Baïkal は良さそうで、早く試してみたい。これなら Mailcow を別のセルフホスト型メールサービスに置き換えることも検討できる
最近、かなり積極的にクラウド離れを進めている。数百GBのデータがあったが、移行は断続的にやっても2週間もかからず、期待よりずっとよく動いている
外部に任せているサービスはメールだけで、MXレコードやIMAP/SMTPを扱うのが嫌だから
それ以外では、デュアルコアCeleron CPUを搭載したAsus PN40が大いに役立っている。アクセスしやすく、M.2 NVMEとSATAドライブのスロットがあり、1か所で安全なバックアップ構成を作れるのが利点
CalDAVは少し怪しいという点には同意するが、Nextcloudが必要の大半を満たしてくれた。Redisを付けても一部の処理は遅いことがあるが、カレンダー、Dropbox/Google Driveの代替、ドキュメント、写真、バックアップ、自動同期までうまくこなしている
そこに各種オープンソースソリューションを足せば、音楽・動画ストリーミング、監視、通知、VPN、リーダー、管理ツールや各種ユーティリティまで全部まかなえる
このミニPCは性能が低めなのに、ISPが優秀なおかげで余裕をかなり残して全部さばけていて、自分のような人をさらに5〜6人受け入れても無理のないレベル
古いものがまた新しくなっている感じ
2000年代初めには、Mozilla Sunbirdで個人のWin2kサーバー上のIIS WebDAVモジュール経由でカレンダーをホストしていた
CardDAVはまだなかった気がするが、間違っているかもしれない
結局そのデータは全部Google Calendarに取り込み、かなり昔までさかのぼると大学の課題のようなものまで残っていて、なかなか面白い
いくつかの予定がサーバーにアップされず失ったことがあり、DAViCalサーバーと一緒にCalDAVで使っていた
Sunbirdは終了しており、Windowsで動く適切な代替があるかどうかは最近探していない
Baikalは面白そう
以前、数台のApple iOSデバイス間でカレンダーを共有するためにDAViCalを使っていた。動いているときはうまく動いたが、iOSのバージョンが変わるにつれて不安定になるようで、結局あきらめた
いつかまた見直すことがあれば、DAViCalとRadicale、そして今ならBaikalも一緒に見るつもり
https://www.davical.org/
https://radicale.org/v3.html
最近はNextcloudに移った。共有ホスティング側でデプロイを引き受けてくれ、統合されたWeb UIがあって設定の負担がずっと少なかったため
Baikalを何年も使っているが、かなり堅牢。ただし一人で使っていて、元記事のすべての要件があるわけではない
デスクトップではThunderbird内蔵カレンダーで接続し、携帯電話ではF-DroidのDAVx5とFossify Calendarを使っている
Fossify CalendarはSimple Mobile ToolsのCalendarアプリからフォークしたものだが、名前に反して、これまで見たカレンダーアプリの中で最も機能が豊富
いくつものメニューやダイアログを掘らせるのではなく、すべてのオプションをすぐ見せるシンプルさが気に入っている
職場ではOutlook、自宅ではGoogle Calendarを使っているが、HTTPで同期すると完全に不安定
タイムゾーンを常に壊しているようで、UTCで発行することもあればそうでないこともあり、会議の時刻がいつもずれる
この2つの大手プロバイダーが、なぜこんな基本機能を直せないのか理解できない
自分のシステムを使えば解決するならすぐやるが、Outlookで作った会議が2時間ずれて取り込まれる同じ事態が起きそうで心配
また多くの会社は、データ流出の問題から個人カレンダーと業務カレンダー間の同期を実際に禁止している
こういうことを写真にもやりたい。DropboxとGoogleからしばらく離れたいと思っていて、優先事項は2つ
1つ目は、自分の写真から思い出機能のようなものを作り、携帯電話に送りたい。記念日、似た活動、
自分とX(Xは配偶者、家族、友人など)、時間に伴う変化といった基準でまとめる方式2つ目は、デバイスで撮った写真を自分のサーバーに保存できること
異なる場所に2TBの容量を持つTrueNASマシンを3台用意してあり、この機能をゆっくり作っていきたい
https://www.synology.com/en-global/dsm/feature/photos
https://www.synology.com/en-global/dsm/feature/moments
別の方法として、サーバーなしでDecSyncを使う道もある
ThunderbirdやEvolutionがAndroidアプリと同期し、カレンダー、連絡先、タスク、RSSまで他のピアに複製される。F-Droidからも入手できる
https://github.com/39aldo39/DecSync