Google Meetの会議を自動で開くChrome拡張機能を作ってみた

※記事の最後に拡張機能の取り込み方記載してます。

#*はじめに*
---
みなさん、タスクに没頭するあまり、予定していた会議をうっかりすっぽかしてしまった経験はありませんか？

私もそんな場面に何度も遭遇しました。
例えば、作業中に気づいたら会議の開始時間を30分過ぎていた、なんてことが！
そんな時、X（旧Twitter）でエゴサしてみたら・・・

![GemBpDgaMAA7L79.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/d5897d19-6d81-b97a-1f0a-b0e912cb6f5b.jpeg)


どうやら、同じような悩みを抱える人は少なくないようです。

そこで、Chromeウェブストアでこの問題を解決してくれる拡張機能を探してみたのですが、
ない・・・！

仕方がないので、すっぽかさないよう入室前まで立ち上がる拡張機能を作ることにしました。

退出機能は近々また今度追加します。

#拡張機能を作ってみた
---
というわけで、早速作った結果がこちら！


![スクリーンショット 2024-12-14 112130.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/04502000-f887-c6c3-fbcc-33274316113e.png)

#機能の仕様
---
このChrome拡張機能「GoogleMeetStartAuto」は、
以下の機能を提供します。

・会議名の設定

・Google MeetのURL設定（TeamsとかのURLでもいけます）

・起動する時刻の設定

・会議の曜日指定

・隔週設定（毎週や3週間に1回など、4週間おきまでなら柔軟に設定可能）
<br>

・保存した会議の一覧

・保存した会議の削除

保存した会議の編集機能はあえて作成してません。なくても成立する＆シンプルにしたいので。
<br>
UIについては、今回は即席で作ったので、今後アップデート予定です。

即席かつMVP版なので許してください。



#構成ファイルとコード
---


この拡張機能のディレクトリ構成は以下の通り。
```
GoogleMeetStartAuto/
├── manifest.json
├── background.js
├── popup.html
└── popup.js
```


以下に、この拡張機能のコードを公開。自由にカスタマイズして使ってね。

```manifest.json
{
  "manifest_version": 3,
  "name": "Scheduled Google Meet Opener",
  "version": "1.2",
  "description": "指定の時間と曜日になったらGoogle Meetを自動的に開くChrome拡張機能。",
  "permissions": [
    "alarms",
    "storage"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  }
}
```

```backgroud.js
chrome.runtime.onInstalled.addListener(() => {
  chrome.storage.sync.clear();
});

chrome.alarms.onAlarm.addListener((alarm) => {
  const meetingId = alarm.name;
  chrome.storage.sync.get(meetingId, (data) => {
    if (data[meetingId]) {
      chrome.tabs.create({ url: data[meetingId].url });
    }
  });
});

function createWeeklyAlarm(meetingId, dayOfWeek, hours, minutes) {
  const now = new Date();
  const dayAdjustment = (dayOfWeek >= now.getDay()) ? (dayOfWeek - now.getDay()) : (7 - (now.getDay() - dayOfWeek));
  let alarmTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayAdjustment, hours, minutes, 0, 0);
  if (alarmTime < now) alarmTime.setDate(alarmTime.getDate() + 7);

  chrome.alarms.create(meetingId, {
    when: alarmTime.getTime(),
    periodInMinutes: 7 * 24 * 60 // 1週間分のミニッツ
  });
}
```

