Merge branch 'master' into codex/upstream-pr
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
routeErrorPattern = null,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
@@ -65,9 +66,13 @@
|
||||
const detailMatched = detailPattern instanceof RegExp
|
||||
? detailPattern.test(text)
|
||||
: false;
|
||||
const routeErrorMatched = routeErrorPattern instanceof RegExp
|
||||
? routeErrorPattern.test(text)
|
||||
: false;
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !maxCheckAttemptsBlocked) {
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -78,7 +83,9 @@
|
||||
retryEnabled: isActionEnabled(retryButton),
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,6 +155,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
idlePollCount = 0;
|
||||
clickCount += 1;
|
||||
@@ -199,6 +212,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
|
||||
);
|
||||
|
||||
+22
-34
@@ -53,6 +53,10 @@ async function persistSeenCodes() {
|
||||
}
|
||||
|
||||
function buildSeenCodeSessionKey(step, payload = {}) {
|
||||
const explicitSessionKey = String(payload?.sessionKey || '').trim();
|
||||
if (explicitSessionKey) {
|
||||
return explicitSessionKey;
|
||||
}
|
||||
const timestamp = Number(payload?.filterAfterTimestamp);
|
||||
if (Number.isFinite(timestamp) && timestamp > 0) {
|
||||
return `${step}:${timestamp}`;
|
||||
@@ -95,8 +99,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
}
|
||||
|
||||
if (message.type === 'DELETE_ALL_EMAILS') {
|
||||
Promise.resolve(deleteAllMailboxEmails(message.step)).catch(() => {});
|
||||
sendResponse({ ok: true });
|
||||
Promise.resolve(deleteAllMailboxEmails(message.step)).then((deleted) => {
|
||||
sendResponse({ ok: true, deleted });
|
||||
}).catch((err) => {
|
||||
sendResponse({ ok: false, error: err?.message || String(err || '删除邮件失败') });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -474,6 +481,10 @@ async function openMailAndDeleteAfterRead(item, step) {
|
||||
async function deleteAllMailboxEmails(step) {
|
||||
try {
|
||||
await returnToInbox();
|
||||
const initialItems = findMailItems();
|
||||
if (initialItems.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectAllControl = findSelectAllControl();
|
||||
if (!selectAllControl) {
|
||||
@@ -491,8 +502,15 @@ async function deleteAllMailboxEmails(step) {
|
||||
}
|
||||
|
||||
simulateClick(deleteButton);
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
await sleep(250);
|
||||
if (findMailItems().length === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
await sleepRandom(200, 500);
|
||||
return true;
|
||||
return findMailItems().length === 0;
|
||||
} catch (err) {
|
||||
console.warn(MAIL2925_PREFIX, `Step ${step}: delete-all cleanup failed:`, err?.message || err);
|
||||
return false;
|
||||
@@ -551,11 +569,7 @@ async function handlePollEmail(step, payload) {
|
||||
throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
}
|
||||
|
||||
const knownMailIds = getCurrentMailIds(initialItems);
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
|
||||
log(`步骤 ${step}:已记录当前 ${knownMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
@@ -568,26 +582,10 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
const items = findMailItems();
|
||||
if (items.length > 0) {
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
const newMailIds = new Set();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const itemId = getMailItemId(item, index);
|
||||
if (!knownMailIds.has(itemId)) {
|
||||
newMailIds.add(itemId);
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const itemId = getMailItemId(item, index);
|
||||
const isNewMail = newMailIds.has(itemId);
|
||||
const itemTimestamp = parseMailItemTimestamp(item);
|
||||
|
||||
if (!useFallback && !isNewMail) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewText = getMailItemText(item);
|
||||
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
|
||||
continue;
|
||||
@@ -613,21 +611,11 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
seenCodes.add(candidateCode);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && !isNewMail
|
||||
? (bodyCode ? '回退匹配邮件正文' : '回退匹配邮件')
|
||||
: (bodyCode ? '新邮件正文' : '新邮件');
|
||||
const source = bodyCode ? '邮件正文' : '邮件预览';
|
||||
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已找到验证码:${candidateCode}(来源:${source}${timeLabel})`, 'ok');
|
||||
return { ok: true, code: candidateCode, emailTimestamp: Date.now() };
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
knownMailIds.add(getMailItemId(item, index));
|
||||
});
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
|
||||
+85
-44
@@ -253,39 +253,17 @@ async function resendVerificationCode(step, timeout = 45000) {
|
||||
|
||||
function is405MethodNotAllowedPage() {
|
||||
const pageText = document.body?.textContent || '';
|
||||
return /405\s+Method\s+Not\s+Allowed/i.test(pageText)
|
||||
|| /Route\s+Error.*405/i.test(pageText);
|
||||
return AUTH_ROUTE_ERROR_PATTERN.test(pageText);
|
||||
}
|
||||
|
||||
async function handle405ResendError(step, remainingTimeout = 30000) {
|
||||
const start = Date.now();
|
||||
let retryCount = 0;
|
||||
|
||||
while (Date.now() - start < remainingTimeout) {
|
||||
throwIfStopped();
|
||||
|
||||
if (!is405MethodNotAllowedPage()) {
|
||||
// Page recovered — back to verification page
|
||||
log(`步骤 ${step}:405 错误已恢复,页面已返回验证码页面。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const retryBtn = getAuthRetryButton();
|
||||
if (retryBtn) {
|
||||
retryCount++;
|
||||
log(`步骤 ${step}:检测到 405 错误页面,正在点击"Try again"(第 ${retryCount} 次)...`, 'warn');
|
||||
await humanPause(300, 800);
|
||||
simulateClick(retryBtn);
|
||||
|
||||
// Wait 3 seconds before checking again
|
||||
await sleep(3000);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${step}:405 错误恢复超时,无法返回验证码页面。URL: ${location.href}`);
|
||||
await recoverCurrentAuthRetryPage({
|
||||
logLabel: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
|
||||
pathPatterns: [],
|
||||
step,
|
||||
timeoutMs: Math.max(1000, remainingTimeout),
|
||||
});
|
||||
log(`步骤 ${step}:405 错误已恢复,页面已返回验证码页面。`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -759,6 +737,8 @@ const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手
|
||||
const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|unable\s+to\s+create\s+(?:your\s+)?account|couldn'?t\s+create\s+(?:your\s+)?account|something\s+went\s+wrong|invalid\s+(?:birthday|birth|date)|生日|出生日期/i;
|
||||
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
||||
const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405/i;
|
||||
const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::';
|
||||
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
|
||||
|
||||
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
|
||||
@@ -769,6 +749,7 @@ const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
routeErrorPattern: AUTH_ROUTE_ERROR_PATTERN,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
@@ -809,6 +790,12 @@ function getVerificationErrorText() {
|
||||
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
|
||||
}
|
||||
|
||||
function createSignupUserAlreadyExistsError() {
|
||||
return new Error(
|
||||
`${SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX}步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。`
|
||||
);
|
||||
}
|
||||
|
||||
function isStep5Ready() {
|
||||
return Boolean(
|
||||
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
|
||||
@@ -1125,9 +1112,11 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
||||
const titleMatched = AUTH_TIMEOUT_ERROR_TITLE_PATTERN.test(text)
|
||||
|| AUTH_TIMEOUT_ERROR_TITLE_PATTERN.test(document.title || '');
|
||||
const detailMatched = AUTH_TIMEOUT_ERROR_DETAIL_PATTERN.test(text);
|
||||
const routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.test(text);
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !maxCheckAttemptsBlocked) {
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1138,16 +1127,33 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
||||
retryEnabled: isActionEnabled(retryButton),
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
}
|
||||
|
||||
function getSignupAuthRetryPathPatterns() {
|
||||
return [
|
||||
/\/create-account\/password(?:[/?#]|$)/i,
|
||||
/\/email-verification(?:[/?#]|$)/i,
|
||||
];
|
||||
}
|
||||
|
||||
function getLoginAuthRetryPathPatterns() {
|
||||
return [
|
||||
/\/log-in(?:[/?#]|$)/i,
|
||||
/\/email-verification(?:[/?#]|$)/i,
|
||||
];
|
||||
}
|
||||
|
||||
function getAuthRetryPathPatternsForFlow(flow = 'auth') {
|
||||
switch (flow) {
|
||||
case 'signup':
|
||||
case 'signup_password':
|
||||
return [/\/create-account\/password(?:[/?#]|$)/i];
|
||||
return getSignupAuthRetryPathPatterns();
|
||||
case 'login':
|
||||
return [/\/log-in(?:[/?#]|$)/i];
|
||||
return getLoginAuthRetryPathPatterns();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -1164,16 +1170,19 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
flow = 'auth',
|
||||
logLabel = '',
|
||||
maxClickAttempts = 5,
|
||||
pathPatterns = null,
|
||||
step = null,
|
||||
timeoutMs = 12000,
|
||||
waitAfterClickMs = 3000,
|
||||
} = payload;
|
||||
const pathPatterns = getAuthRetryPathPatternsForFlow(flow);
|
||||
const resolvedPathPatterns = Array.isArray(pathPatterns)
|
||||
? pathPatterns
|
||||
: getAuthRetryPathPatternsForFlow(flow);
|
||||
if (authPageRecovery?.recoverAuthRetryPage) {
|
||||
return authPageRecovery.recoverAuthRetryPage({
|
||||
logLabel,
|
||||
maxClickAttempts,
|
||||
pathPatterns,
|
||||
pathPatterns: resolvedPathPatterns,
|
||||
step,
|
||||
timeoutMs,
|
||||
waitAfterClickMs,
|
||||
@@ -1187,7 +1196,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
let idlePollCount = 0;
|
||||
while (clickCount < maxClickAttempts) {
|
||||
throwIfStopped();
|
||||
const retryState = getCurrentAuthRetryPageState(flow);
|
||||
const retryState = getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns });
|
||||
if (!retryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
@@ -1199,6 +1208,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
if (retryState.maxCheckAttemptsBlocked) {
|
||||
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
||||
}
|
||||
if (retryState.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
idlePollCount = 0;
|
||||
clickCount += 1;
|
||||
@@ -1208,7 +1220,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
const settleStart = Date.now();
|
||||
while (Date.now() - settleStart < waitAfterClickMs) {
|
||||
throwIfStopped();
|
||||
if (!getCurrentAuthRetryPageState(flow)) {
|
||||
if (!getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns })) {
|
||||
return {
|
||||
recovered: true,
|
||||
clickCount,
|
||||
@@ -1228,7 +1240,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
const finalRetryState = getCurrentAuthRetryPageState(flow);
|
||||
const finalRetryState = getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns });
|
||||
if (!finalRetryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
@@ -1239,13 +1251,16 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
if (finalRetryState.maxCheckAttemptsBlocked) {
|
||||
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
||||
}
|
||||
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
|
||||
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`);
|
||||
}
|
||||
|
||||
function getSignupPasswordTimeoutErrorPageState() {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/create-account\/password(?:[/?#]|$)/i],
|
||||
pathPatterns: getSignupAuthRetryPathPatterns(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1556,6 +1571,7 @@ function inspectSignupVerificationState() {
|
||||
return {
|
||||
state: 'error',
|
||||
retryButton: timeoutPage?.retryButton || null,
|
||||
userAlreadyExistsBlocked: Boolean(timeoutPage?.userAlreadyExistsBlocked),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1629,9 +1645,12 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
|
||||
recoveryRound += 1;
|
||||
|
||||
if (snapshot.state === 'error') {
|
||||
if (snapshot.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
await recoverCurrentAuthRetryPage({
|
||||
flow: 'signup_password',
|
||||
logLabel: `${prepareLogLabel}:检测到密码页超时报错,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
|
||||
flow: 'signup',
|
||||
logLabel: `${prepareLogLabel}:检测到注册认证重试页,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
|
||||
step: 4,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
@@ -1675,6 +1694,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
while (Date.now() - start < resolvedTimeout) {
|
||||
throwIfStopped();
|
||||
|
||||
if (step === 4) {
|
||||
const signupRetryState = getCurrentAuthRetryPageState('signup');
|
||||
if (signupRetryState?.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
}
|
||||
|
||||
const errorText = getVerificationErrorText();
|
||||
if (errorText) {
|
||||
return { invalidCode: true, errorText };
|
||||
@@ -1695,6 +1721,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
const signupRetryState = getCurrentAuthRetryPageState('signup');
|
||||
if (signupRetryState?.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerificationPageStillVisible()) {
|
||||
return {
|
||||
invalidCode: true,
|
||||
@@ -1797,7 +1830,7 @@ async function fillVerificationCode(step, payload) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 6: Login with registered account (on OAuth auth page)
|
||||
// Step 7: Login with registered account (on OAuth auth page)
|
||||
// ============================================================
|
||||
|
||||
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
|
||||
@@ -2039,10 +2072,18 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
||||
|
||||
async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||
const hasPassword = Boolean(String(payload?.password || '').trim());
|
||||
|
||||
if (currentSnapshot.passwordInput) {
|
||||
if (!payload.password) {
|
||||
throw new Error('登录时缺少密码,步骤 7 无法继续。');
|
||||
if (!hasPassword) {
|
||||
if (currentSnapshot.switchTrigger) {
|
||||
log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn');
|
||||
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, {
|
||||
message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。',
|
||||
});
|
||||
}
|
||||
|
||||
log('步骤 7:已进入密码页,准备填写密码...');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// content/vps-panel.js — Content script for CPA panel (steps 1, 9)
|
||||
// content/vps-panel.js — Content script for CPA panel (steps 7, 10 / OAuth URL request)
|
||||
// Injected on: CPA panel (user-configured URL)
|
||||
//
|
||||
// Actual DOM structure (after login click):
|
||||
|
||||
Reference in New Issue
Block a user