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:
Quinn Ftw 2025-12-28 16:10:52 -08:00
parent 94da62eea6
commit 107c8554d6
23 changed files with 1867 additions and 0 deletions

View 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"
}
}

View 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);

View file

@ -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);
});

View file

@ -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

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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();

View 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,
}));
}

View 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;
}
// ==========================================`;
}

View 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';

View 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);
}
}
// =============================================`;
}

View 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
}));
}
// ==========================================`;
}

View 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;
}
// =============================================`;
}

View 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 });
// =============================================`;
}

View 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;

View 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];
}
// ==============================================================`;
}

View file

@ -0,0 +1,2 @@
export { generateCardParser } from './card-parser.js';
export { generateLocationFilter } from './location-filter.js';

View file

@ -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));
}
// ================================================`;
}

View 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;
}

View 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"]
}