From 6bcb884a9140d03466aa22ba6637e9fadbc6d727 Mon Sep 17 00:00:00 2001 From: Igor Mamaev Date: Mon, 20 Apr 2026 23:54:23 +0300 Subject: [PATCH] extension --- tools/chrome-cookie-exporter/README.md | 31 +++++ tools/chrome-cookie-exporter/background.js | 30 +++++ tools/chrome-cookie-exporter/manifest.json | 21 +++ tools/chrome-cookie-exporter/popup.css | 50 +++++++ tools/chrome-cookie-exporter/popup.html | 23 ++++ tools/chrome-cookie-exporter/popup.js | 143 +++++++++++++++++++++ 6 files changed, 298 insertions(+) create mode 100644 tools/chrome-cookie-exporter/README.md create mode 100644 tools/chrome-cookie-exporter/background.js create mode 100644 tools/chrome-cookie-exporter/manifest.json create mode 100644 tools/chrome-cookie-exporter/popup.css create mode 100644 tools/chrome-cookie-exporter/popup.html create mode 100644 tools/chrome-cookie-exporter/popup.js diff --git a/tools/chrome-cookie-exporter/README.md b/tools/chrome-cookie-exporter/README.md new file mode 100644 index 0000000..c420677 --- /dev/null +++ b/tools/chrome-cookie-exporter/README.md @@ -0,0 +1,31 @@ +# Chrome Cookie Exporter + +Minimal Chrome extension for copying the auth values this project needs. + +## What it copies + +1. `cookie.txt`: combined Yandex cookies for Calendar access +2. `.env` lines: `TIME_TEAM_ID`, `TIME_COOKIE`, and `TIME_CSRF` +3. `ktalk_auth.txt`: the exact `Authorization` header value from a KTalk request + +## Load in Chrome + +1. Open `chrome://extensions` +2. Enable Developer mode +3. Click Load unpacked +4. Select `tools/chrome-cookie-exporter` + +## Use + +1. Sign in to `calendar.yandex.ru`, `time.cu.ru`, and `centraluniversity.ktalk.ru` +2. Open the extension popup +3. For Time, open a space/channel page so the app sends a `/api/v4/teams/.../channels/...` request +4. Click the button you need +5. Paste the copied value into the matching local file or `.env` + +## Notes + +- The extension only reads cookies from the three target services. +- Time team id is taken from observed `time.cu.ru/api/v4/teams/.../channels/...` request URLs. +- KTalk auth is taken from the real `Authorization` request header and copied as-is, including the `Session` prefix. +- If Time CSRF is missing, refresh `time.cu.ru` after login and try again. diff --git a/tools/chrome-cookie-exporter/background.js b/tools/chrome-cookie-exporter/background.js new file mode 100644 index 0000000..4dbd115 --- /dev/null +++ b/tools/chrome-cookie-exporter/background.js @@ -0,0 +1,30 @@ +const KTALK_URL_PATTERN = "https://centraluniversity.ktalk.ru/*"; +const TIME_API_URL_PATTERN = "https://time.cu.ru/api/v4/teams/*"; + +chrome.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const authorizationHeader = details.requestHeaders?.find( + (header) => header.name.toLowerCase() === "authorization" + ); + + if (!authorizationHeader?.value) { + return; + } + + chrome.storage.local.set({ ktalkAuthorization: authorizationHeader.value }); + }, + { urls: [KTALK_URL_PATTERN] }, + ["requestHeaders"] +); + +chrome.webRequest.onBeforeRequest.addListener( + (details) => { + const match = details.url.match(/\/api\/v4\/teams\/([^/]+)\/channels/i); + if (!match) { + return; + } + + chrome.storage.local.set({ timeTeamId: match[1] }); + }, + { urls: [TIME_API_URL_PATTERN] } +); diff --git a/tools/chrome-cookie-exporter/manifest.json b/tools/chrome-cookie-exporter/manifest.json new file mode 100644 index 0000000..8d39a9b --- /dev/null +++ b/tools/chrome-cookie-exporter/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 3, + "name": "CU Cookie Exporter", + "version": "0.1.0", + "description": "Copy required Yandex Calendar, Time, and KTalk auth values.", + "permissions": ["cookies", "clipboardWrite", "storage", "webRequest"], + "host_permissions": [ + "https://calendar.yandex.ru/*", + "https://yandex.ru/*", + "https://passport.yandex.ru/*", + "https://time.cu.ru/*", + "https://centraluniversity.ktalk.ru/*" + ], + "action": { + "default_title": "CU Cookie Exporter", + "default_popup": "popup.html" + }, + "background": { + "service_worker": "background.js" + } +} diff --git a/tools/chrome-cookie-exporter/popup.css b/tools/chrome-cookie-exporter/popup.css new file mode 100644 index 0000000..6222518 --- /dev/null +++ b/tools/chrome-cookie-exporter/popup.css @@ -0,0 +1,50 @@ +body { + margin: 0; + background: #111827; + color: #e5e7eb; + font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.popup { + width: 340px; + padding: 16px; +} + +h1 { + margin: 0 0 8px; + font-size: 18px; +} + +.hint { + margin: 0 0 12px; + color: #9ca3af; +} + +button { + display: block; + width: 100%; + margin: 0 0 10px; + padding: 10px 12px; + border: 0; + border-radius: 8px; + background: #2563eb; + color: #fff; + font: inherit; + cursor: pointer; +} + +button:hover { + background: #1d4ed8; +} + +.status { + margin: 6px 0 0; + padding: 10px; + min-height: 84px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + border-radius: 8px; + background: #0b1220; + color: #cbd5e1; +} diff --git a/tools/chrome-cookie-exporter/popup.html b/tools/chrome-cookie-exporter/popup.html new file mode 100644 index 0000000..faddd4e --- /dev/null +++ b/tools/chrome-cookie-exporter/popup.html @@ -0,0 +1,23 @@ + + + + + + CU Cookie Exporter + + + +
+

