PythonでTeams電話の操作を自動化してみる

# やりたいこと
やりたいことはシンプルです。
Teams通話の**転送先**を、出勤時に「**通話グループまたは代理人**」に設定し、退勤時は「**ボイスメール**」に戻す、というもの。
実際にはアカウント毎に各チーム分、この操作が必要になるため、それが毎日となると負担も大きいわけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/1c1aaf56-4939-612e-38a0-262f4ad2059b.png)
要は留守電なので、下の動画で解説してる方法でも実現できそうですが、Teams（というかマイクロソフト製品全般）にそこまで詳しくない上に、マイクロソフトのパートナー会社に聞くこともできず、よく分かりません。

https://youtu.be/HXFvTrPEGLY?t=500

Teamsの設定をコマンドで行うなら、PowerShellから [Connect-MicrosoftTeams](https://docs.microsoft.com/en-us/powershell/module/teams/connect-microsoftteams) で接続しゴニョゴニョするのが定番と思うのですが、**転送先**に相当するオブジェクト／プロパティが見つからないのです。探し方が悪いだけかも。

ちまちま調べるより、サーバで書いちゃった方が早そうなので、Python/Flaskベースで「Teams留守電切替アプリ」を作ってみました。

# 実装
## サインイン
Teamsはデスクトップ版もありますが、[Selenium](https://selenium-python.readthedocs.io)でWeb版Teamsにサインインします。

```python
driver.get('https://teams.microsoft.com')
WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located)
driver.implicitly_wait(30)
driver.find_element_by_id('i0116').send_keys(user_id)  # ユーザID
driver.find_element_by_id('idSIButton9').click()       # 次へ
driver.find_element_by_id('i0118').send_keys(passwd)   # パスワード
time.sleep(3)
driver.find_element_by_id('idSIButton9').click()       # サインイン
```

## ２要素認証
２要素認証が必須のアカウントでは [pyotp](https://pypi.org/project/pyotp) でワンタイムパスワードを生成します。

```python
import pyotp

totp = pyotp.TOTP(secret_key)
driver.find_element_by_id('idTxtBx_SAOTCC_OTC').send_keys(totp.now())  # コード
driver.find_element_by_id('idSubmit_SAOTCC_Continue').click()  # 検証
```

## エラー時の再試行
もしこんな画面になったら、再試行をクリックさせます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/7c4671fe-5b44-55ce-9dd6-61805ee9cb4e.png)

```python
oops_btn = driver.find_elements_by_class_name('oops-button')
if len(oops_btn) > 0:
	driver.execute_script('arguments[0].click()', oops_btn[0])
```

## 通話設定画面
通話設定の画面を出します。

```python
driver.find_element_by_id('settings-menu-button').click()  # 設定
driver.switch_to.active_element.send_keys(Keys.ENTER)
driver.find_element_by_xpath('//*[@id="options-dialog-focus-default"]/div/span[text()="通話"]').click()  # 通話
driver.find_element_by_id('immediate-dropdown-btn').click()  # 転送先ドロップダウン
```

入力に応じて転送先を切り替えます。

```python
if voice_flag:
	driver.find_element_by_xpath('/html/body/ul/li/a[text()="ボイス メール"]').click()  # 転送先をボイスメールに設定
	print('Switch to enable voice mail')
else:
	driver.find_element_by_xpath('/html/body/ul/li/a[text()="' + forward + '"]').click()  # 転送先を通話グループ/代理人に設定
	print('Switch to disable voice mail')
```

## 例外処理
何か異常があれば、スクリーンショットを取り、スタックトレースの文字情報を重ね合わせた上でフロントエンドに返します。
再試行するか手動で操作するかは、ユーザがWebアプリケーションから応答します。
画像の加工は [opencv-python](https://pypi.org/project/opencv-python) で行いました。

```python
import cv2
import traceback

except Exception as e:
	driver.save_screenshot(screenshot_file)
	img = cv2.imread(screenshot_file)
	exc = traceback.format_exc()
	for i, line in enumerate(exc.split('\n')):
		cv2.putText(img, line, (10, 100 + i * 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 160, 240), 1, cv2.LINE_AA)
	cv2.imwrite(screenshot_file, img)
	print(exc)
```

成功／失敗を判定し、[いらすとや](https://www.irasutoya.com)で拾った透過画像をマスク処理してスクリーンショットと合成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/3f6a5f6c-8f23-c11e-1805-e4a10f98566e.png)
画像の合成は、こちらの記事を参考にさせていただきました。

https://qiita.com/mo256man/items/f7524dd34718a01fb3df

## 失敗の通知
[Incoming Webhook](https://docs.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) でWebフックを作成し、`curl`コマンドで通知しました。

```bash:Bashスクリプト
FLAG=$1
while read CLIENT; do
	if /usr/local/bin/foo.py $CLIENT $FLAG; then
		echo "$CLIENT($FLAG) Success."
	else
		if [ $FLAG -eq 0 ]; then
			TEXT="留守電の解除に失敗しました。クライアントは「$CLIENT」です。"
		else
			TEXT="留守電の登録に失敗しました。クライアントは「$CLIENT」です。"
		fi
		curl -X POST -d '{"text":"'$TEXT'"}' https://foo.webhook.office.com/webhookb2/XXXXXX
	fi
done <<EOF
foo
bar
baz
qux
quux
corge
grault
EOF
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/86583c79-a4d9-20f3-6a8e-4ca9cba081bb.png)