feat: add script-generator feature scaffold
Add script generation utility service. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
94da62eea6
commit
107c8554d6
23 changed files with 1867 additions and 0 deletions
22
features/script-generator/package.json
Normal file
22
features/script-generator/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@lilith/script-generator",
|
||||
"version": "0.1.0",
|
||||
"description": "Configurable browser script generator for automation tasks",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"generate": "tsx src/cli.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"automation",
|
||||
"browser-scripts",
|
||||
"console-scripts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"tsx": "^4.7.0"
|
||||
}
|
||||
}
|
||||
94
features/script-generator/src/cli.ts
Normal file
94
features/script-generator/src/cli.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env node
|
||||
import { seekingAutoFavoriteGenerator, defaultSeekingConfig } from './scripts/seeking-auto-favorite.js';
|
||||
import type { SeekingAutoFavoriteConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* Parse CLI arguments into config overrides
|
||||
*/
|
||||
function parseArgs(args: string[]): Partial<SeekingAutoFavoriteConfig> {
|
||||
const overrides: Partial<SeekingAutoFavoriteConfig> = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const next = args[i + 1];
|
||||
|
||||
switch (arg) {
|
||||
case '--min-age':
|
||||
overrides.minAge = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--max-age':
|
||||
overrides.maxAge = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--no-verified':
|
||||
overrides.requireVerified = false;
|
||||
break;
|
||||
case '--base-delay':
|
||||
overrides.baseDelay = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--random-delay':
|
||||
overrides.randomDelayMax = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--focus-delay':
|
||||
overrides.focusToClickDelay = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--after-click-delay':
|
||||
overrides.afterClickDelay = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
Seeking.com Auto-Favorite Script Generator
|
||||
|
||||
Usage: npx tsx src/cli.ts [options]
|
||||
|
||||
Options:
|
||||
--min-age <n> Minimum age (default: 35)
|
||||
--max-age <n> Maximum age (default: no limit)
|
||||
--no-verified Don't require verified badge
|
||||
--base-delay <ms> Base delay between actions (default: 3000)
|
||||
--random-delay <ms> Max random delay to add (default: 2000)
|
||||
--focus-delay <ms> Delay after focus before click (default: 1000)
|
||||
--after-click-delay <ms> Delay after clicking heart (default: 3000)
|
||||
--help, -h Show this help
|
||||
|
||||
Default behavior:
|
||||
- Matches profiles with NO state/country suffix (California)
|
||||
- Matches Las Vegas, Strip, Paradise, Henderson, North Las Vegas
|
||||
- Requires verified badge
|
||||
- Age 35+
|
||||
|
||||
Example:
|
||||
npx tsx src/cli.ts --min-age 30 --max-age 45
|
||||
|
||||
Output:
|
||||
Prints the JavaScript code to paste into browser console.
|
||||
`);
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
const overrides = parseArgs(args);
|
||||
const config: SeekingAutoFavoriteConfig = { ...defaultSeekingConfig, ...overrides };
|
||||
|
||||
const result = seekingAutoFavoriteGenerator.generate(config);
|
||||
|
||||
console.log('// ' + '='.repeat(60));
|
||||
console.log('// ' + result.description);
|
||||
console.log('// ' + '='.repeat(60));
|
||||
console.log('');
|
||||
console.log(result.code);
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Background script - handles extension lifecycle
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
console.log('Seeking Auto-Favorite extension installed');
|
||||
|
||||
// Set default config
|
||||
browser.storage.local.set({
|
||||
minAge: 35,
|
||||
maxAge: 0,
|
||||
requireVerified: true,
|
||||
locCalifornia: true,
|
||||
locLasVegas: true,
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
});
|
||||
|
||||
// Initialize badge
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: '#666' });
|
||||
browser.browserAction.setBadgeText({ text: '' });
|
||||
});
|
||||
|
||||
// Update icon badge based on state
|
||||
function updateBadge(isRunning, isPaused, count) {
|
||||
if (isRunning && !isPaused) {
|
||||
// Running - green badge with count
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: '#27ae60' });
|
||||
browser.browserAction.setBadgeText({ text: count > 0 ? String(count) : '▶' });
|
||||
} else if (isRunning && isPaused) {
|
||||
// Paused - orange badge
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: '#f39c12' });
|
||||
browser.browserAction.setBadgeText({ text: '⏸' });
|
||||
} else {
|
||||
// Stopped/Idle - show count if any, otherwise empty
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: '#666' });
|
||||
browser.browserAction.setBadgeText({ text: count > 0 ? String(count) : '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Track current state
|
||||
let currentCount = 0;
|
||||
let currentRunning = false;
|
||||
let currentPaused = false;
|
||||
|
||||
// Relay messages between popup and content scripts
|
||||
browser.runtime.onMessage.addListener((msg, sender) => {
|
||||
// Update badge on stats update
|
||||
if (msg.action === 'statsUpdate' && msg.state) {
|
||||
currentCount = msg.state.totalClicked || 0;
|
||||
updateBadge(currentRunning, currentPaused, currentCount);
|
||||
}
|
||||
|
||||
// Update badge on status update
|
||||
if (msg.action === 'statusUpdate') {
|
||||
currentRunning = msg.isRunning;
|
||||
currentPaused = msg.isPaused;
|
||||
updateBadge(currentRunning, currentPaused, currentCount);
|
||||
}
|
||||
|
||||
// Forward to popup
|
||||
if (msg.action === 'statsUpdate' || msg.action === 'statusUpdate') {
|
||||
browser.runtime.sendMessage(msg).catch(() => {
|
||||
// Popup might be closed, that's ok
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load persisted state on startup
|
||||
browser.storage.local.get(['state', 'isRunning', 'isPaused']).then(stored => {
|
||||
currentCount = stored.state?.totalClicked || 0;
|
||||
currentRunning = stored.isRunning || false;
|
||||
currentPaused = stored.isPaused || false;
|
||||
updateBadge(currentRunning, currentPaused, currentCount);
|
||||
});
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
// Content script - runs on seeking.com pages
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let isRunning = false;
|
||||
let isPaused = false;
|
||||
let shouldStop = false;
|
||||
let config = {};
|
||||
// State now tracks:
|
||||
// - processed: UIDs we've seen (to avoid re-checking)
|
||||
// - favorited: UIDs we've successfully hearted
|
||||
// - verified: UIDs we've verified are still hearted (with timestamp)
|
||||
let state = {
|
||||
processed: [],
|
||||
favorited: [],
|
||||
verified: {}, // { uid: timestamp }
|
||||
totalClicked: 0,
|
||||
totalFailed: 0,
|
||||
startTime: null
|
||||
};
|
||||
|
||||
// Track if we're on a profile page for heart clicking
|
||||
let pendingProfileFavorite = null; // { returnUrl, uid, name }
|
||||
|
||||
// How often to verify already-favorited users (5% chance)
|
||||
const VERIFY_CHANCE = 0.05;
|
||||
// How long before we re-verify a user (24 hours)
|
||||
const VERIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// ============== MEMBER TRACKING ==============
|
||||
function isAlreadyFavorited(uid) {
|
||||
return state.favorited.includes(uid);
|
||||
}
|
||||
|
||||
function shouldVerifyFavorite(uid) {
|
||||
if (!isAlreadyFavorited(uid)) return false;
|
||||
|
||||
// Check if recently verified
|
||||
const lastVerified = state.verified[uid];
|
||||
if (lastVerified && Date.now() - lastVerified < VERIFY_INTERVAL_MS) {
|
||||
return false; // Recently verified, skip
|
||||
}
|
||||
|
||||
// Random chance to verify
|
||||
return Math.random() < VERIFY_CHANCE;
|
||||
}
|
||||
|
||||
function markFavorited(uid) {
|
||||
if (!state.favorited.includes(uid)) {
|
||||
state.favorited.push(uid);
|
||||
}
|
||||
state.verified[uid] = Date.now();
|
||||
}
|
||||
|
||||
function markVerified(uid) {
|
||||
state.verified[uid] = Date.now();
|
||||
}
|
||||
|
||||
// ============== TIMING ==============
|
||||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
function randomize(base) { return Math.round(base + base * (0.25 + Math.random() * 0.75)); }
|
||||
|
||||
async function wait(baseMs) {
|
||||
const actual = randomize(baseMs);
|
||||
if (actual > 1000) {
|
||||
const chunks = Math.floor(actual / 500);
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
await sleep(500);
|
||||
await idleMouseWiggle();
|
||||
if (i === Math.floor(chunks / 2)) await idleScroll();
|
||||
}
|
||||
await sleep(actual % 500);
|
||||
} else {
|
||||
await sleep(actual);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
// ============== MOUSE ==============
|
||||
let mousePos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
|
||||
function dispatchMouseMove(x, y) {
|
||||
const evt = new MouseEvent('mousemove', {
|
||||
bubbles: true, cancelable: true, view: window,
|
||||
clientX: x, clientY: y, screenX: window.screenX + x, screenY: window.screenY + y
|
||||
});
|
||||
(document.elementFromPoint(x, y) || document.body).dispatchEvent(evt);
|
||||
document.dispatchEvent(evt);
|
||||
window.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
async function moveMouseTo(x, y, el) {
|
||||
const startX = mousePos.x, startY = mousePos.y;
|
||||
const steps = 15 + Math.floor(Math.random() * 10);
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const p = i / steps;
|
||||
const ease = 1 - Math.pow(1 - p, 3);
|
||||
const curve = Math.sin(p * Math.PI) * 25 * (1 - p);
|
||||
const cx = startX + (x - startX) * ease + (Math.random() - 0.5) * 2;
|
||||
const cy = startY + (y - startY) * ease + curve + (Math.random() - 0.5) * 2;
|
||||
mousePos = { x: cx, y: cy };
|
||||
dispatchMouseMove(cx, cy);
|
||||
await sleep(randomize(20));
|
||||
}
|
||||
mousePos = { x, y };
|
||||
}
|
||||
|
||||
async function simulateClick(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2 + (Math.random() - 0.5) * rect.width * 0.3;
|
||||
const y = rect.top + rect.height / 2 + (Math.random() - 0.5) * rect.height * 0.3;
|
||||
|
||||
await moveMouseTo(x, y, el);
|
||||
await sleep(randomize(50));
|
||||
|
||||
const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, button: 0, buttons: 1 };
|
||||
el.dispatchEvent(new MouseEvent('mouseenter', opts));
|
||||
el.dispatchEvent(new MouseEvent('mouseover', opts));
|
||||
await sleep(randomize(30));
|
||||
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
||||
await sleep(randomize(80));
|
||||
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
||||
await sleep(randomize(10));
|
||||
el.dispatchEvent(new MouseEvent('click', opts));
|
||||
el.click();
|
||||
}
|
||||
|
||||
async function idleScroll() {
|
||||
if (Math.random() < 0.3) {
|
||||
window.scrollBy({ top: (Math.random() - 0.5) * 200, behavior: 'smooth' });
|
||||
await sleep(randomize(300));
|
||||
}
|
||||
}
|
||||
|
||||
async function idleMouseWiggle() {
|
||||
if (Math.random() < 0.4) {
|
||||
dispatchMouseMove(mousePos.x + (Math.random() - 0.5) * 50, mousePos.y + (Math.random() - 0.5) * 50);
|
||||
await sleep(randomize(100));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== TOAST DETECTION ==============
|
||||
let toastDetected = false;
|
||||
new MutationObserver(muts => {
|
||||
for (const m of muts) for (const n of m.addedNodes) {
|
||||
if (n.nodeType === 1) {
|
||||
const t = n.textContent?.toLowerCase() || '';
|
||||
if (t.includes('failed to create favorite') || t.includes('failed to favorite')) {
|
||||
toastDetected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// ============== LOCATION ==============
|
||||
function matchLocation(loc) {
|
||||
if (!loc) return false;
|
||||
const l = loc.toLowerCase().trim();
|
||||
|
||||
if (config.locCalifornia && !l.includes(',')) return true;
|
||||
if (config.locLasVegas) {
|
||||
const lvCities = ['las vegas', 'strip', 'paradise', 'henderson', 'north las vegas'];
|
||||
if (lvCities.some(c => l.includes(c))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============== CARD PARSING ==============
|
||||
function parseCard(card) {
|
||||
let uid, heartBtn, name = 'Unknown', age, location, verified = false, favorited = false;
|
||||
let profileUrl = null;
|
||||
let heartHidden = false;
|
||||
|
||||
const sa = card.getAttribute('data-cy-search');
|
||||
if (sa) {
|
||||
uid = sa.replace('single-', '');
|
||||
const hc = card.querySelector('div[role="button"]');
|
||||
if (hc?.querySelector('svg[data-cy-icon]')) {
|
||||
heartBtn = hc;
|
||||
favorited = !!hc.querySelector('svg[data-cy-icon="heart-filled"]');
|
||||
} else {
|
||||
heartHidden = true;
|
||||
}
|
||||
name = card.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
|
||||
verified = !!(card.querySelector('svg[data-cy-badge="idv-verified"]') || card.querySelector('svg[data-cy-badge="verified"]'));
|
||||
const info = card.querySelector('[data-cy-user-info="age-location-container"]')?.textContent?.match(/(\d+)\s*·\s*(.+)/);
|
||||
if (info) { age = +info[1]; location = info[2].trim(); }
|
||||
// Extract profile URL from the card link
|
||||
const link = card.querySelector('a[href*="/member/"]');
|
||||
if (link) profileUrl = link.href;
|
||||
} else if (card.getAttribute('data-cy-feed') === 'card') {
|
||||
const btn = card.querySelector('button[data-cy-button="favorite"]');
|
||||
uid = btn?.dataset?.memberUid;
|
||||
heartBtn = btn;
|
||||
if (!btn || btn.offsetParent === null) {
|
||||
heartHidden = true;
|
||||
}
|
||||
name = card.querySelector('span[title]')?.getAttribute('title') || 'Unknown';
|
||||
verified = !!card.querySelector('svg[data-cy-badge="verified"]');
|
||||
favorited = !!card.querySelector('svg[data-cy-icon="favorited"]');
|
||||
const info = card.querySelector('div[style*="font-size: 16px"]')?.textContent?.match(/(\d+)\s*·\s*(.+)/);
|
||||
if (info) { age = +info[1]; location = info[2].trim(); }
|
||||
// Extract profile URL from the card link
|
||||
const link = card.querySelector('a[href*="/member/"]');
|
||||
if (link) profileUrl = link.href;
|
||||
}
|
||||
|
||||
return { uid, heartBtn, name, age, location, verified, favorited, profileUrl, heartHidden };
|
||||
}
|
||||
|
||||
// ============== PROFILE PAGE HANDLING ==============
|
||||
function isProfilePage() {
|
||||
return window.location.pathname.startsWith('/member/');
|
||||
}
|
||||
|
||||
function getProfileHeartButton() {
|
||||
// Profile page heart button selector
|
||||
return document.querySelector('button[data-cy-button="sticky-favorite-profile"]');
|
||||
}
|
||||
|
||||
function isProfileFavorited() {
|
||||
const btn = getProfileHeartButton();
|
||||
return btn?.querySelector('svg[data-cy-icon="heart-filled"]') ||
|
||||
btn?.querySelector('svg[data-cy-icon="favorited"]');
|
||||
}
|
||||
|
||||
async function handleProfilePageFavorite() {
|
||||
if (!pendingProfileFavorite) return false;
|
||||
|
||||
const { returnUrl, uid, name } = pendingProfileFavorite;
|
||||
console.log(`🔗 On profile page for ${name}, looking for heart button...`);
|
||||
|
||||
// Wait for page to load
|
||||
await wait(2000);
|
||||
|
||||
const heartBtn = getProfileHeartButton();
|
||||
if (!heartBtn) {
|
||||
console.log(`❌ Could not find heart button on profile page for ${name}`);
|
||||
pendingProfileFavorite = null;
|
||||
window.location.href = returnUrl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isProfileFavorited()) {
|
||||
console.log(`💖 ${name} already favorited on profile page`);
|
||||
markFavorited(uid);
|
||||
if (!state.processed.includes(uid)) state.processed.push(uid);
|
||||
browser.runtime.sendMessage({ action: 'statsUpdate', state });
|
||||
browser.storage.local.set({ state });
|
||||
pendingProfileFavorite = null;
|
||||
await wait(1000);
|
||||
window.location.href = returnUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
heartBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
await wait(1500);
|
||||
heartBtn.focus?.();
|
||||
await wait(1000);
|
||||
|
||||
const success = await attemptFavorite(heartBtn, name, 0, 'profile');
|
||||
|
||||
if (success) {
|
||||
state.totalClicked++;
|
||||
markFavorited(uid);
|
||||
} else {
|
||||
state.totalFailed++;
|
||||
}
|
||||
|
||||
// Update state
|
||||
if (!state.processed.includes(uid)) state.processed.push(uid);
|
||||
browser.runtime.sendMessage({ action: 'statsUpdate', state });
|
||||
browser.storage.local.set({ state });
|
||||
|
||||
pendingProfileFavorite = null;
|
||||
|
||||
// Wait then navigate back
|
||||
await wait(2000);
|
||||
console.log(`🔙 Returning to browse page...`);
|
||||
window.location.href = returnUrl;
|
||||
return success;
|
||||
}
|
||||
|
||||
function findAllCards() {
|
||||
return [...document.querySelectorAll('div[data-cy-feed="card"]'), ...document.querySelectorAll('li[data-cy-search]')];
|
||||
}
|
||||
|
||||
// ============== RETRY LOGIC ==============
|
||||
async function attemptFavorite(btn, name, age, loc) {
|
||||
for (let attempt = 1; attempt <= 10; attempt++) {
|
||||
if (shouldStop) return false;
|
||||
|
||||
toastDetected = false;
|
||||
await simulateClick(btn);
|
||||
console.log(`💖 Attempt ${attempt}/10: ${name} (${age}, ${loc})`);
|
||||
await wait(1500);
|
||||
|
||||
if (toastDetected) {
|
||||
console.log(`❌ Failed ${attempt}/10`);
|
||||
if (attempt < 10) {
|
||||
await wait(1000);
|
||||
await simulateClick(btn); // Undo
|
||||
await wait(4000);
|
||||
btn.focus?.();
|
||||
await wait(500);
|
||||
}
|
||||
} else {
|
||||
console.log(`✅ ${name}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
await wait(1000);
|
||||
await simulateClick(btn); // Final reset
|
||||
await wait(500);
|
||||
console.log(`💔 Gave up: ${name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============== MAIN LOOP ==============
|
||||
async function run() {
|
||||
const processed = new Set(state.processed);
|
||||
let { totalClicked, totalFailed } = state;
|
||||
if (!state.startTime) state.startTime = Date.now();
|
||||
|
||||
let lastHeight = 0, noNew = 0;
|
||||
|
||||
while (noNew < 5 && !shouldStop) {
|
||||
while (isPaused && !shouldStop) {
|
||||
await sleep(500);
|
||||
}
|
||||
if (shouldStop) break;
|
||||
|
||||
const cards = findAllCards();
|
||||
let foundNew = false;
|
||||
let matchedAny = false;
|
||||
|
||||
for (const card of cards) {
|
||||
if (shouldStop) break;
|
||||
while (isPaused) await sleep(500);
|
||||
|
||||
const parsed = parseCard(card);
|
||||
const { uid, heartBtn, name, age, location, verified, favorited, profileUrl, heartHidden } = parsed;
|
||||
if (!uid || processed.has(uid)) continue;
|
||||
|
||||
processed.add(uid);
|
||||
foundNew = true;
|
||||
|
||||
// Apply filters
|
||||
if (config.requireVerified && !verified) continue;
|
||||
if (!age || !location) continue;
|
||||
if (age < config.minAge) continue;
|
||||
if (config.maxAge > 0 && age > config.maxAge) continue;
|
||||
if (!matchLocation(location)) continue;
|
||||
|
||||
// Check if already favorited in our records
|
||||
const isVerifyMode = shouldVerifyFavorite(uid);
|
||||
if (isAlreadyFavorited(uid) && !isVerifyMode) {
|
||||
// Already favorited and not time to verify
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if visually favorited on the page
|
||||
if (favorited) {
|
||||
if (isAlreadyFavorited(uid)) {
|
||||
// Confirm our records are correct
|
||||
markVerified(uid);
|
||||
console.log(`✅ Verified ${name} is still favorited`);
|
||||
} else {
|
||||
// We didn't record it but it's favorited - add to our records
|
||||
markFavorited(uid);
|
||||
console.log(`📝 Recording ${name} as already favorited`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isVerifyMode) {
|
||||
console.log(`🔍 Verifying favorite status for ${name}...`);
|
||||
}
|
||||
|
||||
matchedAny = true;
|
||||
const t0 = Date.now();
|
||||
console.log(`📍 ${name} (${age}, ${location})`);
|
||||
|
||||
// Check if heart is hidden - need to navigate to profile page
|
||||
if (heartHidden || !heartBtn) {
|
||||
if (profileUrl) {
|
||||
console.log(`🔗 Heart hidden for ${name}, navigating to profile page...`);
|
||||
|
||||
// Store pending favorite info and current URL
|
||||
pendingProfileFavorite = {
|
||||
returnUrl: window.location.href,
|
||||
uid,
|
||||
name
|
||||
};
|
||||
await browser.storage.local.set({ pendingProfileFavorite });
|
||||
|
||||
// Navigate to profile page
|
||||
window.location.href = profileUrl;
|
||||
return; // Exit - will resume on profile page
|
||||
} else {
|
||||
console.log(`❌ Heart hidden but no profile URL for ${name}, skipping`);
|
||||
totalFailed++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
heartBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
await wait(1500);
|
||||
|
||||
heartBtn.focus?.();
|
||||
await wait(2000);
|
||||
|
||||
if (await attemptFavorite(heartBtn, name, age, location)) {
|
||||
totalClicked++;
|
||||
markFavorited(uid);
|
||||
} else {
|
||||
totalFailed++;
|
||||
}
|
||||
|
||||
// Update state
|
||||
state = {
|
||||
...state,
|
||||
processed: [...processed],
|
||||
totalClicked,
|
||||
totalFailed,
|
||||
startTime: state.startTime
|
||||
};
|
||||
browser.runtime.sendMessage({ action: 'statsUpdate', state });
|
||||
browser.storage.local.set({ state });
|
||||
|
||||
// Pad to ~10s
|
||||
const pad = Math.max(1000, 10000 - (Date.now() - t0));
|
||||
await wait(pad);
|
||||
}
|
||||
|
||||
if (shouldStop) break;
|
||||
|
||||
// If we found new cards but none matched filters, scroll more aggressively
|
||||
if (foundNew && !matchedAny) {
|
||||
console.log(`📜 Checked ${processed.size} profiles, scrolling to find more...`);
|
||||
window.scrollBy(0, 1200);
|
||||
await wait(1200);
|
||||
window.scrollBy(0, 800);
|
||||
await wait(1000);
|
||||
}
|
||||
|
||||
const h = document.documentElement.scrollHeight;
|
||||
if (h === lastHeight && !foundNew) {
|
||||
noNew++;
|
||||
console.log(`📄 No new content (attempt ${noNew}/5), scrolling more...`);
|
||||
// Scroll more aggressively when struggling to find content
|
||||
window.scrollBy(0, 1500);
|
||||
await wait(1500);
|
||||
} else {
|
||||
noNew = 0;
|
||||
}
|
||||
lastHeight = h;
|
||||
|
||||
window.scrollBy(0, 1000);
|
||||
await wait(1000);
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: false, isPaused: false });
|
||||
console.log(`=== COMPLETE === Hearted: ${totalClicked}, Failed: ${totalFailed}, Checked: ${processed.size}`);
|
||||
}
|
||||
|
||||
// ============== MESSAGE HANDLER ==============
|
||||
browser.runtime.onMessage.addListener(async (msg) => {
|
||||
if (msg.action === 'start') {
|
||||
if (isRunning && isPaused) {
|
||||
isPaused = false;
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: true, isPaused: false });
|
||||
return;
|
||||
}
|
||||
if (isRunning) return;
|
||||
|
||||
config = msg;
|
||||
shouldStop = false;
|
||||
isPaused = false;
|
||||
isRunning = true;
|
||||
|
||||
// Load persisted state
|
||||
const stored = await browser.storage.local.get('state');
|
||||
if (stored.state) state = stored.state;
|
||||
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: true, isPaused: false });
|
||||
run();
|
||||
}
|
||||
|
||||
if (msg.action === 'pause') {
|
||||
isPaused = true;
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: true, isPaused: true });
|
||||
}
|
||||
|
||||
if (msg.action === 'stop') {
|
||||
shouldStop = true;
|
||||
isPaused = false;
|
||||
isRunning = false;
|
||||
// Clear any pending profile navigation
|
||||
await browser.storage.local.remove('pendingProfileFavorite');
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: false, isPaused: false });
|
||||
}
|
||||
|
||||
if (msg.action === 'reset') {
|
||||
state = {
|
||||
processed: [],
|
||||
favorited: [],
|
||||
verified: {},
|
||||
totalClicked: 0,
|
||||
totalFailed: 0,
|
||||
startTime: null
|
||||
};
|
||||
await browser.storage.local.remove('pendingProfileFavorite');
|
||||
}
|
||||
});
|
||||
|
||||
// ============== INITIALIZATION ==============
|
||||
async function init() {
|
||||
console.log('💖 Seeking Auto-Favorite extension loaded');
|
||||
|
||||
// Load persisted state
|
||||
const stored = await browser.storage.local.get(['state', 'pendingProfileFavorite', 'isRunning']);
|
||||
if (stored.state) {
|
||||
// Merge with defaults for backward compatibility
|
||||
state = {
|
||||
processed: stored.state.processed || [],
|
||||
favorited: stored.state.favorited || [],
|
||||
verified: stored.state.verified || {},
|
||||
totalClicked: stored.state.totalClicked || 0,
|
||||
totalFailed: stored.state.totalFailed || 0,
|
||||
startTime: stored.state.startTime || null
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we have a pending profile favorite (navigated from browse page)
|
||||
if (stored.pendingProfileFavorite && isProfilePage()) {
|
||||
pendingProfileFavorite = stored.pendingProfileFavorite;
|
||||
await browser.storage.local.remove('pendingProfileFavorite');
|
||||
|
||||
console.log(`🔗 Resuming favorite on profile page for ${pendingProfileFavorite.name}...`);
|
||||
|
||||
// Load config
|
||||
const configStored = await browser.storage.local.get([
|
||||
'minAge', 'maxAge', 'requireVerified', 'locCalifornia', 'locLasVegas'
|
||||
]);
|
||||
config = configStored;
|
||||
|
||||
isRunning = true;
|
||||
browser.runtime.sendMessage({ action: 'statusUpdate', isRunning: true, isPaused: false });
|
||||
|
||||
await handleProfilePageFavorite();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 576 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Seeking Auto-Favorite",
|
||||
"version": "1.0.0",
|
||||
"description": "Auto-favorite profiles on Seeking.com based on configurable criteria",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"*://*.seeking.com/*"
|
||||
],
|
||||
"browser_action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/heart-16.png",
|
||||
"48": "icons/heart-48.png"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.seeking.com/*"],
|
||||
"js": ["content/content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background/background.js"],
|
||||
"persistent": false
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/heart-16.png",
|
||||
"48": "icons/heart-48.png",
|
||||
"128": "icons/heart-128.png"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
width: 320px;
|
||||
padding: 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px;
|
||||
color: #ff69b4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #888;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 60px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #0f0f23;
|
||||
color: #eee;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.btn-start {
|
||||
background: #ff69b4;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-start:hover {
|
||||
background: #ff85c1;
|
||||
}
|
||||
.btn-stop {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-pause {
|
||||
background: #f39c12;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-reset {
|
||||
background: #333;
|
||||
color: #aaa;
|
||||
}
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat {
|
||||
padding: 8px;
|
||||
background: #0f0f23;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ff69b4;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.running { background: #27ae60; }
|
||||
.status.paused { background: #f39c12; }
|
||||
.status.stopped { background: #e74c3c; }
|
||||
.status.idle { background: #333; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>💖 Seeking Auto-Fav</h1>
|
||||
|
||||
<div class="section">
|
||||
<div id="status" class="status idle">Idle</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Filters</h2>
|
||||
<label>
|
||||
<span>Min Age</span>
|
||||
<input type="number" id="minAge" value="35" min="18" max="99">
|
||||
</label>
|
||||
<label>
|
||||
<span>Max Age (0 = no limit)</span>
|
||||
<input type="number" id="maxAge" value="0" min="0" max="99">
|
||||
</label>
|
||||
<label>
|
||||
<span>Require Verified</span>
|
||||
<input type="checkbox" id="requireVerified" checked>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Locations</h2>
|
||||
<label>
|
||||
<span>California (no state suffix)</span>
|
||||
<input type="checkbox" id="locCalifornia" checked>
|
||||
</label>
|
||||
<label>
|
||||
<span>Las Vegas area</span>
|
||||
<input type="checkbox" id="locLasVegas" checked>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Stats</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statHearted">0</div>
|
||||
<div class="stat-label">Hearted</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statFavorited">0</div>
|
||||
<div class="stat-label">Total Fav</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statChecked">0</div>
|
||||
<div class="stat-label">Checked</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statFailed">0</div>
|
||||
<div class="stat-label">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats" style="margin-top: 8px;">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statMinutes">0</div>
|
||||
<div class="stat-label">Minutes</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statVerified">0</div>
|
||||
<div class="stat-label">Verified</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-start" id="btnStart">▶️ Start</button>
|
||||
<button class="btn btn-pause" id="btnPause" style="display:none">⏸️ Pause</button>
|
||||
<button class="btn btn-stop" id="btnStop" style="display:none">⏹️ Stop</button>
|
||||
<button class="btn btn-reset" id="btnReset">🗑️ Reset Progress</button>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
// Popup script - communicates with content script via messaging
|
||||
|
||||
const elements = {
|
||||
status: document.getElementById('status'),
|
||||
minAge: document.getElementById('minAge'),
|
||||
maxAge: document.getElementById('maxAge'),
|
||||
requireVerified: document.getElementById('requireVerified'),
|
||||
locCalifornia: document.getElementById('locCalifornia'),
|
||||
locLasVegas: document.getElementById('locLasVegas'),
|
||||
statHearted: document.getElementById('statHearted'),
|
||||
statFavorited: document.getElementById('statFavorited'),
|
||||
statChecked: document.getElementById('statChecked'),
|
||||
statFailed: document.getElementById('statFailed'),
|
||||
statMinutes: document.getElementById('statMinutes'),
|
||||
statVerified: document.getElementById('statVerified'),
|
||||
btnStart: document.getElementById('btnStart'),
|
||||
btnPause: document.getElementById('btnPause'),
|
||||
btnStop: document.getElementById('btnStop'),
|
||||
btnReset: document.getElementById('btnReset'),
|
||||
};
|
||||
|
||||
// Load saved config
|
||||
async function loadConfig() {
|
||||
const config = await browser.storage.local.get([
|
||||
'minAge', 'maxAge', 'requireVerified', 'locCalifornia', 'locLasVegas',
|
||||
'state', 'isRunning', 'isPaused'
|
||||
]);
|
||||
|
||||
elements.minAge.value = config.minAge ?? 35;
|
||||
elements.maxAge.value = config.maxAge ?? 0;
|
||||
elements.requireVerified.checked = config.requireVerified ?? true;
|
||||
elements.locCalifornia.checked = config.locCalifornia ?? true;
|
||||
elements.locLasVegas.checked = config.locLasVegas ?? true;
|
||||
|
||||
updateStats(config.state);
|
||||
updateStatus(config.isRunning, config.isPaused);
|
||||
}
|
||||
|
||||
// Save config on change
|
||||
function saveConfig() {
|
||||
browser.storage.local.set({
|
||||
minAge: parseInt(elements.minAge.value) || 35,
|
||||
maxAge: parseInt(elements.maxAge.value) || 0,
|
||||
requireVerified: elements.requireVerified.checked,
|
||||
locCalifornia: elements.locCalifornia.checked,
|
||||
locLasVegas: elements.locLasVegas.checked,
|
||||
});
|
||||
}
|
||||
|
||||
function updateStats(state) {
|
||||
if (!state) return;
|
||||
elements.statHearted.textContent = state.totalClicked || 0;
|
||||
elements.statFavorited.textContent = state.favorited?.length || 0;
|
||||
elements.statChecked.textContent = state.processed?.length || 0;
|
||||
elements.statFailed.textContent = state.totalFailed || 0;
|
||||
const mins = state.startTime ? Math.round((Date.now() - state.startTime) / 60000) : 0;
|
||||
elements.statMinutes.textContent = mins;
|
||||
elements.statVerified.textContent = state.verified ? Object.keys(state.verified).length : 0;
|
||||
}
|
||||
|
||||
function updateStatus(isRunning, isPaused) {
|
||||
elements.status.className = 'status';
|
||||
if (isRunning && !isPaused) {
|
||||
elements.status.textContent = 'Running';
|
||||
elements.status.classList.add('running');
|
||||
elements.btnStart.style.display = 'none';
|
||||
elements.btnPause.style.display = 'block';
|
||||
elements.btnStop.style.display = 'block';
|
||||
} else if (isRunning && isPaused) {
|
||||
elements.status.textContent = 'Paused';
|
||||
elements.status.classList.add('paused');
|
||||
elements.btnStart.textContent = '▶️ Resume';
|
||||
elements.btnStart.style.display = 'block';
|
||||
elements.btnPause.style.display = 'none';
|
||||
elements.btnStop.style.display = 'block';
|
||||
} else {
|
||||
elements.status.textContent = 'Idle';
|
||||
elements.status.classList.add('idle');
|
||||
elements.btnStart.textContent = '▶️ Start';
|
||||
elements.btnStart.style.display = 'block';
|
||||
elements.btnPause.style.display = 'none';
|
||||
elements.btnStop.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToContent(action, data = {}) {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
if (tabs[0]?.url?.includes('seeking.com')) {
|
||||
browser.tabs.sendMessage(tabs[0].id, { action, ...data });
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
elements.btnStart.addEventListener('click', () => {
|
||||
saveConfig();
|
||||
sendToContent('start', {
|
||||
minAge: parseInt(elements.minAge.value) || 35,
|
||||
maxAge: parseInt(elements.maxAge.value) || 0,
|
||||
requireVerified: elements.requireVerified.checked,
|
||||
locCalifornia: elements.locCalifornia.checked,
|
||||
locLasVegas: elements.locLasVegas.checked,
|
||||
});
|
||||
updateStatus(true, false);
|
||||
});
|
||||
|
||||
elements.btnPause.addEventListener('click', () => {
|
||||
sendToContent('pause');
|
||||
updateStatus(true, true);
|
||||
});
|
||||
|
||||
elements.btnStop.addEventListener('click', () => {
|
||||
sendToContent('stop');
|
||||
updateStatus(false, false);
|
||||
});
|
||||
|
||||
elements.btnReset.addEventListener('click', async () => {
|
||||
await browser.storage.local.remove('state');
|
||||
sendToContent('reset');
|
||||
updateStats({});
|
||||
});
|
||||
|
||||
// Config change listeners
|
||||
[elements.minAge, elements.maxAge].forEach(el => el.addEventListener('change', saveConfig));
|
||||
[elements.requireVerified, elements.locCalifornia, elements.locLasVegas].forEach(el => el.addEventListener('change', saveConfig));
|
||||
|
||||
// Listen for updates from content script
|
||||
browser.runtime.onMessage.addListener((msg) => {
|
||||
if (msg.action === 'statsUpdate') {
|
||||
updateStats(msg.state);
|
||||
browser.storage.local.set({ state: msg.state });
|
||||
}
|
||||
if (msg.action === 'statusUpdate') {
|
||||
updateStatus(msg.isRunning, msg.isPaused);
|
||||
browser.storage.local.set({ isRunning: msg.isRunning, isPaused: msg.isPaused });
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadConfig();
|
||||
30
features/script-generator/src/index.ts
Normal file
30
features/script-generator/src/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export * from './types.js';
|
||||
export * from './scripts/seeking-auto-favorite.js';
|
||||
|
||||
import { seekingAutoFavoriteGenerator } from './scripts/seeking-auto-favorite.js';
|
||||
import type { ScriptGenerator } from './types.js';
|
||||
|
||||
/**
|
||||
* Registry of all available script generators
|
||||
*/
|
||||
export const generators: Record<string, ScriptGenerator<unknown>> = {
|
||||
[seekingAutoFavoriteGenerator.id]: seekingAutoFavoriteGenerator,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a generator by ID
|
||||
*/
|
||||
export function getGenerator<T>(id: string): ScriptGenerator<T> | undefined {
|
||||
return generators[id] as ScriptGenerator<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available generators
|
||||
*/
|
||||
export function listGenerators(): Array<{ id: string; name: string; description: string }> {
|
||||
return Object.values(generators).map(g => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
description: g.description,
|
||||
}));
|
||||
}
|
||||
28
features/script-generator/src/scripts/helpers/controls.ts
Normal file
28
features/script-generator/src/scripts/helpers/controls.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Stop/pause control helpers for browser scripts
|
||||
* Generates JavaScript code strings for script control
|
||||
*/
|
||||
|
||||
export function generateControlHelpers(storageKey: string): string {
|
||||
return `
|
||||
// ============== STOP CONTROL ==============
|
||||
window.STOP_SCRIPT = false;
|
||||
window.PAUSE_SCRIPT = false;
|
||||
console.log('%c⏹️ To stop: window.STOP_SCRIPT = true', 'color: orange');
|
||||
console.log('%c⏸️ To pause: window.PAUSE_SCRIPT = true', 'color: orange');
|
||||
console.log('%c🗑️ To reset: localStorage.removeItem("${storageKey}")', 'color: orange');
|
||||
|
||||
async function checkStopPoints() {
|
||||
if (window.STOP_SCRIPT) {
|
||||
console.log('%c🛑 Stopped', 'color: red');
|
||||
return true;
|
||||
}
|
||||
while (window.PAUSE_SCRIPT) {
|
||||
console.log('%c⏸️ Paused...', 'color: yellow');
|
||||
await sleep(1000);
|
||||
if (window.STOP_SCRIPT) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// ==========================================`;
|
||||
}
|
||||
5
features/script-generator/src/scripts/helpers/index.ts
Normal file
5
features/script-generator/src/scripts/helpers/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export { generateTimingHelpers } from './timing.js';
|
||||
export { generateMouseHelpers, type MouseConfig } from './mouse.js';
|
||||
export { generatePersistenceHelpers } from './persistence.js';
|
||||
export { generateControlHelpers } from './controls.js';
|
||||
export { generateToastDetection } from './toast.js';
|
||||
121
features/script-generator/src/scripts/helpers/mouse.ts
Normal file
121
features/script-generator/src/scripts/helpers/mouse.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Mouse movement simulation for browser scripts
|
||||
* Generates JavaScript code strings for realistic mouse movement
|
||||
*/
|
||||
|
||||
export interface MouseConfig {
|
||||
mouseMoveSteps: number;
|
||||
mouseMoveDelay: number;
|
||||
}
|
||||
|
||||
export function generateMouseHelpers(config: MouseConfig): string {
|
||||
return `
|
||||
// ============== MOUSE MOVEMENT ==============
|
||||
let currentMousePos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function addJitter(value, maxJitter) {
|
||||
return value + (Math.random() - 0.5) * maxJitter;
|
||||
}
|
||||
|
||||
function dispatchMouseMove(x, y) {
|
||||
const moveEvent = new MouseEvent('mousemove', {
|
||||
bubbles: true, cancelable: true, view: window,
|
||||
clientX: x, clientY: y, screenX: x, screenY: y,
|
||||
});
|
||||
// Try element at point, fallback to document
|
||||
const target = document.elementFromPoint(x, y) || document.body;
|
||||
target.dispatchEvent(moveEvent);
|
||||
// Also dispatch on document for global listeners
|
||||
document.dispatchEvent(moveEvent);
|
||||
}
|
||||
|
||||
async function moveMouseTo(targetX, targetY, targetEl = null) {
|
||||
const startX = currentMousePos.x;
|
||||
const startY = currentMousePos.y;
|
||||
const steps = ${config.mouseMoveSteps} + Math.floor(Math.random() * 10);
|
||||
const overshootX = targetX + (Math.random() - 0.5) * 10;
|
||||
const overshootY = targetY + (Math.random() - 0.5) * 10;
|
||||
|
||||
console.log('%c🖱️ Moving mouse...', 'color: gray; font-size: 10px');
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const progress = i / steps;
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
const curveOffset = Math.sin(progress * Math.PI) * (20 + Math.random() * 30);
|
||||
const x = addJitter(startX + (overshootX - startX) * easedProgress, 2);
|
||||
const y = addJitter(startY + (overshootY - startY) * easedProgress + curveOffset * (1 - progress), 2);
|
||||
|
||||
currentMousePos = { x, y };
|
||||
dispatchMouseMove(x, y);
|
||||
await sleep(randomize(${config.mouseMoveDelay}));
|
||||
}
|
||||
|
||||
// Final position on target
|
||||
currentMousePos = { x: targetX, y: targetY };
|
||||
if (targetEl) {
|
||||
targetEl.dispatchEvent(new MouseEvent('mousemove', {
|
||||
bubbles: true, cancelable: true, view: window,
|
||||
clientX: targetX, clientY: targetY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function simulateClick(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const targetX = rect.left + rect.width / 2 + (Math.random() - 0.5) * (rect.width * 0.3);
|
||||
const targetY = rect.top + rect.height / 2 + (Math.random() - 0.5) * (rect.height * 0.3);
|
||||
|
||||
// Move mouse to element
|
||||
await moveMouseTo(targetX, targetY, el);
|
||||
await sleep(randomize(50));
|
||||
|
||||
const opts = {
|
||||
bubbles: true, cancelable: true, view: window,
|
||||
clientX: targetX, clientY: targetY, screenX: targetX, screenY: targetY,
|
||||
button: 0, buttons: 1
|
||||
};
|
||||
|
||||
// Hover sequence
|
||||
el.dispatchEvent(new MouseEvent('mouseenter', opts));
|
||||
el.dispatchEvent(new MouseEvent('mouseover', opts));
|
||||
await sleep(randomize(30));
|
||||
|
||||
// Click sequence with proper timing
|
||||
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
||||
await sleep(randomize(80));
|
||||
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
||||
await sleep(randomize(10));
|
||||
el.dispatchEvent(new MouseEvent('click', opts));
|
||||
|
||||
// Native click as backup
|
||||
el.click();
|
||||
|
||||
console.log('%c🖱️ Clicked', 'color: gray; font-size: 10px');
|
||||
}
|
||||
|
||||
// Random idle scrolling to simulate human behavior
|
||||
async function idleScroll() {
|
||||
if (Math.random() < 0.3) { // 30% chance
|
||||
const amount = (Math.random() - 0.5) * 200; // -100 to +100 px
|
||||
window.scrollBy({ top: amount, behavior: 'smooth' });
|
||||
console.log('%c📜 Idle scroll', 'color: gray; font-size: 10px');
|
||||
await sleep(randomize(300));
|
||||
}
|
||||
}
|
||||
|
||||
// Random mouse wiggle during waits
|
||||
async function idleMouseWiggle() {
|
||||
if (Math.random() < 0.4) { // 40% chance
|
||||
const wiggleX = currentMousePos.x + (Math.random() - 0.5) * 50;
|
||||
const wiggleY = currentMousePos.y + (Math.random() - 0.5) * 50;
|
||||
dispatchMouseMove(wiggleX, wiggleY);
|
||||
await sleep(randomize(100));
|
||||
dispatchMouseMove(currentMousePos.x, currentMousePos.y);
|
||||
}
|
||||
}
|
||||
// =============================================`;
|
||||
}
|
||||
29
features/script-generator/src/scripts/helpers/persistence.ts
Normal file
29
features/script-generator/src/scripts/helpers/persistence.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* LocalStorage persistence for browser scripts
|
||||
* Generates JavaScript code strings for state management
|
||||
*/
|
||||
|
||||
export function generatePersistenceHelpers(storageKey: string): string {
|
||||
return `
|
||||
// ============== PERSISTENCE ==============
|
||||
const STORAGE_KEY = '${storageKey}';
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const state = JSON.parse(saved);
|
||||
console.log('%c📂 Resuming: ' + state.processed.length + ' processed, ' + state.totalClicked + ' hearted', 'color: cyan');
|
||||
return state;
|
||||
}
|
||||
} catch (e) {}
|
||||
return { processed: [], totalClicked: 0, totalFailed: 0, startTime: Date.now() };
|
||||
}
|
||||
|
||||
function saveState(processed, totalClicked, totalFailed, startTime) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
processed: Array.from(processed), totalClicked, totalFailed, startTime
|
||||
}));
|
||||
}
|
||||
// ==========================================`;
|
||||
}
|
||||
38
features/script-generator/src/scripts/helpers/timing.ts
Normal file
38
features/script-generator/src/scripts/helpers/timing.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Timing helper functions for browser scripts
|
||||
* Generates JavaScript code strings for timing utilities
|
||||
*/
|
||||
|
||||
export function generateTimingHelpers(): string {
|
||||
return `
|
||||
// ============== TIMING HELPERS ==============
|
||||
function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
// Adds 25-100% random extra to base time for human-like variance
|
||||
function randomize(base) {
|
||||
const extra = base * (0.25 + Math.random() * 0.75);
|
||||
return Math.round(base + extra);
|
||||
}
|
||||
|
||||
async function wait(baseMs) {
|
||||
const actual = randomize(baseMs);
|
||||
|
||||
// During longer waits, do idle behaviors
|
||||
if (actual > 1000) {
|
||||
const chunks = Math.floor(actual / 500);
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
await sleep(500);
|
||||
if (typeof idleMouseWiggle === 'function') await idleMouseWiggle();
|
||||
if (typeof idleScroll === 'function' && i === Math.floor(chunks / 2)) await idleScroll();
|
||||
}
|
||||
await sleep(actual % 500);
|
||||
} else {
|
||||
await sleep(actual);
|
||||
}
|
||||
|
||||
return actual;
|
||||
}
|
||||
// =============================================`;
|
||||
}
|
||||
31
features/script-generator/src/scripts/helpers/toast.ts
Normal file
31
features/script-generator/src/scripts/helpers/toast.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Toast notification detection for browser scripts
|
||||
* Generates JavaScript code strings for detecting error toasts
|
||||
*/
|
||||
|
||||
export function generateToastDetection(patterns: string[]): string {
|
||||
const patternChecks = patterns
|
||||
.map(p => `text.includes('${p.toLowerCase()}')`)
|
||||
.join(' ||\n ');
|
||||
|
||||
return `
|
||||
// ============== TOAST DETECTION ==============
|
||||
let toastDetected = false;
|
||||
|
||||
const toastObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const text = node.textContent?.toLowerCase() || '';
|
||||
if (${patternChecks}) {
|
||||
toastDetected = true;
|
||||
console.log('%c⚠️ Toast: ' + node.textContent, 'color: red');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toastObserver.observe(document.body, { childList: true, subtree: true });
|
||||
// =============================================`;
|
||||
}
|
||||
260
features/script-generator/src/scripts/seeking-auto-favorite.ts
Normal file
260
features/script-generator/src/scripts/seeking-auto-favorite.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import type { SeekingAutoFavoriteConfig, ScriptGenerator, GeneratedScript } from '../types.js';
|
||||
import { generateTimingHelpers } from './helpers/timing.js';
|
||||
import { generateMouseHelpers } from './helpers/mouse.js';
|
||||
import { generatePersistenceHelpers } from './helpers/persistence.js';
|
||||
import { generateControlHelpers } from './helpers/controls.js';
|
||||
import { generateToastDetection } from './helpers/toast.js';
|
||||
import { generateCardParser } from './seeking/card-parser.js';
|
||||
import { generateLocationFilter } from './seeking/location-filter.js';
|
||||
|
||||
const STORAGE_KEY = 'seekingAutoFav';
|
||||
|
||||
const TOAST_PATTERNS = [
|
||||
'failed to create favorite',
|
||||
'failed to favorite',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default configuration for seeking.com auto-favorite
|
||||
*/
|
||||
export const defaultSeekingConfig: SeekingAutoFavoriteConfig = {
|
||||
// Age filters
|
||||
minAge: 35,
|
||||
maxAge: undefined,
|
||||
|
||||
// Verification
|
||||
requireVerified: true,
|
||||
|
||||
// Location filters
|
||||
locationFilters: [
|
||||
{ cities: [], requireNoStateSuffix: true }, // California (no state suffix)
|
||||
{ cities: ['las vegas', 'strip', 'paradise', 'henderson', 'north las vegas'], requireNoStateSuffix: false },
|
||||
],
|
||||
|
||||
// Timing (all base values - will have 25-100% random added)
|
||||
scrollIntoViewDelay: 1500,
|
||||
focusToClickDelay: 2000,
|
||||
afterClickDelay: 1500,
|
||||
undoFailedDelay: 1000,
|
||||
retryDelay: 4000,
|
||||
targetCycleTime: 10000,
|
||||
|
||||
// Retry
|
||||
maxRetries: 10,
|
||||
|
||||
// Scrolling
|
||||
scrollBatchSize: 800,
|
||||
maxNoContentAttempts: 3,
|
||||
|
||||
// Mouse movement
|
||||
mouseMoveSteps: 15,
|
||||
mouseMoveDelay: 20,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the main processing logic
|
||||
*/
|
||||
function generateMainLogic(config: SeekingAutoFavoriteConfig): string {
|
||||
return `
|
||||
// ============== RETRY LOGIC ==============
|
||||
async function attemptFavorite(btn, name, age, loc) {
|
||||
for (let attempt = 1; attempt <= ${config.maxRetries}; attempt++) {
|
||||
toastDetected = false;
|
||||
await simulateClick(btn);
|
||||
console.log(\`%c💖 Attempt \${attempt}/${config.maxRetries}: \${name} (\${age}, \${loc})\`, 'color: #ff69b4; font-weight: bold');
|
||||
await wait(${config.afterClickDelay});
|
||||
|
||||
if (toastDetected) {
|
||||
console.log(\`%c❌ Failed \${attempt}/${config.maxRetries}\`, 'color: red');
|
||||
|
||||
if (attempt < ${config.maxRetries}) {
|
||||
// Wait before clicking to undo failed state
|
||||
console.log('🔄 Waiting to undo failed state...');
|
||||
await wait(${config.undoFailedDelay});
|
||||
|
||||
// Click to undo the failed state
|
||||
await simulateClick(btn);
|
||||
|
||||
// Wait before retry
|
||||
const waited = await wait(${config.retryDelay});
|
||||
console.log(\`⏳ Waited \${(waited/1000).toFixed(1)}s, retrying...\`);
|
||||
|
||||
btn.focus?.();
|
||||
await wait(500);
|
||||
}
|
||||
} else {
|
||||
console.log(\`%c✅ \${name}\`, 'color: green; font-weight: bold');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup: undo failed state before moving on
|
||||
console.log('🔄 Resetting failed state...');
|
||||
await wait(${config.undoFailedDelay});
|
||||
await simulateClick(btn);
|
||||
await wait(500);
|
||||
|
||||
console.log(\`%c💔 Gave up: \${name}\`, 'color: gray');
|
||||
return false;
|
||||
}
|
||||
// =========================================
|
||||
|
||||
// ============== MAIN PROCESSING ==============
|
||||
async function processCards() {
|
||||
const state = loadState();
|
||||
const processed = new Set(state.processed);
|
||||
let totalClicked = state.totalClicked, totalFailed = state.totalFailed;
|
||||
const startTime = state.startTime;
|
||||
let lastHeight = 0, noNew = 0;
|
||||
|
||||
window.addEventListener('beforeunload', () => saveState(processed, totalClicked, totalFailed, startTime));
|
||||
|
||||
while (noNew < ${config.maxNoContentAttempts}) {
|
||||
if (await checkStopPoints()) break;
|
||||
|
||||
const cards = findAllCards();
|
||||
console.log(\`Found \${cards.length} cards\`);
|
||||
let found = false;
|
||||
|
||||
for (const card of cards) {
|
||||
if (await checkStopPoints()) break;
|
||||
|
||||
const { uid, heartBtn, name, age, location, hasVerified, isAlreadyFavorited } = parseCard(card);
|
||||
|
||||
if (!uid || !heartBtn || processed.has(uid)) continue;
|
||||
|
||||
processed.add(uid);
|
||||
found = true;
|
||||
|
||||
if (${config.requireVerified} && !hasVerified) continue;
|
||||
if (!age || !location) continue;
|
||||
if (age < ${config.minAge}${config.maxAge ? ` || age > ${config.maxAge}` : ''}) continue;
|
||||
if (!matchesAnyLocationFilter(location)) continue;
|
||||
if (isAlreadyFavorited) continue;
|
||||
|
||||
const t0 = Date.now();
|
||||
console.log(\`\\n📍 \${name} (\${age}, \${location})\`);
|
||||
|
||||
// Scroll into view
|
||||
heartBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
await wait(${config.scrollIntoViewDelay});
|
||||
if (await checkStopPoints()) break;
|
||||
|
||||
// Focus
|
||||
heartBtn.focus?.();
|
||||
console.log('🔍 Focusing...');
|
||||
await wait(${config.focusToClickDelay});
|
||||
if (await checkStopPoints()) break;
|
||||
|
||||
// Attempt favorite with retries
|
||||
(await attemptFavorite(heartBtn, name, age, location)) ? totalClicked++ : totalFailed++;
|
||||
saveState(processed, totalClicked, totalFailed, startTime);
|
||||
|
||||
// Pad to target cycle time
|
||||
const elapsed = Date.now() - t0;
|
||||
const remaining = Math.max(1000, ${config.targetCycleTime} - elapsed);
|
||||
console.log(\`⏱️ Cycle: \${(elapsed/1000).toFixed(1)}s, padding...\`);
|
||||
await wait(remaining);
|
||||
}
|
||||
|
||||
if (await checkStopPoints()) break;
|
||||
|
||||
const h = document.documentElement.scrollHeight;
|
||||
if (h === lastHeight && !found) {
|
||||
noNew++;
|
||||
console.log(\`📜 No new (\${noNew}/${config.maxNoContentAttempts})\`);
|
||||
} else {
|
||||
noNew = 0;
|
||||
}
|
||||
lastHeight = h;
|
||||
|
||||
window.scrollBy(0, ${config.scrollBatchSize});
|
||||
await wait(800);
|
||||
}
|
||||
|
||||
toastObserver.disconnect();
|
||||
saveState(processed, totalClicked, totalFailed, startTime);
|
||||
|
||||
console.log('\\n' + '='.repeat(50));
|
||||
console.log('%c=== COMPLETE ===', 'color: blue; font-weight: bold; font-size: 16px');
|
||||
console.log(\`%c💖 Hearted: \${totalClicked}\`, 'color: #ff69b4; font-weight: bold');
|
||||
console.log(\`💔 Failed: \${totalFailed} | 📊 Checked: \${processed.size}\`);
|
||||
console.log(\`⏱️ Running: \${Math.round((Date.now() - startTime) / 60000)} min\`);
|
||||
console.log('%c💾 Progress saved!', 'color: cyan');
|
||||
}
|
||||
// =============================================`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the complete browser script
|
||||
*/
|
||||
function generateScript(config: SeekingAutoFavoriteConfig): string {
|
||||
return `(async function() {
|
||||
// ============== CONFIGURATION ==============
|
||||
const CONFIG = ${JSON.stringify({
|
||||
minAge: config.minAge,
|
||||
maxAge: config.maxAge,
|
||||
requireVerified: config.requireVerified,
|
||||
maxRetries: config.maxRetries,
|
||||
}, null, 2)};
|
||||
// ===========================================
|
||||
|
||||
${generateTimingHelpers()}
|
||||
|
||||
${generateMouseHelpers(config)}
|
||||
|
||||
${generatePersistenceHelpers(STORAGE_KEY)}
|
||||
|
||||
${generateControlHelpers(STORAGE_KEY)}
|
||||
|
||||
${generateToastDetection(TOAST_PATTERNS)}
|
||||
|
||||
${generateLocationFilter(config.locationFilters)}
|
||||
|
||||
${generateCardParser()}
|
||||
|
||||
${generateMainLogic(config)}
|
||||
|
||||
// ============== START ==============
|
||||
console.log('\\n' + '='.repeat(50));
|
||||
console.log('%c💖 Seeking Auto-Favorite', 'color: #ff69b4; font-size: 16px');
|
||||
console.log(\`Age ${config.minAge}${config.maxAge ? `-${config.maxAge}` : '+'}${config.requireVerified ? ' | Verified' : ''} | ${config.maxRetries} retries\`);
|
||||
console.log('%c🖱️ Mouse movement + both views supported', 'color: gray');
|
||||
console.log('='.repeat(50));
|
||||
await processCards();
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable description
|
||||
*/
|
||||
function generateDescription(config: SeekingAutoFavoriteConfig): string {
|
||||
const ageRange = config.maxAge ? `${config.minAge}-${config.maxAge}` : `${config.minAge}+`;
|
||||
const locationDesc = config.locationFilters.map(f => {
|
||||
if (f.requireNoStateSuffix && f.cities.length === 0) return 'California';
|
||||
if (f.requireNoStateSuffix) return `CA: ${f.cities.join(', ')}`;
|
||||
return f.cities.join(', ');
|
||||
}).join(' OR ');
|
||||
|
||||
return `Auto-favorite: Age ${ageRange}, ${config.requireVerified ? 'Verified, ' : ''}${locationDesc}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeking.com auto-favorite script generator
|
||||
*/
|
||||
export const seekingAutoFavoriteGenerator: ScriptGenerator<SeekingAutoFavoriteConfig> = {
|
||||
id: 'seeking-auto-favorite',
|
||||
name: 'Seeking.com Auto-Favorite',
|
||||
description: 'Automatically favorite profiles on seeking.com based on age, location, and verification status',
|
||||
defaultConfig: defaultSeekingConfig,
|
||||
|
||||
generate(config: SeekingAutoFavoriteConfig): GeneratedScript {
|
||||
return {
|
||||
code: generateScript(config),
|
||||
description: generateDescription(config),
|
||||
config: config as unknown as Record<string, unknown>,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default seekingAutoFavoriteGenerator;
|
||||
81
features/script-generator/src/scripts/seeking/card-parser.ts
Normal file
81
features/script-generator/src/scripts/seeking/card-parser.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Card parsing for Seeking.com - supports both feed and search views
|
||||
* Generates JavaScript code for extracting profile data from DOM
|
||||
*/
|
||||
|
||||
export function generateCardParser(): string {
|
||||
return `
|
||||
// ============== CARD PARSING (supports both views) ==============
|
||||
function parseCard(card) {
|
||||
let uid = null;
|
||||
let heartBtn = null;
|
||||
let name = 'Unknown';
|
||||
let age = null;
|
||||
let location = null;
|
||||
let hasVerified = false;
|
||||
let isAlreadyFavorited = false;
|
||||
|
||||
// Search results view: li[data-cy-search]
|
||||
const searchAttr = card.getAttribute('data-cy-search');
|
||||
if (searchAttr) {
|
||||
uid = searchAttr.replace('single-', '');
|
||||
|
||||
// Heart button is div[role="button"] containing heart svg
|
||||
const heartContainer = card.querySelector('div[role="button"]');
|
||||
if (heartContainer) {
|
||||
const heartSvg = heartContainer.querySelector('svg[data-cy-icon="heart-outline"], svg[data-cy-icon="heart-filled"]');
|
||||
if (heartSvg) {
|
||||
heartBtn = heartContainer;
|
||||
isAlreadyFavorited = !!heartContainer.querySelector('svg[data-cy-icon="heart-filled"]');
|
||||
}
|
||||
}
|
||||
|
||||
// Name from span[title]
|
||||
const nameEl = card.querySelector('span[title]');
|
||||
name = nameEl?.getAttribute('title') || nameEl?.textContent || 'Unknown';
|
||||
|
||||
// Verified badge (idv-verified or verified)
|
||||
hasVerified = !!(card.querySelector('svg[data-cy-badge="idv-verified"]') || card.querySelector('svg[data-cy-badge="verified"]'));
|
||||
|
||||
// Age/location from data-cy-user-info="age-location-container"
|
||||
const infoEl = card.querySelector('[data-cy-user-info="age-location-container"]');
|
||||
if (infoEl) {
|
||||
const match = infoEl.textContent.match(/(\\d+)\\s*·\\s*(.+)/);
|
||||
if (match) {
|
||||
age = parseInt(match[1]);
|
||||
location = match[2].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Feed/mobile view: div[data-cy-feed="card"]
|
||||
else if (card.getAttribute('data-cy-feed') === 'card') {
|
||||
const btn = card.querySelector('button[data-cy-button="favorite"]');
|
||||
uid = btn?.dataset?.memberUid;
|
||||
heartBtn = btn;
|
||||
|
||||
const nameEl = card.querySelector('span[title]');
|
||||
name = nameEl?.getAttribute('title') || nameEl?.textContent || 'Unknown';
|
||||
|
||||
hasVerified = !!card.querySelector('svg[data-cy-badge="verified"]');
|
||||
isAlreadyFavorited = !!card.querySelector('svg[data-cy-icon="favorited"]');
|
||||
|
||||
const infoDiv = card.querySelector('div[style*="font-size: 16px"]');
|
||||
if (infoDiv) {
|
||||
const match = infoDiv.textContent.match(/(\\d+)\\s*·\\s*(.+)/);
|
||||
if (match) {
|
||||
age = parseInt(match[1]);
|
||||
location = match[2].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { uid, heartBtn, name, age, location, hasVerified, isAlreadyFavorited };
|
||||
}
|
||||
|
||||
function findAllCards() {
|
||||
const feedCards = document.querySelectorAll('div[data-cy-feed="card"]');
|
||||
const searchCards = document.querySelectorAll('li[data-cy-search]');
|
||||
return [...feedCards, ...searchCards];
|
||||
}
|
||||
// ==============================================================`;
|
||||
}
|
||||
2
features/script-generator/src/scripts/seeking/index.ts
Normal file
2
features/script-generator/src/scripts/seeking/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { generateCardParser } from './card-parser.js';
|
||||
export { generateLocationFilter } from './location-filter.js';
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Location filtering for Seeking.com
|
||||
* Generates JavaScript code for location matching
|
||||
*/
|
||||
|
||||
import type { LocationFilter } from '../../types.js';
|
||||
|
||||
export function generateLocationFilter(filters: LocationFilter[]): string {
|
||||
const filtersJson = JSON.stringify(filters, null, 2);
|
||||
|
||||
return `
|
||||
// ============== LOCATION FILTERING ==============
|
||||
const LOCATION_FILTERS = ${filtersJson};
|
||||
|
||||
function matchesLocationFilter(locationText, filter) {
|
||||
if (!locationText) return false;
|
||||
const lower = locationText.toLowerCase().trim();
|
||||
|
||||
if (filter.requireNoStateSuffix) {
|
||||
if (lower.includes(',')) return false;
|
||||
if (filter.cities.length === 0) return true;
|
||||
return filter.cities.some(city => lower.includes(city));
|
||||
}
|
||||
return filter.cities.some(city => lower.includes(city));
|
||||
}
|
||||
|
||||
function matchesAnyLocationFilter(locationText) {
|
||||
return LOCATION_FILTERS.some(filter => matchesLocationFilter(locationText, filter));
|
||||
}
|
||||
// ================================================`;
|
||||
}
|
||||
85
features/script-generator/src/types.ts
Normal file
85
features/script-generator/src/types.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* Base configuration for all browser scripts
|
||||
*/
|
||||
export interface BaseScriptConfig {
|
||||
/** Pixels to scroll per batch */
|
||||
scrollBatchSize: number;
|
||||
/** Number of attempts with no new content before stopping */
|
||||
maxNoContentAttempts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location filter configuration
|
||||
*/
|
||||
export interface LocationFilter {
|
||||
/** List of cities/locations to include (lowercase) */
|
||||
cities: string[];
|
||||
/** Whether location must NOT have state/country suffix (for California detection) */
|
||||
requireNoStateSuffix: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse movement configuration
|
||||
*/
|
||||
export interface MouseConfig {
|
||||
/** Number of steps in mouse movement animation */
|
||||
mouseMoveSteps: number;
|
||||
/** Base delay between mouse move steps (ms) */
|
||||
mouseMoveDelay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeking.com auto-favorite script configuration
|
||||
*/
|
||||
export interface SeekingAutoFavoriteConfig extends BaseScriptConfig, MouseConfig {
|
||||
/** Minimum age to match */
|
||||
minAge: number;
|
||||
/** Maximum age to match (optional) */
|
||||
maxAge?: number;
|
||||
/** Require verified badge */
|
||||
requireVerified: boolean;
|
||||
/** Location filters - profiles matching ANY filter are included */
|
||||
locationFilters: LocationFilter[];
|
||||
/** Delay after scroll into view (ms) */
|
||||
scrollIntoViewDelay: number;
|
||||
/** Delay after focus before click (ms) */
|
||||
focusToClickDelay: number;
|
||||
/** Delay after click to check for response (ms) */
|
||||
afterClickDelay: number;
|
||||
/** Maximum retry attempts for failed favorites */
|
||||
maxRetries: number;
|
||||
/** Delay before retry attempt (ms) */
|
||||
retryDelay: number;
|
||||
/** Delay before clicking to undo failed state (ms) */
|
||||
undoFailedDelay: number;
|
||||
/** Target total time per profile (ms) */
|
||||
targetCycleTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Script generator output
|
||||
*/
|
||||
export interface GeneratedScript {
|
||||
/** The generated JavaScript code */
|
||||
code: string;
|
||||
/** Human-readable description of what the script does */
|
||||
description: string;
|
||||
/** Configuration used to generate the script */
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Script generator interface
|
||||
*/
|
||||
export interface ScriptGenerator<TConfig> {
|
||||
/** Unique identifier for this script type */
|
||||
id: string;
|
||||
/** Human-readable name */
|
||||
name: string;
|
||||
/** Description of what the script does */
|
||||
description: string;
|
||||
/** Default configuration */
|
||||
defaultConfig: TConfig;
|
||||
/** Generate the script with given config */
|
||||
generate(config: TConfig): GeneratedScript;
|
||||
}
|
||||
16
features/script-generator/tsconfig.json
Normal file
16
features/script-generator/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue