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内でそれを読み取り内容を書き換えて送信するように作成しています。

Power AutomateのTeams投稿でAdaptive Cardを使ってみた

今回使用するファイルやサービス

adaptive.jsonの中身は、Designerを利用して作成しています。色々なテンプレートが用意されているので、そこから挙動を学ぶ事も可能です。今回は割りとシンプルなデザインとして、ボタンなどは利用せず、通知を主としたものにしています。

事前準備

Azureプロジェクトの準備

Microsoft AzureにGraph APIを利用するプロジェクトを作成する必要があります。下記のエントリーを参考に準備しましょう。Scopeについては「openid, Profile、User.Read、Files.ReadWrite、offline_access、ChannelMessage.Send」があればOKです。管理者の承認は不要です。

Access Tokenを取得する認証系のコードなども記載していますので、Node.jsのプロジェクトに追加しましょう。

electronでAzure AD認証を行い、Graph APIを叩く – 準備編

対象チャンネルのURLを取得する

Graph APIで叩く場合に必要です。このURLの取得はいたって簡単。

  1. ChromeでTeamsにログインする
  2. Teamsの対象のチャンネルを開く
  3. チャンネル横の「…」をクリックして、「チャンネルへのリンクを取得」をクリック
  4. コピーをクリックする
  5. あとで関数でこのURLからgroupIdChannelIdを取り出すようにしています。

今回使用するモジュール

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ファイルを書き換えましょう。

Power AutomateのTeams投稿でAdaptive Cardを使ってみた

図:キレイなカード形式の情報が投稿されました

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での送信テスト

図:送信結果

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)