Fix silent market CSV export
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { normalizeFractionRateDisplay } from "../../shared/rate-normalizer";
|
||||
import {
|
||||
writeMarketListRequestSnapshot
|
||||
} from "./market-list-request-snapshot";
|
||||
import { parseMarketListResponse } from "./market-list-row";
|
||||
|
||||
const BRIDGE_MARKER = "__SCES_MARKET_PAGE_BRIDGE_INSTALLED__";
|
||||
const MARKET_SEARCH_REQUEST_PATH = "/gw/api/gsearch/search_for_author_square";
|
||||
const SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows";
|
||||
|
||||
type MarketRow = {
|
||||
@@ -24,6 +29,7 @@ function installMarketPageBridge() {
|
||||
}
|
||||
|
||||
window[BRIDGE_MARKER] = true;
|
||||
installMarketRequestSnapshotBridge();
|
||||
syncSerializedMarketRows();
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
@@ -39,7 +45,16 @@ function installMarketPageBridge() {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function installMarketRequestSnapshotBridge() {
|
||||
installFetchSnapshotBridge();
|
||||
installXmlHttpRequestSnapshotBridge();
|
||||
}
|
||||
|
||||
function syncSerializedMarketRows() {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSerializedRows = JSON.stringify(readSerializedMarketRows());
|
||||
if (
|
||||
document.documentElement.getAttribute(SERIALIZED_MARKET_ROWS_ATTRIBUTE) !==
|
||||
@@ -52,6 +67,131 @@ function syncSerializedMarketRows() {
|
||||
}
|
||||
}
|
||||
|
||||
function installFetchSnapshotBridge() {
|
||||
if (typeof window.fetch !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const requestSnapshot = readFetchSnapshot(input, init);
|
||||
const response = await originalFetch(input, init);
|
||||
if (requestSnapshot) {
|
||||
const clonedResponse = response.clone();
|
||||
void captureMarketSnapshotFromResponse(requestSnapshot, () =>
|
||||
clonedResponse.json()
|
||||
);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
function installXmlHttpRequestSnapshotBridge() {
|
||||
const OriginalXmlHttpRequest = window.XMLHttpRequest;
|
||||
if (!OriginalXmlHttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalOpen = OriginalXmlHttpRequest.prototype.open;
|
||||
const originalSend = OriginalXmlHttpRequest.prototype.send;
|
||||
const originalSetRequestHeader = OriginalXmlHttpRequest.prototype.setRequestHeader;
|
||||
|
||||
OriginalXmlHttpRequest.prototype.open = function (
|
||||
method: string,
|
||||
url: string | URL,
|
||||
...rest: unknown[]
|
||||
) {
|
||||
(
|
||||
this as XMLHttpRequest & {
|
||||
__scesMarketSnapshot?: {
|
||||
headers: Record<string, string>;
|
||||
method: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
).__scesMarketSnapshot = {
|
||||
headers: {},
|
||||
method,
|
||||
url: String(url)
|
||||
};
|
||||
return originalOpen.call(this, method, url, ...(rest as [boolean?, string?, string?]));
|
||||
};
|
||||
|
||||
OriginalXmlHttpRequest.prototype.setRequestHeader = function (
|
||||
name: string,
|
||||
value: string
|
||||
) {
|
||||
(
|
||||
this as XMLHttpRequest & {
|
||||
__scesMarketSnapshot?: {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
}
|
||||
).__scesMarketSnapshot?.headers &&
|
||||
((this as XMLHttpRequest & {
|
||||
__scesMarketSnapshot?: {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
}).__scesMarketSnapshot!.headers[name] = value);
|
||||
return originalSetRequestHeader.call(this, name, value);
|
||||
};
|
||||
|
||||
OriginalXmlHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
|
||||
const snapshotState = (
|
||||
this as XMLHttpRequest & {
|
||||
__scesMarketSnapshot?: {
|
||||
body?: string;
|
||||
headers: Record<string, string>;
|
||||
method: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
).__scesMarketSnapshot;
|
||||
if (snapshotState) {
|
||||
snapshotState.body = typeof body === "string" ? body : undefined;
|
||||
this.addEventListener("load", () => {
|
||||
if (this.status < 200 || this.status >= 300 || typeof this.responseText !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
void captureMarketSnapshotFromResponse(snapshotState, async () =>
|
||||
JSON.parse(this.responseText)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return originalSend.call(this, body);
|
||||
};
|
||||
}
|
||||
|
||||
async function captureMarketSnapshotFromResponse(
|
||||
snapshot: {
|
||||
body?: string;
|
||||
headers?: Record<string, string>;
|
||||
method: string;
|
||||
url: string;
|
||||
},
|
||||
readPayload: () => Promise<unknown>
|
||||
) {
|
||||
if (!isMarketSearchRequest(snapshot.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await readPayload();
|
||||
if (!parseMarketListResponse(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeMarketListRequestSnapshot(document, {
|
||||
body: snapshot.body,
|
||||
headers: snapshot.headers,
|
||||
method: snapshot.method,
|
||||
url: snapshot.url
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function readSerializedMarketRows() {
|
||||
const marketList = readMarketList();
|
||||
return marketList
|
||||
@@ -71,7 +211,59 @@ function readSerializedMarketRows() {
|
||||
.filter((row) => Boolean(row.authorId || row.authorName));
|
||||
}
|
||||
|
||||
function readFetchSnapshot(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
): {
|
||||
body?: string;
|
||||
headers?: Record<string, string>;
|
||||
method: string;
|
||||
url: string;
|
||||
} | null {
|
||||
const request = input instanceof Request ? input : null;
|
||||
const method = init?.method ?? request?.method ?? "GET";
|
||||
const url = request?.url ?? String(input);
|
||||
const body =
|
||||
typeof init?.body === "string"
|
||||
? init.body
|
||||
: typeof request?.bodyUsed === "boolean" && request.bodyUsed
|
||||
? undefined
|
||||
: undefined;
|
||||
const headers = serializeHeaders(init?.headers ?? request?.headers);
|
||||
|
||||
return {
|
||||
body,
|
||||
headers,
|
||||
method,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
function serializeHeaders(
|
||||
headers: HeadersInit | undefined
|
||||
): Record<string, string> | undefined {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
return Object.fromEntries(headers.entries());
|
||||
}
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
return Object.fromEntries(headers);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(headers).map(([key, value]) => [key, String(value)])
|
||||
);
|
||||
}
|
||||
|
||||
function readMarketList(): MarketRow[] {
|
||||
if (typeof document === "undefined") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const marketRoot = document.querySelector(".base-author-list") as
|
||||
| (HTMLElement & {
|
||||
__vue__?: {
|
||||
@@ -102,6 +294,15 @@ function readMarketList(): MarketRow[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function isMarketSearchRequest(url: string): boolean {
|
||||
return (
|
||||
url === MARKET_SEARCH_REQUEST_PATH ||
|
||||
url.startsWith(`${MARKET_SEARCH_REQUEST_PATH}?`) ||
|
||||
url.includes(`${MARKET_SEARCH_REQUEST_PATH}?`) ||
|
||||
url.endsWith(MARKET_SEARCH_REQUEST_PATH)
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeMarketList(value: unknown[]): boolean {
|
||||
const firstRow = value[0];
|
||||
return isRecord(firstRow) && ("star_id" in firstRow || "attribute_datas" in firstRow);
|
||||
|
||||
Reference in New Issue
Block a user