Node.jsでTeamsにAdaptive Cardを送信する
先日、Power AutomateでのTeams送信でAdaptive Cardの送信を実現しましたが、次にNode.js(Electron)のアプリからGraph APIを利用して送信する必要性が出てきたので、その手法を実装中です。利用するためには、node-fetchを利用し、APIを叩く必要があるのですが、Adaptive Cardを送信するには少しテクニックが必要でしたので、ここでまとめて置こうと思います。
Adaptive Cardの本体部分については、adaptive.jsonというファイルとして用意し、Node.js内でそれを読み取り内容を書き換えて送信するように作成しています。
目次
今回使用するファイルやサービス
adaptive.jsonの中身は、Designerを利用して作成しています。色々なテンプレートが用意されているので、そこから挙動を学ぶ事も可能です。今回は割りとシンプルなデザインとして、ボタンなどは利用せず、通知を主としたものにしています。
事前準備
Azureプロジェクトの準備
Microsoft AzureにGraph APIを利用するプロジェクトを作成する必要があります。下記のエントリーを参考に準備しましょう。Scopeについては「openid, Profile、
」があればOKです。管理者の承認は不要です。Access Tokenを取得する認証系のコードなども記載していますので、Node.jsのプロジェクトに追加しましょう。
対象チャンネルのURLを取得する
Graph APIで叩く場合に必要です。このURLの取得はいたって簡単。
- ChromeでTeamsにログインする
- Teamsの対象のチャンネルを開く
- チャンネル横の「…」をクリックして、「チャンネルへのリンクを取得」をクリック
- コピーをクリックする
- あとで関数でこのURLからgroupIdとChannelIdを取り出すようにしています。
今回使用するモジュール
Node.jsでGraph APIを叩くために必要なモジュールは以下の通りnpm installでNodeのプロジェクトにインストールしておきましょう。Proxyを経由する場合には、proxy-agentモジュールも必要になります。
node-fetchでVersion 2.6.6を指定してる理由は、最新のNode-fetchは、ES Module形式でしか提供していないため、素のJSでは動作しない為。
ソースコード
adaptive.jsonのコード
{ "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.4", "body": [ { "speak": "傷病台帳新規データ登録通知", "type": "ColumnSet", "columns": [ { "type": "Column", "width": 2, "items": [ { "type": "TextBlock", "text": "empid", "wrap": true }, { "type": "TextBlock", "text": "username", "weight": "Bolder", "size": "ExtraLarge", "spacing": "None", "wrap": true }, { "type": "TextBlock", "text": "compname", "isSubtle": true, "spacing": "None", "wrap": true }, { "type": "TextBlock", "text": "台帳に新規にデータが追加登録されました。確認しましょう。", "size": "Small", "wrap": true, "maxLines": 3 } ] }, { "type": "Column", "width": 1, "items": [ { "type": "Image", "url": "https://icons.iconarchive.com/icons/icojam/blue-bits/128/profile-add-icon.png" } ] } ] } ], "msteams": { "width": "Full" } }
- テンプレートなので、empidやempnameなどの項目はあとでNode.js側で置き換えます。
- JSON形式ではあるのですが、JSON.parseして値を書き換えて、送信時にJSON.stringifyして送るとエラーとなるため、このデータはJSONとしては扱いません。(ここ嵌まりました)
Node.jsのコード
実際には、passportやpassport-azure-adを用いた、Access Tokenの取得やTokenリフレッシュ等のコードも用いていますが、以下では主要な送信部分のみを掲載しています。Token関係の処理は前述のエントリーに記載がありますので、参考にして実装しましょう(自分の場合、Token関係は暗号化して保存し、復号化して読み込みまで実装しています。)
//必要なモジュールを読み込む const fs = require('fs'); const fetch = require('node-fetch'); //プロキシーの設定 const proxyUri = "ここにプロキシーのURLを入れる"; const agent = new ProxyAgent(proxyUri); //TeamsへAdaptive Cardを送信する関数 function sendTeams(access_token, callback) { //送信用の変数を用意 let teamspoint = "https://graph.microsoft.com/beta/teams/"; //送信エンドポイント let teamsurl = "ここにTeamsチャンネルのURLを入れる" //groupidを取り出す let name = "groupId" let groupId = getParam(name, teamsurl); //channelIdを取り出す let channelId = getParam2(teamsurl); //channelnameを取得する let channelname = "General"; //リクエストURLを構築 let url = teamspoint + groupId + "/channels/" + channelId + "/messages" //リクエストヘッダ let headers = { Authorization: "Bearer " + access_token, 'Content-Type': 'application/json' } //adaptive.jsonを読み込み let adaptive = __dirname + '/js/adaptive.json' let adjson = String(fs.readFileSync(adaptive, 'utf8')); //JSONデータの書き換え adjson = adjson.replace(/compname/g,"会社名"); adjson = adjson.replace(/empid/g,"社員ID"); adjson = adjson.replace(/username/g,"社員名"); adjson = adjson.replace(/\r?\n/g,''); //改行コードを削除する //リクエストBody let bodyman = { "subject":"台帳新規データ追加通知", "importance":"high", "body": { "contentType": "html", "content": "<at id = '0'>" + channelname + "</at><br><attachment id='4465B062-EE1C-4E0F-B944-3B7AF61EAF40'></attachment>" }, "attachments": [ { "id": "4465B062-EE1C-4E0F-B944-3B7AF61EAF40", "contentType": "application/vnd.microsoft.card.adaptive", "content":adjson } ], "mentions": [ { "id": "0", "mentionText": channelname, "mentioned": { "conversation": { "id": channelId, "displayName": channelname, "conversationIdentityType": "channel" } } } ] } //リクエストオプション let options = { method: 'post', agent: agent, headers: headers, body: JSON.stringify(bodyman), } //node-fetchでリクエスト let status = ""; fetch(url, options) .then((res) => { //ステータスコードを取得 status = res.status; //body部分を取得 return res; }).then((jsonData) => { if (status == 201) { //Quotaに引っかからない為にウェイトを入れる setTimeout(function () { callback(["OK", "送信完了"]); }, 500); }else{ //エラーコードとメッセージを取得 new Promise((resolve, reject) => { let word = jsonData.json(); resolve(word); }).then((ret) => { //エラーメッセージを取り出す let msg = ret.error.message; //エラーメッセージ callback(["NG", status + "エラー:\n" + msg]); }).catch(e => { //エラーメッセージ callback(["NG", status + "エラー:\n" + e]); }) } }).catch((err) => { //エラーメッセージ callback(["NG", err]); return; }); } //URLパラメータからGroudIDを取り出す function getParam(name, url) { // パラメータを格納する用の配列を用意 let paramArray = []; // URLにパラメータが存在する場合 name = name.replace(/[\[\]]/g, "\\$&"); let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; //パラメータを返す return decodeURIComponent(results[2].replace(/\+/g, " ")); } //URLパラメータからchannelIDを取り出す function getParam2(url) { //URLのparse let parser = new URL(url); let pathman = parser.pathname; //配列に区切ってchannelIDのところだけ取得 let array = pathman.split('/'); let channelId = array[3]; //パラメータを返す return decodeURIComponent(channelId.replace(/\+/g, " ")); }
- TeamsのURLからgroupId(teamsId)と、channelIdの2つをそれぞれ取り出す為のgetParam,getParam2の2つの関数を用意しておきます。
- sendTeams関数は引数として、呼び出し元で用意したaccess_tokenを受け取ってますが、実際には送信内容を書き換える為のデータ等も引数で用意する必要があります(会社名や社員名等)
- 今回はプロキシ経由用にagentを用意してリクエストオプションに含めています
- adaptive.jsonは文字コードはUTF8である必要があります。またfs.readFileSyncで読み取ってもJSON.parseはしません
- JSON.parseしていないので、直接replaceにて正規表現でcompnameなどの文字列を置換します。
- bodymanのIDは適当で問題なし。contentには置換作業後のJSONデータを付加する
- リクエストオプションでは、bodymanをJSON.stringifyするのを忘れずに
- node-fetchにてPOSTでリクエストを送信。成功時には201が返ってくる。
- リクエスト成功時に、連続してTeamsに送信する場合を想定して、Teamsの送信制限に掛からないように500msのウェイトを入れてから、callbackで返しています。
実行結果
今回はボタンなどは付けていないので、シンプルに以下のようなカードが表示されます。Teamsなのでカードバージョンは1.4まで対応しています。カードをより派手に見せたい場合は、以下のエントリーを参考にadaptive.jsonファイルを書き換えましょう。
図:キレイなカード形式の情報が投稿されました
Graph Explorerでテストする
前述でも記述した内容になりますが、Graph APIにてAdaptive Cardの情報を送る場合、attachmentのセクションの中のcontentにadaptive.jsonの内容を記述して送信するわけなのですが、素のJSONで送ると「Invalid request body was sent.」であったり、「Failed to process content in attachment with Id ''.」といったようなエラーが出て送信できません。
Graph Explorerでテストをする場合には、以下のように、contentの内容はダブルコーテーションをバックスラッシュでエスケープした内容でなければならないので、テストする場合にはよくよく注意が必要です(プログラムの場合は、JSON.stringifyしてから送ってるので問題にならない)。
{ "body": { "contentType": "html", "content": "<attachment id=\"1\"/>" }, "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "id": "1", "content": "{\"type\":\"AdaptiveCard\",\"body\":[{\"text\":\"<at>Tomato</at> へろーわーるど\",\"wrap\":true,\"type\":\"TextBlock\"}],\"version\":\"1.4\",\"msteams\":{\"entities\":[{\"type\":\"mention\",\"text\":\"<at>Tomato</at>\",\"mentioned\":{\"id\":\"29:131...Rg\",\"name\":\"Tomato\"}}]}}" } ] }
図:Graph Explorerでの送信テスト
図:送信結果
関連リンク
- Using a Power Automate flow, how do I convert JSON array to a delimited string?
- Convert JSON array to string array
- Repeating items in Teams adaptive cards
- Samples and Templates
- Bot Builder v4 でボット開発 : ダイアログから高度なアダプティブカードを送る
- how to send an adaptive card through microsoft Graph API
- graph-api: Adaptive card with mention doesn't render date properly
- Using MS Graph Explorer to Send a Adaptive Card to a Channel - Can I Remove the Padding at the Top?
- Send an adaptive card through Microsoft Graph API - Error when getting Submit Action
- Why Adaptive card is always showing error something went wrong please try again on submit?
- How to retrieve Adaptive Card data on Microsoft Teams bot?
- Adaptive card in teams not workig - REST API
- How to read the attachment of sharepoint online lists using the Graph API
- javascript - エラー:ノードフェッチのインポート時にESモジュールのrequire()はサポートされていません