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
31 changes: 31 additions & 0 deletions tools/chrome-cookie-exporter/README.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions tools/chrome-cookie-exporter/background.js
Original file line number Diff line number Diff line change
@@ -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] }
);
21 changes: 21 additions & 0 deletions tools/chrome-cookie-exporter/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
50 changes: 50 additions & 0 deletions tools/chrome-cookie-exporter/popup.css
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 23 additions & 0 deletions tools/chrome-cookie-exporter/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CU Cookie Exporter</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main class="popup">
<h1>CU Cookie Exporter</h1>
<p class="hint">Open the three services in Chrome first so their cookies are available.</p>

<button id="copy-yandex" type="button">Copy Yandex `cookie.txt`</button>
<button id="copy-time" type="button">Copy Time `.env` lines</button>
<button id="copy-ktalk" type="button">Copy KTalk `ktalk_auth.txt`</button>

<pre id="status" class="status">Ready.</pre>
</main>

<script src="popup.js"></script>
</body>
</html>
143 changes: 143 additions & 0 deletions tools/chrome-cookie-exporter/popup.js
Original file line number Diff line number Diff line change
@@ -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.");
});
Loading