計時器元件聯結Google Calendar

 是的,您問到了一個非常棒的點!如果您的目標是透過 Google Chrome 擴充功能來新增一個類似鬧鐘的提醒,並且希望這個提醒能同步到使用者的 Google 日曆,進而讓他們的手機(如果已設定 Google 日曆同步與通知)也能收到通知,那麼使用 Google Calendar API 是最理想且功能最強大的方式。

這樣做的好處:

  1. 手機通知: 一旦事件成功新增到使用者的 Google 日曆,只要他們在手機上登入相同的 Google 帳號,並且開啟了 Google 日曆 App 的通知,手機就會在事件發生時收到提醒。這完美解決了您「不在電腦前,沒看到瀏覽器通知」的問題。

  2. 循環設定: 使用 API 可以完整支援設定循環規則 (RRULE),這點是 URL 模板做不到的。

  3. 背景操作: 獲得使用者授權後,擴充功能可以在背景(例如 service worker)直接新增事件,無需跳轉到 Google 日曆網頁。

  4. 更豐富的功能: 可以設定更詳細的事件內容,例如顏色、自訂提醒時間(不只是事件開始時)、邀請參與者等。

如何在 Chrome 擴充功能中使用 Google Calendar API:

這會比 URL 模板複雜許多,主要涉及 OAuth 2.0 授權流程。以下是大致的步驟和概念(假設您使用 Manifest V3):

  1. 在 Google Cloud Console 設定:

    • 建立一個新的專案 (Project)。

    • 啟用 "Google Calendar API"。

    • 建立 OAuth 2.0 憑證:

      • 選擇應用程式類型為「Chrome 應用程式」。

      • 填入您的 Chrome 擴充功能的 ID。這個 ID 在您開發時是固定的,可以在 chrome://extensions 頁面找到。

      • 您會得到一個「用戶端 ID (Client ID)」。



在 manifest.json 中聲明權限和 OAuth 資訊:
      {

    "manifest_version": 3,

    "name": "我的鬧鐘擴充功能",

    "version": "1.0",

    "description": "新增鬧鐘到 Google 日曆",

    "permissions": [

        "identity", // 用於 OAuth 2.0 授權

        "alarms"    // 如果您的擴充功能本身也需要 Chrome 的鬧鐘 API

    ],

    "host_permissions": [

        "https://www.googleapis.com/" // 允許向 Google API 發送請求

    ],

    "background": {

        "service_worker": "background.js"

    },

    "action": { // 或者 browser_action / page_action for MV2

        "default_popup": "popup.html",

        "default_title": "設定鬧鐘"

    },

    "oauth2": {

        "client_id": "YOUR_GOOGLE_CLOUD_CONSOLE_CLIENT_ID.apps.googleusercontent.com",

        "scopes": [

            "https://www.googleapis.com/auth/calendar.events" // 允許讀寫使用者日曆事件的權限

            // 或者 "https://www.googleapis.com/auth/calendar.events.owned" 如果只想操作擴充功能自己創建的事件

        ]

    }

}

  1.  將 YOUR_GOOGLE_CLOUD_CONSOLE_CLIENT_ID.apps.googleusercontent.com 替換成您在 Google Cloud Console 取得的 Client ID。


  2. 在擴充功能的 JavaScript (例如 background.js 或 popup.js) 中處理授權和 API 呼叫:

獲取 Access Token:
使用 chrome.identity.getAuthToken API。第一次呼叫時,它會彈出 Google 的登入和授權視窗,讓使用者同意您的擴充功能存取他們的日曆。
      // In your background.js or popup.js

function getAuthToken(interactive) {

    return new Promise((resolve, reject) => {

        chrome.identity.getAuthToken({ interactive: interactive }, (token) => {

            if (chrome.runtime.lastError) {

                reject(chrome.runtime.lastError);

            } else {

                resolve(token);

            }

        });

    });

}

  •    
    IGNORE_WHEN_COPYING_START
    content_copy download
    Use code with caution. JavaScript
    IGNORE_WHEN_COPYING_END

