🤖

Microsoft Teamsで(Bot frameworkを使わずに)ボットを作るには

2023/02/07に公開

Microsoft Teamsの連携はSlackと比べると本当に使いづらい。

過去にQiitaで書いてたように、たとえばユーザメッセージを投稿するだけでも管理者の承認が必要だ。
https://qiita.com/YusukeIwaki/items/130f4bad0ee519232adb

今回は、Slackだと一瞬でできるボットユーザの作成がMicrosoft Teamsだとどのように達成できるかを書いてみる。 本当に骨が折れる。Microsoft Teams使いたくない。

そもそもボットが必要か?

たとえば

  • GitHub ActionsからMicrosoft Teamsへデプロイ完了通知をしたい
  • 定期リマインダーをMicrosoft Teamsへ表示したい

とかそういう用途であれば、ボットではなくIncoming Webhookを使えばいい。(これならSlackのときと手間が大して変わらない)

あるいは

  • メンションされたら予め持っている答えを返すだけのbot
  • アプリケーションのデプロイを受け付けるbot

など、5秒以内に応答ができるbotであればOutgoing Webhookを使うとよい。

Microsoft Teamsの発言を拾う(返信はしない)機能だったり、メンションを受け付けて外部APIを叩いて結果を返すような何かをする(ようは、5秒以上時間をかけて応答する)機能が必要なときには、ボットが必要になる。

https://learn.microsoft.com/en-us/microsoftteams/platform/sbs-gs-bot?tabs=vscode%2Cviscode

全体の流れ

のまえに、少しだけまえおきをすると・・・

「とりあえずBot frameworkを使え」と書いてるページが本当に多い。マイクロソフトの公式ページのチュートリアルはまず間違いなくそうだ。ただ、Slackボットを作るのに「まずhubotを入れましょう」と言われるのと同じような違和感で、ボットを作りたい欲求の根源は、べつにそんな得体のしれないフレームワークを学習して使いたいわけではなく、業務効率化をしたいのだ。

ということで、今回はBot frameworkを使わない素の仕組みにフォーカスする。ざっくりとした流れは以下の絵のとおりだ。

必要なもの

  • Azure Bot
  • 認証アプリ (Azure AD Registered App)
    • GraphAPIのOAuth2認証のときにも使ったやつ。
    • 今回はBot connector APIの認証でJWTトークンを発行するために使う
  • バックエンドのWebサービス
    • HerokuでExpressとかSinatraとかをホストすればいい
    • もちろん AWS API Gateway + AWS Lambda や Google App Scriptでもいい
  • ワーカー
    • AWS Lambdaとか、Redis Queue的なやつとか
    • Sinatra+Pumaなら雑にThread使うでもいい
    • どのくらい処理時間がかかるかによってインフラを決めればいい
  • 情シス部門とのコネ
    • Microsoft Teamsでメンション可能な状態にするには、作成したアプリパッケージを管理者に承認操作してもらわないといけないのだ!!

まずはbotユーザをつくるぞ

順を追って雑に解説していく。

まずは、Azure Botを作るところから。これは何かというと、Botのゲートウェイサービスてきなもの。

ボットとして、いろんなサービスの相手を請け負ってくれる役割のもの。以下のページに、具体的にどんなサービスの相手ができるかが書かれている。

https://learn.microsoft.com/en-us/azure/bot-service/bot-service-manage-channels?view=azure-bot-service-4.0

コンソールでポチポチしてボットユーザーをつくる

Azure Bot のリソースを作る。

↓このあたりは、他のAzureサービスと同様。

んで、いきなり「なに?」となるのがここ。

これは、「誰が使えるアプリですか?」と聞かれている。
自分の会社のMicrosoft Teamsでだけ使いたいならSingle Tenant、他の会社のMicrosoft 365ユーザの皆様方にも使ってもらうようなボットを作るならMulti Tenant。

