Skip to content

iCal フィード(カレンダー購読 URL)の設計 ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

プリザンターには、サイトのレコードを iCalendar(.ics)として配信する機能はありません。このページは、サイトごとに購読用 URL を発行し、カレンダーアプリから定期的に取り込めるようにする本体改修の設計メモです。

前提にした現行実装(1.5.8.1)は、フォーム機能の「推測できない URL」と、バックグラウンドサーバースクリプトの「実行ユーザーを指定して Context を作る」処理です。どちらもソースで確認した内容を下に書きます。

要件 ​

要件内容
サイト単位1 つのサイトに紐づいたフィードを配信する
推測できない URLフォーム機能と同じく 32 桁の 16 進数を URL に含める
実行ユーザーフィードを作るときに使うユーザーを設定ごとに指定する(権限とタイムゾーンに効く)
タイムゾーン実行ユーザーのタイムゾーンで日時を変換し、終日かどうかを判定する
ビュー保存済みビューを選び、そのフィルタでレコードを絞る
複数設定1 サイトに複数の設定(ビュー × ユーザーの組み合わせ)を持てる
件名・説明の書式標準(画面表示タイトル・内容)か、[列名] を埋め込むテンプレートかを選べる

前提にした現行実装 ​

フォーム機能の URL ​

フォーム機能を有効にしたサイトは、Sites テーブルの Form 列に 32 文字の値を持ちます(SiteModel.cs)。公開 URL は /forms/{guid}/new で、サイト設定画面では小文字にして表示します(SiteUtilities.cs)。

  • 値はブラウザ側で、フォームを有効にするチェックボックスを ON にした時点で作られます(sitesettings.js)。
  • $p.createGuid() は crypto.randomUUID() からハイフンを除いて大文字にした値を返し、使えない環境(HTTP 接続など)では Math.random() で作ったテンプレートにフォールバックします(util.js)。
  • リクエストを受けると、Context がルートの guid を大文字にして保持し(Context.cs)、Sites.Form = guid でサイトを引いて Id と IsForm を設定します(Context.cs、Context.cs)。
  • このとき Form.json の Enabled、または契約設定の拡張機能 Form のどちらかが有効でないとサイトは解決されません(Context.cs)。

図を読み込み中…

推測できない値はサーバー側で作る

フォームの GUID はブラウザで作られるため、crypto.randomUUID() が使えない HTTP 環境では Math.random() 由来の値になります。フィードの URL は認証の代わりになるので、新しく作る機能ではサーバー側で RandomNumberGenerator や Guid.NewGuid() から作る設計にします。

実行ユーザーを指定した Context ​

バックグラウンドサーバースクリプトは、テナント ID とユーザー ID から認証済みの Context を作ります(BackgroundServerScriptJob.cs)。

  1. SiteInfo.User でユーザーの所属部署を取る
  2. new Context(tenantId, userId, deptId, request: false, setAuthenticated: true) で作る
  3. SetTenantProperties(force: true) でテナントの設定を読み込む
  4. AbsoluteUri にパラメータ Service.AbsoluteUri を入れる
  5. TimeZoneInfo を UserModel から取り直して設定する

HTTP リクエストを伴わない Context では、URL 生成に使う AbsoluteUri とユーザーのタイムゾーンを明示的に入れている点が重要です。iCal ではリクエストはありますが、ログインしていない(Cookie の無い)カレンダーアプリからのアクセスなので、同じ手順で実行ユーザーの Context を作ります。

ビュー・カレンダー列・書式の既存部品 ​

部品現行実装使い道
ビュー一覧SiteSettings.Views(SiteSettings.cs)設定の ViewId でビューを選ぶ
開始・終了の列View.GetCalendarFromColumn / GetCalendarToColumn(View.cs)カレンダービューと同じ列を DTSTART / DTEND に使う
時刻の有無カレンダー表示は開始列の EditorFormat が Ymdhm のときだけ時刻を出す(HtmlCalendar.cs)Ymd を終日イベントにする
[列名] の抽出SiteSettings.IncludedColumns(SiteSettings.cs)テンプレートに含まれる列を取り出す
閲覧権限Permissions.CanRead(context, siteId)(Permissions.cs)実行ユーザーがサイトを読めるか確認する

URL ​

text
GET /ical/{guid}.ics
  • {guid} は 32 桁の 16 進数。認証の代わりになる秘密の値として扱う
  • 拡張子 .ics を付けるとカレンダーアプリが形式を判別しやすい
  • 応答は Content-Type: text/calendar; charset=utf-8

ルートは既存の Default({controller}/{action})より前に登録します。1.5.8.1 の Startup.cs では FormBinaries → Default → Others → Item → Binaries の順に登録されています(Startup.cs)。

csharp
endpoints.MapControllerRoute(
    name: "ICal",
    pattern: "ical/{guid}.ics",
    defaults: new { Controller = "ICal", Action = "Get" },
    constraints: new { Guid = "[A-Fa-f0-9]{32}" });

フォームは 1 サイト 1 つの値(Sites.Form)ですが、iCal は 1 サイトに複数の設定を持たせるので、GUID を 1 対多で持つ仕組みが必要です。

