Teamsの個人チャットへ自動送信する（２段階認証回避）

# はじめに
　お久しぶりです。プロコン間に合いませんでした。次はきっと間に合わせます（次があったら）。
　今回の記事は、Teamsの個人チャットへ、メッセージを自動送信するプログラムの作成について書いたものです。バイト先で必要になったので、作りました。
　意外と躓きポイントがあったので大変でした。躓いたのは、ログイン（2段階認証）と、seleniumの要素の取得（classでfindする時に空白を含んではいけない）です。一応、githubにプログラムを公開しておきます。使う人がいればですが、好きに使ってください。

# 概要
　本記事で解説する手法は、「Teamsの個人チャットへメッセージを自動送信する」ものである。
　従来のTeasmへ自動送信する手段として<i>Incoming Webhook</i>[1]というツールがあるが、チーム単位でしか操作できない特徴があり、自動送信される相手はそのチームに参加する必要がある。
　本手法では「Selenium」と「起動済みのchorme」を用いることで、そういった縛りを受けず、個人へのメッセージ送信を可能とした。起動済みのchromeを用いるのは、seleniumの操作時に2段階認証のステップを回避する為である。
　本手法のデメリットとしては、保守管理のコストである。Web版のアプリを、無理やりSeleniumで操作している為、Teamsの更新に伴ってプログラムを更新する必要がある。おそらく、Teamsの開発者にとっては、自由に自動操作を行って欲しくないのだろう。

※2021/10/03時点の記載
![webhook.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/4800a2ac-4c92-f9c6-a87e-8d1c51322763.jpeg)

[1] Microsoft Teamsのテクニカルドキュメテーション Webhook とコネクタ ,https://docs.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors

## 目的と対象者
　本記事の目的は、以下の様になっている。

* Teamsの”個人チャットへ”メッセージを自動送信する

また、以下の条件を満たしている必要がある。

* pythonを使える人
* seleniumが使える人（chromeを使える人）
* 起動済みのブラウザをSeleniumで操作できる人（[2]を参照）

[2] PythonのSeleniumを使って、起動済みのブラウザを操作する。, https://qiita.com/mimuro_syunya/items/2464cd2404b67ea5da56
## 結果の様子
　読者とのミスマッチを避けるため、実行した様子の画像を列挙してきます。以下の画像は、すべてseleniumが自動的に行っています。また、WEBブラウザ版のTeams上で動作している点にご注意ください。

①検索バーに、送信相手を特定するキーワード、を入力。
![検索1_検索バー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/6fb47451-2316-fe6d-e9f1-82442f29e36c.jpeg)
②検索結果が表示される。
![検索2_検索結果.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/5631dcb8-1476-0e72-b7ff-bcc06b72face.jpeg)
③タブ「ユーザー」をクリックする。
![検索3_検索結果.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/5f41d3e6-528d-9c04-708b-1d395daefef7.jpeg)
④チャットのメッセージボックスに文字列を入力し、送信ボタンを押す。
![検索4_メッセージ送信.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/fb4eb564-f0a8-a39b-adb3-7351a2ace249.jpeg)

## 公開するソースコード
　github上に公開します。好きに使ってください（もちろん自己責任で）。run.pyを実行すると、ブラウザが起動し、自動送信が始まります（送信ボタンは押さない）。また、ブラウザが起動しない場合は、chrome.batのchrome.exeへのパスを設定すると解決するかもしれないです。

* ソースコード, https://github.com/mimuro-lab/teams_sendmsg

