Microsoft Teamsの非公式APIを使って自作Stream Deck

# はじめに

Microsoft Teamsを見ると、こんな設定項目がありました。
「アカウントの管理」-> 「プライバシー」の部分に。。

![スクリーンショット 2023-04-06 20.44.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430353/fac8186c-2a07-cc4d-fb95-433e24cded11.png)

気になって調べた所、Stream Deckなどと連携させる事が出来るようでした。
参考記事
https://pc.watch.impress.co.jp/docs/news/1475659.html

私は、Stream Deckを持っていないけど。。
試したいと思い、サードパーティ製APIを一般人でも使えないか調べると。。

海外の方が既にまとめてくれてました🙏
https://lostdomain.notion.site/Microsoft-Teams-WebSocket-API-5c042838bc3e4731bdfe679e864ab52a

という事で実際に使ってみたので、まとめました。

**本記事の内容を実施する際には、あくまで自己責任で実施してくださいー**
例外処理とか全然やってないですし、そもそも会社に怒られ・・・

TeamsのMTG情報がObnizに表示されている様子
<img width="400" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430353/19a2e570-8f99-7090-b02c-4ab8926a88c7.jpeg">

Obnizでボタンを押下し、ミュートが切り替わる様子
( Teamsクライアントの映像ないですが、切り替わってます )
![Untitled.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430353/8f847098-7009-8f19-2169-663ff7084bd0.gif)



# 使ったもの
- Obniz
    - Teams MTGの状況を表示
    - ボタン操作でミュート切り替え
- Mac
    - node.jsのプログラム実行環境

# 主な手順
- Step0
    - 実行環境の整備
- Step1
    - WebsocketでTeams MTGのステータスを受け取る
- Step2
    - 上記の情報をObnizへ表示する
- Step3
    - +α ObnizのボタンでTeams MTGを操作する

## 実装してみる

それぞれ説明してますが、面倒な人は最後にコードの全文載せてるので、
一番下までスクロールしてくださいー！

## Step0

node.jsの websocketライブラリやObnizライブラリをインストールする。
Nodeの実行環境はある前提で・・
```
npm install websocket
npm install obniz
```

## Step1
Websocketを使い非公式APIでTeamsの情報を取得する。

```
var client = new WebSocketClient()
//ws接続成功時
client.on('connect', function(connect) {
    ~~中略~~
    connection.on('message', function(message) {
        if (message.type === 'utf8') {
			let str2json = JSON.parse(message.utf8Data)
			console.log(str2json)
			let canLeave = str2json.meetingUpdate.meetingPermissions.canLeave
			let teams_meetingState = str2json.meetingUpdate.meetingState
			let muted = teams_meetingState.isMuted ? "muted" : "unmuted"
			let camera = teams_meetingState.isCameraOn ? "camera on" : "camera off"
			let recording = teams_meetingState.isRecordingOn ? "Recording\n" : ""
			let raise_hand = teams_meetingState.isHandRaised ? "RaiseYourHand" : ""
			let display_str = `${muted}\n${camera}\n${recording}${raise_hand}`
            ~~以下略~~
```

以下のフォーマットでTeamsから情報が飛んできます。
<details><summary>サンプルデータ</summary>


```
{
   "apiVersion":"1.0.0",
   "meetingUpdate":{
      "meetingState":{
         "isMuted":false,
         "isCameraOn":true,
         "isHandRaised":false,
         "isInMeeting":false,
         "isRecordingOn":false,
         "isBackgroundBlurred":false
      },
      "meetingPermissions":{
         "canToggleMute":false,
         "canToggleVideo":true,
         "canToggleHand":false,
         "canToggleBlur":false,
         "canToggleRecord":false,
         "canLeave":false,
         "canReact":false
      }
   }
}
```
</details>

今回はObniz上に表示したい以下の情報を取得してます。
また、Obniz上で表示をするために、出力用の文字列も併せて定義しています。
e.g. isMutedがtrueの場合、muted
- isMuted
    - 自身がミュートの場合、True
- isCameraOn
    - 自身がカメラオンの場合、True
- isRecordingOn
    - 参加している会議が録画中の場合、True
- isHandRaised
    - 自身が挙手中の場合、True
## Step2

