WEB会議の家族乱入事故を回避する仕組みをサクッと作る

この記事は、「Qiita Engineer Festa 2022」に参加するために執筆した記事です。

https://qiita.com/official-events/eeadea1de45a11be944e

https://qiita.com/official-events/86d8555146e1b14fef24

（１粒で2度美味しいを狙ってしまいましたが、メインはClaris Connectの方になります。
WebhookはZoom APIに含めていいのかも微妙でしたし・・・）

# TL;DR

* リモートワークで多くの家庭で発生していると思われるWEB会議への家族乱入事故を回避する仕組みを作った
* Claris Connectと既存の仕組みやWebhookなどを組み合わせることで、極力手間をかけずに作ることができた

# 挨拶と背景

こんにちは、[株式会社dott](https://thedott.io/)でエンジニャー🐱をしているHALと申します。

現在フルリモートで仕事をしていて、日に何度かZoomやMeetなどのWEB会議をやっています。
恐らく自宅でリモートワークをしている方の共通の悩みかと思いますが、会議中に家族が乱入する事故というのが何度か発生しています。
特に業務上支障が出るわけではないのですが、妻としては乱入などしたくないとのことで、「会議中かどうかわかるようにしてくれ」と言われています。

これまではGoogle Calendarを共有して打ち合わせの予定を入れるようにしていましたが、入れ忘れたり、突発の打ち合わせや時間が伸びてしまう場合など、対応しきれない場合もあります。
そんな時のために、会議中であることを見てわかるような仕組みの構築をすることにしました。

ちなみに発想自体は他にも同様のことをやっている方が何名かいらっしゃいます。

https://k-tai.watch.impress.co.jp/docs/column/todays_goods/1313705.html

https://dev.classmethod.jp/articles/onair-at-meeting/

どちらも必要十分な感じですが、自分は「コード不要でなるべく簡単に、完全に自動で、あとスマホ通知も欲しい」という条件を付けてやることにしました。
（スマホ通知は、なんらかの方法で妻のスマホに「会議が始まりました」「会議が終わりました」みたいに通知する想定。）
なお、最終的にはClaris ConnectとZoomの連携のところでうまくいかず、「コード不要」は妥協しました・・・。

# システム構成

全体の構成としては以下のような形になりました。

![システム構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/3a57fecf-e269-fcba-32f5-b25b9c497833.png)


Zoom / Google Meet / Slack ハドルの会議を開始すると、Google Apps Scriptを経由してClaris ConnectにWebhookが飛び、SlackとIFTTTに連携するという形です。

- ZoomのWebhookは直接Claris Connectに繋ぐことができなかったので、Google Apps Scriptを経由する必要がありました。
  - Zoomとの連携はComming soonとなっていたので、ここは不要になるかも？
- Google MeetはWebhookは提供されていませんでしたが、Chrome拡張でちょうど良いのがあったのでそれを使いました。
- Slack ハドルはWebhookイベントでハドルステータスを取得できたのでそれをClaris Connectに繋ぎました。
- Slackへのメッセージ送信によって妻のスマホへの通知を実現します。
- スマートプラグは直接API等でON/OFFできればよかったのですが、ノーコードでは難しそうだったのでIFTTT経由にしました。


**Slackに通知した様子**
![Slackに通知した様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/5794a7f8-3153-b429-9cd0-0a5de6cda535.png)

**ON AIRライト**
スマートプラグでON/OFFするライトはこんな感じで、ケーブルの都合でとりあえず部屋のドアの前に置いてます。
ちなみに木の柵は猫の脱走防止用の柵（DIY）ですw
WEB会議が始まると点灯して、終わると消灯します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/0dda9a18-28a2-93a9-efd4-d25ccee39c14.jpeg" width="200" />

本当はTeamsについても対応したいところではあるのですが、弊社ではTeamsは使っていないため、ユーザー登録などはせずお客さんに招待されてゲストで参加する形になっています。
そのため、Webhookなどを設定するのは難しそうなので諦めました。
やるならGoogle Meet同様にChrome拡張を作るのが一番手間は少なそうです。

# 構築手順
では、実際の構築手順を説明します。

## Claris Connectの準備
まずはClaris Connectの準備をしていきます。
なお、Claris Connectのユーザー登録などについては説明しません。

まずはプロジェクトを新規作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/11e8ccb4-325b-1137-668a-acf55a2db66a.png" width="200" />

Flowを追加する画面が表示されますが、そこは飛ばして最初にWebhookを作成しておきます。
「Webhooks」を選び、「Create a Webhook」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/19b69f16-9c7a-ecb0-749c-2edf161bc168.png)

「Create new webhook」に適当な名前を入力して「Create」ボタンをクリックすると、URLが表示されます。
このURLを後でGASに設定するため、メモしておきます。

