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)オブジェクト ID | Graph のユーザー情報、または Teams のイベント payload | 個人(personal)スコープでの会話作成のキー | プロアクティブで aadObjectId を使うのは personal スコープのみ |
| userId(Teams user ID) | Teams 上のユーザー ID(ボットとユーザーの組み合わせで一意になり得る) | onMembersAdded / conversationUpdate などのイベント | 会話作成の宛先指定 | ボット間で再利用できない、という前提で扱う |
| tenantId | テナント ID | イベント payload、または既知の設定 | 会話作成・送信の必須情報 | マルチテナント対応では必ず保存する |
| serviceUrl | Bot 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 で“先にインストール”してから会話情報を確保する
まだ会話したことのないユーザーに確実にプロアクティブ送信したいなら、次の流れが最も堅牢です。
- Microsoft Graph で対象ユーザーの personal にアプリ(ボット)をインストールする
- インストールを契機に飛んでくるイベント(conversationUpdate / installationUpdate など)を受け、conversationId / serviceUrl / tenantId / userId / aadObjectId を保存する
- 以後は保存した 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 の「ユーザーへのアプリインストール」は、委任権限とアプリケーション権限の両方で可能ですが、今回の “ボットがサーバー側から多数ユーザーへ順次インストールする” 目的では、通常はアプリケーション権限を採用します(テナント管理者の同意が必要)。
| 権限の種類 | 最小権限(例) | より強い権限(例) | 使いどころ |
|---|---|---|---|
| Delegated | TeamsAppInstallation.ReadWriteSelfForUser | TeamsAppInstallation.ReadWriteForUser など | サインインした本人のインストールをアプリ内 UI で促す場合 |
| Application | TeamsAppInstallation.ReadWriteSelfForUser.All | TeamsAppInstallation.ReadWriteForUser.All など | バックエンドの一括インストール(管理者同意前提) |
注意点として、Microsoft Learn の説明では「ユーザーに対してアプリのインストール/取得/更新/削除を行うなら TeamsAppInstallation.ReadWriteForUser 系の権限が必要」と明記されています。要件に応じて “Self(自アプリに限定)で足りるか” を判断し、足りない場合は ReadWriteForUser 系へ上げる、という順で設計するとトラブルが減ります。
インストール後に「会話参照」を取り逃がさない
ユーザーがアプリをインストールしたタイミングでは、ボット側に conversationUpdate(membersAdded)や installationUpdate が届くことがあります。ここで受け取れる userId は、将来そのユーザーへ直接メッセージするためにキャッシュできる、と説明されています。
実装上は「イベントを受けたら conversationReference を生成して DB に保存する」を徹底してください。保存時のキーは、tenantId + aadObjectId(または tenantId + userId)を推奨します。マルチテナント/将来のユーザー統合を考えると、aadObjectId を主キー側に寄せた方が運用が安定しやすいです(Graph 側のユーザー参照も aadObjectId で揃うため)。
保存するレコードの例:
| フィールド | 例 | 用途 |
|---|---|---|
| tenantId | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | 宛先のテナント判定 |
| aadObjectId | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ユーザー一意キー(Entra) |
| userId | 29:... | Bot Framework 側の宛先 ID |
| serviceUrl | https://smba.trafficmanager.net/... | 送信先 |
| conversationId | ... | continueConversation に使用 |
| conversationReference(JSON) | JSON 文字列 | 送信を簡略化 |
| status | installed / 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
チャンネル取得に必要な権限
| API | Delegated(最小) | Application(最小) | 補足 |
|---|---|---|---|
GET /teams/{team-id}/channels | Channel.ReadBasic.All | ChannelSettings.Read.Group | Application の ChannelSettings.Read.Group は RSC(リソース固有同意)を用いる |
GET /teams/{team-id}/channels/{channel-id} | Channel.ReadBasic.All | ChannelSettings.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 を必ず保存しておくと、管理画面やログの可読性が大幅に上がります。
タグ取得に必要な権限
| API | Delegated(最小) | Application(最小) | 備考 |
|---|---|---|---|
GET /teams/{team-id}/tags | TeamworkTag.Read | TeamworkTag.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 など)

コメント