🍊

Pleasanter の通知をカスタマイズしようとしたら陥穽だらけだった

2022/12/20に公開

2022 個人アドベントカレンダー の記事です。

これはなに

通知をうまいことしようとカスタマイズしようとしたら時間が溶けたので罠を記事化

罠 1:通知の HttpClient で JSON が送れない

"通知"機能では HttpClient が使える。
プロセスの通知とか、リマインダーで通知が使えないのは、微妙ではあるが、今の仕様という強弁に答える手がない。

ただ、JSON がおくれないのがつらい。

試したこと

例えば下記のようにカスタム書式を指定すれば JSON 様の文字列を形成できそうに考えられたが上手くいかなかった。

{
  "ApiKey":"",
  "Title":
{name:[タイトル],"ValueOnly":true,"DisplayTypes":"AfterChange"}
  ,
  "Body":"{Url}"
}

もちろん、タイトルには " を手で付ける想定。うまくいかないので送信内容を確認したところ、先頭に *" タイトル " を更新しました。* という文字列が付与されていた ↓(この時点では動的な項目は使わず、全て固定文字列)。

*" test " を更新しました。*
{
  "ApiKey": "*",
  "ApiVersion":1.1,
  "Body":"send"
}

ソースの調査

実装を調査したところ、例えば更新の場合ここの case

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Models/Issues/IssueModel.cs#L3810-L3830

タイトルと Body を指定して Send しているが

                            notification.Send(
                                context: context,
                                ss: ss,
                                title: Displays.Updated(
                                    context: context,
                                    data: Title.DisplayValue).ToString(),
                                body: body,
                                values: values);

HttpClient の場合、ここの case に入って

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Libraries/Settings/Notification.cs#L303-L317

ここで、暗黙に Prefix と title 前置している。

                        new HttpClient(
                            _context: context,
                            _text: $"*{Prefix}{title}*\n{body}")

HttpClient が通知種別の Enum の数値的に後発で作成されたこと、また "*" が通知種別で見て Mail の次にある Slack では文字装飾になれ得ることから、コピペで作ったんだろうと思われる。[1]
Content-Type に "application/json" を指定可能[2]にしておきつつ、必ず JSON として invalid な Body を作る設計はさすがにやばい。

通知をトリガーに Pleasanter 内の別テーブルに API を投げて、受け側のテーブルのサーバスクリプトで処理をしようとしたのだが API キーをメッセージ本文の ApiKey で認証する Pleasanter の仕組み的に、メッセージがそもそも JSON としてパースできないと認証のしようがなくて詰んだ。

罠 2:通知が notification で再現できない

連携が上手くいかないので、サーバスクリプトの notification で代用しようとしたが、うまくいかない。

メールアドレスが置換できない

通知のカスタム書式として {MailAddress} を置換する動作があるが、これが再現できない。
サーバスクリプトからユーザのメールアドレスを取る方法がない。

このメールアドレスは、記録テーブルだとこの辺[3]で置換していて、メールアドレステーブルから直接引いている

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Models/Results/ResultModel.cs#L3333-L3347

       private string ReplacedContextValues(Context context, string value)
        {
            var url = Locations.ItemEditAbsoluteUri(
                context: context,
                id: ResultId);
            var mailAddress = MailAddressUtilities.Get(
                context: context,
                userId: context.UserId);
            value = value
                .Replace("{Url}", url)
                .Replace("{LoginId}", context.User.LoginId)
                .Replace("{UserName}", context.User.Name)
                .Replace("{MailAddress}", mailAddress);
            return value;
        }

なお、マニュアルには書かれていないが仕様として通知で置換されるのは複数あるメールアドレスのうち、最初の 1 つ

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Models/MailAddresses/MailAddressUtilities.cs#L33-L47

        public static string Get(Context context, int userId, bool withFullName = false)
        {
            var mailAddress = Repository.ExecuteScalar_string(
                context: context,
                statements: Rds.SelectMailAddresses(
                    top: 1,
                    column: Rds.MailAddressesColumn().MailAddress(),
                    where: Rds.MailAddressesWhere()
                        .OwnerId(userId)
                        .OwnerType("Users"),
                    orderBy: Rds.MailAddressesOrderBy().MailAddressId()));
            return withFullName
                ? Get(context.User?.Name, mailAddress)
                : mailAddress;
        }

サーバスクリプトではメールアドレステーブルのモデルはないし User モデルでもメールアドレスは取得できないので、直接的にはメールアドレスは取れない。

サイトの設定が読めないので

カラム名が取れない

Web UI の設定では書式は ↓ このようにカラム名を表示名(LabelText)で指定するが

{Url}
{"Name":"[タイトル]"}
{"Name":"[内容]"}
{"Name":"[状況]"}
{"Name":"[管理者]"}
{"Name":"[担当者]"}
{"Name":"[コメント]"}