![スクリーンショット 2022-06-09 23.37.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/c87e4296-5b1d-9b82-ae21-9ade5fa4e65f.png)

一旦Claris Connectはここまでにして、続いてGoogle Apps Scriptの方を作成していきます。

## Google Apps Scriptの作成

続いてZoomのWebhookを一時的に受け付けるGASから作成していきます。

Apps Scriptのホームにアクセスし、新しいプロジェクトを作成します。

https://script.google.com/home

以下のようなコードを貼り付けます。

```js
function doPost(e) {
  var presence_status = JSON.parse(e.postData.getDataAsString()).payload.object.presence_status;

  if (typeof presence_status === 'undefined') {
    return;
  }

  var url = 'Claris Connect Webhook URL'; // ここに先ほど作ったWebhookのURLを入れる
  var status = "begin";

  if (presence_status === "In_Meeting" || presence_status === "Presenting" || presence_status === "On_Phone_Call") {
    url += "?status=begin"
  } else {
    url += "?status=end"
    status = "end"
  }

  var properties = PropertiesService.getScriptProperties();
  var before_status = properties.getProperty("status");

  // 前のステータスから変わっていない場合は何もしない（In_MeetingからPresentingへの変更など）
  if (before_status === status) {
    return;
  } else {
    properties.setProperty("status", status);
  }

  UrlFetchApp.fetch(url, {
    'method': 'get',
  });
  return ContentService.createTextOutput(JSON.stringify({ 'content': 'post ok' })).setMimeType(ContentService.MimeType.JSON);
}
```

画面右上の「デプロイ」→「新しいデプロイ」をクリックします。
歯車マークをクリックして「ウェブアプリ」を選び、「アクセスできるユーザー」を「全員」に設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/d120b860-72c8-a18e-0954-e10c59748c51.png" width="500" />

デプロイ後、URLが表示されるので、メモしておきます。

## Zoom Webhookの設定
続いてZoomのWebhookの設定をしていきます。
Zoomアプリの作成ページにアクセスします。

https://marketplace.zoom.us/develop/create

今回はWebhookのみ利用するのでアプリの種類は「Webhook Only」を選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/a4cf1a34-662a-f573-dacd-66e9fbeed17a.png" width="700" />

アプリ名を適当に入力して「Create」をクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/3654a4d1-41ec-ba76-cb4b-902fd7858b5c.png" width="300" />

アプリの基本情報は適当に入力します。
（今回は外部公開の予定はないので）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/4f7cfc52-7aa6-8e38-d5e9-e6de15f7daf9.png" width="700" />

FeatureでEventSubscriptionsをONにします。
Subscription nameは適当に。
Event notification endpoint URLには、先ほど作成したGASで発行されたURLを指定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/a21f8fa2-cf0b-4999-a69f-1b7aeadd0eab.png" width="700" />

EventsでAddボタンを押してEvent Typeを指定します。
今回はユーザーのステータスを取得したいので、「User Activity」の中の「User's presence status has been updated」にチェックを入れます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/8b1d1249-dc1d-d9ce-d2e8-b95ed4d96687.png" width="700" />

入力し終えたら「Done」をクリックし、さらに「Save」をクリックします。
これでZoom側の設定は完了です。

## Claris Connectの設定
ではClaris Connectに戻って設定を進めます。

Flowsで「Create New Flow」をクリックします。

![Flowの新規作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/fc00c5c2-cbcf-ad63-751f-705e00b94bb7.png)

Webhooksを選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/50d05003-5af2-e7c4-3a93-b1240bcfd9ab.png" width="700" />

そのままContinueをクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/d3800db2-f896-0ec0-6f0a-0e75c32370a2.png" width="700" />

Choose webhookで最初に作成しておいたWebhookを選択します。
ここで「Save Trigger」が押せない状態になっていると思います。
この状態でZoomの会議に参加（一人の会議で良い）してもらうと、Claris Connectが実際にWebhookを受信して、「Save Trigger」が押せるようになるはずです。
押せるようにならない場合は、ZoomのWebhookかGASの方で何か誤りがあるので確認してください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/bb66b313-7a1d-d940-8005-b9b2351fafb7.png" width="700" />

これでトリガーとなるWebhookが作成できました。
続いて会議の開始・終了の条件分岐を追加します。
「If-Then」を選びます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/e41b6861-b07b-ee69-93f4-3eed4f94caa8.png" width="700" />

「Select from Step Data.」をクリックすると、Webhookの方で受け取ったリクエストの情報が見れます。
ここで下の方の「url_params」の「status」を選択します。
（GASの中で開始と終了に応じて切り替えている箇所）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/f10ea472-8e94-6a0f-1962-876ef3aca0cb.png" width="700" />

