🍀

Google Meetのタブを自動で閉じるChrome 拡張を作った

2023/06/04に公開

作成した拡張機能の概要

Google Meetの通話を終了することをトリガーとして自動的に対象のタブを閉じる拡張機能です。

https://chrome.google.com/webstore/detail/auto-google-meet-window-c/gmljpedhelhmmfeidfedliocemknlgmb/related?hl=ja&authuser=2

https://github.com/Yuisei-Maruyama/Auto-Google-Meet-Tab-Closer

動機

自分は普段作業する上で、ブラウザのタブが途轍もないことになってしまうことが度々あります。
タブの閉じ忘れ

なぜこのような状態になってしまうのかの原因を整理してみました。

  • 多くのタスクを処理しようとするため
  • 一時的な情報を保持するため
  • 集中して読みたいものを「後で読む」ため
  • 頻繁にアクセスするウェブページを自分の目に付く場所に置いておきたいため

タブが増えてしまうことによるメリットとデメリット

メリット:
  1. 情報への瞬時のアクセス:
    多くのタブを開いておけば、それぞれのウェブページへすぐにアクセスできるので、多くの情報源を一度に参照できる。また頻繁に訪れるウェブページへのアクセススピードを高めることができる。

  2. 並列作業:
    プルリクエストをレビューする際や調査を行う際に、一つのタブで作業を行いながら、他のタブで別の作業を進めることができる。

  3. ブックマークの代用:
    あとで参照したいウェブページに触れる機会を視覚的に増やすことができる。

デメリット:
  1. 視認性の低下と情報の離散:
    開いているタブが多すぎると、必要な情報を探すのが困難になり、各タブが何を含んでいるのかを把握することが難しくなることがある。
    開かれているウェブページが同じであると、タブに表示されるロゴやタイトルでも判別が不可能になる可能性がある。

  2. PCのパフォーマンス低下:
    多くのタブを開くと、それぞれのウェブページがメモリとCPUを消費し、PC全体のパフォーマンスが低下し、動作が遅くなることがある。

解決方法

上記のタブが増えてしまうことの原因を鑑みた時に、まず不必要なタブが存在していないかを考えました。
そこで散見された事例として、GoogleMeetによる会議作成・終了画面が多く残ってしまっていることに気が付きました。

GoogleMeetによる会議作成画面

GoogleMeetによる会議終了画面

この問題をGoogleの拡張機能によって、会議が終了されたことを監視して、自動で閉じるように解決できないかを考えてみました。

使用技術

Node.js

サーバーサイドでのJavaScriptの実行を可能にするオープンソースのランタイム環境です。

TypeScript

JavaScriptのスーパーセットで、静的型付けやクラス、インターフェースなど、大規模開発に役立つ機能を提供します。TypeScriptは最終的にJavaScriptにトランスパイルされ、ブラウザで実行できます。

esbuild

https://esbuild.github.io/

JavaScriptとTypeScriptのビルドツールで、非常に高速なビルド時間を提供します。
また他のトランスパイラーやバンドラー(例えばWebpackやBabel)よりも速く動作することで知られています。
Go言語で書かれているため、ビルドタスクを並行に実行でき、大量のファイルを効率的に同時に変換することができます。

esbuild-register

https://github.com/egoist/esbuild-register

Node.jsでesbuildを使って、ES6+のJavaScriptやTypeScriptをトランスパイルできるようにします。Node.jsで直接実行する際に、ES6+のコードやTypeScriptを即座にトランスパイルします。
esbuildをベースにすることで、今までNode.jsのスクリプト実行で使用されることが多かった ts-node よりも実行スピードが速く動作します。

esbuild-registerとts-nodeのベンチマークを計測した記事

作成したファイルの一覧と説明

ファイル一覧

ファイル名 説明
manifest.json 拡張機能のメタデータが格納されている重要なファイルで、拡張機能の名前、バージョン、アイコン、許可、バックグラウンドスクリプト、コンテンツスクリプトなどを定義します。
background scripts 拡張機能がインストールされている間、特定のイベントが発生したときに実行されるScriptファイルになります。
copy-assets.ts Node.jsのスクリプトで、特定の拡張子を持つファイルをコピーするScriptファイルになります。

各ファイルの説明

manifest.json

{
  "manifest_version": 3,
  "name": "Auto Google Meet Window Closer",
  "version": "1.0",
  "description": "Google Chrome Extension that automatically closes window tabs when you leave a meeting.",
  "host_permissions": ["https://meet.google.com/*"],
  "permissions": ["webNavigation", "scripting"],
  "action": {
    "default_locale": "ja_JP",
    "default_icon": {
      "16": "icon16.png",
      "48": "icon48.png",
      "128": "icon128.png"
    },
    "default_title": "Auto Google Meet Window Closer"
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  }
}
プロパティ名 説明
manifest_version マニフェストファイルのバージョン。
name 拡張機能の名前。
version 拡張機能のバージョン。
description 拡張機能の説明。
host_permissions 拡張機能がアクセスを要求するホストのパターン。本拡張機能がhttps://meet.google.com/* のURLにアクセスする権限を付与する。
permissions 拡張機能が使用するAPIのアクセス権限を示す。本拡張機能が "webNavigation"と"scripting"のAPIを使用することを定義する。