{UserName}<{MailAddress}>

(サイトパッケージ上の)設定値としては、↓ こうやって保持している

{Url}
{"Name":"[Title]"}
{"Name":"[Body]"}
{"Name":"[Status]"}
{"Name":"[Manager]"}
{"Name":"[Owner]"}
{"Name":"[Comments]"}

{UserName}<{MailAddress}>

こうなっていると model からデータを取るのはやりやすい。
しかし問題はサーバスクリプトではサイト設定のカラム情報を取れないので、ここから LabelText を復元できない。

分類型のコード値と対応する表示値が分からない

例えば「状況」のようにコード値と表示値を使い分けている場合でもコード値しか取ることができない。

余談だが、もし、サーバスクリプトからサイトパッケージの情報が取れさえすれば、一覧で状況以外の項目で、短い表示名を出すことが可能になりそう。(行表示の前で特定の列の表示を変更する処理を挟めば)

また、ユーザや組織、グループといった項目についても、それが何の値か分からない。
できることとしては、分類型の場合 10 進数の羅列の場合、各マスターから対応する ID がいるか検定するほかないが、重複しない保証はないので確定できない。
そもそも、テーブルのリンクやルックアップ、純粋にコード値が整数といったものが全てが正しく引けなくなるから、やらないほうがマシまである。
リンクなどを考えると、サイト設定だけじゃなく、「表示値」を取る仕組みがサーバスクリプトに必要そう。

ConsiderMultiline の判定ができない。

項目がマークダウン形式かどうか判断できないので、複数行項目として改行を入れるべきか識別できない。

このあたりいずれもサーバスクリプトの columns オブジェクトが全ての属性情報を網羅していないことに起因すると言える。

無効になっているかが分からない

設計として、無効になっているものをサーバスクリプトで送信しようとしたのだが、notification.Disabled が正しく判定できなかった。
設定値としては「無効」になっていると「Disabled」が「true」になることをサイトパッケージで確認した。[4]

実装を確認したところ notification.GetDisabled をサイト設定から移植していないため、クラスのデフォルト値(bool? のため null)になっていた。

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Libraries/ServerScripts/ServerScriptModelNotificationModel.cs#L30-L45

        public ServerScriptModelNotificationModel(Context context, SiteSettings ss, int Id)
        {
            Context = context;
            SiteSettings = ss;
            var notification = NotificationUtilities.Get(context: Context, ss: SiteSettings)
                ?.Where(o => o.Id == Id)
                .FirstOrDefault();
            this.Id = notification.Id;
            Type = notification.Type;
            Prefix = notification.Prefix;
            Address = notification.Address;
            UseCustomFormat = notification.UseCustomFormat;
            Format = notification.Format;
            Title = notification.Subject;
            Body = notification.Body;
        }

カスタム書式の Prefix について

機能的な問題ではないが微妙なところ[5]なのでメモ

  • Prefix は"項目名"に対するプレフィクスであって、項目の出力行そのものへのプレフィクスではない。

https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Libraries/Settings/NotificationColumnFormat.cs#L73-L78

        private string Header(Column column)
        {
            return ValueOnly == true
                ? string.Empty
                : $"{Prefix}{column.LabelText}{GetDelimiter()}{SeperateLine(column)}";
        }

ここの Header は項目に対するヘッダで、項目名を表現しており、 ↑ のとおり ValueOnly とすると Prefix がつかない。

おわりに

今回は、ソースコードの調査や SysLogs を見るためにローカル環境を作るなど、調査に時間を取られてやりたいことを実装するところまで至らなかった。
可能な限りで notification をサーバスクリプトで再現する、もともとやりたかったことを次回着手予定。

メディア種別 - 通知内容のメディアタイプ(Content-Type)を指定します。既定値は application/json です。

脚注
  1. ChatWork で使える「囲み枠タグ(見出しあり)」はタイトル + 本文で通知する Pleasanter の書式に合致していると思うのだけれどもそれは使っていなくて、使えない markdown 書式をコピペするのはやめてほしい。
    https://help.chatwork.com/hc/ja/articles/203127904-投稿に装飾はできますか- ↩︎

  2. 指定可能というか、application/json が既定値。
    https://pleasanter.org/manual/httpclient ↩︎

  3. 期限付きテーブルだとこのへん
    https://github.com/Implem/Implem.Pleasanter/blob/02d84f957b783608c2f38943bc7722c6e2c28ae2/Implem.Pleasanter/Models/Issues/IssueModel.cs#L3686 ↩︎

  4. デフォルト値は省略されるので、無効と無効にしていないときを両方エクスポートして確認 ↩︎

  5. 変更前後の値を区切る「⇒」を表現する要素の属性名が "Allow" なのも微妙ではある。 ↩︎

GitHubで編集を提案

Discussion