真ん中がIFの比較条件です。
今回は「Text:Equal to Case-Sensitive」にしました。
完全一致ですね。
右側に一致させたい値（ここではbegin）を入力し、Saveをクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/8526dc71-ca3a-6fd9-93d9-221d6672de3b.png" width="700" />

続いてアクションを選択します。
Ifの右側の＋ボタンからActionをクリックします。
すると以下のようにIfで条件に一致する場合と一致しない場合の処理で分岐ができます。
true側をクリックして、SlackをActionとして追加しましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/4cd5af79-c335-0988-9eba-b592969617f3.png" width="700" />

Post message(quick)を選択して「Continue」をクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/f7c0ec80-9fac-81a9-f920-637d6a28547d.png" width="700" />

Slackとの連携が必要です。
初めての場合は「Connect New Slack Account」から連携してください。
連携済みの場合は連携済みのアカウントを選択して「Continue」をクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/486c9821-81bf-6b56-5f51-cf2080f08b66.png" width="700" />

対象のチャンネルとメッセージ内容などを入力します。
こちらは開始メッセージなので「【会議中...】Web会議を開始しました。」などを入力しました。
ちなみに自分はdropboxに置いた画像ファイルを「Message Attachments」の「Thumbnail URL」に設定して視認性を上げました。
dropboxの画像の共有URLの末尾を「?dl=0」から「?raw=1」に変えることで利用できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/6ab46ac8-2a6a-cddc-0d64-943b8fe62387.png" width="700" />

追加したSlack Actionの後ろに「Stop」を追加します。
あくまでも3 Step目はIF分の中に入っているだけなので、ストップせずに以下の画像のように5 Step目で会議終了のメッセージを送るようにすると、開始と終了の両方が送られてしまいます。

開始メッセージ同様に終了のメッセージをIfのfalse側に追加すれば概ねOKです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/541e256e-566b-2a0b-127c-3061368ec680.png" width="700" />

ここまでの状態で、一度右上の「disabled」を「enabled」に変更して動作を確認してみましょう。
Zoomの会議に参加・退出した際に、Slackにメッセージが投稿されればOKです。

## Google Meet用Chrome拡張の追加
Google Meetに関しては、会議の参加・退出時にWebhookを呼ぶ、というそのものズバリの拡張機能があるのでそれを使わせてもらいます。

https://chrome.google.com/webstore/detail/google-meet-webhook/kfigcifpogfjcnmobcbnihhnnfbjlcfb

拡張機能のインストール後、拡張機能のオプション画面でWebhookのURLを設定します。
上段が会議参加時なので、Claris ConnectのWebhook URLの末尾に「?status=begin」を付与したものを設定します。
下段はWebhook URLの末尾に「?status=end」を付与したものを設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/6b0f1dd9-61f7-f31c-b4f1-ca8a3b795bf2.png" width="400" />

これでMeet側も対応できました。
ただ、この拡張機能はURLを見て会議参加・終了を認識しているらしく、会議終了時にブラウザを閉じずにMeetのホーム画面を開いたりすると誤って会議開始と認識されてしまうようです。
会議終了時はそのままそのタブは閉じてしまった方がいいでしょう。

## Slack ハドルのWebhookの設定
続いてSlackのハドルの対応をしていきます。
SlackのハドルはWebhookのEvent Subscriptionに対応しているのでそれを使用します。

まずはGASのスクリプトを変更していきます。
※Claris Connectに直接繋ぎたかったのですが、ハドルのイベントの仕様上、ハドルのステータス変更以外の変更もイベントが発生してしまうようなので、GASのスクリプトでハドルの変更だけを検知するようにしました。

変更後のGASはこんな感じです。

```js
function doPost(e) {
  console.log(JSON.parse(e.postData.getDataAsString()))
  var challenge = JSON.parse(e.postData.getDataAsString()).challenge;
  var payload = JSON.parse(e.postData.getDataAsString()).payload;
  var event = JSON.parse(e.postData.getDataAsString()).event;

  var url = 'Claris Connect Webhook URL';

  if (challenge !== undefined) {
    return ContentService.createTextOutput(JSON.stringify({ challenge })).setMimeType(ContentService.MimeType.JSON);
  } else if (payload !== undefined) {
    var presence_status = payload.object.presence_status;
    var status = "begin";

    if (presence_status === "In_Meeting" || presence_status === "Presenting" || presence_status === "On_Phone_Call") {
      url += "?status=begin"
    } else {
      url += "?status=end"
      status = "end"
    }
  } else if (event !== undefined) {
    var huddle_state = event.user.profile.huddle_state;
    var status = "begin";

    if (huddle_state === "in_a_huddle") {
      url += "?status=begin"
    } else {
      url += "?status=end"
      status = "end"
    }
  } else {
    return;
  }

  var properties = PropertiesService.getScriptProperties();
  var before_status = properties.getProperty("status");

  // 前のステータスから変わっていない場合は何もしない（In_MeetingからPresentingへの変更など）
  if (before_status === status) {
    return;
  } else {
    properties.setProperty("status", status);
  }

  UrlFetchApp.fetch(url, {
    'method': 'get',
  });
  return ContentService.createTextOutput(JSON.stringify({ 'content': 'post ok' })).setMimeType(ContentService.MimeType.JSON);
}

function doGet(e) {
  console.log(e)
  return;
}
```

