Handle unavailable ChatGPT shares
This commit is contained in:
+12
-1
@@ -26,6 +26,9 @@ function publicErrorFor(error: unknown): string {
|
|||||||
if (/captcha|verify|robot/i.test(message)) {
|
if (/captcha|verify|robot/i.test(message)) {
|
||||||
return "ChatGPT asked for verification, so this public share could not be captured automatically.";
|
return "ChatGPT asked for verification, so this public share could not be captured automatically.";
|
||||||
}
|
}
|
||||||
|
if (/unavailable|no longer public|not public|redirected away/i.test(message)) {
|
||||||
|
return "This ChatGPT share is not available publicly anymore, so it cannot be captured.";
|
||||||
|
}
|
||||||
if (/timeout/i.test(message)) {
|
if (/timeout/i.test(message)) {
|
||||||
return "The public ChatGPT share took too long to load.";
|
return "The public ChatGPT share took too long to load.";
|
||||||
}
|
}
|
||||||
@@ -271,9 +274,11 @@ async function extractConversation(sourceUrl: string): Promise<ExtractedConversa
|
|||||||
if (!page) throw new Error("Chromium CDP page target was not created.");
|
if (!page) throw new Error("Chromium CDP page target was not created.");
|
||||||
await page.setViewportSize({ width: 1360, height: 900 }).catch(() => undefined);
|
await page.setViewportSize({ width: 1360, height: 900 }).catch(() => undefined);
|
||||||
page.setDefaultTimeout(config.captureTimeoutMs);
|
page.setDefaultTimeout(config.captureTimeoutMs);
|
||||||
|
const expectedPath = new URL(sourceUrl).pathname;
|
||||||
await page
|
await page
|
||||||
.waitForFunction(
|
.waitForFunction(
|
||||||
() => {
|
(sharePath) => {
|
||||||
|
if (!location.pathname.startsWith(sharePath)) return true;
|
||||||
const bodyText = document.body?.innerText || document.body?.textContent || "";
|
const bodyText = document.body?.innerText || document.body?.textContent || "";
|
||||||
if (/verify you are human|captcha|unusual activity/i.test(bodyText)) return true;
|
if (/verify you are human|captcha|unusual activity/i.test(bodyText)) return true;
|
||||||
if (document.querySelectorAll("[data-message-author-role]").length > 0) return true;
|
if (document.querySelectorAll("[data-message-author-role]").length > 0) return true;
|
||||||
@@ -281,12 +286,18 @@ async function extractConversation(sourceUrl: string): Promise<ExtractedConversa
|
|||||||
if ((markdown?.textContent || "").trim().length > 0) return true;
|
if ((markdown?.textContent || "").trim().length > 0) return true;
|
||||||
return /(?:^|\s)(You said:|ChatGPT said:)/.test(bodyText);
|
return /(?:^|\s)(You said:|ChatGPT said:)/.test(bodyText);
|
||||||
},
|
},
|
||||||
|
expectedPath,
|
||||||
{ timeout: config.captureTimeoutMs }
|
{ timeout: config.captureTimeoutMs }
|
||||||
)
|
)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
|
await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const actualUrl = page.url();
|
||||||
|
if (!new URL(actualUrl).pathname.startsWith(expectedPath)) {
|
||||||
|
throw new Error(`ChatGPT share is unavailable or no longer public: page redirected away to ${actualUrl}.`);
|
||||||
|
}
|
||||||
|
|
||||||
const extracted = await withTimeout(page.evaluate(() => {
|
const extracted = await withTimeout(page.evaluate(() => {
|
||||||
function cleanHtml(root: Element): string {
|
function cleanHtml(root: Element): string {
|
||||||
const clone = root.cloneNode(true) as Element;
|
const clone = root.cloneNode(true) as Element;
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ export class Store {
|
|||||||
.run(status, detail?.errorPublic ?? null, detail?.errorDetail ?? null, new Date().toISOString(), id);
|
.run(status, detail?.errorPublic ?? null, detail?.errorDetail ?? null, new Date().toISOString(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retryCapture(id: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`UPDATE captures
|
||||||
|
SET status = 'queued', title = NULL, error_public = NULL, error_detail = NULL,
|
||||||
|
data_json = NULL, metadata_json = NULL, captured_at = NULL, updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
)
|
||||||
|
.run(new Date().toISOString(), id);
|
||||||
|
}
|
||||||
|
|
||||||
completeCapture(id: string, input: { slug: string; title: string; dataJson: string; metadataJson: string }): void {
|
completeCapture(id: string, input: { slug: string; title: string; dataJson: string; metadataJson: string }): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
this.db
|
this.db
|
||||||
|
|||||||
+1
-1
@@ -130,7 +130,7 @@ app.post(route("/api/captures/:id/retry"), requireAdmin, (req, res) => {
|
|||||||
res.status(409).json({ error: "Successful captures are immutable. Create a new capture instead." });
|
res.status(409).json({ error: "Successful captures are immutable. Create a new capture instead." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
store.updateStatus(row.id, "queued");
|
store.retryCapture(row.id);
|
||||||
captureQueue.enqueue(row.id);
|
captureQueue.enqueue(row.id);
|
||||||
res.json({ capture: publicCapture(store.getCapture(row.id)) });
|
res.json({ capture: publicCapture(store.getCapture(row.id)) });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user