今回は、自分たちの業務効率化をしたいので、Single Tenantを選択することになる。

Microsoft App IDを作る?既存の使う?というのは、どちらでもよい。これは何かというと、バックエンドWebサービスからAzure Botに対してメッセージ送信要求を出すときの認証トークン発行に使うもの。

自前で作っても手順はこれだけなので、勉強したい人や勝手に作られるのが不安な人は自分で作ったほうが良いだろう。

あとはcreateするだけ。

バックエンドのサーバーを用意する

これは本当になんでもいい。

今回は、Ruby+Sinatraで組んでngrokで外からアクセスできる形にして試してみる。

app.rb
require 'sinatra/base'

class App < Sinatra::Base
  get '/' do
    'It works!'
  end

  post '/webhook' do
    json_body = JSON.parse(request.body.read)
    puts "Parameter: #{json_body}"

    # HTTP 200 OKならなんでもよい
    status 200
    content_type :json
    { status: 200 }.to_json
  end
end
config.ru
require './app'
run App
% bundle exec rackup -p 3000 
Puma starting in single mode...
* Puma version: 6.0.2 (ruby 3.2.0-p0) ("Sunflower")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 88625
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

Herokuに上げる前に一旦ngrokで動作確認をする。

% ngrok http 3000

           
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://6335-240d-1e-1d8-df00-bc7a-fa20-f211-938c.ngrok.io -> http://localhost:3000
Forwarding                    https://6335-240d-1e-1d8-df00-bc7a-fa20-f211-938c.ngrok.io -> http://localhost:3000
% curl https://6335-240d-1e-1d8-df00-bc7a-fa20-f211-938c.ngrok.io
It works!

Azure BotのConfigurationでバックエンドサービスのURLを指定する↓

Test in WebChatを使うと、デバッグ用にメッセージを送ることができる↓

こんなかんじのJSONが送られてくる。

13.94.37.83 - - [07/Feb/2023:01:50:40 +0900] "POST /webhook HTTP/1.1" 200 14 0.0107 Parameter: {"type"=>"message", "id"=>"zbBkgDsnZRHuTvv5PuUot-as|0000000", "timestamp"=>"2023-02-06T16:50:51.440134Z", "localTimestamp"=>"2023-02-07T01:50:51.299+09:00", "localTimezone"=>"Asia/Tokyo", "serviceUrl"=>"https://webchat.botframework.com/", "channelId"=>"webchat", "from"=>{"id"=>"541478d2-6a03-47b3-aeb7-8675cdd45db3", "name"=>""}, "conversation"=>{"id"=>"zbBkgDsnZRHuTvv5PuUot-as"}, "recipient"=>{"id"=>"yusukeiwaki-bottest@sbLpzi0JPOw", "name"=>"yusukeiwaki-bottest"}, "textFormat"=>"plain", "locale"=>"en-US", "text"=>"こんにちは!", "attachments"=>[], "entities"=>[{"type"=>"ClientCapabilities", "requiresBotState"=>true, "supportsListening"=>true, "supportsTts"=>true}], "channelData"=>{"clientActivityID"=>"16757022512893zxseyj4zj4"}}

ここまでで、Outgoing Webhook的なものは作れた。で、問題は どうやったらチャットメッセージに返信できるの? というところ。次の章で説明する。

やってきたメッセージに返信をする

この記事に実は全貌が書いてある。

https://learn.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bot-conversations/bots-conversations

ただ、メッセージを返信するところの冒頭に

Sending replies to messages

To reply to an existing message, call ReplyToActivity in .NET or session.send in Node.js. The Bot Builder SDK handles all the details.

Bot Builder SDKという単語が出てくるので、なにかSDKを入れる必要があるの??と思ってしまう。そうではない。

If you choose to use the REST API, you can also call the /v3/conversations/{conversationId}/activities/{activityId} endpoint.