Slackのイベントのchallengeの対応と、実際のイベントの判定処理を追加しています。
ちなみに、ハドルとZoomを同時に使ったりするとうまくいきませんのでその点はご注意ください。
同時に使えるようにしたければ、`properties.setProperty("status", status);`をZoomとハドルで分ければいけると思います。

続いて、Slackのイベントを取得するアプリを作成します。

https://api.slack.com/apps

こちらから新しいアプリを作成します。
必要なのはEvent Subscriptionだけなので、Enableにして、`user_huddle_changed`のイベントだけ追加します。

![スクリーンショット 2022-06-10 22.55.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/e52691c1-9d24-12e9-b30b-73ca55367436.png)

URLには新しくデプロイしたGASのURLを指定してください。
これだけで、Slackのハドルの開始・終了時も同様に通知＆ライトのON/OFFができるはずです。

## ON AIRライトの点灯（Switch bot スマートプラグ ＆ IFTTTの連携）
最後に物理的なライトのON/OFFです。
ON AIRのライトはただの物理ライトで、電源に繋いでスイッチでON/OFFするようなものです。

https://www.amazon.co.jp/gp/product/B085G4F72K/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1

そこでSwitch botのスマートプラグを使用してIFTTTから電源のON/OFFを切り替えることにしました。

https://www.amazon.co.jp/gp/product/B09PYLWNGV/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1

Switch botスマートプラグのセットアップは割愛します。
普通にアプリを入れてユーザー登録し、BluetoothとWi-Fiの接続を行うだけです。

IFTTTとの連携が標準で行えるので、まずはIFTTTでアプレットを作成します。
トリガーとしてWebhookを指定します。
選ぶのは「Receive a web request」です。
Event Nameは「on_air」とかにしておきましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/5141dd27-9e3a-86f6-7544-5c76a9e32ba4.png" width="500" />

続いて「Then」でSwitch Botを選択します。
追加するのは「Power On/Off」です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/fdd9bbff-cc3a-331c-90e5-6148ba49f54f.png" width="300" />

ここでSwitch Botのセットアップをする際に作成したアカウントと連携します。
追加したスマートプラグを選び、Power Onを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/ef6e1d08-4f76-f1cc-110c-d4abeed53c8e.png" width="300" />

同様にPower Offのパターンも作成しましょう。

最後に、Claris Connect側でIFTTTのWebhookを呼び出すWebhookを追加します。

![スクリーンショット 2022-06-10 23.59.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/349904d3-e577-e27c-a0ae-d173698c6b37.png)

なお、指定するURLは、IFTTTの以下の画面から確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/21e2edd1-5ac9-bd54-6e73-bb4b3e892c3f.png" width="300" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/f78287bb-8c0c-eb32-a402-747df4dc7dc8.png" width="300" />

Webhookの作成時に設定したEvent Name（on_air）を埋め込んでURLにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/f382ee95-3ad3-aba7-b3b9-7c1da2d1eaf9.png" width="500" />

最終的なClaris Connectの方はこんな感じになりました。
3〜5が会議開始時の処理、6,7が会議終了時の処理になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/2720be62-a715-d66a-9cb2-2ee82d3e1b33.png)

お疲れ様でした。
これで物理ライトのON/OFFもできるようになったと思います。

# 最後に
今回はなるべくコードを書かなくて済むようにClaris Connectを使って連携し、会議中であることを知らせるSlack通知と物理ライトのON/OFFまで実現しました。
いわゆるノーコードのツールは、元から用意された範囲にうまく収まる仕組みを考えられると、構築自体はとても簡単です。
今回も、実際の構築作業は１時間程度で終わってしまいました。
（記事にまとめる方が全然時間かかりました...）

どうしてもコードを書く必要が発生しても、（今回は試してませんが）Claris Connectの場合はAWS Lambdaと連携したり、Filemakerの方と連携することも可能なので、ノーコードツール単体よりもできることの幅は広そうです。
少し残念だったのは、Zoomとの連携機能がまだComming soon状態であったり、Slackの連携機能がちょっと古いAPIになっているというところですね。
今後のアップデートに期待したいところです。

また、今回はとにかく手をかけずに作ることを目指したため、Teamsに対応できなかったという点もあるため、ゆくゆくはTeamsにも対応した仕組みを構築したいと思います。
さらにアップデートできたら、また記事にして公開したいです。