Microsoft Teams ボットのプロアクティブ メッセージ設計(Entra IDから送信)とGraph APIでチャンネル/タグ取得する方法

Microsoft Teams のボットで「ユーザーが話しかける前に通知したい」「Team ID からチャンネルIDやタグを一覧化したい」は定番の悩みです。本記事では Bot Framework と Microsoft Graph API を組み合わせ、Entra ID しかない状態からのプロアクティブ送信設計と、チャンネル/タグ/タグメンバー取得の実装ポイントを具体例付きで解説します。

目次

Teams ボットの「プロアクティブ メッセージ」で押さえるべき前提

プロアクティブ メッセージは、ユーザーからの入力に対する返信ではなく、ボット側が任意のタイミングで送るメッセージです。返信と違って「今まさに処理中の turnContext」がないため、送信前に会話(conversation)を作成・特定できる情報が必要になります。さらに、プロアクティブでは新しいグループチャットや新しいチーム内チャンネルを“作成する”ことはできず、ボットを含むアプリがあらかじめ対象のスコープ(個人/チーム/チャネル)にインストールされている必要があります。

この「事前にインストールされている必要がある」という制約が、まだ会話したことがないユーザーへの 1:1 メッセージ送信を難しくしている本質です。

必要な ID とデータを整理する

設計を始める前に、混同しやすい ID を分解しておくと後工程が圧倒的に楽になります。

項目意味どこで取得できるか主な用途注意点
Entra ID(aadObjectId)ユーザーの Entra(旧 Azure AD)オブジェクト IDGraph のユーザー情報、または Teams のイベント payload個人(personal)スコープでの会話作成のキープロアクティブで aadObjectId を使うのは personal スコープのみ
userId(Teams user ID)Teams 上のユーザー ID(ボットとユーザーの組み合わせで一意になり得る)onMembersAdded / conversationUpdate などのイベント会話作成の宛先指定ボット間で再利用できない、という前提で扱う
tenantIdテナント IDイベント payload、または既知の設定会話作成・送信の必須情報マルチテナント対応では必ず保存する
serviceUrlBot Framework Connector の送信先 URL受信したアクティビティ(推奨)、やむを得ない場合はグローバル URL会話作成・送信の必須情報極力ハードコードしない(環境・リージョン差異)
conversationId会話(1:1 やチャネルスレッド)の識別子conversationUpdate / installationUpdate 等以降の送信(continueConversation)一度取得したら保存し、毎回作り直さない
conversationReference上記をまとめた「送信の宛先情報セット」TurnContext から生成して保存プロアクティブ送信の最短ルートユーザーがアンインストールしたら無効になる

Entra ID しかないユーザーへ、ボットから先に 1:1 を送れるか

結論は「条件付きで可能」です。

  • 可能になる条件:ボットを含む Teams アプリが、そのユーザーの personal(個人)スコープにインストール済みであること
  • 必要な情報:aadObjectId または userId、tenantId、serviceUrl
  • 制約:aadObjectId を使ったプロアクティブは personal スコープのみ。メールアドレスや UPN を宛先指定に使ってのプロアクティブ送信はサポートされない

一方で、現実的な運用で一番詰まりやすいのが「そもそもユーザーの personal にアプリが入っていない」ケースです。この状態では、aadObjectId が分かっていても、送信時に 403(例:ForbiddenOperationException)で失敗しがちです。

現場で安定する解法:Graph で“先にインストール”してから会話情報を確保する

まだ会話したことのないユーザーに確実にプロアクティブ送信したいなら、次の流れが最も堅牢です。

  1. Microsoft Graph で対象ユーザーの personal にアプリ(ボット)をインストールする
  2. インストールを契機に飛んでくるイベント(conversationUpdate / installationUpdate など)を受け、conversationId / serviceUrl / tenantId / userId / aadObjectId を保存する
  3. 以後は保存した conversationReference(または conversationId + tenantId)で、いつでも continueConversation できる

この方式のメリットは、初回送信の“成功率”が上がるだけでなく、2回目以降の送信が高速・安全になる点です(毎回会話作成を試す必要がなくなるため)。

Graph API:ユーザーに Teams アプリをインストールする

ユーザーの personal スコープにアプリをインストールする代表的なエンドポイントは以下です。

POST /users/{user-id | user-principal-name}/teamwork/installedApps

リクエストボディは、アプリカタログ上の Teams アプリ ID(teamsApp)を [email protected] でバインドします。

POST https://graph.microsoft.com/v1.0/users/{user-id}/teamwork/installedApps
Content-Type: application/json

{
  "[email protected]": "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{teamsAppId}"
}

また、アプリがリソース固有のアクセス許可(RSC)を要求する構成の場合、インストール時に同意(consentedPermissionSet)を含めて渡すパターンもあります。運用上「チームごとに最小権限で動かす」設計を採るときに効きます。

インストールに必要な権限(最小権限の考え方)

