PythonでZoomミーティングに自動出席しよう

### 更新 (2021/03/05)
もっとスマートなZoomミーティング入室を行う方法があったみたいです....
[ZOOMに自動的に接続する方法（コピペで簡単）](https://blog.isarver.com/automatically-join-zoom/)

上の記事で紹介されている**コマンドを用いて自動入室を行い、それに加えてPywinautoで画面キャプチャと自動退出をするという方式に変更**しました。
それによりSeleniumは不要になりました。

この記事は今回の変更をすべて反映しています。

### 概要
**Zoomミーティングに自動入室して画面キャプチャを行う**スクリプトを作りました。
簡潔に言えば、「**リアルタイム授業をオンデマンド授業にしよう**」って感じです。

新型コロナウイルスによる影響でほとんどの学校でオンライン授業を強いられているかと思います。
オンライン授業に伴い、ビデオコミュニケーションツールであるZoomを利用している方も多いと思います。

そんな中で、なにか**急な用事ができて授業を受けられない！**みたいなことが起きるかもしれませんよね？
すごく困りますよね？こういうときどうしますか？
友達のノートを写させてもらいますか？友達にZoom授業を録画してもらって送ってもらいますか？
でも、それって少なからず友達に迷惑をかけますし、もっと言えば友達に頼めない状況下だったら....(悲しい)

そんなもしもの状況に立ち向かうためのスクリプトです。

僕は上に記したような**どうしても、やむを得ずZoom授業に参加できない状況下にあるときのみ**の利用を想定して作ったので、
**決して、決して、"授業サボりたい"とか"寝坊しても安心"とかそういう悪いこと考えての利用は駄目ですよ！！**

### 環境
**-Software-**
Windows 10 Home
Anaconda3 64-bit(Python3.8)
VSCode
Zoom 5.4.9
**-Library-**
Pywinauto 0.6.8
**-Hardware-**
CPU: Intel core i9 9900K
RAM: 16GB 3200MHz

### 参考
・[ZOOMに自動的に接続する方法（コピペで簡単）](https://blog.isarver.com/automatically-join-zoom/)
・[オンライン授業にPython+Seleniumで自動出席したい！](https://qiita.com/LemniscaterN/items/80b2f8ca99c0693d42ff)
・[Pywinautoリファレンス](https://pywinauto.readthedocs.io/en/latest/code/code.html#main-user-modules)
・[pywinauto の使い方メモ](https://qiita.com/nobu-maple/items/51901b2e27e4448f102b)

### 非推奨事項・免責事項
#### 非推奨事項
概要でも触れたとおり、本スクリプトは、
**やむを得ずZoom授業に参加できない状況下にあるときのみの利用を想定**して作成しました。
それ以外の目的での利用は非推奨です。
また、上記の目的にしても、然るべき者に事情を伝え承認を得ましょう。

#### 免責事項
本スクリプトの利用で発生したあらゆる損失や不利益について、筆者は一切の責任を負いません。
利用は自己責任でお願いします。

### ソースコード
```python
from pywinauto import Application
import pywinauto

import time

def captureZoom():
    while True:
        try:
            # Zoomミーティングウィンドウ情報取得
            zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
            break
        except pywinauto.findbestmatch.MatchError: # Zoomミーティング入室していない -> 再取得
            continue

    zoom[u"Zoom ミーティング"].set_focus() # 録画対象
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画開始
    time.sleep(5400) # 90分
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画終了

    # Zoom Client閉じる
    zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
    if zoom[u"Zoom ミーティング"].exists():
        zoom[u"Zoom ミーティング"].set_focus()
        pywinauto.keyboard.send_keys("%{F4}")
        pywinauto.keyboard.send_keys("{ENTER}")

def main():
    try:
        captureZoom()
    except pywinauto.findbestmatch.MatchError: # ターゲットが見つからなかった -> 再試行
        print("The dialog was not found. Retrying...")
        main()
    except pywinauto.findwindows.ElementNotFoundError: # ターゲットが見つからなかった -> 再試行
        print("The dialog was not found. Retrying...")
        main()

if __name__ == '__main__':
    main()
```

### 実行結果・諸注意・使い方
#### 実行結果
動作の様子(Youtube)
<iframe width="640" height="480" src="https://www.youtube.com/embed/dpysG00rPhI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

#### 諸注意

~~1. スクリプト実行中は**マウス操作・キーボード操作をしない**ようにお願いします。~~
　↑ スクリプト実行始めと終わり時以外であれば操作しても問題ないです。
2. ミーティング退出は**指定時間経過により退出**します。
3. **ミーティングID、パスワードが固定されている場合のみ有用**です。
4. Zoomは最新バージョンに更新してください。
5. 本スクリプトでは画面キャプチャに**Windows10 PC標準機能のゲームバーの画面録画機能**を使っています。そのためWindows10以外のOSでは動作しません。

#### 使い方
まず、**Pywinautoをインストール**しましょう。

```
pip install pywinauto
```

次にスクリプトに記述されている**time.sleep()のカッコ内に授業時間を秒単位で入力**してください。

```python
    time.sleep(5400) # 授業時間90分 = 5400秒
```

保存したら、メモ帳で下のようなbatファイルを作成。

```
set MEETINGID=<meeting_id>
set PASSWORD=<hashing_pass>
start zoommtg:"//zoom.us/join?action=join&confno=%MEETINGID%&pwd=%PASSWORD%"
python <script-path>
timeout /t <wait-time>
```
**`<meeting_id>`はミーティングID**です。
**`<hashing_pass>`はハッシュ化されたパスワード**です。
上２つは招待URLから取り出してコピペしてください。([詳細](https://blog.isarver.com/automatically-join-zoom/#toc5))

```text:招待URL
https://zoom.us/j/<meeting_id>?pwd=<hashing_pass>
招待URL上の<meeting_id>と<hashing_pass>を取り出して、batファイルに記述。
```

**`<script-path>`はスクリプトファイルのパス**です。
**`<wait-time>`はスクリプト実行終了後の待機時間**です。

batファイルを作成できたら、**タスクスケジューラで任意の時間にbatファイルを実行するように設定**します。
ここの説明は割愛します。[こちら](https://www.atmarkit.co.jp/ait/articles/1305/31/news049.html)を参考にしてください。

あとは設定した時間にPCが起動されていれば、スクリプトが実行されるはずです。

### 説明
一応スクリプトの説明をしておきます。

まずライブラリですが、
**[Pywinauto](https://pywinauto.readthedocs.io/en/latest/)はWindows操作をPythonで自動化できるライブラリ**です。キーボード入力やマウス操作、

スクリプトの説明です。

```python:main関数
def main():
    try:
        captureZoom()
    except pywinauto.findbestmatch.MatchError: # ターゲットが見つからなかった -> 再試行
        print("The dialog was not found. Retrying...")
        main()
    except pywinauto.findwindows.ElementNotFoundError: # ターゲットが見つからなかった -> 再試行
        print("The dialog was not found. Retrying...")
        main()
```
**`pywinauto.findbestmatch.MatchError`はPywinautoの例外で検索対象のウィンドウが存在しなかった場合に発生**します。
**`pywinauto.findwindows.ElementNotFoundError`も上の例外とほぼ同じです**。

例外発生時は再実行するようにしています。

```python:captureZoom()
def captureZoom():
    while True:
        try:
            # Zoomミーティングウィンドウ情報取得
            zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
            break
        except pywinauto.findbestmatch.MatchError: # Zoomミーティング入室していない -> 再取得
            continue

    zoom[u"Zoom ミーティング"].set_focus() # 録画対象
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画開始
    time.sleep(5400) # 90分
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画終了

    # Zoom Client閉じる
    zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
    if zoom[u"Zoom ミーティング"].exists():
        zoom[u"Zoom ミーティング"].set_focus()
        pywinauto.keyboard.send_keys("%{F4}")
        pywinauto.keyboard.send_keys("{ENTER}")
```

コマンド実行によりZoomミーティングに自動入室すると、**Zoomミーティングという名前のウィンドウが出ますから、そのウィンドウの情報を取得**します。まだ入室していなければ再度取得を試みます。

```python
    while True:
        try:
            # Zoomミーティングウィンドウ情報取得
            zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
            break
        except pywinauto.findbestmatch.MatchError: # Zoomミーティング入室していない -> 再取得
            continue
```

ウィンドウ情報取得後、録画対象としてウィンドウフォーカスしてゲームバーの画面録画機能による録画を開始します。
**ゲームバーの画面録画は`Win+Alt+R`で録画/停止できますので、Pywinautoでそのようなキーボード操作をするように記述**します。
そして`time.sleep()`で指定した時間が経過次第、録画を止めます。

```python
    zoom[u"Zoom ミーティング"].set_focus() # 録画対象
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画開始
    time.sleep(5400) # 90分
    pywinauto.keyboard.send_keys("{VK_LWIN down}%{R down}{VK_LWIN up}{R up}") # 録画終了
```

Zoomミーティングを退室する処理は結構力技で、**ウィンドウフォーカスした後、`Alt+F4`でウィンドウを閉じるショートカットを送信**します。
そうすると、**`ミーティングを退出`というダイアログがでてくるので`Enter`で決定し退出**します。

```python
    # Zoom Client閉じる
    zoom = Application(backend="uia").connect(best_match=u"Zoom ミーティング")
    if zoom[u"Zoom ミーティング"].exists():
        zoom[u"Zoom ミーティング"].set_focus()
        pywinauto.keyboard.send_keys("%{F4}")
        pywinauto.keyboard.send_keys("{ENTER}")
```

### おわりに
以上となります。
正直、こちらのスクリプトは公開しようか迷ったのですが、Pywinautoという神ライブラリを知ってほしかったので、こんなことができるぞ！という意も込めて公開しました。
また、[オンライン授業にPython+Seleniumで自動出席したい！](https://qiita.com/LemniscaterN/items/80b2f8ca99c0693d42ff)という記事で試行錯誤してChromeダイアログがSeleniumで操作できずに実現できなかったということだったので、なんとかならないかなと思った次第でもありました。

そして何度も言いますが悪用は厳禁です。