cicy-desktop 2.1.280 → 2.1.281
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/windows/add-team.bat +43 -0
- package/src/backends/homepage-react/assets/index-BzYBKzQW.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/main.js +5 -1
- package/src/profiles/profile-store.js +29 -0
- package/src/tabbrowser/panel-cells.js +84 -5
- package/src/tabbrowser/panel-preload.js +3 -0
- package/src/tabbrowser/telegram-identity.js +77 -0
- package/src/tabbrowser/telegram-matrix.html +152 -43
- package/test/profile-store-note.test.js +27 -0
- package/test/telegram-identity.test.js +41 -0
- package/test/telegram-matrix.test.js +14 -8
- package/workers/render/src/App.jsx +29 -7
- package/src/backends/homepage-react/assets/index-Bok65e-y.js +0 -377
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BzYBKzQW.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-C6Zrdgc0.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/src/main.js
CHANGED
|
@@ -168,8 +168,12 @@ if (process.platform === "win32") {
|
|
|
168
168
|
const DEEPLINK_SCHEMES = ["cicy-desktop", "cicy"]; // primary, then legacy
|
|
169
169
|
const isDeepLink = (u) =>
|
|
170
170
|
typeof u === "string" && DEEPLINK_SCHEMES.some((s) => u.startsWith(`${s}://`));
|
|
171
|
+
// Source/dev runs (`electron <appdir>`) must register execPath + the app dir,
|
|
172
|
+
// otherwise Windows stores a bare `electron.exe "%1"` handler that launches an
|
|
173
|
+
// empty Electron shell — and clobbers the installed app's registration.
|
|
174
|
+
const deeplinkArgs = electronApp.isPackaged ? [] : [process.execPath, [path.resolve(process.argv[1] || electronApp.getAppPath())]];
|
|
171
175
|
for (const s of DEEPLINK_SCHEMES) {
|
|
172
|
-
try { if (!electronApp.isDefaultProtocolClient(s)) electronApp.setAsDefaultProtocolClient(s); } catch {}
|
|
176
|
+
try { if (!electronApp.isDefaultProtocolClient(s, ...deeplinkArgs)) electronApp.setAsDefaultProtocolClient(s, ...deeplinkArgs); } catch {}
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
// Deep links can arrive before any BrowserWindow exists (cold start via
|
|
@@ -305,6 +305,33 @@ function setProxy(backend, idx, url) {
|
|
|
305
305
|
throw new Error(`Unknown backend: ${backend}`);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// removeProfile — drop the profile record. Electron: deletes account-<N>.json
|
|
309
|
+
// (the caller is responsible for the session's storage). Returns true if it existed.
|
|
310
|
+
function removeProfile(backend, idx) {
|
|
311
|
+
if (backend === "electron") {
|
|
312
|
+
const f = electronFile(idx);
|
|
313
|
+
if (!fs.existsSync(f)) return false;
|
|
314
|
+
fs.unlinkSync(f);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (backend === "chrome") {
|
|
318
|
+
const cfg = readChromeConfig();
|
|
319
|
+
if (!cfg[`profile_${idx}`]) return false;
|
|
320
|
+
delete cfg[`profile_${idx}`];
|
|
321
|
+
fs.writeFileSync(CHROME_JSON, JSON.stringify(cfg, null, 2));
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Unknown backend: ${backend}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// setNote — free-form per-profile note (both backends).
|
|
328
|
+
function setNote(backend, idx, note) {
|
|
329
|
+
const text = typeof note === "string" ? note.trim() : "";
|
|
330
|
+
if (backend === "chrome") return mutateChrome(idx, (e) => ({ ...e, note: text }));
|
|
331
|
+
if (backend === "electron") return mutateElectron(idx, (d) => ({ ...d, note: text }));
|
|
332
|
+
throw new Error(`Unknown backend: ${backend}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
308
335
|
// setLogin — upsert a rich login record (any subset of LOGIN_FIELDS). Keyed by
|
|
309
336
|
// `name` (site name). Works identically for both backends.
|
|
310
337
|
function setLogin(backend, idx, login) {
|
|
@@ -345,6 +372,8 @@ module.exports = {
|
|
|
345
372
|
listProfiles,
|
|
346
373
|
getProfile,
|
|
347
374
|
setProxy,
|
|
375
|
+
setNote,
|
|
376
|
+
removeProfile,
|
|
348
377
|
setLogin,
|
|
349
378
|
addLogin,
|
|
350
379
|
removeLogin,
|
|
@@ -46,6 +46,7 @@ function profileForCell(url, requested) {
|
|
|
46
46
|
const idx = Number(requested);
|
|
47
47
|
return Number.isInteger(idx) && idx > 0 ? idx : DEFAULT_PROFILE;
|
|
48
48
|
}
|
|
49
|
+
const telegramIdentity = require("./telegram-identity");
|
|
49
50
|
const appliedProxy = new Set(); // partitions whose proxy is already configured
|
|
50
51
|
function ensureCellSessionProxy(idx) {
|
|
51
52
|
const part = partitionFor(idx);
|
|
@@ -117,6 +118,7 @@ class PanelCells {
|
|
|
117
118
|
},
|
|
118
119
|
});
|
|
119
120
|
const wc = view.webContents;
|
|
121
|
+
const rec = { view, url: "", profile: profileIdx, visible: true, identity: null };
|
|
120
122
|
try { view.setBackgroundColor("#ffffff"); } catch (e) {}
|
|
121
123
|
// Chromium uses a dark canvas for transparent/un-styled documents when the
|
|
122
124
|
// app theme is dark (for example a plain `Not Found` response). Keep real
|
|
@@ -178,7 +180,7 @@ class PanelCells {
|
|
|
178
180
|
let loading = false;
|
|
179
181
|
try { loading = wc.isLoading(); } catch (e) {}
|
|
180
182
|
const failed = /^chrome-error:/.test(url) || !!lastError;
|
|
181
|
-
this.sendState({ id: cellId, url, title, loading, wcId: wc.id, failed, error: failed ? lastError : null });
|
|
183
|
+
this.sendState({ id: cellId, url, title, loading, wcId: wc.id, failed, error: failed ? lastError : null, identity: rec.identity || null });
|
|
182
184
|
};
|
|
183
185
|
// Telegram Web 是 PWA:代理挂掉时 service worker 照样从缓存吐出 app shell,主框架
|
|
184
186
|
// 「加载成功」、did-fail-load 不触发,可页面其实一点网都没有(用户看到的就是一片白
|
|
@@ -219,8 +221,38 @@ class PanelCells {
|
|
|
219
221
|
push();
|
|
220
222
|
};
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
// Telegram 身份:登录后从页面自己的 localStorage/IndexedDB 读出 @username,写进
|
|
225
|
+
// profile 的 logins(name=telegram),并推给面板列表。登录流程刚结束时用户表可能
|
|
226
|
+
// 还没落库,所以延迟探几次;拿到就停。
|
|
227
|
+
let identSeq = 0;
|
|
228
|
+
const detectIdentity = () => {
|
|
229
|
+
if (profileIdx === 0) return;
|
|
230
|
+
let url = ""; try { url = wc.getURL(); } catch (e) {}
|
|
231
|
+
if (!telegramIdentity.isTelegramUrl(url)) return;
|
|
232
|
+
const seq = ++identSeq;
|
|
233
|
+
const delays = [2500, 8000, 20000, 45000];
|
|
234
|
+
const tick = async (i) => {
|
|
235
|
+
if (seq !== identSeq || wc.isDestroyed()) return;
|
|
236
|
+
let raw = null;
|
|
237
|
+
try { raw = await wc.executeJavaScript(telegramIdentity.TELEGRAM_IDENTITY_SCRIPT, true); } catch (e) {}
|
|
238
|
+
const it = telegramIdentity.normalizeTelegramIdentity(raw);
|
|
239
|
+
if (seq !== identSeq) return;
|
|
240
|
+
if (it && (it.username || it.displayName || it.phone)) {
|
|
241
|
+
const prev = rec.identity;
|
|
242
|
+
rec.identity = it;
|
|
243
|
+
if (!prev || prev.username !== it.username || prev.displayName !== it.displayName || prev.phone !== it.phone) {
|
|
244
|
+
try { require("../profiles/profile-store").setLogin("electron", profileIdx, telegramIdentity.telegramLoginRecord(it)); } catch (e) {}
|
|
245
|
+
this.sendState({ id: cellId, wcId: wc.id, identity: it, loading: false, url, title: (() => { try { return wc.getTitle(); } catch (e) { return ""; } })() });
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (i + 1 < delays.length) setTimeout(() => tick(i + 1), delays[i + 1]);
|
|
250
|
+
};
|
|
251
|
+
setTimeout(() => tick(0), delays[0]);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
wc.on("did-start-loading", () => { probeSeq++; identSeq++; this.sendState({ id: cellId, loading: true, wcId: wc.id }); });
|
|
255
|
+
wc.on("did-stop-loading", () => { push(); probeReachable(); detectIdentity(); });
|
|
224
256
|
wc.on("did-fail-load", (_e, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
225
257
|
if (isMainFrame === false || errorCode === -3) return; // 子框架 / ERR_ABORTED(用户导航打断)不算失败
|
|
226
258
|
lastError = { code: errorCode, description: String(errorDescription || ""), url: String(validatedURL || "") };
|
|
@@ -232,7 +264,7 @@ class PanelCells {
|
|
|
232
264
|
wc.on("did-navigate-in-page", push);
|
|
233
265
|
wc.on("page-title-updated", push);
|
|
234
266
|
|
|
235
|
-
|
|
267
|
+
rec.view = view;
|
|
236
268
|
this.views.set(String(cellId), rec);
|
|
237
269
|
return rec;
|
|
238
270
|
}
|
|
@@ -281,7 +313,7 @@ class PanelCells {
|
|
|
281
313
|
if (!wc || wc.isDestroyed()) continue;
|
|
282
314
|
let url = "", title = "", loading = false;
|
|
283
315
|
try { url = wc.getURL(); title = wc.getTitle(); loading = wc.isLoading(); } catch (e) {}
|
|
284
|
-
out.push({ id, url, title, loading, wcId: wc.id, failed: /^chrome-error:/.test(url), error: wc.cicyLastError || null });
|
|
316
|
+
out.push({ id, url, title, loading, wcId: wc.id, failed: /^chrome-error:/.test(url), error: wc.cicyLastError || null, identity: rec.identity || null });
|
|
285
317
|
}
|
|
286
318
|
return out;
|
|
287
319
|
}
|
|
@@ -359,6 +391,9 @@ function installIpc(findTab) {
|
|
|
359
391
|
accountIdx: Number(p.accountIdx),
|
|
360
392
|
name: String(p.name || ""),
|
|
361
393
|
proxy: p.proxy && p.proxy.enabled ? String(p.proxy.url || "") : "",
|
|
394
|
+
note: String(p.note || ""),
|
|
395
|
+
telegram: telegramIdentity.telegramIdentityFromProfile(p),
|
|
396
|
+
ipInfo: p.ipInfo && p.ipInfo.ip ? { ip: String(p.ipInfo.ip), area: String(p.ipInfo.area || ""), probedAt: String(p.ipInfo.probedAt || "") } : null,
|
|
362
397
|
}));
|
|
363
398
|
} catch (err) { return []; }
|
|
364
399
|
});
|
|
@@ -376,6 +411,50 @@ function installIpc(findTab) {
|
|
|
376
411
|
await applyProfileProxy(profile.accountIdx, profile.proxy);
|
|
377
412
|
return { accountIdx: profile.accountIdx, name: profile.name, proxy: profile.proxy.url };
|
|
378
413
|
});
|
|
414
|
+
// 出口 IP / 地区:经该 profile 的 session(=它的代理)去问 IP 服务,结果写回 profile.ipInfo。
|
|
415
|
+
ipcMain.handle("panelcells:probe-ip", async (e, { accountIdx }) => {
|
|
416
|
+
if (!ctx(e)) throw new Error("Invalid panel");
|
|
417
|
+
const id = Number(accountIdx);
|
|
418
|
+
if (!Number.isInteger(id) || id <= 0) throw new Error("Invalid Electron profile ID");
|
|
419
|
+
const profileStore = require("../profiles/profile-store");
|
|
420
|
+
const { probeIpViaSession } = require("../utils/ip-probe");
|
|
421
|
+
const part = partitionFor(id);
|
|
422
|
+
// 没打开过的 profile 其 session 还没配代理,会探成本机 IP —— 先按存储的代理配好。
|
|
423
|
+
if (!appliedProxy.has(part)) {
|
|
424
|
+
const prof = profileStore.getProfile("electron", id);
|
|
425
|
+
const rules = profileStore.proxyRules(prof && prof.proxy);
|
|
426
|
+
await session.fromPartition(part).setProxy({ proxyRules: rules || "direct://", proxyBypassRules: "127.0.0.1,localhost,[::1]" });
|
|
427
|
+
appliedProxy.add(part);
|
|
428
|
+
}
|
|
429
|
+
const info = await probeIpViaSession(session.fromPartition(part));
|
|
430
|
+
if (!info.ip) return { accountIdx: id, ipInfo: null, error: "探测失败:代理不通或 IP 服务不可达" };
|
|
431
|
+
const view = profileStore.setIpInfo("electron", id, info);
|
|
432
|
+
return { accountIdx: id, ipInfo: view.ipInfo };
|
|
433
|
+
});
|
|
434
|
+
// 删除 profile:关掉所有面板里该 profile 的视图,清空其 session 存储(登录态、cookie),
|
|
435
|
+
// 再删记录。页面侧已二次确认。
|
|
436
|
+
ipcMain.handle("panelcells:remove-profile", async (e, { accountIdx }) => {
|
|
437
|
+
if (!ctx(e)) throw new Error("Invalid panel");
|
|
438
|
+
const id = Number(accountIdx);
|
|
439
|
+
if (!Number.isInteger(id) || id <= 0) throw new Error("Invalid Electron profile ID");
|
|
440
|
+
for (const pc of registry.values()) {
|
|
441
|
+
for (const [key, rec] of [...pc.views]) { if (rec.profile === id) pc.destroyCell(key); }
|
|
442
|
+
}
|
|
443
|
+
const part = partitionFor(id);
|
|
444
|
+
try { await session.fromPartition(part).clearStorageData(); } catch (err) {}
|
|
445
|
+
try { await session.fromPartition(part).clearCache(); } catch (err) {}
|
|
446
|
+
appliedProxy.delete(part);
|
|
447
|
+
const removed = require("../profiles/profile-store").removeProfile("electron", id);
|
|
448
|
+
return { accountIdx: id, removed };
|
|
449
|
+
});
|
|
450
|
+
ipcMain.handle("panelcells:set-profile-note", async (e, { accountIdx, note }) => {
|
|
451
|
+
if (!ctx(e)) throw new Error("Invalid panel");
|
|
452
|
+
const id = Number(accountIdx);
|
|
453
|
+
if (!Number.isInteger(id) || id <= 0) throw new Error("Invalid Electron profile ID");
|
|
454
|
+
const text = String(note || "").trim().slice(0, 500);
|
|
455
|
+
const profile = require("../profiles/profile-store").setNote("electron", id, text);
|
|
456
|
+
return { accountIdx: id, note: String(profile.note || "") };
|
|
457
|
+
});
|
|
379
458
|
ipcMain.handle("panelcells:snapshots", async (e) => {
|
|
380
459
|
const pc = ctx(e);
|
|
381
460
|
return pc ? pc.snapshots() : [];
|
|
@@ -15,6 +15,9 @@ contextBridge.exposeInMainWorld("panelAPI", {
|
|
|
15
15
|
profiles: () => ipcRenderer.invoke("panelcells:profiles"),
|
|
16
16
|
addProfile: () => ipcRenderer.invoke("panelcells:add-profile"),
|
|
17
17
|
setProfileProxy: (accountIdx, proxy) => ipcRenderer.invoke("panelcells:set-profile-proxy", { accountIdx, proxy }),
|
|
18
|
+
setProfileNote: (accountIdx, note) => ipcRenderer.invoke("panelcells:set-profile-note", { accountIdx, note }),
|
|
19
|
+
probeIp: (accountIdx) => ipcRenderer.invoke("panelcells:probe-ip", { accountIdx }),
|
|
20
|
+
removeProfile: (accountIdx) => ipcRenderer.invoke("panelcells:remove-profile", { accountIdx }),
|
|
18
21
|
snapshots: () => ipcRenderer.invoke("panelcells:snapshots"),
|
|
19
22
|
// divider drag: BrowserViews sit ABOVE the page and would swallow pointermove —
|
|
20
23
|
// detach them for the duration of the drag (frame-only preview), reattach on up.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// telegram-identity.js — read "who is logged in" out of a Telegram Web (K)
|
|
5
|
+
// session so the matrix list can show each profile's @username.
|
|
6
|
+
//
|
|
7
|
+
// Telegram Web K keeps the current account id in localStorage `user_auth`
|
|
8
|
+
// ({id, dcID, date}) and caches user records in IndexedDB `tweb-account-<n>`
|
|
9
|
+
// (store `users`, keyed by the stringified user id). The self record carries
|
|
10
|
+
// username / first_name / last_name / phone. Nothing here touches auth keys.
|
|
11
|
+
|
|
12
|
+
const TELEGRAM_HOST_RE = /(^|\.)web\.telegram\.org$/i;
|
|
13
|
+
|
|
14
|
+
function isTelegramUrl(url) {
|
|
15
|
+
try { return TELEGRAM_HOST_RE.test(new URL(String(url || "")).hostname); } catch (e) { return false; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Runs inside the (sandboxed) Telegram page via executeJavaScript. Returns a
|
|
19
|
+
// plain object or null; never throws (the caller treats any failure as "unknown").
|
|
20
|
+
const TELEGRAM_IDENTITY_SCRIPT = `(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const auth = JSON.parse(localStorage.getItem("user_auth") || "null");
|
|
23
|
+
const id = auth && (auth.id || auth.user_id);
|
|
24
|
+
if (!id) return null;
|
|
25
|
+
const key = String(id);
|
|
26
|
+
const names = (indexedDB.databases ? (await indexedDB.databases()).map((d) => d.name) : [])
|
|
27
|
+
.filter((n) => /^tweb-account-\\d+$/.test(String(n || "")));
|
|
28
|
+
if (!names.length) names.push("tweb-account-1");
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
let db = null;
|
|
31
|
+
try {
|
|
32
|
+
db = await new Promise((res, rej) => { const r = indexedDB.open(name); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); r.onblocked = () => rej(new Error("blocked")); });
|
|
33
|
+
if (!db.objectStoreNames.contains("users")) { db.close(); continue; }
|
|
34
|
+
const user = await new Promise((res) => { const tx = db.transaction("users", "readonly"); const r = tx.objectStore("users").get(key); r.onsuccess = () => res(r.result || null); r.onerror = () => res(null); });
|
|
35
|
+
db.close();
|
|
36
|
+
if (user && String(user.id) === key) {
|
|
37
|
+
return { id: key, username: user.username || "", firstName: user.first_name || "", lastName: user.last_name || "", phone: user.phone || "" };
|
|
38
|
+
}
|
|
39
|
+
} catch (e) { try { db && db.close(); } catch (_) {} }
|
|
40
|
+
}
|
|
41
|
+
return { id: key, username: "", firstName: "", lastName: "", phone: "" };
|
|
42
|
+
} catch (e) { return null; }
|
|
43
|
+
})()`;
|
|
44
|
+
|
|
45
|
+
function normalizeTelegramIdentity(raw) {
|
|
46
|
+
if (!raw || typeof raw !== "object") return null;
|
|
47
|
+
const s = (v) => (typeof v === "string" ? v.trim() : typeof v === "number" ? String(v) : "");
|
|
48
|
+
const id = s(raw.id);
|
|
49
|
+
if (!id) return null;
|
|
50
|
+
const username = s(raw.username).replace(/^@/, "");
|
|
51
|
+
// accepts the raw page shape (firstName/lastName) or an already-normalized one (displayName)
|
|
52
|
+
const displayName = s(raw.displayName) || [s(raw.firstName), s(raw.lastName)].filter(Boolean).join(" ");
|
|
53
|
+
return { id, username, displayName, phone: s(raw.phone) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Profile-store login record for this identity (keyed by name "telegram").
|
|
57
|
+
function telegramLoginRecord(identity) {
|
|
58
|
+
const it = normalizeTelegramIdentity(identity);
|
|
59
|
+
if (!it) return null;
|
|
60
|
+
return {
|
|
61
|
+
url: "https://web.telegram.org",
|
|
62
|
+
name: "telegram",
|
|
63
|
+
username: it.username,
|
|
64
|
+
mobile: it.phone,
|
|
65
|
+
note: it.displayName,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Reverse: identity view from a stored profile (for list rendering).
|
|
70
|
+
function telegramIdentityFromProfile(profile) {
|
|
71
|
+
const logins = profile && Array.isArray(profile.logins) ? profile.logins : [];
|
|
72
|
+
const l = logins.find((x) => String((x && x.name) || "").toLowerCase() === "telegram");
|
|
73
|
+
if (!l) return null;
|
|
74
|
+
return { username: String(l.username || ""), displayName: String(l.note || ""), phone: String(l.mobile || "") };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { isTelegramUrl, TELEGRAM_IDENTITY_SCRIPT, normalizeTelegramIdentity, telegramLoginRecord, telegramIdentityFromProfile };
|
|
@@ -35,14 +35,36 @@
|
|
|
35
35
|
.name { font-weight: 650; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
36
36
|
.chip { flex: none; padding: 1px 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; }
|
|
37
37
|
.st { flex: none; color: var(--muted); font-size: 11px; white-space: nowrap; margin-left: auto; }
|
|
38
|
-
.line2 { display: flex; align-items: center; gap:
|
|
39
|
-
.line2 .
|
|
40
|
-
.line2 .
|
|
41
|
-
.line2 .
|
|
42
|
-
.line2 .
|
|
43
|
-
.line2
|
|
44
|
-
.line2
|
|
45
|
-
|
|
38
|
+
.line2 { display: flex; align-items: center; gap: 8px; margin-top: 4px; padding-left: 16px; font-size: 11.5px; color: var(--muted); min-width: 0; }
|
|
39
|
+
.line2 .tg { flex: none; max-width: 55%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #b4b4bb; }
|
|
40
|
+
.line2 .tg b { color: var(--fg); font-weight: 600; }
|
|
41
|
+
.line2 .tg.none { color: #6b6b72; }
|
|
42
|
+
.line2 .note { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #9a9aa2; cursor: text; outline: 0; border-radius: 4px; padding: 0 4px; margin: 0 -4px; }
|
|
43
|
+
.line2 .note.empty { color: #55555c; font-style: italic; }
|
|
44
|
+
.line2 .note:hover { color: #fff; }
|
|
45
|
+
/* 编辑态就地变成可输入,尺寸/位置与只读态完全一致,不发生位移 */
|
|
46
|
+
.line2 .note.editing { color: var(--fg); font-style: normal; background: #111113; box-shadow: 0 0 0 1px var(--accent); text-overflow: clip; }
|
|
47
|
+
.line2 .ip { flex: none; max-width: 45%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; line-height: 16px; cursor: pointer; }
|
|
48
|
+
.line2 .ip.none { color: #6b6b72; font-family: inherit; }
|
|
49
|
+
.line2 .ip.busy { color: #f0b429; }
|
|
50
|
+
.line2 .ip.bad { color: #ff8a8a; }
|
|
51
|
+
.line2 .ip:hover { color: #fff; }
|
|
52
|
+
.cfg { display: none; margin: 8px 0 2px 16px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--head); }
|
|
53
|
+
.row.cfg-open .cfg { display: block; }
|
|
54
|
+
.row.cfg-open .gear { color: var(--accent); }
|
|
55
|
+
.cfg .f { display: flex; align-items: center; gap: 6px; }
|
|
56
|
+
.cfg .f + .f { margin-top: 6px; }
|
|
57
|
+
.cfg label { flex: none; width: 34px; font-size: 11px; color: var(--muted); }
|
|
58
|
+
.cfg input { flex: 1; min-width: 0; height: 26px; border: 1px solid var(--line); border-radius: 6px; padding: 0 8px; background: #111113; color: var(--fg); font: 11.5px ui-monospace, monospace; outline: 0; }
|
|
59
|
+
.cfg input:focus { border-color: var(--accent); }
|
|
60
|
+
.cfg .hint { font-size: 11px; color: var(--muted); padding-left: 40px; }
|
|
61
|
+
.cfg .hint b { color: #b4b4bb; font-weight: 500; }
|
|
62
|
+
.cfg .danger { display: flex; align-items: center; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); }
|
|
63
|
+
.cfg .danger .sp { flex: 1; }
|
|
64
|
+
.cfg .confirm { display: none; align-items: center; gap: 6px; margin-top: 8px; padding: 6px 8px; border-radius: 6px; background: rgba(255,92,92,.12); color: #ffb3b3; font-size: 11.5px; }
|
|
65
|
+
.cfg .confirm.on { display: flex; }
|
|
66
|
+
.cfg .confirm .sp { flex: 1; }
|
|
67
|
+
button.del { background: #b3261e; }
|
|
46
68
|
.row-err { padding: 4px 0 0 16px; color: #ff8a8a; font-size: 11px; display: none; }
|
|
47
69
|
.row-err.on { display: block; }
|
|
48
70
|
#empty { display: none; padding: 40px 24px; color: var(--muted); text-align: center; line-height: 1.8; }
|
|
@@ -92,7 +114,7 @@
|
|
|
92
114
|
<div>
|
|
93
115
|
<h2>无法连接 Telegram</h2>
|
|
94
116
|
<p id="err-detail"></p>
|
|
95
|
-
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-
|
|
117
|
+
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-cfg">代理设置</button></div>
|
|
96
118
|
</div>
|
|
97
119
|
</div>
|
|
98
120
|
</div>
|
|
@@ -120,6 +142,43 @@
|
|
|
120
142
|
const stateOf = (idx) => cellState.get(Number(idx)) || {};
|
|
121
143
|
const dotClass = (st) => st.loading ? 'loading' : st.failed ? 'failed' : (st.url ? 'ok' : '');
|
|
122
144
|
const stateText = (st, opened) => st.loading ? '加载中…' : st.failed ? '连接失败' : st.url ? '已加载' : (opened ? '' : '未打开');
|
|
145
|
+
const fmtAgo = (iso) => { const t = Date.parse(iso || ''); if (!t) return ''; const m = Math.round((Date.now() - t) / 60000); return m < 1 ? '刚刚' : m < 60 ? `${m} 分钟前` : m < 1440 ? `${Math.round(m / 60)} 小时前` : `${Math.round(m / 1440)} 天前`; };
|
|
146
|
+
function renderIp(p, mode) {
|
|
147
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
148
|
+
const el = row.querySelector('.ip');
|
|
149
|
+
el.className = 'ip';
|
|
150
|
+
if (mode === 'busy') { el.classList.add('busy'); el.textContent = '测 IP…'; el.title = ''; return; }
|
|
151
|
+
if (mode && mode.error) { el.classList.add('bad'); el.textContent = 'IP 探测失败'; el.title = mode.error + '(点击重试)'; return; }
|
|
152
|
+
const info = p.ipInfo;
|
|
153
|
+
if (info && info.ip) {
|
|
154
|
+
el.textContent = info.area ? `${info.area} · ${info.ip}` : info.ip;
|
|
155
|
+
el.title = `出口 IP ${info.ip}${info.area ? ',' + info.area : ''},${fmtAgo(info.probedAt) || '未知时间'}探测(点击重测)`;
|
|
156
|
+
} else { el.classList.add('none'); el.textContent = '测 IP'; el.title = '经该 Profile 的代理探测出口 IP / 地区'; }
|
|
157
|
+
}
|
|
158
|
+
async function probeIp(p) {
|
|
159
|
+
renderIp(p, 'busy');
|
|
160
|
+
try {
|
|
161
|
+
const r = await window.panelAPI.probeIp(p.accountIdx);
|
|
162
|
+
if (r && r.ipInfo) { p.ipInfo = r.ipInfo; renderIp(p); }
|
|
163
|
+
else renderIp(p, { error: (r && r.error) || '探测失败' });
|
|
164
|
+
} catch (e) { renderIp(p, { error: e.message || String(e) }); }
|
|
165
|
+
}
|
|
166
|
+
// 身份:优先用本次会话探到的(cellState.identity),否则用 profile 里存的(上次登录)。
|
|
167
|
+
const identityOf = (p) => stateOf(p.accountIdx).identity || p.telegram || null;
|
|
168
|
+
function renderIdentity(p) {
|
|
169
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
170
|
+
const el = row.querySelector('.tg');
|
|
171
|
+
const it = identityOf(p);
|
|
172
|
+
if (it && (it.username || it.displayName || it.phone)) {
|
|
173
|
+
el.classList.remove('none');
|
|
174
|
+
el.innerHTML = (it.username ? `<b>@${esc(it.username)}</b>` : '') + (it.displayName ? `${it.username ? ' · ' : ''}${esc(it.displayName)}` : '') + (!it.username && !it.displayName && it.phone ? `+${esc(it.phone)}` : '');
|
|
175
|
+
el.title = [it.username && `@${it.username}`, it.displayName, it.phone && `+${it.phone}`].filter(Boolean).join(' ');
|
|
176
|
+
} else {
|
|
177
|
+
el.classList.add('none');
|
|
178
|
+
el.textContent = openedProfiles.has(p.accountIdx) && stateOf(p.accountIdx).url ? '未登录' : '—';
|
|
179
|
+
el.title = '';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
123
182
|
|
|
124
183
|
function flash(text, ms = 2500) {
|
|
125
184
|
statusEl.textContent = text; statusEl.dataset.sticky = '1';
|
|
@@ -143,8 +202,8 @@
|
|
|
143
202
|
function describeError(p, st) {
|
|
144
203
|
const e = st.error || {};
|
|
145
204
|
const desc = e.description ? `<code>${esc(e.description)}</code>` : '';
|
|
146
|
-
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 Telegram。${desc}<br
|
|
147
|
-
return `当前为直连,网络无法访问 Telegram。${desc}<br
|
|
205
|
+
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 Telegram。${desc}<br>点 ⚙ 检查或更换该 Profile 的代理,可用「测 IP」验证。`;
|
|
206
|
+
return `当前为直连,网络无法访问 Telegram。${desc}<br>点 ⚙ 为该 Profile 设置代理。`;
|
|
148
207
|
}
|
|
149
208
|
|
|
150
209
|
function renderPreviewState() {
|
|
@@ -169,43 +228,77 @@
|
|
|
169
228
|
const row = document.createElement('div');
|
|
170
229
|
row.className = 'row'; row.dataset.id = String(p.accountIdx); row.title = `persist:sandbox-${p.accountIdx}`;
|
|
171
230
|
row.innerHTML = `
|
|
172
|
-
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon reload" title="重新加载">⟳</button></div>
|
|
173
|
-
<div class="line2"><span class="
|
|
174
|
-
|
|
231
|
+
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon gear" title="代理设置">⚙</button><button class="icon reload" title="重新加载">⟳</button></div>
|
|
232
|
+
<div class="line2"><span class="tg none">—</span><span class="note" data-role="note" title="点击编辑备注,Enter 保存 / Esc 取消" spellcheck="false"></span><span class="ip none" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</span></div>
|
|
233
|
+
<div class="cfg">
|
|
234
|
+
<div class="f"><label>代理</label><input data-role="proxy" placeholder="留空 = 直连,例如 http://127.0.0.1:20001" spellcheck="false"><button class="sm save">保存</button><button class="sm ghost test">测 IP</button></div>
|
|
235
|
+
<div class="hint">保存后已打开的会话会重新加载;<b>测 IP</b> 经该代理访问 IP 服务,结果显示在上方。</div>
|
|
236
|
+
<div class="danger"><span>删除此 Profile 会一并清除它的 Telegram 登录态和缓存。</span><span class="sp"></span><button class="sm ghost rm">删除</button></div>
|
|
237
|
+
<div class="confirm"><span>确认删除 <b>${esc(p.name)} #${p.accountIdx}</b>?不可恢复。</span><span class="sp"></span><button class="sm del rm-yes">确认删除</button><button class="sm ghost rm-no">取消</button></div>
|
|
238
|
+
</div>
|
|
175
239
|
<div class="row-err"></div>`;
|
|
176
|
-
const line2 = row.querySelector('.line2');
|
|
177
|
-
const val = line2.querySelector('.val');
|
|
178
|
-
const input = line2.querySelector('[data-role="proxy"]');
|
|
179
240
|
const errEl = row.querySelector('.row-err');
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
241
|
+
const noteEl = row.querySelector('.note');
|
|
242
|
+
const showNote = () => { noteEl.textContent = p.note || '添加备注'; noteEl.classList.toggle('empty', !p.note); };
|
|
243
|
+
showNote();
|
|
244
|
+
let editing = false;
|
|
245
|
+
const startEdit = (e) => {
|
|
246
|
+
e && e.stopPropagation();
|
|
247
|
+
if (editing) return;
|
|
248
|
+
editing = true;
|
|
249
|
+
noteEl.classList.add('editing'); noteEl.classList.remove('empty');
|
|
250
|
+
noteEl.textContent = p.note || '';
|
|
251
|
+
noteEl.contentEditable = 'plaintext-only';
|
|
252
|
+
noteEl.focus();
|
|
253
|
+
const range = document.createRange(); range.selectNodeContents(noteEl);
|
|
254
|
+
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
|
255
|
+
};
|
|
256
|
+
const stopEdit = () => { editing = false; noteEl.contentEditable = 'false'; noteEl.classList.remove('editing'); showNote(); };
|
|
257
|
+
const saveNote = async () => {
|
|
258
|
+
if (!editing) return;
|
|
259
|
+
const value = noteEl.textContent.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
260
|
+
stopEdit();
|
|
261
|
+
if (value === (p.note || '')) return;
|
|
186
262
|
try {
|
|
187
|
-
const updated = await window.panelAPI.
|
|
188
|
-
p.
|
|
189
|
-
// 代理只在会话加载时生效,所以已打开的必须重载。**没打开过的不要**在这里置
|
|
190
|
-
// loading:主进程没有对应的 cell,既不会创建视图也不会有 panelcells:state 回来,
|
|
191
|
-
// 行会永远卡在「加载中…」(实测 bug)。保持「未打开」,下次打开自然用新代理。
|
|
192
|
-
if (openedProfiles.has(p.accountIdx)) {
|
|
193
|
-
reloadProfile(p.accountIdx);
|
|
194
|
-
flash(updated.proxy ? `#${p.accountIdx} 代理已应用,重新加载中` : `#${p.accountIdx} 已切换直连,重新加载中`);
|
|
195
|
-
} else {
|
|
196
|
-
flash(updated.proxy ? `#${p.accountIdx} 代理已保存,打开时生效` : `#${p.accountIdx} 已保存为直连,打开时生效`);
|
|
197
|
-
}
|
|
263
|
+
const updated = await window.panelAPI.setProfileNote(p.accountIdx, value);
|
|
264
|
+
p.note = updated.note; showNote();
|
|
198
265
|
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
199
|
-
finally { btn.disabled = false; }
|
|
200
266
|
};
|
|
267
|
+
noteEl.onclick = (e) => { e.stopPropagation(); startEdit(e); };
|
|
268
|
+
noteEl.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); saveNote(); } if (e.key === 'Escape') { e.preventDefault(); stopEdit(); } };
|
|
269
|
+
noteEl.onblur = () => saveNote();
|
|
270
|
+
// 代理设置抽屉(⚙):不常用,默认收起,列表保持干净。
|
|
271
|
+
const proxyInput = row.querySelector('[data-role="proxy"]');
|
|
272
|
+
const saveBtn = row.querySelector('.cfg .save');
|
|
273
|
+
const toggleCfg = (e) => { e.stopPropagation(); const open = !row.classList.contains('cfg-open'); row.classList.toggle('cfg-open', open); if (open) { proxyInput.value = p.proxy || ''; proxyInput.focus(); } };
|
|
274
|
+
const saveProxy = async () => {
|
|
275
|
+
saveBtn.disabled = true; errEl.classList.remove('on');
|
|
276
|
+
try {
|
|
277
|
+
const updated = await window.panelAPI.setProfileProxy(p.accountIdx, proxyInput.value.trim());
|
|
278
|
+
p.proxy = updated.proxy; p.ipInfo = null; renderIp(p);
|
|
279
|
+
if (openedProfiles.has(p.accountIdx)) { reloadProfile(p.accountIdx); flash(`#${p.accountIdx} 代理已应用,重新加载中`); }
|
|
280
|
+
else flash(`#${p.accountIdx} 代理已保存,打开时生效`);
|
|
281
|
+
row.classList.remove('cfg-open');
|
|
282
|
+
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
283
|
+
finally { saveBtn.disabled = false; }
|
|
284
|
+
};
|
|
285
|
+
row.querySelector('.gear').onclick = toggleCfg;
|
|
286
|
+
row.querySelector('.cfg').onclick = (e) => e.stopPropagation();
|
|
287
|
+
saveBtn.onclick = saveProxy;
|
|
288
|
+
proxyInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') saveProxy(); if (e.key === 'Escape') row.classList.remove('cfg-open'); };
|
|
289
|
+
row.querySelector('.cfg .test').onclick = () => probeIp(p);
|
|
290
|
+
row.querySelector('.ip').onclick = (e) => { e.stopPropagation(); probeIp(p); };
|
|
291
|
+
const confirmEl = row.querySelector('.cfg .confirm');
|
|
292
|
+
row.querySelector('.rm').onclick = () => { confirmEl.classList.add('on'); row.querySelector('.rm-yes').focus(); };
|
|
293
|
+
row.querySelector('.rm-no').onclick = () => confirmEl.classList.remove('on');
|
|
294
|
+
row.querySelector('.rm-yes').onclick = async () => {
|
|
295
|
+
const btn = row.querySelector('.rm-yes'); btn.disabled = true;
|
|
296
|
+
try { await removeProfile(p); }
|
|
297
|
+
catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); btn.disabled = false; }
|
|
298
|
+
};
|
|
299
|
+
row._openCfg = () => { if (!row.classList.contains('cfg-open')) toggleCfg(new Event('click')); row.scrollIntoView({ block: 'nearest' }); };
|
|
201
300
|
row.onclick = () => selectProfile(p.accountIdx);
|
|
202
|
-
val.onclick = startEdit; line2.querySelector('.pen').onclick = startEdit;
|
|
203
|
-
line2.querySelector('.cancel').onclick = stopEdit;
|
|
204
|
-
line2.querySelector('.save').onclick = (e) => { e.stopPropagation(); save(input.value); };
|
|
205
|
-
input.onclick = (e) => e.stopPropagation();
|
|
206
|
-
input.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') save(input.value); if (e.key === 'Escape') stopEdit(); };
|
|
207
301
|
row.querySelector('.reload').onclick = (e) => { e.stopPropagation(); reloadProfile(p.accountIdx); };
|
|
208
|
-
row._save = save; row._startEdit = startEdit;
|
|
209
302
|
return row;
|
|
210
303
|
}
|
|
211
304
|
|
|
@@ -226,6 +319,8 @@
|
|
|
226
319
|
if (!rows.has(p.accountIdx)) { const row = buildRow(p); rows.set(p.accountIdx, row); rowsEl.appendChild(row); }
|
|
227
320
|
rows.get(p.accountIdx).classList.toggle('active', !!selected && selected.accountIdx === p.accountIdx);
|
|
228
321
|
renderRowState(p.accountIdx);
|
|
322
|
+
renderIdentity(p);
|
|
323
|
+
renderIp(p);
|
|
229
324
|
}
|
|
230
325
|
}
|
|
231
326
|
|
|
@@ -250,8 +345,10 @@
|
|
|
250
345
|
const next = { ...prev, loading: !!state.loading };
|
|
251
346
|
if (state.url !== undefined) { next.url = failed ? '' : state.url; next.title = state.title || ''; }
|
|
252
347
|
if (!state.loading) { next.failed = failed; next.error = state.error || (failed ? prev.error : null); }
|
|
348
|
+
if (state.identity) next.identity = state.identity;
|
|
253
349
|
cellState.set(idx, next);
|
|
254
350
|
renderRowState(idx);
|
|
351
|
+
{ const p = profiles.find((x) => x.accountIdx === idx); if (p) renderIdentity(p); }
|
|
255
352
|
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
256
353
|
}
|
|
257
354
|
async function pullStates({ adopt = false } = {}) {
|
|
@@ -277,6 +374,19 @@
|
|
|
277
374
|
selectProfile(target);
|
|
278
375
|
}
|
|
279
376
|
|
|
377
|
+
async function removeProfile(p) {
|
|
378
|
+
const idx = p.accountIdx;
|
|
379
|
+
await window.panelAPI.removeProfile(idx);
|
|
380
|
+
openedProfiles.delete(idx); cellState.delete(idx);
|
|
381
|
+
const row = rows.get(idx); if (row) row.remove(); rows.delete(idx);
|
|
382
|
+
profiles = profiles.filter((x) => x.accountIdx !== idx);
|
|
383
|
+
if (selected && selected.accountIdx === idx) {
|
|
384
|
+
const next = profiles[0] ? profiles[0].accountIdx : 0;
|
|
385
|
+
if (next) selectProfile(next); else { selected = null; localStorage.removeItem('telegram-matrix-profile'); selectedName.textContent = '未选择 Profile'; render(); renderPreviewState(); syncPreview(); }
|
|
386
|
+
} else render();
|
|
387
|
+
flash(`Profile #${idx} 已删除`);
|
|
388
|
+
}
|
|
389
|
+
|
|
280
390
|
addButton.onclick = async () => {
|
|
281
391
|
addButton.disabled = true; flash('正在创建…', 60000);
|
|
282
392
|
try {
|
|
@@ -289,8 +399,7 @@
|
|
|
289
399
|
};
|
|
290
400
|
document.getElementById('reload').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
291
401
|
document.getElementById('err-retry').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
292
|
-
document.getElementById('err-
|
|
293
|
-
document.getElementById('err-edit').onclick = () => { const row = selected && rows.get(selected.accountIdx); if (row) { row.scrollIntoView({ block: 'nearest' }); row._startEdit(); } };
|
|
402
|
+
document.getElementById('err-cfg').onclick = () => { const row = selected && rows.get(selected.accountIdx); if (row) row._openCfg(); };
|
|
294
403
|
|
|
295
404
|
new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(preview);
|
|
296
405
|
window.addEventListener('resize', syncPreview);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
|
|
7
|
+
test("setNote persists a trimmed note on electron profiles", () => {
|
|
8
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-note-"));
|
|
9
|
+
const prevHome = process.env.HOME, prevUp = process.env.USERPROFILE;
|
|
10
|
+
process.env.HOME = home; process.env.USERPROFILE = home;
|
|
11
|
+
delete require.cache[require.resolve("../src/profiles/profile-store")];
|
|
12
|
+
try {
|
|
13
|
+
const store = require("../src/profiles/profile-store");
|
|
14
|
+
fs.mkdirSync(store.ELECTRON_DIR, { recursive: true });
|
|
15
|
+
const view = store.setNote("electron", 3, " 主号,别乱动 ");
|
|
16
|
+
assert.equal(view.note, "主号,别乱动");
|
|
17
|
+
assert.equal(store.getProfile("electron", 3).note, "主号,别乱动");
|
|
18
|
+
assert.equal(store.setNote("electron", 3, "").note, "");
|
|
19
|
+
assert.equal(store.removeProfile("electron", 3), true);
|
|
20
|
+
assert.equal(store.getProfile("electron", 3), null);
|
|
21
|
+
assert.equal(store.removeProfile("electron", 3), false);
|
|
22
|
+
} finally {
|
|
23
|
+
process.env.HOME = prevHome; if (prevUp === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prevUp;
|
|
24
|
+
delete require.cache[require.resolve("../src/profiles/profile-store")];
|
|
25
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const vm = require("node:vm");
|
|
4
|
+
const {
|
|
5
|
+
isTelegramUrl,
|
|
6
|
+
TELEGRAM_IDENTITY_SCRIPT,
|
|
7
|
+
normalizeTelegramIdentity,
|
|
8
|
+
telegramLoginRecord,
|
|
9
|
+
telegramIdentityFromProfile,
|
|
10
|
+
} = require("../src/tabbrowser/telegram-identity");
|
|
11
|
+
|
|
12
|
+
test("recognizes telegram web urls only", () => {
|
|
13
|
+
assert.equal(isTelegramUrl("https://web.telegram.org/k/"), true);
|
|
14
|
+
assert.equal(isTelegramUrl("https://web.telegram.org/a/#123"), true);
|
|
15
|
+
assert.equal(isTelegramUrl("https://example.com/web.telegram.org"), false);
|
|
16
|
+
assert.equal(isTelegramUrl("chrome-error://chromewebdata/"), false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("identity script is valid JS and returns null without storage", async () => {
|
|
20
|
+
new vm.Script(TELEGRAM_IDENTITY_SCRIPT); // syntax
|
|
21
|
+
const ctx = vm.createContext({ localStorage: { getItem: () => null }, indexedDB: {} });
|
|
22
|
+
assert.equal(await vm.runInContext(TELEGRAM_IDENTITY_SCRIPT, ctx), null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("identity script never leaks auth material", () => {
|
|
26
|
+
assert.doesNotMatch(TELEGRAM_IDENTITY_SCRIPT, /auth_key|server_salt|account\d/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("normalizes identity and maps to a profile login record", () => {
|
|
30
|
+
const it = normalizeTelegramIdentity({ id: 42, username: "@alice", firstName: " Alice ", lastName: "L", phone: "1234" });
|
|
31
|
+
assert.deepEqual(it, { id: "42", username: "alice", displayName: "Alice L", phone: "1234" });
|
|
32
|
+
assert.deepEqual(telegramLoginRecord(it), { url: "https://web.telegram.org", name: "telegram", username: "alice", mobile: "1234", note: "Alice L" });
|
|
33
|
+
assert.equal(normalizeTelegramIdentity(null), null);
|
|
34
|
+
assert.equal(normalizeTelegramIdentity({ username: "x" }), null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("reads identity back from a stored profile", () => {
|
|
38
|
+
const p = { logins: [{ name: "gmail", username: "a@b" }, { name: "Telegram", username: "alice", mobile: "1", note: "Alice" }] };
|
|
39
|
+
assert.deepEqual(telegramIdentityFromProfile(p), { username: "alice", displayName: "Alice", phone: "1" });
|
|
40
|
+
assert.equal(telegramIdentityFromProfile({ logins: [] }), null);
|
|
41
|
+
});
|