・webNavigation: ブラウザのナビゲーションイベント(ページの読み込み、URLの変更など)を監視する。

・ scripting: 拡張機能がページにスクリプトを注入する機能を制御する。
action デフォルトのロケール(言語設定)、アイコン、タイトルを指定する。
background バックグラウンドスクリプトまたはバックグラウンドページを指定する。ここではバックグラウンドとして動作するサービスワーカーとして "background.js"を指定している。
icons 拡張機能のアイコンを定義する。
content_security_policy 拡張機能のContent Security Policyを定義する。

background.ts

interface Message {
  closeTab: boolean
}

function addEndCallListener() {
  const endCallButton = document.querySelector(
    'button[aria-label="通話から退出"]'
  )

  if (endCallButton) {
    endCallButton.addEventListener('click', () => {
      chrome.runtime.sendMessage({ closeTab: true })
    })
  }
}

chrome.webNavigation.onHistoryStateUpdated.addListener(
  (details: chrome.webNavigation.WebNavigationTransitionCallbackDetails) => {
    chrome.scripting.executeScript(
      {
        target: { tabId: details.tabId },
        func: addEndCallListener,
      },
      () => {
        if (chrome.runtime.lastError) {
          console.error(chrome.runtime.lastError.message)
        }
      }
    )
  },
  {
    url: [{ urlMatches: 'https://meet.google.com/*' }],
  }
)

chrome.runtime.onMessage.addListener(
  (message: Message, sender: chrome.runtime.MessageSender) => {
    if (message.closeTab) {
      chrome.tabs.remove(sender.tab!.id!)
    }
  }
)

(説明)

  1. addEndCallListenerで、「通話から退出」というaria-labelを持つボタン(Google Meetの通話を終了するボタン)を探し、それにclickイベントのリスナーを追加します。
    そのリスナーは、ボタンがクリックされると{ closeTab: true }というメッセージを送信します。

  2. chrome.webNavigation.onHistoryStateUpdated.addListenerを使用して、URLがhttps://meet.google.com/* に一致するタブで履歴の状態が更新されたときにaddEndCallListener関数を実行します。
    上記により、Google Meetの通話が始まった時に「通話から退出」ボタンにイベントリスナーが追加されます。

chrome.webNavigation.onHistoryStateUpdated.addListenerは引数として、下記2つを受け取ります。

引数 説明
Callback関数 履歴状態が更新されたときに実行される関数で、detailsには更新が発生したタブやフレーム、URLなどの詳細情報が含まれます。
フィルタオブジェクト(オプション) このオブジェクトを使用して、リスナーが特定のURLパターンに対してのみ発火するように制限できます。
  1. chrome.runtime.onMessage.addListenerを使用して上記のメッセージをリッスンします。
    { closeTab: true }というメッセージを受け取ったら、そのメッセージを送信したタブを閉じます。

copy-assets.ts

import fs from 'fs'
import path from 'path'

const srcDir = '.'
const destDir = './dist'

function copyFilesWithExtensions(
  extensions: string[],
  filenames?: string[]
): void {
  const files = fs.readdirSync(srcDir).filter((file) => {
    const fileExtension = path.extname(file)
    const isExtensionValid = extensions.includes(fileExtension)
    if (filenames) {
      return isExtensionValid && filenames.includes(file)
    }
    return isExtensionValid
  })

  console.log(files)

  for (const file of files) {
    const srcFile = path.join(srcDir, file)
    const destFile = path.join(destDir, file)
    fs.copyFileSync(srcFile, destFile)
  }
}

copyFilesWithExtensions(['.png'])
copyFilesWithExtensions(['.json'], ['manifest.json'])

(説明)
本ファイルの目的は、ルートディレクトリから./distディレクトリ配下へのファイルのコピーを行うことです。
GoogleChromeの拡張機能はJavaScriptしか対応していないため、TypeScriptをトランスパイルする必要があります。
それに伴い、ルートディレクトリ配下からmanifest.jsonや画像ファイルもコピーする必要があったため、作成しました。

本ファイルのcopyFilesWithExtensions()では引数として、extensions(コピーしたいファイルの拡張子のリスト)とfilenames(コピーしたいファイル名のリスト)を受け取ります。
filenamesが指定されなかった場合、必然的に対象の拡張子がついた全てのファイルがコピー対象になります。

上記のdistディレクトリをzipファイルにして、Googleの審査を通し、審査が通ったらChromeウェブストアに公開されます。

これから実現できたら嬉しいこと

本機能を搭載しても現状まだ不要なタブが多く残ってしまうことがあります。
次のアクションプランとしては、開かれているタブを全て取得して、指定した時間の中で開かれた形跡がないタブがあれば自動的に閉じる機能を実装しようと考えています。

OT Official

Discussion