通知のカスタムフォーマットとリマインダーの内部動作
「テーブルの管理 → 通知」の本文(カスタムフォーマット)は、1 行ずつ、項目の書式を表す JSON として読まれ、読めなかった行だけが {Url} などの置換を受けてそのまま出力されます。この仕組みを知らないと、「{Url} が展開されない」「[Title] が置き換わらない」といった現象の理由が分かりません。 このページでは、通知の本文・件名・宛先の組み立て方と、リマインダーの実行タイミング・日付項目・タイムゾーンの扱いを、1.5.8.1 のソースで確かめた内容でまとめます。
要点
- 本文の各行は
{"Name":"[Title]"}のような JSON として読まれる。項目が特定できた行はその項目の値に、それ以外の行は{Url}・{LoginId}・{UserName}・{MailAddress}の 4 つだけを置換して出力する [項目名]がそのまま置き換わるのは件名と、プロセスの通知の件名・本文だけ- JSON の
Prefixなどに書いた{Url}は展開されない - リマインダーは
Service.jsonのTimeZoneDefaultの時計で動く。サーバーの OS のタイムゾーンと違うと、送信時刻がその時差だけずれる
通知の本文の組み立て
既定の書式とカスタムフォーマット
「カスタムフォーマットを使う」にチェックを入れて保存したときだけ、入力した書式が Format として保存されます(既定の書式と同じ内容なら保存されません)。Format が空のときは、次の既定の書式が使われます(Notification.cs)。
{Url}
{"Name":"[Title]"}
{"Name":"[Body]"}
{"Name":"[Status]"}
…(通知対象の項目ごとに 1 行)
{UserName}<{MailAddress}>1 行ずつ JSON として読む
本文は NoticeBody が作ります。書式を \n で分割し、各行の前後の空白を除いてから NotificationColumnFormat として JSON を読みます(ResultModel.cs)。
図を読み込み中…
- JSON として読めない行(
{Url}だけの行、普通の文章など)は、例外を握りつぶしてnull扱いになり、そのまま出力されます(Jsons.cs)。 - JSON として読めても、
Nameから項目が特定できなければ同じく「そのまま出力」の側に入ります。{}だけの行や、Nameを持たない JSON(Adaptive Card の JSON など)はこちらです。 - 項目が特定できた行は、その項目の値に置き換わり、行の元の文字列は出力されません。
- 本文は Issues(期限付きテーブル)・Results(記録テーブル)・Wikis で同じ構造で、CodeDefiner のテンプレート(Model_Notice_Body.txt)から生成されています。
項目の特定は SiteSettings.IncludedColumns が行います。Name の中から正規表現 (?<=\[).+?(?=\]) で角括弧の中身を取り出し、項目名(ClassA など)と完全一致する項目を探します(SiteSettings.cs)。表示名(ラベル)では一致しません。
HttpClient 通知に複数行の JSON を書いてもよい
各行は前後の空白を除かれ、\n でつなぎ直されます。JSON では改行は空白と同じ扱いなので、複数行に整形した JSON もそのまま有効な JSON として送られます。困るのは、ある 1 行だけで Name に [項目名] を含む JSON オブジェクトとして読めてしまう場合です(その行は項目の値に置き換わります)。Newtonsoft.Json はプロパティ名の大文字・小文字を区別しないため、"name" も Name として読まれます。
項目の値の出し方(ToNotice)
特定できた項目は、型ごとの ToNotice で文字列にします(ToNoticeExtensions.cs)。
- 選択肢を持つ項目(リンク項目を含む)は、値を選択肢の表示名に変換します。複数選択はカンマと空白(
,+ 半角空白)でつなぎます(ToNoticeExtensions.cs)。リンク項目の表示名を出すのに特別な書き方は要りません。 - 日時は操作したユーザーのタイムゾーンに変換してから表示形式に合わせます(ToNoticeExtensions.cs)。
NoticeBodyの分岐にあるのは、タイトル・内容・状況・管理者・担当者・ロック・コメント・作成者・更新者と、分類・数値・日付・説明・チェック・添付ファイルの各項目です。期限付きテーブルでは開始・完了・作業量・進捗率も加わります。ID や更新日時などは項目として特定されても分岐が無いため、何も出力されません(ResultModel.cs)。
更新時の通知では、その項目が変わっていない行は出力されません(Always が true なら出します)。変わった行は既定で「変更前 => 変更後」の形になります(NotificationColumnFormat.cs)。
書式行の JSON プロパティ
NotificationColumnFormat のプロパティと、未指定のときの動きです(NotificationColumnFormat.cs)。
| プロパティ | 未指定のとき | 内容 |
|---|---|---|
Name | ― | [項目名]。これが無いと項目行にならない |
Prefix | 空 | 見出し(項目の表示名)の前に付ける文字列 |
Delimiter | " : " | 見出しと値の区切り |
Allow | " => " | 変更前と変更後の区切り |
DiffTypes | standard | DiffMatchPatch にすると、変更前後を並べずに差分を記号で示す |
StartBracket / EndBracket | ( / ) | DiffMatchPatch の差分を囲む括弧 |
DeletePrefixSymbol / DeleteSuffixSymbol | - / 空 | 削除された部分の前後に付ける記号 |
AddPrefixSymbol / AddSuffixSymbol | + / 空 | 追加された部分の前後に付ける記号 |
Always | 出さない | true なら、更新時に変わっていなくても出す |
DisplayTypes | 変更前後 | 0 変更前後、1 変更前だけ、2 変更後だけ |
ValueOnly | 見出しを出す | true なら見出し(Prefix・表示名・区切り)を出さない |
ConsiderMultiLine | 改行する | Markdown の項目で、見出しと値・変更前後の間に改行を入れる。false で入れない |
{"Name":"[Title]","Always":true,"ValueOnly":true}
{"Name":"[Status]","DisplayTypes":2}
{"Name":"[Body]","DiffTypes":"DiffMatchPatch","StartBracket":"【","EndBracket":"】","DeletePrefixSymbol":"削除:","AddPrefixSymbol":"追加:"}件名・宛先・プロセスの通知
| 場所 | [項目名] | {Url} など 4 つ | 処理 |
|---|---|---|---|
| 本文(通知) | 書式行の Name だけ | 項目行以外の行だけ | NoticeBody |
| 件名(通知) | 置換する | 置換する | ReplacedDisplayValues |
| 件名・本文(プロセスの通知) | 置換する | 置換する | ReplacedDisplayValues |
| 宛先・CC・BCC | ユーザーのメールアドレスに解決 | 置換しない | ReplacedAddress |
| プレフィックス | 置換しない | 置換しない | 件名の前にそのまま連結 |
- 件名は
[項目名]を項目の表示値に置き換え、そのあと{Url}などを置換します(ResultModel.cs)。件名の[NotificationTrigger]は、作成・更新・削除に応じた語に置き換わります。件名が空なら「"タイトル" を作成しました。」のような既定の件名になります(ResultModel.cs)。 - プロセスの通知は、件名も本文も
ReplacedDisplayValuesを通るので、本文中の[項目名]がそのまま値に置き換わります(ResultModel.cs)。 - 宛先の
[Manager]・[Owner]・[ClassA]などは、その項目の値のユーザーのメールアドレスに置き換わります。メール通知では[RelatedUsers]で管理者・担当者・作成者・更新者をまとめて宛先にできます(Notification.cs、Notification.cs)。 - プレフィックスは、メールでは件名の先頭に、Slack などでは
*プレフィックス件名*の形でそのまま連結されます(Notification.cs)。HttpClient 通知は本文だけを送り、件名もプレフィックスも使いません(Notification.cs)。
{Url} などの 4 つの置換内容です(ResultModel.cs)。
| 変数 | 置換内容 |
|---|---|
{Url} | レコードの編集画面の絶対 URL |
{LoginId} | 操作したユーザーのログイン ID |
{UserName} | 操作したユーザーの表示名 |
{MailAddress} | 操作したユーザーのメールアドレス |
置き換わらない書き方
| 書き方 | 結果 | 理由 |
|---|---|---|
{"Name":"[Title]","Prefix":"{Url} "} | {Url} がそのまま出る | 項目行は ReplacedContextValues を通らない。Delimiter・Allow・括弧・記号も同じ |
{"Name":"[Title]"}{Url} | JSON がそのまま出て、{Url} だけ置換される | 1 行に JSON と文字列が続くと JSON として読めず、項目行にならない |
{} | {} がそのまま出る | JSON としては読めるが Name が無く、4 つの変数のどれにも一致しない |
本文の行に [Title](JSON でない行) | [Title] がそのまま出る | 項目の置換は書式行の Name だけ。件名では置き換わる |
件名に [@ClassA] | そのまま出る | 項目は見つかるが、置換は [ClassA] の文字列で行うため一致しない(ResultModel.cs) |
[ClassA~1234,ClassB](リンク先の項目) | 項目行にならない・件名でも置換されない | IncludedColumns は自サイトの項目名と完全一致で探し、リンク先の項目を解決しない |
{Url} などの値が無い | 空文字に置き換わる | 置換値が null だと string.Replace は削除と同じになる(メールアドレス未設定のユーザーなど) |
1 行に JSON と文字列を続けたときに JSON として読めないのは、JsonConvert.DeserializeObject が JSON の後ろに余分な内容があると例外にするためです(1.5.8.1 の Newtonsoft.Json は 13.0.4)。値の前に URL を出したいときは、{Url} を独立した行に書きます。
リンク先の項目を通知に出す改修案と、Prefix などの {Url} を展開する改修案は 通知のプレースホルダの改修案 にまとめています。
リマインダー
実行の流れと間隔
リマインダーは次の 2 通りのどちらかで動きます。
| 方式 | 条件 | 間隔 |
|---|---|---|
| バックグラウンドサービス(Quartz) | BackgroundService.json の Reminder が true | ReminderCheckIntervalSeconds(既定 60 秒。30 〜 3600 秒に丸められ、範囲外なら Warning を SysLogs に記録) |
| URL の呼び出し | Reminder.json の Enabled が true で、上が false(既定) | 外部から /reminderschedules/remind を GET で呼んだとき(匿名でアクセス可) |
(ReminderBackgroundTimer.cs、BackgroundService.json、ReminderSchedulesController.cs)
1 回の実行では、ReminderSchedules テーブルから ScheduledTime が現在時刻以前のものを取り出して順に送り、送るたびに ScheduledTime を次回の時刻に更新します。対象が無くなるか Reminder.json の Span(既定 30 秒)を過ぎるまで繰り返します(ReminderScheduleUtilities.cs、Reminder.cs)。
Reminder.json | 既定値 | 意味 |
|---|---|---|
Interval | 500 | 1 件送るごとに待つミリ秒。送信間隔(毎日・毎週)とは関係ない |
Span | 30 | 1 回の実行で繰り返す秒数 |
Limit | 1000 | 1 通に載せるレコードの上限 |
送信の間隔は、リマインダーの「開始日時」と「繰り返し」(毎日・毎週など)から計算した ScheduledTime だけで決まります。
- 送信はサイトの最終更新者(
Sites.Updator)のユーザーとして行われ、サーバースクリプトは動きません(ReminderScheduleUtilities.cs)。 - 送信中に例外が起きると、そのリマインダーのスケジュールを削除し、エラー内容をリマインダーの宛先にメールします。次回からは送られません(Reminder.cs)。
日付の項目
リマインダーは、指定した日付の項目(「項目」欄)を基準に「今日から Range 日以内」のレコードを集め、日付ごとにまとめて本文を作ります(Reminder.cs)。
- 選べるのは日時型で、作成日時・更新日時ではない項目だけです。空の選択肢はありません(SiteSettings.cs、SiteUtilities.cs)。
- 未指定なら
CompletionTime(完了)を使います。記録テーブルのようにCompletionTimeが無いと項目が決まらず、GetDataTableで例外になって、上のとおりスケジュールが削除されます(Reminder.cs)。 - 条件は「日付 < 今日 +
Range日」で、ビューの条件に AND で付きます。日付が空のレコードは、ビューの条件に合っても対象になりません(SQL でNULLとの比較は偽になるため)。「期限切れを除く」「過去に完了したものも送る」の条件も同じ日付の項目で比べます。
日付の項目を使わず、ビューの条件だけで送る改修案は リマインダーの改修案 にまとめています。
タイムゾーン
リマインダーの処理で使う context はログインを経ないため、タイムゾーンは Service.json の TimeZoneDefault(配布時の既定は UTC)のままです(Context.cs、Context.cs)。このため次のようになります。
| 処理 | 基準の時計 |
|---|---|
| 開始日時 | 入力した値をそのまま保存(タイムゾーン変換なし) |
保存時の最初の ScheduledTime | 開始日時から、保存したユーザーのタイムゾーンの「今」以降で最初の回(SiteModel.cs) |
| 送るかどうかの判定 | ScheduledTime <= DateTime.Now.ToLocal(context)、つまり TimeZoneDefault の「今」(ReminderScheduleUtilities.cs) |
送信後の次回 ScheduledTime | 開始日時から、TimeZoneDefault の「今」以降で最初の回(Times.cs) |
対象レコードの範囲(今日 + Range 日、過去の完了) | TimeZoneDefault の「今日」の 0 時を、サーバーローカルで格納された日付と直接比べる |
開始日時は「TimeZoneDefault の時計で何時か」として扱われます。TimeZoneDefault が UTC のままでサーバーや利用者が日本時間なら、14:56 に設定した毎日のリマインダーは日本時間の 23:56 に届きます。判定と次回の計算は同じ context で行うため、確認したソースでは、この時差で同じリマインダーが続けて送られる経路は見当たりません。
TimeZoneDefault をサーバーの OS のタイムゾーン(日本なら Tokyo Standard Time)に合わせると、上の変換はすべて恒等変換になり、ずれは起きません(Times.cs)。タイムゾーン全体の仕組みは タイムゾーンの考え方 を参照してください。