Bot Connector APIを使えばいいよと書かれている。まさにこれだ。

Bot connector APIを使用してメッセージに応答するぞ

Bot connector APIについてはそれ用のリファレンスが存在している。
https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-quickstart?view=azure-bot-service-4.0

先程のやってきたメッセージには serviceUrl とか conversation とかそれっぽいものが載ってきていた。

  • Conversationというのは、Teamsでいうところのスレッド、LINEでいうところの部屋
  • Activityというのは、メッセージ/発言

を表すようだ。つまり、Activityを作ればいい。

% export SERVICE_URL=https://webchat.botframework.com/
% export CONVERSATION_ID=zbBkgDsnZRHuTvv5PuUot-as
% cat reply.json 
{
  "type": "message",
  "from": {
    "id": "yusukeiwaki-bottest@sbLpzi0JPOw",
    "name": "yusukeiwaki-bottest"
  },
  "text": "My bot's reply"
}

% cat reply.json| http POST $SERVICE_URL//v3/conversations/$CONVERSATION_ID/activities 
HTTP/1.1 401 Unauthorized
ARR-Disable-Session-Affinity: true
Cache-Control: no-cache
Content-Length: 0
Date: Mon, 06 Feb 2023 17:02:00 GMT
Expires: -1
Pragma: no-cache
Strict-Transport-Security: max-age=31536000
WWW-Authenticate: Bearer realm="webchat.botframework.com"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block

おっと、401 Unauthorized。そうだ。Authorizationを指定する必要があるのだ。

Bot connector APIの認証方法はここに書かれている。

https://learn.microsoft.com/en-us/previous-versions/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-3.0

いくつか方法があるっぽいことがごちゃごちゃと書かれているが、ようするにこれでいい↓

require 'net/http'

def fetch_access_token
  # https://learn.microsoft.com/ja-jp/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0&tabs=singletenant#bot-to-connector
  resp = Net::HTTP.post_form(
    URI("https://login.microsoftonline.com/#{ENV['MICROSOFT_TENANT_ID']}/oauth2/v2.0/token"),
    {
      'grant_type' => 'client_credentials',
      'client_id' => ENV['MICROSOFT_CLIENT_ID'],
      'client_secret' => ENV['MICROSOFT_CLIENT_SECRET'],
      'scope' => 'https://api.botframework.com/.default',
    }
  )
  JSON.parse(resp.body)['access_token']
end

(あとから気づいたが、よく見たらここに書いてあった...)

Client Secretは、認証アプリの↓ここで取得する。

ちなみにAzure Botを作るときに一緒に認証情報を作った場合には、ここにすでに作られている謎の認証情報があるが、値を知るすべがないため、新しくシークレットを作る。

 % bin/console
irb(main):001:0> fetch_access_token
=> "eyJ0eXAiOiJKV1QiLCJhbGciOi........r4m1LOk6shLfKzg"     
% export SERVICE_URL=https://webchat.botframework.com/
% export CONVERSATION_ID=zbBkgDsnZRHuTvv5PuUot-as
% export ACCESS_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOi........r4m1LOk6shLfKzg
% cat reply.json 
{
  "type": "message",
  "from": {
    "id": "yusukeiwaki-bottest@sbLpzi0JPOw",
    "name": "yusukeiwaki-bottest"
  },
  "text": "My bot's reply"
}

% cat reply.json| http POST $SERVICE_URL//v3/conversations/$CONVERSATION_ID/activities Authorization:"Bearer $ACCESS_TOKEN"
HTTP/1.1 200 OK
ARR-Disable-Session-Affinity: true
Cache-Control: no-cache
Content-Encoding: gzip
Content-Length: 167
Content-Type: application/json; charset=utf-8
Date: Mon, 06 Feb 2023 17:14:42 GMT
Expires: -1
Pragma: no-cache
Strict-Transport-Security: max-age=31536000
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
x-ms-request-id: 5a229f7e318864419a987a55da30053e

