Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/js/popup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getCurrentYearMonth, downloadICS, addOneEventToCalendar } from "./utils.js";
import {
getCurrentYearMonth,
downloadICS,
addOneEventToCalendar,
addBatchEventsToCalendar,
} from "./utils.js";

function initializeInputs() {
const { year, month } = getCurrentYearMonth();
Expand Down Expand Up @@ -112,18 +117,28 @@ function setupEventListeners() {
async function insertEventsToGCal(events) {
// Google OAuth token
const token = await getGoogleAuthToken();

// add event Google Calendar
for (const event of events) {
const batchAddEvents = true;

if (!batchAddEvents) {
// add event Google Calendar
for (const event of events) {
try {
await addOneEventToCalendar(token, event);
console.log("add Event Success", event.title);
} catch (err) {
console.error("add Event error:", event.title, err);
}
}
} else if (batchAddEvents) {
// batch API,done after...
try {
await addOneEventToCalendar(token, event);
console.log("add Event Success", event.title);
const batchResponse = await addBatchEventsToCalendar(token, events);
console.log("Batch request sent!");
console.log(batchResponse);
} catch (err) {
console.error("add Event error:", event.title, err);
console.error("Batch request failed:", err);
}
}

// batch API,done after...
}

async function launchWebAuthFlowForGoogle() {
Expand Down
93 changes: 93 additions & 0 deletions src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,96 @@ export async function addOneEventToCalendar(token, event) {

return res.json();
}

function normalizeBatchText(raw) {
return raw.replace(/\r?\n/g, "\r\n");
}

function generateBatchRequestBody(events, boundary = "batch_boundary") {
const CRLF = "\r\n";
let body = "";

events.forEach((event, i) => {
body += `--${boundary}${CRLF}`;
body += `Content-Type: application/http${CRLF}`;
body += `Content-ID: <item-${i + 1}>${CRLF}${CRLF}`;

body += `POST /calendar/v3/calendars/primary/events HTTP/1.1${CRLF}`;
body += `Content-Type: application/json${CRLF}${CRLF}`;

const eventPayload = {
summary: event.title,
description: event.description,
start: {
dateTime: event.startDate.toISOString(),
timeZone: "Asia/Taipei",
},
end: {
dateTime: event.endDate.toISOString(),
timeZone: "Asia/Taipei",
},
};

body += JSON.stringify(eventPayload) + CRLF + CRLF;
});

body += `--${boundary}--${CRLF}`;
return body;
}

function parseGoogleBatchResponse(responseText) {
const boundary = responseText.match(/--batch_[^\r\n-]+/)?.[0]?.slice(2);
const parts = responseText.split(`--${boundary}`);
const results = [];

for (const part of parts) {
const trimmed = part.trim();
if (!trimmed || !trimmed.includes("HTTP/1.1")) continue;

const statusMatch = trimmed.match(/HTTP\/1.1 (\d{3}) (.+)/);
const status = statusMatch ? parseInt(statusMatch[1], 10) : null;
const statusText = statusMatch ? statusMatch[2] : "";

const sections = trimmed.split(/\n{2,}|\r\n{2,}/);
const rawBody = sections[sections.length - 1];

const jsonStart = rawBody.indexOf("{");
const jsonEnd = rawBody.lastIndexOf("}");

let body = null;
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
const jsonStr = rawBody.slice(jsonStart, jsonEnd + 1);
try {
body = JSON.parse(jsonStr);
} catch (e) {
console.warn("JSON parse failed:", e);
}
}

results.push({ status, ok: status >= 200 && status < 300, statusText, body });
}

return results;
}

// Google Calendar API batch add Event
export async function addBatchEventsToCalendar(token, events) {
const boundary = "batch_boundary_" + Date.now();
const body = generateBatchRequestBody(events, boundary);

const res = await fetch("https://www.googleapis.com/batch/calendar/v3", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": `multipart/mixed; boundary=${boundary}`,
},
body,
});

const text = await res.text();
if (!res.ok) {
throw new Error(`Batch API Error: ${res.status}\n${text}`);
}

return parseGoogleBatchResponse(normalizeBatchText(text));
}