Support new ChatGPT share paths
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
V1 scope:
|
||||
|
||||
- accepts only public `https://chatgpt.com/share/...` and `https://chat.openai.com/share/...` URLs
|
||||
- accepts public ChatGPT links under the legacy `/share/...` path and the current `/s/t_...` path
|
||||
- admin-only capture/delete UI at `${AISHARE_BASE_PATH}/admin`
|
||||
- public reader pages generated as static artifacts under `/data/html/<slug>`
|
||||
- screenshots under `/data/screenshots/<slug>`
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ function renderShell(): void {
|
||||
|
||||
<section class="panel capture-panel">
|
||||
<form id="capture-form" class="capture-form">
|
||||
<label class="url-field">ChatGPT share URL<input name="sourceUrl" required inputmode="url" autocomplete="url" placeholder="https://chatgpt.com/share/…" /></label>
|
||||
<label class="url-field">ChatGPT share URL<input name="sourceUrl" required inputmode="url" autocomplete="url" placeholder="https://chatgpt.com/s/t_…" /></label>
|
||||
${isAdmin ? '<label class="slug-field">Custom slug<input name="slug" placeholder="Optional" /></label>' : ""}
|
||||
<label class="retention-field">Keep for<select name="retentionDays" aria-label="Retention period"><option value="3" selected>3 days</option><option value="7">7 days</option><option value="10">10 days</option><option value="30">30 days</option></select></label>
|
||||
<button class="primary" type="submit">Capture</button>
|
||||
|
||||
+14
-3
@@ -13,6 +13,7 @@ import { artifactAssetsDir, artifactIndexPath, safeAssetName, screenshotPath, sc
|
||||
import { slugify } from "./slug.js";
|
||||
import { writeConversationArtifact, writeFailureArtifact } from "./render.js";
|
||||
import { finalizeKatexAssets, prepareKatexAssets } from "./katex-assets.js";
|
||||
import { isChatGptSharePath } from "./url.js";
|
||||
import type { CaptureRow, NormalizedConversation, NormalizedMessage, ScreenshotVariant } from "./types.js";
|
||||
|
||||
interface ExtractedConversation {
|
||||
@@ -420,10 +421,17 @@ async function extractConversation(sourceUrl: string): Promise<ExtractedConversa
|
||||
await waitForCdp(port, chromiumProcess);
|
||||
browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`, { timeout: config.captureTimeoutMs });
|
||||
const context = browser.contexts()[0];
|
||||
let page = context.pages().find((candidate) => candidate.url().includes("/share/")) || context.pages()[0];
|
||||
const isSharePage = (candidate: { url(): string }): boolean => {
|
||||
try {
|
||||
return isChatGptSharePath(new URL(candidate.url()).pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let page = context.pages().find(isSharePage) || context.pages()[0];
|
||||
for (let attempt = 0; !page && attempt < 100; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
page = context.pages().find((candidate) => candidate.url().includes("/share/")) || context.pages()[0];
|
||||
page = context.pages().find(isSharePage) || context.pages()[0];
|
||||
}
|
||||
if (!page) throw new Error("Chromium CDP page target was not created.");
|
||||
await page.setViewportSize({ width: 1360, height: 900 }).catch(() => undefined);
|
||||
@@ -527,7 +535,10 @@ async function extractConversation(sourceUrl: string): Promise<ExtractedConversa
|
||||
|
||||
const title =
|
||||
document.querySelector("h1")?.textContent?.trim() ||
|
||||
document.title.replace(/\s*[|-]\s*ChatGPT\s*$/i, "").trim() ||
|
||||
document.title
|
||||
.replace(/^ChatGPT\s*[|-]\s*/i, "")
|
||||
.replace(/\s*[|-]\s*ChatGPT\s*$/i, "")
|
||||
.trim() ||
|
||||
"ChatGPT Share";
|
||||
|
||||
const roleNodes = Array.from(document.querySelectorAll("[data-message-author-role]"));
|
||||
|
||||
+9
-2
@@ -1,3 +1,10 @@
|
||||
const legacySharePath = /^\/share\/[^/]+\/?$/;
|
||||
const postSharePath = /^\/s\/t_[A-Za-z0-9_-]+\/?$/;
|
||||
|
||||
export function isChatGptSharePath(pathname: string): boolean {
|
||||
return legacySharePath.test(pathname) || postSharePath.test(pathname);
|
||||
}
|
||||
|
||||
export function validateChatGptShareUrl(raw: string): string {
|
||||
let url: URL;
|
||||
try {
|
||||
@@ -12,8 +19,8 @@ export function validateChatGptShareUrl(raw: string): string {
|
||||
|
||||
const host = url.hostname.toLowerCase();
|
||||
const allowedHost = host === "chatgpt.com" || host === "chat.openai.com";
|
||||
if (!allowedHost || !url.pathname.startsWith("/share/")) {
|
||||
throw new Error("Only public ChatGPT share URLs under /share/ are accepted.");
|
||||
if (!allowedHost || !isChatGptSharePath(url.pathname)) {
|
||||
throw new Error("Only public ChatGPT share URLs under /share/ or /s/t_ are accepted.");
|
||||
}
|
||||
|
||||
url.hash = "";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { isChatGptSharePath, validateChatGptShareUrl } from "../dist/url.js";
|
||||
|
||||
test("accepts current ChatGPT post share URLs", () => {
|
||||
const source = "https://chatgpt.com/s/t_6a5f58aa36d08191b4d794e9aac88146";
|
||||
assert.equal(validateChatGptShareUrl(source), source);
|
||||
assert.equal(isChatGptSharePath("/s/t_6a5f58aa36d08191b4d794e9aac88146"), true);
|
||||
});
|
||||
|
||||
test("keeps accepting legacy ChatGPT share URLs", () => {
|
||||
assert.equal(
|
||||
validateChatGptShareUrl("https://chatgpt.com/share/12345678-abcd-4321-abcd-1234567890ab"),
|
||||
"https://chatgpt.com/share/12345678-abcd-4321-abcd-1234567890ab"
|
||||
);
|
||||
assert.equal(
|
||||
validateChatGptShareUrl("https://chat.openai.com/share/12345678-abcd-4321-abcd-1234567890ab"),
|
||||
"https://chat.openai.com/share/12345678-abcd-4321-abcd-1234567890ab"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects non-share and deceptive ChatGPT paths", () => {
|
||||
for (const source of [
|
||||
"https://chatgpt.com/s/example",
|
||||
"https://chatgpt.com/sharex/example",
|
||||
"https://chatgpt.com/share/example/extra",
|
||||
"https://example.com/s/t_6a5f58aa36d08191b4d794e9aac88146"
|
||||
]) {
|
||||
assert.throws(() => validateChatGptShareUrl(source), /Only public ChatGPT share URLs/);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user