設定の保存先 ​

案 A: 専用テーブル(推奨) ​

図を読み込み中…

列説明
GuidURL に含める値。一意インデックスを付け、WHERE Guid = @guid で引く
TenantId実行ユーザーの Context を作るのに使う(SiteId から逆引きしてもよい)
UserId実行ユーザー
ViewId適用するビュー。0 か、ビューが削除されていれば既定のビュー(フィルタなし)
SummaryStyle / SummaryFormatStandard か Custom。Custom のときのテンプレート(例: [Title] ([ClassA]))
DescriptionStyle / DescriptionFormat同上(例: [Body] と担当者を並べる)
Disabled無効フラグ

テーブルは CodeDefiner の定義に追加します。

案 B: SiteSettings の JSON に入れる ​

SiteSettings に SettingList<ICalSetting> を追加する方法です。スキーマ変更は要りませんが、URL から設定を引くために全サイトの SiteSettings を読むか、DB ごとに書き方の違う JSON 検索が必要になります。

観点案 A(専用テーブル)案 B(SiteSettings)
GUID の検索インデックスで引ける全行の JSON を走査
スキーマ変更テーブル追加(CodeDefiner)なし
DB の方言影響なしJSON 検索は方言差あり
似た既存機能Binaries の Guid 検索リマインダーの設定

処理の流れ ​

図を読み込み中…

csharp
[AllowAnonymous]
public class ICalController : Controller
{
    [HttpGet]
    public IActionResult Get(string guid)
    {
        var setting = ICalSettingsRepository.GetByGuid(guid);
        if (setting == null || setting.Disabled) return NotFound();
        var context = CreateContext(tenantId: setting.TenantId, userId: setting.UserId);
        if (!Permissions.CanRead(context: context, siteId: setting.SiteId)) return NotFound();
        var ss = SiteSettingsUtilities.Get(context: context, siteId: setting.SiteId);
        var ics = ICalUtilities.Generate(context: context, ss: ss, setting: setting);
        return File(Encoding.UTF8.GetBytes(ics), "text/calendar; charset=utf-8", "calendar.ics");
    }
}

ICalSettingsRepository・ICalUtilities は新設するクラスの仮の名前です。

日時とタイムゾーン ​

開始列の EditorFormatiCal の型例
YmdDATE(終日)DTSTART;VALUE=DATE:20260301
Ymdhm / YmdhmsDATE-TIMEDTSTART;TZID=Asia/Tokyo:20260301T090000
  • DB の値は ToLocal(context) で実行ユーザーのタイムゾーンに変換してから出力する。TZID 付きのローカル時刻にするなら VTIMEZONE も出力する(自前で組み立てるか、Ical.Net などのライブラリを使う)
  • 終日イベントの DTEND は RFC 5545 では翌日を指定する
  • 実行ユーザーのタイムゾーン設定を変えると、同じ URL でも出力される時刻が変わる

SUMMARY と DESCRIPTION の書式 ​

スタイルSUMMARYDESCRIPTION
標準画面表示タイトル(Items.Title を結合した ItemTitle)内容(Body)
カスタム[列名] を埋め込んだテンプレート同左

カスタムでは ss.IncludedColumns(format) でテンプレート中の列を取り出し、SELECT に追加してまとめて取得してから [列名] を値で置き換えます。カレンダー表示用の既存の取得処理は Body を取らないので、標準の DESCRIPTION を出すときも Body を SELECT に足す必要があります。内容や説明項目には HTML・Markdown が入ることがあるので、出力前にタグを除去します。

text
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//Pleasanter iCal//JA
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:pleasanter-{SiteId}-{Id}@example.com
DTSTAMP:20260225T024100Z
DTSTART;VALUE=DATE:20260301
DTEND;VALUE=DATE:20260302
SUMMARY:タスク名
DESCRIPTION:内容
END:VEVENT
END:VCALENDAR

管理画面 ​

リマインダーと同じ「一覧に追加・編集・削除・並べ替え」の形(SettingList)で、サイトの管理画面にタブを追加します。

項目説明
タイトル管理用の名前
実行ユーザー権限とタイムゾーンに効く
ビュー適用するフィルタ
SUMMARY / DESCRIPTION のスタイルと書式標準かカスタムか、カスタムのテンプレート
無効フィードを止める
URL自動生成(読み取り専用・コピーボタン)。再発行ボタンも用意する

注意点 ​

項目内容
URL の秘密性URL が漏れると誰でも読める。再発行(GUID の作り直し)を用意する
実行ユーザーの無効化実行ユーザーが無効・削除されたら 404 か空のカレンダーを返す
ビューの削除既定のビュー(全件)に戻る。意図しない公開にならないよう、削除時は設定も無効にする案もある
ポーリング負荷カレンダーアプリは定期的に取りに来る。Cache-Control や ETag を返す
テナントの特定設定に TenantId を持たせるか、SiteId から逆引きする

関連ページ ​

変更履歴

第1版外部連携の改修・設計メモ(iCal・RSS/Atom・Webhook 送受信・iPaaS・短縮 URL・POP 受信・マスターデータ同期)を追加