```popup.html
<!DOCTYPE html>
<html>
<head>
  <title>Meet Scheduler</title>
  <style>
    body { font-family: Arial, sans-serif; }
    .meeting { margin-top: 10px; }
  </style>
</head>
<body>
  <h1>Meet Scheduler</h1>

  <h2>New Meeting</h2>
  <label for="name">Meeting Name:</label>
  <input type="text" id="name">
  <br>
  <label for="url">Google Meet URL:</label>
  <input type="text" id="url">
  <br>
  <label for="time">Time (HH:MM):</label>
  <input type="time" id="time">
  <br>
  <label for="days">Days of the Week:</label><br>
  <input type="checkbox" id="mon" name="days" value="1"> Monday<br>
  <input type="checkbox" id="tue" name="days" value="2"> Tuesday<br>
  <input type="checkbox" id="wed" name="days" value="3"> Wednesday<br>
  <input type="checkbox" id="thu" name="days" value="4"> Thursday<br>
  <input type="checkbox" id="fri" name="days" value="5"> Friday<br>
  <input type="checkbox" id="sat" name="days" value="6"> Saturday<br>
  <input type="checkbox" id="sun" name="days" value="0"> Sunday<br>
  <br>
  <label for="frequency">Frequency:</label><br>
  <select id="frequency">
    <option value="1">Weekly</option>
    <option value="2">Every two weeks</option>
    <option value="3">Every three weeks</option>
    <option value="4">Every four weeks</option>
  </select>
  <br>
  <button id="save">Save</button>

  <h2>Saved Meetings</h2>
  <div id="meetings"></div>

  <script src="popup.js"></script>
</body>
</html>
```
```popuo.js
document.getElementById("save").addEventListener("click", () => {
  saveMeeting();
});

function saveMeeting() {
  const meetingId = `meeting-${new Date().getTime()}`;
  const name = document.getElementById("name").value;
  const url = document.getElementById("url").value;
  const time = document.getElementById("time").value.split(':');
  const hours = parseInt(time[0]);
  const minutes = parseInt(time[1]);
  const days = [...document.querySelectorAll('input[name="days"]:checked')].map(d => parseInt(d.value));
  const frequency = document.getElementById("frequency").value;

  if (!url || !name || time.length !== 2 || isNaN(hours) || isNaN(minutes) || days.length === 0) {
    alert('Please fill all fields correctly.');
    return;
  }

  const meetingData = {
    name: name,
    url: url,
    days: days,
    hours: hours,
    minutes: minutes,
    frequency: parseInt(frequency)
  };

  console.log('Saving meeting:', meetingId, meetingData);
  
  chrome.storage.sync.set({ [meetingId]: meetingData }, () => {
    console.log('Meeting saved in storage:', meetingId);
    days.forEach(day => {
      createAlarm(meetingId, day, hours, minutes, frequency);
    });

    loadMeetings();
    alert('Meet URL and time saved!');
  });
}

function loadMeetings() {
  chrome.storage.sync.get(null, (items) => {
    console.log('Loading meetings from storage:', items);
    const meetingsDiv = document.getElementById("meetings");
    meetingsDiv.innerHTML = '';
    for (let key in items) {
      if (items.hasOwnProperty(key)) {
        const meeting = items[key];
        const days = meeting.days || [];
        const meetingElement = document.createElement("div");
        meetingElement.className = 'meeting';
        meetingElement.innerHTML = `
          <strong>Name:</strong> ${meeting.name} <br>
          <strong>URL:</strong> <a href="${meeting.url}" target="_blank">${meeting.url}</a> <br>
          <strong>Time:</strong> ${meeting.hours.toString().padStart(2, '0')}:${meeting.minutes.toString().padStart(2, '0')} <br>
          <strong>Days:</strong> ${days.map(dayToString).join(', ')} <br>
          <strong>Frequency:</strong> ${frequencyToString(meeting.frequency)}
          <br>
          <button onClick="deleteMeeting('${key}')">Delete</button>
        `;
        meetingsDiv.appendChild(meetingElement);
      }
    }
  });
}

function deleteMeeting(meetingId) {
  chrome.storage.sync.remove(meetingId, () => {
    chrome.alarms.clear(meetingId, () => {
      console.log('Meeting deleted:', meetingId);
      loadMeetings();
    });
  });
}

function dayToString(day) {
  const daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
  return daysOfWeek[day];
}

function frequencyToString(frequency) {
  switch(frequency) {
    case 1: return 'Weekly';
    case 2: return 'Every two weeks';
    case 3: return 'Every three weeks';
    case 4: return 'Every four weeks';
  }
}

function createAlarm(meetingId, dayOfWeek, hours, minutes, frequency) {
  const now = new Date();
  const dayAdjustment = (dayOfWeek >= now.getDay()) ? (dayOfWeek - now.getDay()) : (7 - (now.getDay() - dayOfWeek));
  let alarmTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayAdjustment, hours, minutes, 0, 0);
  if (alarmTime < now) {
    alarmTime.setDate(alarmTime.getDate() + 7);
  }

  const periodInMinutes = frequency * 7 * 24 * 60; // 週、2週、3週、4週ごとに設定

  chrome.alarms.create(meetingId, {
    when: alarmTime.getTime(),
    periodInMinutes: periodInMinutes
  });
}

document.addEventListener("DOMContentLoaded", loadMeetings);
document.addEventListener("click", function (e) {
  if (e.target && e.target.tagName === "BUTTON" && e.target.innerText === "Delete") {
    const meetingId = e.target.getAttribute("onClick").match(/deleteMeeting\('([^']+)'\)/)[1];
    deleteMeeting(meetingId);
  }
});
```

拡張機能の取り込み方
---
1.VSCodeかなんかで上記のコードファイルを１つのファイルにまとめてね。
こんな感じ↓
![スクリーンショット 2024-12-14 114615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/c4bc958f-6ac0-0d41-2099-99ecbecd2220.png)

2.Chromeで"chrome://extensions/"を検索。

3.画面右上のデベロッパーモードをオンにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/69f1eab9-4bed-b8cb-dd65-bd27e519c2fa.png)



4.「パッケージ化されていない拡張機能を読み込む」を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/7cdf0f06-16f4-39b3-4dc5-95a95b887459.png)


5.取り込みたいファイルを選択。今回だと「GoogleMeetStartAuto」になる。名前は正味何でもええです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/14119bb5-be7b-ac7c-5a80-40d10e11c99e.png)

6.Chromeの画面右上にSのアイコン出てるので、ピンマークを押して、「S」のアイコンを押してみてね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/31ebf804-b54e-bddb-a873-03ad5140d98e.png)

7.設定画面がこんな感じで出てきます。
![スクリーンショット 2024-12-14 112130.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946734/04502000-f887-c6c3-fbcc-33274316113e.png)

使い方
---
1.Meeting Name: 会議名を入力します（例: "週次ミーティング"）。

2.Google Meet URL: 会議のURL（Google Meetリンク）を入力します。

3.Time (HH:MM): 会議の開始時間を24時間形式で入力します（例: "14:30"）。

4.Days of the Week: 該当する曜日にチェックを入れます。
月曜日: Monday
火曜日: Tuesday
...（以下略）

5.Frequency: 会議の繰り返し頻度を選択します（例: 毎週、2週間おきなど）。

6.Save ボタンをクリックして設定を保存します。

7.登録した会議の確認
ポップアップの Saved Meetings セクションに、登録した会議のリストが表示されます。
ここから会議の情報を確認・削除できます。

8.自動でGoogle Meetが開く
設定した曜日と時間になると、自動的にGoogle Meetが新しいタブで開かれます。
作業に集中していても、会議を忘れる心配がなくなります。たぶん。

おわりに
---
Chromeウェブストアに出そうと思ったけど登録料＄５掛かるらしい。出しません。
ちなみに冒頭でも少し記載したように、URLの設定はGoogleMeetのURL以外にも使えます。
TeamsのURLでも何かのサイトでも。とりあえず、こやつは時間になったらChormeの新規タブが開くシステムです。
<br>
使ってみて、フィードバック・改善点あったらください。直します。