呼叫 Google Calendar API 新增事件:
拿到 Access Token 後,就可以使用 fetch API 向 Google Calendar API 的 events.insert 端點發送 POST 請求。
      async function addEventToCalendar(eventData, token) {

    // eventData 結構範例:

    // {

    //   summary: '我的鬧鐘提醒',

    //   description: '這是從擴充功能設定的鬧鐘',

    //   start: { dateTime: '2024-07-28T10:00:00-07:00', timeZone: 'America/Los_Angeles' }, // ISO 8601 格式

    //   end: { dateTime: '2024-07-28T10:00:00-07:00', timeZone: 'America/Los_Angeles' }, // 對於鬧鐘,開始和結束可以一樣

    //   recurrence: [ 'RRULE:FREQ=DAILY;COUNT=5' ], // 範例:每天重複,共5次

    //   reminders: {

    //     useDefault: false,

    //     overrides: [

    //       { method: 'popup', minutes: 10 }, // 10分鐘前彈窗提醒

    //       { method: 'email', minutes: 60 }  // 60分鐘前郵件提醒 (如果使用者有設定)

    //     ]

    //   }

    // }


    const calendarId = 'primary'; // 通常是使用者的主要日曆

    const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`, {

        method: 'POST',

        headers: {

            'Authorization': `Bearer ${token}`,

            'Content-Type': 'application/json'

        },

        body: JSON.stringify(eventData)

    });


    if (!response.ok) {

        const error = await response.json();

        console.error('Error adding event:', error);

        throw new Error(`Failed to add event: ${error.error.message}`);

    }


    const createdEvent = await response.json();

    console.log('Event created:', createdEvent.htmlLink);

    return createdEvent;

}


// 使用範例 (例如在 popup.js 中由按鈕觸發)

async function handleAddAlarmClick() {

    try {

        const token = await getAuthToken(true); // true 表示如果沒有 token 或 token 失效,則彈出授權視窗


        // 準備事件資料 (從您的鬧鐘元件獲取)

        const alarmTitle = document.getElementById('alarmTitleInput').value;

        const alarmDateTime = document.getElementById('alarmDateTimeInput').value; // 需要是 ISO 8601 格式

        const recurrenceRule = document.getElementById('recurrenceRuleInput').value; // 例如 RRULE:FREQ=WEEKLY;BYDAY=MO


        // 重要:時間處理

        // JavaScript 的 Date 物件和時區處理可能比較棘手。

        // 確保 start.dateTime 和 end.dateTime 是正確的 ISO 8601 格式,並包含時區資訊。

        // 例如: 2024-07-28T09:00:00-07:00 (PDT)

        // 或  2024-07-28T16:00:00Z (UTC)


        const eventData = {

            summary: alarmTitle || '來自擴充功能的鬧鐘',

            start: {

                dateTime: alarmDateTime, // 'YYYY-MM-DDTHH:MM:SSZ' 或 'YYYY-MM-DDTHH:MM:SS±HH:MM'

                // timeZone: 'Asia/Taipei' // 如果 dateTime 不含時區偏移,可以指定 timeZone

            },

            end: {

                dateTime: alarmDateTime, // 對於鬧鐘,結束時間可以和開始時間一樣

                // timeZone: 'Asia/Taipei'

            },

            reminders: {

                useDefault: false, // 不使用日曆預設提醒

                overrides: [

                    { method: 'popup', minutes: 0 }, // 事件發生時彈窗提醒

                    // { method: 'popup', minutes: 10 } // 事件前10分鐘彈窗

                ]

            }

        };


        if (recurrenceRule) {

            eventData.recurrence = [recurrenceRule]; // 例如 ['RRULE:FREQ=DAILY;UNTIL=20241231T170000Z']

        }



        await addEventToCalendar(eventData, token);

        alert('鬧鐘已成功新增到您的 Google 日曆!');


    } catch (error) {

        console.error('Authentication or API error:', error);

        if (error.message && error.message.includes("No 'access_token'")) {

            alert('授權失敗,請重試或檢查您的 Google 帳戶設定。');

        } else if (error.message && error.message.includes("User cancelled")) {

            alert('您取消了授權。');

        } else {

            alert(`新增鬧鐘失敗: ${error.message}`);

        }

    }

}

  •    
    IGNORE_WHEN_COPYING_START
    content_copy download
    Use code with caution. JavaScript
    IGNORE_WHEN_COPYING_END


關於 RRULE (循環規則):
RRULE 的格式遵循 iCalendar 標準 (RFC 5545)。一些範例:

  • 每天:RRULE:FREQ=DAILY

  • 每週一:RRULE:FREQ=WEEKLY;BYDAY=MO

  • 每月的第一天:RRULE:FREQ=MONTHLY;BYMONTHDAY=1

  • 每年7月4日:RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=4

  • 重複5次:RRULE:FREQ=DAILY;COUNT=5

  • 直到特定日期結束 (UTC 時間):RRULE:FREQ=DAILY;UNTIL=20241231T170000Z

總結:

是的,透過 Google Chrome 擴充功能使用 Google Calendar API 是實現「將鬧鐘加入日曆並在手機收到通知(且支援循環)」的正確且強大的方法。這需要處理 OAuth 2.0 授權,但一旦完成,您的擴充功能將能提供非常好的使用者體驗。

記得仔細閱讀 Google Calendar API 和 Chrome Identity API 的官方文件,並妥善處理錯誤和使用者授權流程。



==========

Q: 目標對象分內部和外部, 那一開始肯定是在自己電腦測試, 完成後, 才能上傳, 所以是不是得建2個憑証, 一個選內部一個選外部?

A: 這個問題非常好,涉及到 OAuth 同意畫面 (OAuth Consent Screen) 的設定。您不需要建立兩個不同的 OAuth 用戶端 ID 憑證,但您需要正確設定「OAuth 同意畫面」的使用者類型 (User Type)

您看到的「內部 (Internal)」和「外部 (External)」選項是在設定 OAuth 同意畫面 時出現的,而不是在建立具體的「OAuth 用戶端 ID」時(雖然它們相關)。

正確的做法是:

  1. 選擇「外部 (External)」使用者類型:

    • 即使在開發和測試階段,也強烈建議直接選擇「外部」

    • 原因:

      • 「內部」類型適用於您是 Google Workspace (以前的 G Suite) 管理員,且您的擴充功能只會被您 Workspace 組織內部的成員使用。對於大多數公開發布到 Chrome 線上應用程式商店的擴充功能(即使一開始只有您自己測試),這是不適用的。

      • 您的目標最終是讓任何 Google 使用者(包括您自己使用 @gmail.com 或其他非特定 Workspace 的帳號進行測試)都能使用您的擴充功能,這就必須選擇「外部」。



  2. 利用「測試」發布狀態:

    • 當您將使用者類型設定為「外部」後,您的 OAuth 同意畫面會有一個**「發布狀態」(Publishing status),一開始會是「測試」(Testing)**。

    • 在「測試」狀態下:

      • 只有您明確加入到「測試人員」(Test users) 列表中的 Google 帳號才能夠通過 OAuth 授權流程。

      • 您需要將您自己用來測試的 Google 帳號加入到這個「測試人員」列表中。 您可以在「OAuth 同意畫面」設定頁面下方找到添加測試人員的選項。

      • 同意畫面會顯示一個「未經驗證的應用程式」警告,這是正常的,因為您的應用程式尚未經過 Google 的審核。


    • 這就完美地滿足了您的需求: 您可以安全地在自己的電腦上,使用您自己的 Google 帳號(只要已添加到測試人員列表)進行完整的開發和測試,而無需擔心其他人會意外授權。


  3. 發布擴充功能時:

    • 當您的擴充功能開發完成,準備上傳到 Chrome 線上應用程式商店並公開發布時,您需要:

      • 回到 Google Cloud Console 的「OAuth 同意畫面」設定。

      • 點擊**「發布應用程式」(Publish app)** 按鈕,將狀態從「測試」改為**「生產環境」(In production)**。

      • 可能需要進行驗證: 因為您的擴充功能請求了存取使用者日曆這樣較為敏感的權限 (scope),Google 很可能會要求您提交應用程式以供驗證 (Verification)。您需要提供隱私權政策網址、應用程式用途說明,甚至可能需要提供一個簡短的影片來展示 OAuth 流程和權限的使用方式。

      • **通過驗證後,**任何 Google 使用者都可以授權您的擴充功能,並且不會再看到「未經驗證的應用程式」警告。



總結:

  • 直接選擇「外部 (External)」使用者類型。

  • 在開發和測試階段,保持應用程式處於**「測試」發布狀態**,並將您自己的 Google 帳號加入**「測試人員」**列表。

  • 準備公開發布時,將狀態改為**「生產環境」,並根據 Google 的要求完成應用程式驗證**流程。

這樣您只需要設定一組 OAuth 同意畫面一個 OAuth 用戶端 ID 即可,既能滿足測試需求,也能為最終發布做好準備。


Q: 關於元件的ID, 我沒上傳時元件是用載入未封裝項目安裝的, 這個就自己產生一個ID, 然後上傳後安裝的是另一個ID, 我這做法是不是有問題, ID有2個, 正確應該怎麼分開本地版和上傳版?

A: 是的,您觀察到的現象——本地載入未封裝項目時有一個 ID,上傳到 Chrome 線上應用程式商店 (CWS) 後安裝下來是另一個 ID——是 Chrome 擴充功能的「預設行為」,但這通常不是理想的開發和測試流程,尤其在需要穩定 ID(例如設定 OAuth)時會產生問題。

您目前的做法並不算「錯誤」,只是沒有做額外的步驟來統一 ID。

為什麼會有兩個 ID?

  1. 本地載入未封裝 (Load unpacked): 當您在 chrome://extensions 中使用「載入未封裝項目」時,Chrome 會為該次載入的擴充功能指派一個臨時的、基於您本地環境計算出來的 ID。如果您移除再重新載入,或者換臺電腦載入,這個 ID 很可能會改變。

  2. 商店安裝 (Installed from CWS): 當您第一次將擴充功能上傳到 Chrome 線上應用程式商店時,商店會為這個擴充功能永久地指派一個唯一的 ID。所有從這個商店列表安裝的使用者,以及之後所有的更新,都會使用這個固定的商店 ID。

為什麼這會有問題?

主要問題在於依賴擴充功能 ID 的功能,最典型的就是您正在使用的 OAuth 2.0 驗證

  • 您在 Google Cloud Console 中設定 OAuth 用戶端 ID 時,需要指定允許使用該憑證的 Chrome 擴充功能 ID

  • 如果您設定的是商店指派的 ID,那麼在您本地用「載入未封裝項目」(帶著臨時 ID)測試時,OAuth 流程會失敗,因為本地的臨時 ID 與 Google Cloud Console 中登記的不符。

  • 反之,如果您登記的是本地測試時的某個臨時 ID,那麼上傳到商店後,所有使用者(包括您自己從商店安裝的版本)都會因為 ID 不符而無法通過 OAuth 驗證。

正確的做法:固定擴充功能 ID

為了讓本地開發測試和商店發布版本使用同一個固定的 ID,您需要在您的擴充功能中加入一個關鍵步驟:使用 key 欄位

步驟:

  1. 首次打包擴充功能以生成金鑰:

    • 確保您的 manifest.json 檔案準備好。

    • 進入 chrome://extensions/。

    • 啟用右上角的「開發人員模式」。

    • 點擊「封裝擴充功能」(Pack extension) 按鈕。

    • 在「擴充功能根目錄」欄位,選擇您擴充功能的原始碼資料夾。

    • 「私密金鑰檔案」欄位留空 (第一次打包時必須留空!)

    • 點擊「封裝擴充功能」。

    • Chrome 會在您原始碼資料夾的上一層目錄(通常是這樣)生成兩個檔案:

      • 一個 .crx 檔 (這是打包後的擴充功能安裝檔)。

      • 一個 .pem 檔 (這是私密金鑰檔案)


    • 極度重要:請將這個 .pem 檔案備份到一個絕對安全、不會遺失的地方!並且絕對不要公開它! 失去這個 .pem 檔案意味著您將永遠無法再更新您在 Chrome 商店上發布的這個擴充功能。


  2. 將公鑰添加到 manifest.json:

    • 您需要從剛才生成的 .pem 私密金鑰中提取對應的公鑰,並將其 Base64 編碼後的值加入到您的 manifest.json 檔案中。

    • 如何獲取公鑰值?最簡單的方法通常是:

      • 將您剛才打包生成的 .crx 檔(或者包含 manifest.json 的源碼 zip 檔)首次上傳到 Chrome 開發人員資訊主頁(即使是草稿狀態)。

      • 上傳成功處理後,在該擴充功能的「套件」(Package) 頁面,您會看到一個「公開金鑰」(Public key) 的欄位,裡面有一長串 Base64 編碼的字元。這就是您需要的公鑰值!

      • 複製這整串公鑰值。


回到您本地的 manifest.json 檔案,在頂層加入一個 key 欄位,並將複製的公鑰值貼上:
      {

  "manifest_version": 3,

  "name": "我的鬧鐘擴充功能",

  "version": "1.1", // 記得更新版本號

  "key": "MIIBIjANBgk...YOUR_COPIED_PUBLIC_KEY_STRING...AQAB", // 把這裡替換成您複製的公鑰值

  "description": "...",

  "permissions": [ ... ],

  // ... 其他設定

}

  •    


  1. 之後的開發與測試:

    • 現在,只要您的 manifest.json 檔案中包含了這個 key 欄位(並且其值與您商店列表的公鑰一致),無論您是在本地使用「載入未封裝項目」,還是在商店更新後安裝,Chrome 都會為您的擴充功能指派同一個固定的 ID(這個 ID 是根據 key 計算出來的)。

    • 您現在可以在 Google Cloud Console 中放心地使用這個固定的 ID 來設定您的 OAuth 用戶端 ID 了。無論是本地測試還是商店版本,ID 都將一致。


總結:

正確分離本地版和上傳版的關鍵在於獲得一個固定的、跨環境一致的擴充功能 ID。透過首次打包生成 .pem 私密金鑰,並將對應的公鑰添加到 manifest.json 的 key 欄位,即可實現此目標。這是 Chrome 擴充功能開發的標準實踐,強烈建議您這樣做,尤其是在需要依賴穩定 ID 的場景下。

再次強調:務必妥善保管您的 .pem 私密金鑰檔案!


Q: 那個pem是什麼時候用?

A:好的,這是一個非常關鍵的問題!

那個 .pem 檔案(私密金鑰檔案)只在一個特定且非常重要的時刻使用:

當您要「封裝擴充功能」(Pack extension) 以便「上傳更新」(Upload an update) 到您現有的 Chrome 線上應用程式商店 (CWS) 列表時。

詳細說明:

  1. 身份證明: .pem 檔案包含了您的私密金鑰,它是與您 manifest.json 中 "key" 欄位(以及 CWS 開發人員資訊主頁上顯示的「公開金鑰」)配對的另一半。擁有這個私密金鑰是證明您是該擴充功能原始發布者的唯一方式。

  2. 簽署更新包: 當您開發完新版本,準備上傳更新時,您需要使用 Chrome 的「封裝擴充功能」功能(或命令行工具)來創建一個新的 .zip 或 .crx 文件。在這個封裝過程中,您必須指定當初生成或使用的那個 .pem 私密金鑰檔案。

  3. 數位簽章: 封裝工具會使用您提供的 .pem 檔案中的私密金鑰,對新版本的擴充功能包進行數位簽章 (Digital Signature)。

  4. CWS 驗證: 當您將這個帶有數位簽章的更新包上傳到 CWS 開發人員資訊主頁時,CWS 會執行以下驗證:

    • 它會讀取您更新包上的數位簽章。

    • 它會使用儲存在 CWS 系統中、與您的擴充功能 ID 關聯的那個公開金鑰(也就是您複製到 manifest.json 的那個)來嘗試驗證這個簽章。

    • 如果驗證成功(表示簽章確實是由對應的私密金鑰生成的),CWS 就確認您是合法的擁有者,允許您上傳並發布這個更新。

    • 如果驗證失敗(例如,您用了錯誤的 .pem 檔案,或者根本沒有用 .pem 簽名),CWS 會拒絕您的上傳,因為它無法確認您的身份。


總結 .pem 的用途:

  • 僅用於打包/簽署要上傳到 CWS 的更新版本。

  • 目的是向 CWS 證明您是該擴充功能列表的合法所有者。

它不用於:

  • 本地開發和測試(Load unpacked)。

  • 從 CWS 安裝擴充功能。

  • 讓本地 ID 與商店 ID 保持一致(這是靠 manifest.json 中的 key 欄位實現的)。

所以,回到您之前的擔憂:如果您真的遺失了最初上傳時使用的那個 .pem 檔案,那麼您將無法透過標準流程更新您在 CWS 上的現有擴充功能列表。 您仍然可以在本地開發(因為您已經把公鑰 key 加到了 manifest.json),但無法將這些更新發布給您現有的使用者。

Q:我之前上傳元件都是直接在檔案總管把元件資料夾壓成zip, 就上傳了, 現在是要透過"封裝擴充功能", 產生的crx, 上傳crx更新元件?

A:您之前的做法——直接壓縮元件資料夾成 .zip 檔並上傳到開發人員資訊主頁——是完全正確且最常用的方式來更新您在 Chrome 線上應用程式商店 (CWS) 的擴充功能!

不需要改成透過「封裝擴充功能」產生 .crx 再上傳。

讓我解釋一下兩者的區別以及 .pem 在其中的角色:

  1. 上傳 .zip 檔案 (您之前的做法,推薦):

    • 流程: 您將包含 manifest.json(現在裡面有 "key" 欄位)和所有其他原始碼的資料夾壓縮成 .zip 文件。然後,您登入開發人員資訊主頁,找到您的擴充功能列表,並上傳這個 .zip 文件作為新版本。

    • 簽章在哪裡發生? 在這個流程中,簽章是由 Google 的伺服器在後端處理的。當 CWS 收到您的 .zip 文件後,它會:

      • 驗證 manifest.json 中的 "key" 是否與該商店列表記錄的公開金鑰匹配。

      • 如果匹配,它會使用儲存在 Google 安全伺服器上的、與您的公開金鑰配對的那個(隱藏的)私密金鑰信息(或者更準確地說,它驗證您帳戶對此列表的所有權,而該列表已綁定該金鑰對)來為您的新版本進行簽署,並將其發布給用戶。

    • .pem 的角色: 在這個 .zip 上傳流程中,您不需要在本地使用那個 .pem 檔案。.pem 檔案的存在(或者說,您對應金鑰對的所有權)是隱含在您能夠登入並操作該商店列表這個事實中的。

  2. 使用「封裝擴充功能」產生 .crx 並上傳:

    • 流程: 您在本地使用 chrome://extensions/ 頁面的「封裝擴充功能」按鈕。在「擴充功能根目錄」選擇您的資料夾,並且必須在「私密金鑰檔案」欄位指定您那個 .pem 檔案的路徑。這會在本地生成一個已經被您的 .pem 私密金鑰簽署過的 .crx 文件。然後您可以上傳這個 .crx 文件到開發人員資訊主頁。

    • 簽章在哪裡發生? 在這個流程中,簽章是在您的本地電腦上完成的,由「封裝擴充功能」工具使用您提供的 .pem 檔案執行。

    • 適用場景: 這種方法相對少見,通常用於:

      • 您想在本地測試一個完全簽署過的 .crx 版本(雖然不常用)。

      • 您想將 .crx 文件分發到 CWS 之外的地方(例如內部網路部署,但現在 Chrome 對安裝非商店來源的擴充功能限制很多)。

      • 極少數情況下,如果 CWS 的 .zip 上傳/處理流程遇到問題,作為備選方案。

結論:

  • 您之前直接壓縮資料夾成 .zip 並上傳的方式,是更新 CWS 擴充功能的標準且推薦方法。

  • 您不需要為了更新而特意去使用「封裝擴充功能」和 .pem 檔案來生成 .crx。

  • .pem 檔案的核心價值在於證明您對該擴充功能 ID 的所有權,並且是如果您選擇在本地打包成 .crx 時所必需的。對於常規的 .zip 更新流程,它在幕後確保了您的身份,但您無需直接操作它。

所以,請繼續使用您熟悉的方法:修改好程式碼 -> 將資料夾壓縮成 .zip -> 上傳到開發人員資訊主頁即可。

(但還是要祈禱您沒有真的遺失那個 .pem 檔案,萬一未來 Google 的政策或流程改變,或者您的帳戶出現問題需要重新驗證所有權時,它可能就變得至關重要了。)



留言

這個網誌中的熱門文章

Offscreen Canvas

外部網頁新增 Google 日曆行程(URL模板)

計算 Canvas 文字最大字體