{
    "id": "zbBkgDsnZRHuTvv5PuUot-as|0000001"
}

Activityが作られたっぽい!ので、先程のWebChatを見てみると、↓のようにメッセージが送られている。

そんなわけで、アプリケーションの方を少し変えて、オウム返しするようにしておこう。

app.rb
require 'net/http'
require 'sinatra/base'

def fetch_access_token
  # https://learn.microsoft.com/ja-jp/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0&tabs=singletenant#bot-to-connector
  resp = Net::HTTP.post_form(
    URI("https://login.microsoftonline.com/#{ENV['MICROSOFT_TENANT_ID']}/oauth2/v2.0/token"),
    {
      'grant_type' => 'client_credentials',
      'client_id' => ENV['MICROSOFT_CLIENT_ID'],
      'client_secret' => ENV['MICROSOFT_CLIENT_SECRET'],
      'scope' => 'https://api.botframework.com/.default',
    }
  )
  JSON.parse(resp.body)['access_token']
end

class App < Sinatra::Base
  get '/' do
    'It works!'
  end

  post '/webhook' do
    json_body = JSON.parse(request.body.read)
    puts "Parameter: #{json_body}"

    if json_body['type'] == 'message'
      Thread.new(json_body) do |params|
        service_url = params['serviceUrl']
        conversation_id = params['conversation']['id']
        access_token = fetch_access_token

        reply = {
          "type": "message",
          "from": {
            "id": "yusukeiwaki-bottest@sbLpzi0JPOw",
            "name": "yusukeiwaki-bottest"
          },
          "text": "Reply for #{params['text']}",
        }
        Net::HTTP.post(
          URI("#{service_url}v3/conversations/#{conversation_id}/activities"),
          reply.to_json,
          {
            'Content-Type' => 'application/json',
            'Authorization' => "Bearer #{access_token}"
          }
        )
      end
    end

    # HTTP 200 OKならなんでもよい
    status 200
    content_type :json
    { status: 200 }.to_json
  end
end

ここまででほとんどの人はすでに体力が0になっているであろうが、Microsoft Teamsの試練はさらに続くぞ・・・。

会社のみんなが使えるようにする

まずは、名前とアイコンを設定する。これはAzure Botのコンソールでポチポチするだけなのでとても簡単。

次に、ChannelでMicrosoft Teamsを有効化する。

有効化されると、 Open in Teams リンクが出てくる。

リンクをクリックすると、個別チャットが開き、メッセージをボットに送りつけるとレスが返ってくる。

・・・・・求めてたのはこれじゃない!!!!

↓こういうスレッドメッセージ途中でボットを呼べるからこそボットなわけで、個別に話しかけることができたところで何も嬉しくない...。

会社のみんながスレッドメッセージ上で使えるようにするには?

最後の砦ながら、これが最も心が折れる。なぜなら、どこにもやり方がまともに書いていないからだ。 マイクロソフトクオリティ〜♪

なんとなくこの辺のページを眺めてみても、個別チャットまでで「おめでとうございます」となっており、全然めでたくないんだけどなーと。
https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots

で、どこに解決の糸口があるかというと、ここ。1つ前のBot Framework v3のdebug and testのページだ。 わかるか!(╯°□°)╯︵ ┻━┻
https://learn.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-test

上記ページには、このような記載があり、どうも「app package」なるものを作ってアップロードする必要があるんだとか。

The most comprehensive way to test your bot is by creating an app package and uploading it to Teams. Uploading the app to Teams is the only method to test the full functionality available to your bot, across all scopes.

app packageを作るには、リンク先のページを見ると↓が出てくる。2023/02/07現在では、これがおそらくマイクロソフト公式の唯一の情報である。

https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package

  • アプリのマニフェスト(たぶんJSON)
  • アプリのアイコンを何種類か