# 実現方法
## １．起動済みのブラウザを立ち上げ、Teamsへログインする。
　まず、以下のコマンドでchrome.exeを立ち上げ（[[2]参照](https://qiita.com/mimuro_syunya/items/2464cd2404b67ea5da56#%E3%83%9D%E3%83%BC%E3%83%88%E3%82%92%E5%9B%BA%E5%AE%9A%E3%81%99%E3%82%8B%E5%BC%95%E6%95%B0%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%AA%E3%81%8C%E3%82%89chromeexe%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B)）、[Web版のTeams](https://teams.microsoft.com/)へログインします。その際、次回のアクセス時に、ログインを省略できるように設定します。それにより、SeleniumからTeamsへの自動操作が可能となります。
　ログインなどの情報はすべて、`--user-data-dir=`に格納されています。もし、起動済みのブラウザを使わずに、SeleniumでTeamsへログインしようとした場合、2段階認証の手順で躓くことになります。また、この手順は扱いは、いわば初期設定となります。

```ruby:portを固定しchromeを開く(cmd上)
"C:\Program Files\Google\Chrome\Application\chrome.exe" -remote-debugging-port=9222 --user-data-dir="適当なパス（chromeのデータがいっぱいできても良い場所）"
```

### ログインの様子
１．chrome.exeを引数を指定しながら開く。
![初期設定1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/b485b51d-bc46-249f-ae69-206beecde885.jpeg)
２．[Web版のTeams](https://teams.microsoft.com/)へログインする。
![初期設定2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/ec2f2a7a-b84a-7dc3-08a8-b684266af151.jpeg)
３．ユーザー情報を入力する際、「パスワードを保存しますか？」で「保存」を押す。
![初期設定3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/652f7628-7a56-cbcd-634a-066a5b244e82.jpeg)
４．2段階認証を行う。
![初期設定4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/e6507603-122a-cbcb-604a-7e7e889cfdc6.jpeg)
５．「今後このメッセージを表示しない」にチェックを入れ、サインインの状態を維持に「はい」と答える。
![初期設定5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/cfc3efca-74fd-0e81-640d-773a95671bb7.jpeg)

## ２．seleniumでメッセージの自動送信を行う。
　手順１が完了したら、temasへの自動操作が可能となります。後はselenium(python)で、ゴリゴリとプログラムを書いていくだけです。ただし、[2]でも述べている通り、起動済みのブラウザを操作する点に注意してください。つまり、必ず以下の手順でプログラムを起動する必要があります。その為、公開するソースコードでは、run.pyによりこの処理を行っています。

１．chrome.exeを引数指定で起動
２．seleniumのプログラムを起動

### 自動送信のソースコード
以下のソースコードを書いておきます。
　手順としては、以下の様な感じです。

①検索欄に`keyward`を入力し、検索する。
②検索結果画面のタブ「ユーザー」をクリックする。
③ユーザー一覧の一番上の枠をクリックする。
④メッセージボックスに`message`を入力し、送信ボタンを押す。

　`keyward`が送信相手を特定する文字列で、`message`が自動送信するメッセージの文字列です。この時、`keyward`でヒットしたユーザーのうち、一番上のユーザーへメッセージを送ります。よって、ヒットする人が1人となるような情報が望ましいです。
　また、一番最後の行は、送信ボタンを押す処理であるため、コメントアウトしています。実際に使う際は、コメントを外してください。

```ruby:teams_sendmsg.py
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys 

keyward = "検索する名前 or メールアドレスor etc（検索結果は１人のみ）"
message = "自動送信するメッセージ"

# make a driver by already opened chrome
options = webdriver.ChromeOptions()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)

wait = WebDriverWait(driver, 15)

# get target URL
driver.get('https://teams.microsoft.com')

WebDriverWait(driver, 60).until(EC.presence_of_all_elements_located)

# input keyward
wait.until(EC.element_to_be_clickable((By.XPATH,'//*[@id="searchInputField"]')))
EleSearch = driver.find_element_by_xpath('//*[@id="searchInputField"]')
EleSearch.clear()
EleSearch.send_keys(keyward)
EleSearch.send_keys(Keys.RETURN)

# click area of user
wait.until(EC.element_to_be_clickable((By.XPATH,'//*[@id="search-result-tabs"]/li[2]/a')))
driver.find_element_by_xpath('//*[@id="search-result-tabs"]/li[2]/a').click()

# click first user
wait.until(EC.element_to_be_clickable((By.XPATH,'//*[@id="peopleSearchContent-0"]/div/div[2]')))
driver.find_element_by_xpath('//*[@id="peopleSearchContent-0"]/div/div[2]').click()

# send message for user
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.cke_wysiwyg_div.cke_reset.cke_enable_context_menu.cke_editable.cke_editable_themed.cke_contents_ltr.cke_show_borders')))
EleMsgBox = driver.find_element_by_css_selector('.cke_wysiwyg_div.cke_reset.cke_enable_context_menu.cke_editable.cke_editable_themed.cke_contents_ltr.cke_show_borders')
EleMsgBox.clear()
EleMsgBox.send_keys(message)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.app-svg.icons-send.icons-rtl-flip')))
EleSendBtn = driver.find_element_by_css_selector('.app-svg.icons-send.icons-rtl-flip')
#EleSendBtn.click()
```

### 躓いたポイント
　とにかく、seleniumの要素取得`driver.find_なんちぁら`で躓きました。私もまだよく理解していませんが、クリックしたいだけなら`xpath`で指定しちゃっても良いのかな、って感想です。`xpath`の取得は、chromeブラウザ上の右クリック「検証」で要素を探して（以下画像のクリックするボタン）、コード上で右クリック「copy」から`xpath`を取得できます。
![chrome_検証.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554112/4941fcf3-d02c-3011-2775-fc6ceec93a83.jpeg)

　また、クラス名でどうしても指定したい場合がありました。その際、くそ長いクラス名を指定してたのですが、上手くいきませんでした。どうやら、by_class_nameでは、スペースを含むクラス名は上手くいかないようです。
　@hanonaibaobabuさんが解決していました。とても参考になりました。解決策としては、css_selectorで指定すると良いそうです。先頭に`.`を置き、スペースを`.`に置き換えると上手くいきます。

https://qiita.com/hanonaibaobabu/items/e547410865d857aa25ec

# おわりに
　今回は、Teamsで個人チャットへのメッセージ自動送信するためのプログラムを作りました。当初の予定では、<i>Incoming Webhook</i>を使う予定でしたが、結局seleniumによるごり押しの方法となってしました。なので、長期にわたる安定な稼働を行うのは難しそうですね。
　<i>Incoming Webhook</i>がもっと柔軟なら良いんですけどね。そういった簡単な自動化、は悪戯されやすいと判したのでしょうかね。
　あと、プロコン間に合いませんでした。論文・テストが重なって、できませんでした（言い訳）。今度こそ頑張ってみます！
　ご質問・ご指摘などございましたら、気軽にコメントください。それじゃ！