Graph の「ユーザーへのアプリインストール」は、委任権限とアプリケーション権限の両方で可能ですが、今回の “ボットがサーバー側から多数ユーザーへ順次インストールする” 目的では、通常はアプリケーション権限を採用します(テナント管理者の同意が必要)。

権限の種類最小権限(例)より強い権限(例)使いどころ
DelegatedTeamsAppInstallation.ReadWriteSelfForUserTeamsAppInstallation.ReadWriteForUser などサインインした本人のインストールをアプリ内 UI で促す場合
ApplicationTeamsAppInstallation.ReadWriteSelfForUser.AllTeamsAppInstallation.ReadWriteForUser.All などバックエンドの一括インストール(管理者同意前提)

注意点として、Microsoft Learn の説明では「ユーザーに対してアプリのインストール/取得/更新/削除を行うなら TeamsAppInstallation.ReadWriteForUser 系の権限が必要」と明記されています。要件に応じて “Self(自アプリに限定)で足りるか” を判断し、足りない場合は ReadWriteForUser 系へ上げる、という順で設計するとトラブルが減ります。

インストール後に「会話参照」を取り逃がさない

ユーザーがアプリをインストールしたタイミングでは、ボット側に conversationUpdate(membersAdded)や installationUpdate が届くことがあります。ここで受け取れる userId は、将来そのユーザーへ直接メッセージするためにキャッシュできる、と説明されています。

実装上は「イベントを受けたら conversationReference を生成して DB に保存する」を徹底してください。保存時のキーは、tenantId + aadObjectId(または tenantId + userId)を推奨します。マルチテナント/将来のユーザー統合を考えると、aadObjectId を主キー側に寄せた方が運用が安定しやすいです(Graph 側のユーザー参照も aadObjectId で揃うため)。

保存するレコードの例:

フィールド例用途
tenantIdxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx宛先のテナント判定
aadObjectIdxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxユーザー一意キー(Entra)
userId29:...Bot Framework 側の宛先 ID
serviceUrlhttps://smba.trafficmanager.net/...送信先
conversationId...continueConversation に使用
conversationReference(JSON)JSON 文字列送信を簡略化
statusinstalled / pending / blocked再送・抑止・監査

プロアクティブ送信でハマりやすいポイント

serviceUrl は“できるだけ受信した値”を使う

プロアクティブ送信では serviceUrl が必須です。推奨は、何らかの受信アクティビティ(インストールイベント、ユーザーの最初の発話など)から取得した serviceUrl を保存して再利用する方法です。どうしても取得できない場合に限り、グローバル URL を使う手段が提示されていますが、ハードコードは避け、クラウド/環境に合わせて使い分けます。

Public: https://smba.trafficmanager.net/teams/
GCC:    https://smba.infra.gcc.teams.microsoft.com/teams
GCC High: https://smba.infra.gov.teams.microsoft.us/teams
DoD:    https://smba.infra.dod.teams.microsoft.us/teams

ユーザーがブロック/ミュート/アンインストールしていると 403 になる

運用で必ず起きます。プロアクティブ送信時に 403 が返り、サブコードとして MessageWritesBlocked が返るケースが示されています。これを「エラー」ではなく「状態」として保存し、以降の送信を止めたり、別チャネル(メール等)へ誘導したりするのが実務的です。

症状代表的な原因対処
403 Forbiddenボットが personal に未インストール/ユーザーがブロック/アンインストールインストール状態を確認して再導線、ブロックは opt-out として扱う
宛先不明・送れないconversationReference 未保存、tenantId/serviceUrl 欠落インストールイベントで確実に保存する
429 Too Many Requests大量送信でスロットリング指数バックオフ、キューイング、送信分散

「Entra ID だけで送れない」のではなく「送れる状態を作る」が正解

「conversation reference がないから無理」と切り捨てるより、Graph でインストール → イベントで参照を確保 → 以後は参照で送るという“状態づくり”を標準フローとして組み込むのが、結果的に最短です。プロアクティブメッセージは便利な反面、ユーザー体験を損ねると簡単にブロックされるため、初回メッセージは「なぜ届いたか」「何ができるか」「止め方(opt-out)」まで書くのが安全です。

Team ID から「チャンネル/タグ/タグメンバー」を Graph API で取得する

Team ID が分かっているなら、Graph API でチーム配下の構成情報をかなりの粒度で引けます。前提として、Teams の team は Microsoft 365 グループに紐づき、グループ ID と team ID は同じ ID 体系で扱われます。

チャンネル一覧を取得する

チャンネル一覧は以下で取得できます。

GET /teams/{team-id}/channels

ここで重要なのが、呼び出す主体がチームメンバーであっても、自分がメンバーではない private / shared channel はレスポンスに出てこない点です。監査目的などで “チーム全体のすべてのチャンネルが欲しい” と考えている場合、この仕様を踏まえて権限・ロール(Teams 管理者権限など)を設計する必要があります。

また、チャンネルの email プロパティの生成は高コストで遅くなるため、必要ないなら $select で除外するとパフォーマンスが上がります。

GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels?$select=id,displayName,description,membershipType,isArchived

チャンネル取得に必要な権限

APIDelegated(最小)Application(最小)補足
GET /teams/{team-id}/channelsChannel.ReadBasic.AllChannelSettings.Read.GroupApplication の ChannelSettings.Read.Group は RSC(リソース固有同意)を用いる
GET /teams/{team-id}/channels/{channel-id}Channel.ReadBasic.AllChannelSettings.Read.Group共有チャネルは「その共有チャネルのメンバー」だけが取得できる旨の注意あり

「一般(General)」チャンネルだけを確実に取りたい場合

すべてのチャンネルを一覧して General を探すより、既定チャンネル(primaryChannel)を直接取得する方が安定します。

GET /teams/{id}/primaryChannel

権限はチャンネル取得と同系統で、Delegated なら Channel.ReadBasic.All、Application なら ChannelSettings.Read.Group が最小権限として整理されています。

タグ一覧(タグ ID/タグ名)を取得する

タグは「チーム内のユーザー集合をラベルで呼び出す」ための機能で、Graph API では teamworkTag として扱われます。チーム配下のタグ一覧は以下で取得します。

GET /teams/{team-id}/tags

レスポンスには id、displayName、description、memberCount、tagType(standard / scheduled など)が含まれます。タグの UI では「@タグ名」でメンションできるため、ID だけでなく displayName を必ず保存しておくと、管理画面やログの可読性が大幅に上がります。

タグ取得に必要な権限

APIDelegated(最小)Application(最小)備考
GET /teams/{team-id}/tagsTeamworkTag.ReadTeamworkTag.Read.All書き込みが必要なら ReadWrite 系へ

タグに紐づくメンバーを取得する

タグメンバーは次のエンドポイントで取得します。

GET /teams/{team-id}/tags/{tag-id}/members

レスポンスは teamworkTagMember の配列で、displayName、tenantId、userId などが返ります。ここでの userId は GUID 形式で返ることが多く、ユーザー詳細(UPN や部門など)が必要なら、追加で Graph のユーザー参照(例:/users/{id})に繋げて正規化すると扱いやすくなります。

権限はタグ一覧と同じ系統(Delegated: TeamworkTag.Read、Application: TeamworkTag.Read.All)です。

取得結果を“使えるデータ”に整形するコツ

Graph からは「ID はあるが、システム内でどう結び付けるか」は別問題です。おすすめは、次の 3 レイヤーで DB を分けることです。

レイヤー保存するもの主キーの例更新頻度狙い
構成レイヤーTeam / Channel / Tag の一覧teamId, channelId, tagId低〜中管理画面・検索の高速化
関連レイヤーTagMember(tagId → userId の対応)tagId + userId中メンション対象・通知対象の解決
送信レイヤーconversationReference 等の送信宛先情報tenantId + aadObjectId中〜高プロアクティブ送信の成功率を上げる

この分離をしておくと、「タグのメンバーに通知を送る」という要件が出たときも、TagMember → Entra ID → conversationReference を辿って送信でき、どこが欠けているか(未インストール、参照未取得、ブロック等)を可視化しやすくなります。

実運用で差がつく設計ポイント

大量ユーザーへのプロアクティブ送信は“バッチ”ではなく“キュー”にする

「Entra ID の一覧を順にインストールして送る」をそのまま同期処理で書くと、スロットリング(429)や一時エラー、イベント到着の遅延で壊れやすくなります。おすすめは、ユーザーごとに状態遷移を持つキュー設計です。

状態意味次のアクション
pending_installインストール要求前Graph で installedApps へ POST
installed_wait_refインストール済みだが会話参照未確定conversationUpdate/installationUpdate を待つ(来たら保存)
ready参照確保済みcontinueConversation で送信
blockedブロック/アンインストール等で送信不可以降の送信を抑止、ユーザーへ再導線

「まず 1 回だけ」ユーザーの信頼を取りに行く

プロアクティブは、ユーザーから見ると突然届くメッセージです。最初の 1 通で信頼を落とすとブロックされ、以後の通知がすべて失われます。初回は次を必ず入れると運用が安定します。

  • なぜ届いたか(例:会社の通知、特定チームの運用、管理者が導入した 等)
  • 何ができるか(例:コマンド例、よくある質問への導線)
  • 通知を止める方法(opt-out の案内)

要点のまとめ

  • Entra ID(aadObjectId)だけでのプロアクティブ送信は、personal スコープ限定かつ「アプリがユーザーにインストール済み」であることが前提
  • 安定運用の王道は、Graph で先にインストール → イベントで会話参照を保存 → continueConversation の流れ
  • チャンネル一覧は /teams/{team-id}/channels、既定チャンネルは /teams/{id}/primaryChannel
  • タグ一覧は /teams/{team-id}/tags、タグメンバーは /teams/{team-id}/tags/{tag-id}/members
  • 権限は最小から組み立て、RSC と Graph 権限を混同しない(特に ChannelSettings.Read.Group など)

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次