作ってzipで固めたらそれがapp packageです、というのはわかるが、App Manifestに関しては、↓ここにJSON SchemaとSample full manifestがあるだけなので、マニフェストをつくるには相当なイマジネーションと忍耐力を要する。

https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema

だいたいこんな感じで指定をするとアップロードができるようになる。

{
    "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.15/MicrosoftTeams.schema.json",
    "manifestVersion": "1.15",
    "version": "0.1.0",
    "id": "96045312-2742-4288-a8bd-0644a607d176",
    "developer": {
        "name": "YusukeIwaki",
        "websiteUrl": "https://github.com/YusukeIwaki/yusukeiwaki-bottest",
        "privacyUrl": "https://github.com/YusukeIwaki/yusukeiwaki-bottest",
        "termsOfUseUrl": "https://github.com/YusukeIwaki/yusukeiwaki-bottest"
    },
    "name": {
        "short": "iwakibot1"
    },
    "description": {
        "short": "YusukeIwakiが作ったボットです",
        "full": "YusukeIwakiが作ったボットです。完全にテスト用途です。"
    },
    "icons": {
        "outline": "iwakibot1-Icon-Logo-32.png",
        "color": "iwakibot1-Icon-Logo-192.png"
    },
    "accentColor": "#000080",
    "bots": [
        {
            "botId": "ca38..........986b",
            "scopes": [
                "team",
                "personal",
                "groupchat"
            ],
            "needsChannelSelector": false,
            "isNotificationOnly": false,
            "supportsFiles": false,
            "supportsCalling": false,
            "supportsVideo": false
        }
    ],
    "permissions": [
        "identity",
        "messageTeamMembers"
    ],
    "devicePermissions": [
        "notifications"
    ],
    "validDomains": [
        "yusukeiwaki-bottest.herokuapp.com"
    ],
    "defaultInstallScope": "team",
    "defaultGroupCapability": {
        "team": "bot",
        "groupchat": "bot"
    }
}

BotIdのところは、↓これを指定する。

そして、マニフェストとアイコンをzipで固めて、

zip pkg.zip manifest.json iwakibot1-Icon-Logo-192.png iwakibot1-Icon-Logo-32.png

Microsoft Teams上で、「アプリ」からカスタムアプリをアップロードする。

これで任務完了・・・ではない

情シス部門の人にお願いする

Microsoft Teamsの"アプリ"の制限として、管理者の承認が必要だ。

社内でMicrosoft 365アカウントを管理している人に事情を話して、このアプリを承認してもらう必要がある。(もしも情シス部門がお堅くて、Slack botなどの知見がない人が担当だったりすると、このプロセスは結構大変なものになる)

操作自体はトグルボタンをAllowにするだけらしいのだが、承認されてから「組織向けに開発」というメニューが出現するまで30分〜1時間ほどかかることがある。(Microsoft TeamsアプリではなくブラウザからTeamsへアクセスするとすぐに見えたりもする)

カスタムアプリの公開については↓にドキュメントがあるわけだが、全く役に立たない。30分待たないと公開されないよとせめて一言でも書いてあれば・・・
https://learn.microsoft.com/en-us/microsoftteams/teams-custom-app-policies-and-settings

そんなわけで、承認後しばらく待って「組織向けに開発」タブが現れたらあとは、好きなTeamにインストールすると、ボットに対するメンションができるようになる。

まとめ

Microsoft Teams上でボットを作るには、ボット作成からMicrosoft Teams上でメンションができる状態になるまでの一気通貫のドキュメントが1つもない。なので、断片的なドキュメントをあちこち見て回る必要がありとにかく骨が折れる。
ボットを作って業務効率化をしたいのに、断片的なドキュメントを読み漁り&Microsoft 365の管理者承認が必要な時点で、業務効率化意識がだいぶ低まってしまうだろう。

ボットを有効活用するならやっぱりSlackを使うといいと思う。

Discussion