何がしたいのか
→ 無駄に分類されているカレンダー群をまとめて一つのカレンダーとして表示したい
- 例:あるバイトのシフトが Google カレンダーで管理されている
- 店舗ごとのカレンダーしか存在せず、全店舗をまとめた全体のシフトカレンダーがない
- 各店舗のカレンダーを一つずつ管理するのではなく、それらに含まれる予定が全て含まれるカレンダーが欲しい
何をしたのか
- 登録しておいた複数のカレンダーフィード(iCalendar 形式)を購読し、それらを統合したカレンダーフィードを単一のエンドポイントから配信するサービスを作った
- カレンダーサービスに一つのカレンダーとして登録できる
- 固定の URL をソースとしてカレンダークライアントに登録しておくだけで、常に最新の予定が反映される
実際に作ったもの
特徴
- セルフホスト
- 自前のサーバーにコンテナを置いておくだけ!
- Rust 製
- (ビルドに時間かかるけど)爆速
実装
カレンダーの統合
とりあえず iCalendar 形式のファイルを読み込んで予定を集め、新しいカレンダーに詰め込んで返す仕組みを作ってしまえばいいです。
Rust には icalendar という iCalendar エンジンのクレートがあるので、これを使えば難なく作れます。
あとは、複数のカレンダーフィードをソースとして登録しておく+できあがったカレンダーフィードを配信用に置いておく必要があるので、なんらかの方法で DB と連携させます。今回は大した機能も容量もいらないので SQLite を使いました。カレンダーのデータはシリアライズして格納します。
カレンダーの同期
カレンダーを統合する仕組みを作っても、各カレンダーが更新されたら手動で統合処理を走らせる、というのでは不便です。
というわけで、一定時間ごとにカレンダーの更新をチェックして、統合カレンダーに反映させる仕組みも作ります。
マシン側で cron 等を使って定期実行させるのでも良いですが、今回はプログラムにコールバックとして先ほどの処理を登録しておき、それを一定時間ごとに発火させるようにしました。
Rust では tokio::time::interval と無限ループを組み合わせるとかすれば、非同期で定期実行させられます。
配信用 API
作ったカレンダーを Google カレンダー等のカレンダークライアントから見られるようにするには、iCalendar 形式のファイルに直接アクセスできる URL を登録する必要があります。
(なお、似たような機能として「インポート」がありますが、これはカレンダークライアント間の移行のためのものです。ツールで生成したカレンダーのファイルをインポートしても同期されることはないので、今回の用途には向いていません。)
配信用の URL を用意する一番簡単な方法は、サーバーにあるカレンダーファイルの場所をそのまま使うことです。ただ、なんかサービスの外部からファイルにアクセスするのは怖いですし、せっかくサービスとして常駐させているので、指定したカレンダーファイルを返す API を作っておきます。
統合カレンダーに名前をつけられるように実装しておけば、それをエンドポイントに渡すことで指定したカレンダーを取得できます。
API の実装自体は、DB に格納されたカレンダーのデータを取ってきて返す、というだけなので単純ですが、レスポンスの MIME タイプを text/calendar というあまり見たことのないものにする必要があります。
外部からのアクセス
ここまでできたら、あとは API のエンドポイントを外部に公開すれば、カレンダークライアントからアクセス可能になります。
リバースプロキシとか Cloudflare Tunnel とかいろいろと方法はあると思いますが、今回は面倒なので普通にサーバーのポートを開放しました(よくない)。
サーバーのパブリック IP から直接エンドポイントにアクセスする URL を Google カレンダーに登録したところ、ちゃんと統合カレンダーが表示されました。
(今回は Google カレンダーを利用しましたが、Apple カレンダー等でも同じように登録できます。)

