mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-05 07:27:53 +08:00
266 lines
8.2 KiB
JavaScript
266 lines
8.2 KiB
JavaScript
// content/relay-firefox.js — Content script for Firefox Relay profile page
|
|
|
|
console.log('[MultiPage:relay-firefox] Content script loaded on', location.href);
|
|
|
|
const {
|
|
getNextRelayMaskLabel = (labels = []) => `t${labels.length + 1}`,
|
|
} = globalThis.MultiPageEmailProvider || {};
|
|
|
|
const LABEL_INPUT_SELECTOR = 'input[placeholder="Add account name"], input[aria-label="Edit the label for this mask"]';
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.type !== 'CREATE_RELAY_MASK' && message.type !== 'DELETE_RELAY_MASK') return;
|
|
|
|
resetStopState();
|
|
|
|
const handler = message.type === 'CREATE_RELAY_MASK'
|
|
? createRelayMask
|
|
: deleteRelayMask;
|
|
|
|
handler(message.payload || {}).then(result => {
|
|
sendResponse(result);
|
|
}).catch(err => {
|
|
if (isStopError(err)) {
|
|
log('Relay: Stopped by user.', 'warn');
|
|
sendResponse({ stopped: true, error: err.message });
|
|
return;
|
|
}
|
|
sendResponse({ error: err.message });
|
|
});
|
|
|
|
return true;
|
|
});
|
|
|
|
function getElementText(el) {
|
|
return [
|
|
el?.innerText,
|
|
el?.textContent,
|
|
el?.getAttribute?.('aria-label'),
|
|
el?.getAttribute?.('title'),
|
|
el?.getAttribute?.('description'),
|
|
].filter(Boolean).join(' ');
|
|
}
|
|
|
|
function isVisible(el) {
|
|
if (!el) return false;
|
|
if (el.hidden) return false;
|
|
const style = window.getComputedStyle(el);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
return false;
|
|
}
|
|
return Boolean(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
|
}
|
|
|
|
function extractMozmail(text) {
|
|
const match = String(text || '').match(/[A-Z0-9._%+-]+@mozmail\.com/i);
|
|
return match ? match[0].toLowerCase() : '';
|
|
}
|
|
|
|
function getMaskButtons(root = document) {
|
|
return Array.from(root.querySelectorAll('button')).filter((button) => extractMozmail(getElementText(button)));
|
|
}
|
|
|
|
function getMaskEmails(root = document) {
|
|
return Array.from(new Set(
|
|
getMaskButtons(root)
|
|
.map((button) => extractMozmail(getElementText(button)))
|
|
.filter(Boolean)
|
|
));
|
|
}
|
|
|
|
function getVisibleLabelInputs(root = document) {
|
|
return Array.from(root.querySelectorAll(LABEL_INPUT_SELECTOR)).filter(isVisible);
|
|
}
|
|
|
|
function getExistingLabels() {
|
|
return Array.from(document.querySelectorAll(LABEL_INPUT_SELECTOR))
|
|
.map((input) => input.value.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function findGenerateButton() {
|
|
return document.querySelector('button[title="Generate new mask"]')
|
|
|| Array.from(document.querySelectorAll('button')).find((button) => /generate new mask/i.test(getElementText(button)));
|
|
}
|
|
|
|
function findDeleteButton(root) {
|
|
return Array.from(root.querySelectorAll('button')).find((button) => /^delete$/i.test(getElementText(button).trim()));
|
|
}
|
|
|
|
function getMaskButtonsIn(root) {
|
|
return Array.from(root.querySelectorAll('button')).filter((button) => extractMozmail(getElementText(button)));
|
|
}
|
|
|
|
function findMaskContainerForButton(button) {
|
|
let current = button?.parentElement || null;
|
|
|
|
while (current && current !== document.body) {
|
|
const maskButtons = getMaskButtonsIn(current);
|
|
if (maskButtons.length === 1 && (current.querySelector(LABEL_INPUT_SELECTOR) || findDeleteButton(current))) {
|
|
return current;
|
|
}
|
|
current = current.parentElement;
|
|
}
|
|
|
|
return button?.closest('li') || button?.parentElement || null;
|
|
}
|
|
|
|
function findMaskRowByEmail(email) {
|
|
const normalizedEmail = String(email || '').toLowerCase();
|
|
const button = getMaskButtons().find((candidate) => extractMozmail(getElementText(candidate)) === normalizedEmail);
|
|
if (!button) return null;
|
|
return findMaskContainerForButton(button);
|
|
}
|
|
|
|
async function waitForNewMaskEmail(previousEmails = new Set(), timeout = 15000) {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < timeout) {
|
|
throwIfStopped();
|
|
const currentEmails = getMaskEmails();
|
|
const nextEmail = currentEmails.find((email) => !previousEmails.has(email));
|
|
if (nextEmail) {
|
|
return nextEmail;
|
|
}
|
|
await sleep(150);
|
|
}
|
|
|
|
throw new Error('Timed out waiting for a new Relay mask to appear.');
|
|
}
|
|
|
|
async function waitForMaskRow(email, timeout = 10000) {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < timeout) {
|
|
throwIfStopped();
|
|
const row = findMaskRowByEmail(email);
|
|
if (row) {
|
|
return row;
|
|
}
|
|
await sleep(150);
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for Relay mask row: ${email}`);
|
|
}
|
|
|
|
async function assignRelayLabel(maskRow) {
|
|
const labelInput = Array.from(maskRow.querySelectorAll(LABEL_INPUT_SELECTOR)).find(isVisible)
|
|
|| maskRow.querySelector(LABEL_INPUT_SELECTOR);
|
|
|
|
if (!labelInput) {
|
|
throw new Error('Could not find Relay label input for the new mask.');
|
|
}
|
|
|
|
const currentValue = labelInput.value.trim();
|
|
if (currentValue) {
|
|
return currentValue;
|
|
}
|
|
|
|
const nextLabel = getNextRelayMaskLabel(getExistingLabels());
|
|
|
|
await humanPause(200, 450);
|
|
fillInput(labelInput, nextLabel);
|
|
labelInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
labelInput.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
|
|
labelInput.blur();
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
throwIfStopped();
|
|
const labels = getExistingLabels();
|
|
if (labels.includes(nextLabel) || labelInput.value.trim() === nextLabel) {
|
|
log(`Relay: Assigned label ${nextLabel}`, 'ok');
|
|
return nextLabel;
|
|
}
|
|
await sleep(150);
|
|
}
|
|
|
|
throw new Error(`Relay label ${nextLabel} was not saved.`);
|
|
}
|
|
|
|
async function createRelayMask(payload = {}) {
|
|
const { generateNew = true } = payload;
|
|
|
|
log(`Relay: ${generateNew ? 'Creating' : 'Reading'} mask...`);
|
|
await waitForElement(LABEL_INPUT_SELECTOR + ', button[title="Generate new mask"]', 20000);
|
|
|
|
const previousEmails = new Set(getMaskEmails());
|
|
if (!generateNew && previousEmails.size > 0) {
|
|
const email = Array.from(previousEmails)[0];
|
|
return { email, label: null, generated: false };
|
|
}
|
|
|
|
const generatorButton = findGenerateButton();
|
|
if (!generatorButton) {
|
|
throw new Error('Could not find "Generate new mask" button on Firefox Relay.');
|
|
}
|
|
|
|
await humanPause(500, 1200);
|
|
simulateClick(generatorButton);
|
|
log('Relay: Clicked "Generate new mask"');
|
|
|
|
const email = await waitForNewMaskEmail(previousEmails);
|
|
const maskRow = await waitForMaskRow(email);
|
|
const label = await assignRelayLabel(maskRow);
|
|
|
|
log(`Relay: Ready mask ${email}`, 'ok');
|
|
return { email, label, generated: true };
|
|
}
|
|
|
|
function findVisibleDialogDeleteButton() {
|
|
const dialogButtons = Array.from(document.querySelectorAll('[role="dialog"] button, dialog button, [aria-modal="true"] button'));
|
|
return dialogButtons.find((button) => isVisible(button) && /delete|confirm|remove/i.test(getElementText(button)));
|
|
}
|
|
|
|
async function waitForMaskRemoval(email, timeout = 15000) {
|
|
const startedAt = Date.now();
|
|
const normalizedEmail = String(email || '').toLowerCase();
|
|
|
|
while (Date.now() - startedAt < timeout) {
|
|
throwIfStopped();
|
|
const exists = getMaskEmails().includes(normalizedEmail);
|
|
if (!exists) {
|
|
return;
|
|
}
|
|
await sleep(200);
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for Relay mask deletion: ${email}`);
|
|
}
|
|
|
|
async function deleteRelayMask(payload = {}) {
|
|
const email = String(payload.email || '').trim().toLowerCase();
|
|
if (!email) {
|
|
throw new Error('No Relay mask email provided for deletion.');
|
|
}
|
|
|
|
log(`Relay: Deleting ${email}...`);
|
|
await waitForElement(LABEL_INPUT_SELECTOR + ', button[title="Generate new mask"]', 20000);
|
|
|
|
const maskRow = await waitForMaskRow(email);
|
|
const detailsButton = Array.from(maskRow.querySelectorAll('button')).find((button) => /show mask details/i.test(getElementText(button)));
|
|
if (detailsButton && isVisible(detailsButton)) {
|
|
await humanPause(150, 300);
|
|
simulateClick(detailsButton);
|
|
await sleep(250);
|
|
}
|
|
|
|
const deleteButton = findDeleteButton(maskRow);
|
|
if (!deleteButton) {
|
|
throw new Error(`Could not find Delete button for Relay mask ${email}.`);
|
|
}
|
|
|
|
await humanPause(200, 400);
|
|
simulateClick(deleteButton);
|
|
|
|
await sleep(300);
|
|
const confirmButton = findVisibleDialogDeleteButton();
|
|
if (confirmButton) {
|
|
await humanPause(150, 300);
|
|
simulateClick(confirmButton);
|
|
}
|
|
|
|
await waitForMaskRemoval(email);
|
|
log(`Relay: Deleted ${email}`, 'ok');
|
|
return { deleted: true, email };
|
|
}
|