Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

173 lines
7.6 KiB
JavaScript

/**
* Weibo delete — remove a single post owned by the logged-in user.
*/
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { requireObjectEvaluateResult, unwrapEvaluateResult } from './utils.js';
const WEIBO_HOST_RE = /(^|\.)weibo\.(com|cn)$/i;
const POST_ID_RE = /^[A-Za-z0-9]{4,32}$/;
function normalizePostId(raw) {
const input = String(raw ?? '').trim();
if (!input) {
throw new ArgumentError('weibo delete: id cannot be empty');
}
let candidate = input;
try {
const url = new URL(input);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new ArgumentError('weibo delete: URL must use http or https');
}
if (!WEIBO_HOST_RE.test(url.hostname)) {
throw new ArgumentError('weibo delete: URL must be a weibo.com or weibo.cn post URL');
}
const parts = url.pathname.split('/').filter(Boolean);
if (url.hostname.toLowerCase().endsWith('weibo.cn') && parts[0] === 'status') {
candidate = parts[1] ?? '';
} else {
candidate = parts.at(-1) ?? '';
}
} catch (error) {
if (error instanceof ArgumentError) throw error;
}
candidate = String(candidate ?? '').trim();
if (!POST_ID_RE.test(candidate)) {
throw new ArgumentError('weibo delete: id must be a numeric idstr, mblogid, or Weibo post URL');
}
return candidate;
}
cli({
site: 'weibo',
name: 'delete',
access: 'write',
description: 'Delete one of my Weibo posts by id',
domain: 'weibo.com',
strategy: Strategy.COOKIE,
args: [
{
name: 'id',
required: true,
positional: true,
help: 'Post ID (numeric idstr or mblogid from URL / weibo me / weibo post output)',
},
],
columns: ['status', 'id', 'mblogid'],
func: async (page, kwargs) => {
if (!page) {
throw new CommandExecutionError('Browser session required for weibo delete');
}
const raw = String(kwargs.id ?? '').trim();
const id = normalizePostId(raw);
await page.goto('https://weibo.com');
await page.wait(2);
const result = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
(async () => {
const input = ${JSON.stringify(id)};
const readCookie = (name) => {
const pair = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith(name + '='));
return pair ? decodeURIComponent(pair.slice(name.length + 1)) : '';
};
// Step 1: resolve mblogid / idstr to canonical idstr via /show.
const showResp = await fetch('/ajax/statuses/show?id=' + encodeURIComponent(input), { credentials: 'include' });
if (showResp.status === 401 || showResp.status === 403) {
return { ok: false, error: 'auth', status: showResp.status };
}
// 404 from /show means the post does not exist (deleted, wrong id, or
// not owned by the logged-in user); map to the same path as a 2xx
// response with no idstr so the caller throws EmptyResultError
// instead of a generic CommandExecutionError("HTTP 404").
if (showResp.status === 404) {
return { ok: false, error: 'not_found', input };
}
if (!showResp.ok) {
return { ok: false, error: 'show_http', status: showResp.status };
}
const showBody = await showResp.json();
if (!showBody || !showBody.idstr) {
return { ok: false, error: 'not_found', input };
}
const idstr = String(showBody.idstr);
const mblogid = showBody.mblogid || '';
// Step 2: destroy. Weibo requires X-Xsrf-Token (double-submit CSRF token).
const token = readCookie('XSRF-TOKEN');
const destroyResp = await fetch('/ajax/statuses/destroy', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Xsrf-Token': token,
},
body: 'id=' + encodeURIComponent(idstr),
});
if (destroyResp.status === 401 || destroyResp.status === 403) {
return { ok: false, error: 'auth', status: destroyResp.status };
}
if (!destroyResp.ok) {
return { ok: false, error: 'destroy_http', status: destroyResp.status };
}
const destroyBody = await destroyResp.json();
// Require an explicit success signal from the API: { ok: 1 }. A
// missing / falsy body must not be silently treated as success.
if (!destroyBody || typeof destroyBody !== 'object') {
return { ok: false, error: 'api', msg: 'destroy returned malformed response', id: idstr };
}
if (destroyBody.ok !== 1) {
return { ok: false, error: 'api', msg: destroyBody.msg || destroyBody.message || 'destroy returned non-ok', id: idstr };
}
// Step 3: postcondition evidence. A write command cannot report success
// until the target no longer resolves after the delete API returns ok.
const verifyResp = await fetch('/ajax/statuses/show?id=' + encodeURIComponent(idstr), { credentials: 'include' });
if (verifyResp.status === 401 || verifyResp.status === 403) {
return { ok: false, error: 'auth', status: verifyResp.status };
}
if (verifyResp.status === 404) {
return { ok: true, id: idstr, mblogid };
}
if (!verifyResp.ok) {
return { ok: false, error: 'verify_http', status: verifyResp.status, id: idstr };
}
let verifyBody = null;
try {
verifyBody = await verifyResp.json();
} catch {
return { ok: false, error: 'verify_malformed', msg: 'verify returned non-JSON response', id: idstr };
}
if (!verifyBody || typeof verifyBody !== 'object') {
return { ok: false, error: 'verify_malformed', msg: 'verify returned malformed response', id: idstr };
}
if (String(verifyBody.idstr || '') === idstr) {
return { ok: false, error: 'still_exists', id: idstr, mblogid: verifyBody.mblogid || mblogid };
}
if (!verifyBody.idstr || verifyBody.ok === 0) {
return { ok: true, id: idstr, mblogid };
}
return { ok: false, error: 'verify_mismatch', msg: 'verify returned a different post id', id: idstr };
})()
`)), 'weibo delete');
if (result.error === 'auth') {
throw new AuthRequiredError('weibo.com', 'Cookie 已过期!请在当前 Chrome 浏览器中重新登录 Weibo。');
}
if (result.error === 'not_found') {
throw new EmptyResultError('weibo delete', `Post not found for id "${String(result.input ?? raw)}". Verify the post still exists and belongs to the logged-in account.`);
}
if (result.error === 'show_http' || result.error === 'destroy_http' || result.error === 'verify_http') {
throw new CommandExecutionError(`weibo delete: HTTP ${result.status}`);
}
if (result.error === 'api' || result.error === 'verify_malformed' || result.error === 'verify_mismatch' || result.error === 'still_exists') {
throw new CommandExecutionError(`weibo delete: ${String(result.msg ?? result.error)}`);
}
if (!result.ok) {
throw new CommandExecutionError('weibo delete returned an unexpected response');
}
return [{ status: 'deleted', id: String(result.id ?? ''), mblogid: String(result.mblogid ?? '') }];
},
});
export const __test__ = {
normalizePostId,
};