star-chart-search-enhancer/src/content/market/market-list-request-snapshot.ts

174 lines
4.6 KiB
TypeScript

export const MARKET_REQUEST_SNAPSHOT_ATTRIBUTE =
"data-sces-market-request-snapshot";
const MARKET_SEARCH_ENDPOINT_PATH = "/gw/api/gsearch/search_for_author_square";
export interface MarketListRequestSnapshot {
body?: string;
headers?: Record<string, string>;
method: string;
url: string;
}
export function readMarketListRequestSnapshot(
document: Document
): MarketListRequestSnapshot | null {
const serializedSnapshot = document.documentElement.getAttribute(
MARKET_REQUEST_SNAPSHOT_ATTRIBUTE
);
if (!serializedSnapshot) {
return readMarketListRequestSnapshotFromPageState(document);
}
try {
const parsedSnapshot = normalizeMarketListRequestSnapshot(
JSON.parse(serializedSnapshot)
);
if (!parsedSnapshot) {
return readMarketListRequestSnapshotFromPageState(document);
}
return parsedSnapshot;
} catch {
return readMarketListRequestSnapshotFromPageState(document);
}
}
export function writeMarketListRequestSnapshot(
document: Document,
snapshot: MarketListRequestSnapshot
): void {
document.documentElement.setAttribute(
MARKET_REQUEST_SNAPSHOT_ATTRIBUTE,
JSON.stringify(snapshot)
);
}
function isMarketListRequestSnapshot(
value: unknown
): value is MarketListRequestSnapshot {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as Partial<MarketListRequestSnapshot>;
return (
typeof candidate.method === "string" &&
typeof candidate.url === "string" &&
(!("body" in candidate) || typeof candidate.body === "string") &&
(!("headers" in candidate) || isStringRecord(candidate.headers))
);
}
function normalizeMarketListRequestSnapshot(
value: unknown
): MarketListRequestSnapshot | null {
if (!value || typeof value !== "object") {
return null;
}
const candidate = value as Partial<MarketListRequestSnapshot> & {
headers?: Record<string, unknown>;
};
const normalizedSnapshot: Partial<MarketListRequestSnapshot> = {
body: typeof candidate.body === "string" ? candidate.body : undefined,
method: typeof candidate.method === "string" ? candidate.method : undefined,
url: typeof candidate.url === "string" ? candidate.url : undefined
};
if (candidate.headers && typeof candidate.headers === "object") {
normalizedSnapshot.headers = Object.fromEntries(
Object.entries(candidate.headers)
.filter(([, entry]) =>
["string", "number", "boolean"].includes(typeof entry)
)
.map(([key, entry]) => [key, String(entry)])
);
}
return isMarketListRequestSnapshot(normalizedSnapshot)
? normalizedSnapshot
: null;
}
function isStringRecord(value: unknown): value is Record<string, string> {
if (!value || typeof value !== "object") {
return false;
}
return Object.values(value).every((entry) => typeof entry === "string");
}
function readMarketListRequestSnapshotFromPageState(
document: Document
): MarketListRequestSnapshot | null {
const reqParams = findMarketReqParams(document);
if (!reqParams) {
return null;
}
return {
body: JSON.stringify(reqParams),
method: "POST",
url: buildMarketSearchUrl(document)
};
}
function findMarketReqParams(document: Document): Record<string, unknown> | null {
const marketRoot = document.querySelector(".base-author-list") as
| (Element & {
__vue__?: {
_setupState?: Record<string, unknown>;
};
})
| null;
const setupState = marketRoot?.__vue__?._setupState;
if (!setupState) {
return null;
}
const queue: unknown[] = Object.values(setupState);
while (queue.length > 0) {
const current = unwrapVueRef(queue.shift());
if (!isRecord(current)) {
continue;
}
const reqParams = unwrapVueRef(current.reqParams);
if (isRecord(reqParams)) {
return reqParams;
}
Object.values(current).forEach((value) => {
queue.push(value);
});
}
return null;
}
function buildMarketSearchUrl(document: Document): string {
if (
document.location?.origin &&
document.location.origin !== "null" &&
document.location.origin !== "about:blank"
) {
return document.location.origin.includes("xingtu.cn")
? MARKET_SEARCH_ENDPOINT_PATH
: new URL(MARKET_SEARCH_ENDPOINT_PATH, document.location.origin).toString();
}
return MARKET_SEARCH_ENDPOINT_PATH;
}
function unwrapVueRef(value: unknown): unknown {
if (isRecord(value) && "value" in value) {
return value.value;
}
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}