Microsoft Graph 実務入門 #6 Teams 連携 — メッセージの読み取りと、通知を送る三つの経路

読了 6分

Teams の回は他の回と構成が違います。読み取りの側はこれまでのパターンのままなのに、書き込み(通知の送信)の側に、このシリーズで最も有名な壁があるからです。「サーバーの自動化がチャネルにお知らせを投稿する」という、誰もが最初に思い浮かべるあのシナリオが、Graph の application 権限ではできません。なぜできないのか、では何を使えばよいのか、が今回の中心です。

読み取り — これまでのパターンのまま #

まずできる側から手早く整理します。チームとチャネルの構造の照会、メッセージの読み取りは、第 3 回の骨格がそのまま通ります。

チーム・チャネル・メッセージの読み取り
async def read_channel(client, team_id: str, channel_id: str):
    # 自分(アプリ)がアクセスできるチーム一覧: delegated は me.joined_teams
    channels = await client.teams.by_team_id(team_id).channels.get()

    messages = await (
        client.teams.by_team_id(team_id)
        .channels.by_channel_id(channel_id)
        .messages.get()
    )
    for m in messages.value or []:
        who = m.from_.user.display_name if m.from_ and m.from_.user else "(アプリ)"
        print(who, ":", (m.body.content or "")[:80])

読み取りの権限は delegated・application の両方に開いています(ChannelMessage.Read.All など。チャネルメッセージの組織全体の読み取りは保護された API に分類され、追加の承認手続きがある場合があるので、ドキュメントの表を確認します)。メッセージの本文が HTML で来ること、大量の収集は第 7 回の delta クエリが定石であることあたりが実務のメモです。

壁 — チャネルメッセージの送信と application 権限 #

ここからが本題です。チャネルにメッセージを送る API の権限の表は、こういう形をしています(公式ドキュメント基準)。

権限の型チャネルメッセージの送信
Delegated (会社アカウント)ChannelMessage.Send
ApplicationTeamwork.Migrate.Allマイグレーション専用

application の列にある Teamwork.Migrate.All は、名前のとおり他システムの過去の会話を Teams へ移す(import)シナリオ専用です。マイグレーションモードに切り替えたチャネルへ、過去のタイムスタンプのメッセージを流し込む特殊な手順であって、運用中のチャネルへリアルタイムの通知を送る用途ではありません。だから client credentials のトークンで POST /teams/{id}/channels/{id}/messages を呼ぶと、権限を全部付与していても 403 が返ります。「読み取りはできるのに書き込みだけ 403」という、検索すると数えきれないほどヒットするあの症状の正体がこれです。

意図された設計だという点も理解しておく必要があります。組織全体のチャネルに無人のアプリが任意に書き込める権限は、スパムやなりすましの通路になりやすく、Microsoft はその扉を最初から閉じておく側を選びました(ドキュメントには「Teams をログファイルとして使うな」という利用規約の警告まで付いています)。したがって設計の段階で下の三つの経路から一つを選ぶのが正しく、403 を回避しようと時間を使うのは行き止まりです。

経路 ① delegated + 自動化アカウント — Graph の中で解決 #

Graph のまま進みたいなら、送信を delegated に替えるのが第一の経路です。通知専用のアカウント(例: bot-notify@contoso.com)を作って対象のチームに招待し、そのアカウントの delegated トークンで送ります。

delegated でチャネルメッセージを送信
from msgraph.generated.models.chat_message import ChatMessage
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.body_type import BodyType

async def post_notice(client, team_id: str, channel_id: str, html: str):
    msg = ChatMessage(body=ItemBody(content_type=BodyType.Html, content=html))
    await (
        client.teams.by_team_id(team_id)
        .channels.by_channel_id(channel_id)
        .messages.post(msg)
    )

無人環境で delegated トークンを維持するのが最大のポイントです。ユーザーなしでサインインの過程を自動化する ROPC のような迂回は推奨されないフローなので避け、一度のサインインの後にリフレッシュトークンを安全に保管・更新する構成(azure-identity のトークンキャッシュの活用)が現実的な形です。メッセージがそのアカウントの名前で投稿されること、アカウントとトークンの管理という運用の負担が生まれることが、この経路のコストです。「メッセージの内容を Graph で組み立てる必要があり、スレッドに返信も付ける」という双方向に近い要件なら、このコストを払う価値があります。

