Microsoft 365の提供するGraph APIを使ってカレンダーの予定を作成・更新することができる。
今回はこの予定を更新する際に困ったことをまとめていく。
というか結論を読めばよく、以降はそれに至るまでの思考のrawで特にとっ散らかった文になってる。
結論
Teams会議を有効化した予定を更新する際は body.content に含まれる会議情報(厳密には joinUrl 相当の内容)が残されている必要がある。
これが残っていないとTeams会議が消えてしまう。
たとえリクエストにTeams会議を有効化する設定を含んでいたとしても。
また、これはドキュメントに記載のある挙動となっている。
learn.microsoft.com
とはいえ、 body.content に自アプリケーションから情報を入れたい場合などもある。
特にミーティング情報をユーザーに触らせたくなく、自アプリケーションのメッセージのみを編集させたい。
その際は、ユニークなcssなど要素が特定できるHTML要素と作って情報を入れたら良さそう。
そうすることで、すでに何か自アプリケーション経由で記述しているかの確認ができる。
また、body.content の戻り値は html タグから始まるHTML構造だが、タグ外の先頭に任意のメッセージを追加してリクエストしてもちゃんとハンドルしてくれて、レスポンスの body.content では html タグ内に追記される。
追記のみひたすらすれば良いならそれでも良さそう。(そんなケースは現実なさそうだが)
確認
前置き
例えば、あるUPNのユーザーの予定を作成する場合は下記のようなリクエストになる。(参考資料に記載のAPI仕様書より他にもエンドポイントは存在する)
POST https://graph.microsoft.com/beta/users/{upn}/events
この時、bodyに isOnlineMeeting と onlineMeetingProvider を指定することで予定にTeams会議を追加することができる。
Teams会議を追加したいときは "onlineMeetingProview": "teamsForBusiness" とすればよい。
{
"subject": "test event",
"body": {
"contentType": "HTML",
"content": "test"
},
"start": {
"dateTime": "2025-04-13T19:00:00",
"timeZone": "Asia/Tokyo"
},
"end": {
"dateTime": "2025-04-13T20:00:00",
"timeZone": "Asia/Tokyo"
},
"attendees": [],
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness"
}
これのレスポンスには下記のようにオンラインミーティングの情報が含まれる。
一部プロパティは除外したのと各種IDやらは適当に書き換えた。
{
"id": "some_event_id",
...
"subject": "test event",
"bodyPreview": "ほげほげ\r\n________________________________________________________________________________\r\nMicrosoft Teams ヘルプが必要ですか?\r\n今すぐ会議に参加する\r\n会議 ID: 000 000 000 000 0\r\nパスコード: hoge\r\n________________________________\r\n開催者向け: 会議オプション\r\n__________________________________",
"webLink": "https://outlook.office365.com/owa/?itemid=hoge&exvsurl=1&path=/calendar/item",
"onlineMeetingUrl": null,
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"body": {
"contentType": "html",
"content": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\ntest<br>\r\n<div class=\"me-email-text\" style=\"max-width:520px; color:#242424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-bottom:24px; overflow:hidden; white-space:nowrap\">________________________________________________________________________________</div>\r\n<div style=\"margin-bottom:12px\"><span class=\"me-email-text\" style=\"font-size:24px; font-weight:700; margin-right:12px\">Microsoft Teams</span>\r\n<a href=\"https://aka.ms/JoinTeamsMeeting?omkt=ja-JP\" id=\"meet_invite_block.action.help\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#5B5FC7\">\r\nヘルプが必要ですか?</a> </div>\r\n<div style=\"margin-bottom:6px\"><a href=\"https://teams.microsoft.com/l/meetup-join/hoge\" id=\"meet_invite_block.action.join_link\" title=\"Meeting join link\" class=\"me-email-headline\" style=\"font-size:20px; font-weight:600; text-decoration:underline; color:#5B5FC7\">今すぐ会議に参加する</a>\r\n</div>\r\n<div style=\"margin-bottom:6px\"><span class=\"me-email-text-secondary\" style=\"font-size:14px; color:#616161\">会議 ID:\r\n</span><span class=\"me-email-text\" style=\"font-size:14px; color:#242424\">000 000 000 000 0</span>\r\n</div>\r\n<div style=\"margin-bottom:24px\"><span class=\"me-email-text-secondary\" style=\"font-size:14px; color:#616161\">パスコード:\r\n</span><span class=\"me-email-text\" style=\"font-size:14px; color:#242424\">hoge</span>\r\n</div>\r\n<div style=\"margin-bottom:24px; max-width:532px\">\r\n<hr style=\"border:0; background:#D1D1D1; height:1px\">\r\n</div>\r\n<div><span class=\"me-email-text-secondary\" style=\"font-size:14px; color:#616161\">開催者向け:\r\n</span><a href=\"https://teams.microsoft.com/meetingOptions/?hoge\" id=\"meet_invite_block.action.organizer_meet_options\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#5B5FC7\">会議オプション</a>\r\n</div>\r\n<div style=\"margin-top:24px; margin-bottom:6px\"></div>\r\n<div style=\"margin-bottom:24px\"></div>\r\n<div style=\"margin-bottom:24px; overflow:hidden; white-space:nowrap\">________________________________________________________________________________</div>\r\n</div>\r\n</body>\r\n</html>\r\n"
},
...
"onlineMeeting": {
"joinUrl": "https://teams.microsoft.com/l/meetup-join/hoge"
}
}
さて、作成においては特段困ったことはない。
Teams会議の案内文が body.content に追記され、会議のURLは onlineMeeting.joinUrl から取得可能となっている。
ここまではいい。
問題は更新時だ。
問題
更新時は以下のようなリクエストを送ってみる。
PATCHリクエストなので、変更したいフィールドだけ指定して変更したい値を送る。
下記は test と送っていた body.content を foobar に更新しようとしている。
PATCH https://graph.microsoft.com/beta/users/{upn}/events/{event_id}
{
"body": {
"contentType": "HTML",
"content": "foobar"
}
}
するとレスポンスからTeams会議に関する情報が一切合切消え、Outlookのカレンダーを見てもTeams会議ではなくなっているので実際に消えているらしい。
これは結論で記載した通りの挙動ではあるものの、アプリ内で body.content にカスタムメッセージを含めた場合などに困ってしまう。
対策
body.content のHTML構造を特に考えずに先頭にメッセージを入れてリクエストすると、レスポンスでは body.content の body タグ内にメッセージが設定される。
ひたすら追記するのみであればこれで良さそう。
ただし、これだと編集が困難になる。
ここで自アプリケーションからのメッセージを適当にdivタグなどで囲い、クラスをつけたらどうなるか。
これはちゃんと保持されてレスポンスにも含まれる。
これを使うことで、戻り値のHTMLをパースして特定のクラスの要素を取得して中身のメッセージを編集したり、なければ文頭に特定クラスをつけた要素を作ってメッセージを詰め込んでリクエストすれば良い。
ここまで書いたところで、最初のメッセージ送付時点で「以下のミーティング情報を編集するな」といった文言を追加したら、それで終わりでもいいのでは?とか気づきを得てしまった。
参考資料