obnizのライブラリを使い、以下のように表示を行います。
canLeaveが、
trueの場合 => MTGに参加中のため、ミュート等のステータスを表示
falseの場合 => そもそもMTGに参加していない状態なので、 Not in MTGを表示します。

```
			if(canLeave){
				obniz.display.print(display_str)
				in_mtg = true
			}else{
				obniz.display.print('Teams Status \n Not in MTG')
				in_mtg = false
			}
```

 ## Step3

Obnizのボタンが押された際に、ミュートの状態を反転させる。
今回は、ミュート状態を切り替えてますが、リアクションをしたり、MTGから強制脱出することも可能です。

非公式APIでは、現在の状態を反転させることができるようなので、
Obnizのボタンが押されたタイミングでミュート状態を切り替えています。
manufactuerやdeviceは適当にOriginalを入れてます。
```
	button.onchange = function(voltage) {
	if(voltage & in_mtg){
		//現在とミュート・アンミュートを入れ替える。
		console.log("push")
		let now = Date.now()
		connection.sendUTF(`{"apiVersion":"1.0.0","service":"toggle-mute","action":"toggle-mute","manufacturer":"Original","device":"Original","timestamp":${now}}`)
	}
	}
```

# 最後に

今回Teams 非公式APIでStream Deckっぽいものを作りましたが、
ちゃんと動作するようで個人的には満足でした。
ボタンをもう少し用意する事ができれば、本家のStream Deck並みに操作する事ができると思います。

あくまで個人用での作成ですので、自身で使う際には会社等に怒られないようにしてください。
( 自己責任でお願いします🙏 )

<details><summary>サンプルコード</summary>


```
const Obniz = require("obniz")

let obniz = new Obniz("{{YOUR_Obniz_ID")
let teams_ws_url = "ws://localhost:8124?token={{YOUR_Teams_Token}}&protocol-version=1.0.0&manufacturer=Original&device=Original&app=Original&app-version=1.4"

let WebSocketClient =require('websocket').client

//ws connection
let connection = null
let in_mtg = false
//obniz接続
obniz.onconnect = async function () {
	obniz.display.clear()
	obniz.display.print('Teams Status \n Not in MTG')
	var button = obniz.wired("Keyestudio_Button", {signal:0, vcc:1, gnd:2});
	button.onchange = function(voltage) {
	if(voltage & in_mtg){
		//現在とミュート・アンミュートを入れ替える。
		console.log("push")
		let now = Date.now()
		connection.sendUTF(`{"apiVersion":"1.0.0","service":"toggle-mute","action":"toggle-mute","manufacturer":"Original","device":"Original","timestamp":${now}}`)
	}
	}
}



var client = new WebSocketClient()
//ws接続失敗時
client.on('connectFailed', function(error) {
    console.log('Connect Error: ' + error.toString())
});

//ws接続成功時
client.on('connect', function(connect) {
    console.log('WebSocket Client Connected')
	connection = connect
    connection.on('error', function(error) {
        console.log("Connection Error: " + error.toString())
    });
    connection.on('close', function() {
        console.log('WebSocket Client Closed')
    });
    connection.on('message', function(message) {
        if (message.type === 'utf8') {
			let str2json = JSON.parse(message.utf8Data)
			console.log(str2json)
			let canLeave = str2json.meetingUpdate.meetingPermissions.canLeave
			let teams_meetingState = str2json.meetingUpdate.meetingState
			let muted = teams_meetingState.isMuted ? "muted" : "unmuted"
			let camera = teams_meetingState.isCameraOn ? "camera on" : "camera off"
			let recording = teams_meetingState.isRecordingOn ? "Recording\n" : ""
			let raise_hand = teams_meetingState.isHandRaised ? "RaiseYourHand" : ""
			let display_str = `${muted}\n${camera}\n${recording}${raise_hand}`
			obniz.display.clear()
			if(canLeave){
				obniz.display.print(display_str)
				in_mtg = true
			}else{
				obniz.display.print('Teams Status \n Not in MTG')
				in_mtg = false
			}
			
			//console.log(str2json.meetingUpdate)
            console.log("Received: Data")
        }
    });
        
});

client.connect(teams_ws_url, 'echo-protocol')
```
</details>