CU Cookie Exporter

+

Open the three services in Chrome first so their cookies are available.

+ + + + + +
Ready.
+
+ + + + diff --git a/tools/chrome-cookie-exporter/popup.js b/tools/chrome-cookie-exporter/popup.js new file mode 100644 index 0000000..e5ceb54 --- /dev/null +++ b/tools/chrome-cookie-exporter/popup.js @@ -0,0 +1,143 @@ +const statusEl = document.getElementById("status"); + +const YANDEX_URLS = [ + "https://calendar.yandex.ru/", + "https://yandex.ru/", + "https://passport.yandex.ru/" +]; + +const TIME_URL = "https://time.cu.ru/"; +const KTALK_URL = "https://centraluniversity.ktalk.ru/"; + +function setStatus(message) { + statusEl.textContent = message; +} + +function getAllCookies(details) { + return new Promise((resolve, reject) => { + chrome.cookies.getAll(details, (cookies) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + resolve(cookies); + }); + }); +} + +function getCookie(details) { + return new Promise((resolve, reject) => { + chrome.cookies.get(details, (cookie) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + resolve(cookie); + }); + }); +} + +async function copyText(text) { + await navigator.clipboard.writeText(text); +} + +function getStorageValue(key) { + return new Promise((resolve, reject) => { + chrome.storage.local.get([key], (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + resolve(result[key] || null); + }); + }); +} + +function joinCookieString(cookies) { + return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; "); +} + +async function loadYandexCookies() { + const cookieMap = new Map(); + for (const url of YANDEX_URLS) { + const cookies = await getAllCookies({ url }); + for (const cookie of cookies) { + cookieMap.set(cookie.name, cookie); + } + } + + const cookies = [...cookieMap.values()].sort((left, right) => left.name.localeCompare(right.name)); + if (!cookies.length) { + throw new Error("No Yandex cookies found. Open Yandex Calendar and sign in first."); + } + + return joinCookieString(cookies); +} + +function pickCsrfToken(cookies) { + const exactNames = ["csrftoken", "csrf", "x-csrf-token", "XSRF-TOKEN", "xsrf-token"]; + for (const name of exactNames) { + const match = cookies.find((cookie) => cookie.name === name); + if (match) { + return match.value; + } + } + + const fuzzyMatch = cookies.find((cookie) => /csrf|xsrf/i.test(cookie.name)); + return fuzzyMatch ? fuzzyMatch.value : null; +} + +async function loadTimeEnvLines() { + const cookies = await getAllCookies({ url: TIME_URL }); + if (!cookies.length) { + throw new Error("No Time cookies found. Open time.cu.ru and sign in first."); + } + + const teamId = await getStorageValue("timeTeamId"); + if (!teamId) { + throw new Error( + "No Time team id captured yet. Open a space in time.cu.ru so it sends a /api/v4/teams/.../channels request." + ); + } + + const csrfValue = pickCsrfToken(cookies); + if (!csrfValue) { + throw new Error("Could not find a Time CSRF cookie. Open the app and refresh once."); + } + + return `TIME_TEAM_ID=${teamId}\nTIME_COOKIE=${joinCookieString(cookies)}\nTIME_CSRF=${csrfValue}`; +} + +async function loadKTalkAuth() { + const token = await getStorageValue("ktalkAuthorization"); + if (!token) { + throw new Error( + "No KTalk Authorization header captured yet. Open KTalk, trigger any API request, then try again." + ); + } + + return token; +} + +async function runCopy(loader, successMessage) { + setStatus("Reading browser cookies..."); + try { + const text = await loader(); + await copyText(text); + setStatus(`${successMessage}\n\n${text}`); + } catch (error) { + setStatus(error instanceof Error ? error.message : String(error)); + } +} + +document.getElementById("copy-yandex").addEventListener("click", () => { + runCopy(loadYandexCookies, "Copied Yandex cookie string for cookie.txt."); +}); + +document.getElementById("copy-time").addEventListener("click", () => { + runCopy(loadTimeEnvLines, "Copied TIME_COOKIE and TIME_CSRF lines."); +}); + +document.getElementById("copy-ktalk").addEventListener("click", () => { + runCopy(loadKTalkAuth, "Copied KTalk auth token for ktalk_auth.txt."); +});