経路 ② Workflows の受信 Webhook — 一方向の通知の最短経路 #

「通知を投稿するだけでいい」が要件の全部なら、Graph を使わないのが正解です。Teams の Workflows(Power Automate)ベースの受信 Webhook が最短の経路です。チャネルに「Webhook リクエストを受け取ったら投稿」のワークフローを追加すると URL が発行され、サーバーはその URL に POST 一回で終わりです。アプリ登録も、トークンも、権限の承認もありません。

Webhook で通知 — 認証不要
import httpx

async def notify(webhook_url: str, title: str, text: str):
    card = {
        "type": "message",
        "attachments": [{
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": {
                "type": "AdaptiveCard", "version": "1.4",
                "body": [
                    {"type": "TextBlock", "size": "Large", "weight": "Bolder", "text": title},
                    {"type": "TextBlock", "wrap": True, "text": text},
                ],
            },
        }],
    }
    async with httpx.AsyncClient() as http:
        (await http.post(webhook_url, json=card)).raise_for_status()

なお、古い資料によくある「Office 365 コネクタ(Incoming Webhook)」は廃止予定の旧方式なので、いま新しく作るなら Workflows 側を使います。メッセージの書式は Adaptive Card という JSON のカード形式で表現し、上の骨格にフィールドを足していけば、デプロイ通知・障害通知には十分です。注意点は一つ、Webhook の URL 自体が秘密だということです。URL を知る誰もがチャネルに書き込めるので、シークレットと同じ等級で管理します。CI のデプロイ通知、監視のアラートのような一方向の通知の大半はこの経路で十分で、実際これが最も広く使われる形です。

経路 ③ ボット (Bot Framework) — 対話型が必要なとき #

ユーザーのメンションに応答し、ボタンの入力を受け、個人の DM(proactive message)を送るレベルになると、Graph でも Webhook でもなくボットの領域です。Bot Framework(Azure Bot Service)でボットを作って Teams アプリとしてインストールする経路で、別の開発モデル(アクティビティ、ターンコンテキスト)を持つ独立した一つのテーマなので、このシリーズの範囲を超えます。ここでは「要件が対話型になったらボットへ移る時点」という道しるべだけ立てておきます。

選び方の整理 #

要件経路コスト
一方向の通知 (デプロイ・アラート・レポート)② Workflows WebhookURL 管理だけ。最短
Graph のデータと結合、スレッド返信、アカウント名義の制御① delegated 自動化アカウントアカウント・トークンの運用
メンション応答、ボタン、DM — 対話型③ ボット別の開発モデル

もう一つ、チャット(chat、1:1・グループ会話)側の送信も delegated 中心だという点はチャネルと同じです。「特定のユーザーに DM で通知」という要件もよく出ますが、その定石はボットの proactive message で、簡単な代替としては第 4 回のメール送信が今も有効な答えだ、というのも実務の感覚です。

まとめ #

  • Teams の読み取り(チーム・チャネル・メッセージ)は既存の骨格のままできます。大量の収集は第 7 回の delta が定石です。
  • チャネルメッセージ送信の application 権限(Teamwork.Migrate.All)は過去の会話の移行専用です。client credentials で送るリアルタイム通知は設計上提供されておらず、403 は回避の対象ではなく経路を替えろという信号です。
  • 一方向の通知は Workflows の受信 Webhook が最短経路です。アプリ登録なしの POST 一回で、代わりに Webhook の URL をシークレットとして管理します。
  • Graph のデータと絡む送信が必要なら delegated + 通知専用アカウント、対話型が必要なら Bot Framework へ移ります。
  • 古い資料の Office 365 コネクタの Webhook は旧式の経路です。新しく作るなら Workflows です。

次回がシリーズの最終回、運用編です。スロットリング(429)、ページネーション、ポーリングを置き換える delta クエリと変更通知(Webhook のサブスクリプション)を扱います。

X