Search for female users in the New England area on FetLife
Size
15.8 KB
Version
1.0.1
Created
Nov 23, 2025
Updated
3 months ago
1// ==UserScript==
2// @name FetLife New England Female User Search
3// @description Search for female users in the New England area on FetLife
4// @version 1.0.1
5// @match https://*.fetlife.com/*
6// @icon https://fetlife.com/favicons/icon.svg
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.xmlhttpRequest
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 // New England states
15 const NEW_ENGLAND_STATES = ['Connecticut', 'Maine', 'Massachusetts', 'New Hampshire', 'Rhode Island', 'Vermont', 'CT', 'ME', 'MA', 'NH', 'RI', 'VT'];
16
17 // Configuration
18 const CONFIG = {
19 searchDelay: 2000, // Delay between profile checks to avoid rate limiting
20 maxProfiles: 100, // Maximum number of profiles to check
21 startUserId: 1, // Starting user ID
22 };
23
24 // Debounce function
25 function debounce(func, wait) {
26 let timeout;
27 return function executedFunction(...args) {
28 const later = () => {
29 clearTimeout(timeout);
30 func(...args);
31 };
32 clearTimeout(timeout);
33 timeout = setTimeout(later, wait);
34 };
35 }
36
37 // Check if location contains New England state
38 function isNewEnglandLocation(location) {
39 if (!location) return false;
40 const locationUpper = location.toUpperCase();
41 return NEW_ENGLAND_STATES.some(state => locationUpper.includes(state.toUpperCase()));
42 }
43
44 // Fetch user profile data
45 async function fetchUserProfile(userId) {
46 return new Promise((resolve, reject) => {
47 GM.xmlhttpRequest({
48 method: 'GET',
49 url: `https://fetlife.com/users/${userId}`,
50 onload: function(response) {
51 if (response.status === 200) {
52 resolve(response.responseText);
53 } else if (response.status === 404) {
54 resolve(null); // User doesn't exist
55 } else {
56 reject(new Error(`HTTP ${response.status}`));
57 }
58 },
59 onerror: function(error) {
60 reject(error);
61 }
62 });
63 });
64 }
65
66 // Parse user profile from HTML
67 function parseUserProfile(html, userId) {
68 const parser = new DOMParser();
69 const doc = parser.parseFromString(html, 'text/html');
70
71 // Extract user information
72 const nickname = doc.querySelector('h2.text-2xl')?.textContent.trim() || 'Unknown';
73
74 // Find gender/sex information
75 let gender = null;
76 const infoItems = doc.querySelectorAll('em.text-gray-400');
77 infoItems.forEach(item => {
78 const text = item.textContent.trim();
79 if (text.includes('♀') || text.toLowerCase().includes('female') || text.toLowerCase().includes('woman')) {
80 gender = 'Female';
81 }
82 });
83
84 // Find location
85 let location = null;
86 const locationElement = doc.querySelector('a[href*="/countries/"]');
87 if (locationElement) {
88 location = locationElement.textContent.trim();
89 }
90
91 // Get age
92 let age = null;
93 const ageMatch = html.match(/(\d+)\s*years?\s*old/i);
94 if (ageMatch) {
95 age = parseInt(ageMatch[1]);
96 }
97
98 // Get profile URL
99 const profileUrl = `https://fetlife.com/users/${userId}`;
100
101 // Get avatar
102 let avatarUrl = null;
103 const avatarImg = doc.querySelector('img[alt*="avatar"]');
104 if (avatarImg) {
105 avatarUrl = avatarImg.src;
106 }
107
108 return {
109 userId,
110 nickname,
111 gender,
112 location,
113 age,
114 profileUrl,
115 avatarUrl
116 };
117 }
118
119 // Search for users
120 async function searchUsers(onProgress, onComplete) {
121 const results = [];
122 let checkedCount = 0;
123 let currentUserId = await GM.getValue('lastSearchedUserId', CONFIG.startUserId);
124
125 console.log(`Starting search from user ID: ${currentUserId}`);
126
127 for (let i = 0; i < CONFIG.maxProfiles; i++) {
128 try {
129 onProgress(`Checking profile ${checkedCount + 1}/${CONFIG.maxProfiles}...`, results.length);
130
131 const html = await fetchUserProfile(currentUserId);
132
133 if (html) {
134 const profile = parseUserProfile(html, currentUserId);
135
136 console.log(`Checked user ${currentUserId}:`, profile);
137
138 // Check if matches criteria
139 if (profile.gender === 'Female' && isNewEnglandLocation(profile.location)) {
140 results.push(profile);
141 console.log(`Found match: ${profile.nickname} from ${profile.location}`);
142 }
143 }
144
145 checkedCount++;
146 currentUserId++;
147
148 // Save progress
149 await GM.setValue('lastSearchedUserId', currentUserId);
150
151 // Delay to avoid rate limiting
152 await new Promise(resolve => setTimeout(resolve, CONFIG.searchDelay));
153
154 } catch (error) {
155 console.error(`Error checking user ${currentUserId}:`, error);
156 currentUserId++;
157 }
158 }
159
160 onComplete(results);
161 }
162
163 // Create search UI
164 function createSearchUI() {
165 // Check if UI already exists
166 if (document.getElementById('fetlife-search-panel')) {
167 return;
168 }
169
170 // Create panel
171 const panel = document.createElement('div');
172 panel.id = 'fetlife-search-panel';
173 panel.style.cssText = `
174 position: fixed;
175 top: 70px;
176 right: 20px;
177 width: 400px;
178 max-height: 80vh;
179 background: #1a1a1a;
180 border: 1px solid #333;
181 border-radius: 8px;
182 padding: 20px;
183 z-index: 10000;
184 color: #e0e0e0;
185 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
186 box-shadow: 0 4px 20px rgba(0,0,0,0.5);
187 overflow-y: auto;
188 `;
189
190 panel.innerHTML = `
191 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
192 <h3 style="margin: 0; color: #fff; font-size: 18px;">New England Female Search</h3>
193 <button id="close-search-panel" style="background: transparent; border: none; color: #999; font-size: 24px; cursor: pointer; padding: 0; line-height: 1;">×</button>
194 </div>
195
196 <div style="margin-bottom: 15px;">
197 <p style="margin: 0 0 10px 0; font-size: 13px; color: #999;">
198 Search for female users in New England states (CT, ME, MA, NH, RI, VT)
199 </p>
200 </div>
201
202 <div style="margin-bottom: 15px;">
203 <button id="start-search-btn" style="
204 width: 100%;
205 padding: 12px;
206 background: #d50000;
207 color: white;
208 border: none;
209 border-radius: 4px;
210 font-size: 14px;
211 font-weight: 600;
212 cursor: pointer;
213 transition: background 0.2s;
214 ">Start Search</button>
215
216 <button id="stop-search-btn" style="
217 width: 100%;
218 padding: 12px;
219 background: #666;
220 color: white;
221 border: none;
222 border-radius: 4px;
223 font-size: 14px;
224 font-weight: 600;
225 cursor: pointer;
226 margin-top: 8px;
227 display: none;
228 ">Stop Search</button>
229
230 <button id="reset-search-btn" style="
231 width: 100%;
232 padding: 8px;
233 background: transparent;
234 color: #999;
235 border: 1px solid #444;
236 border-radius: 4px;
237 font-size: 12px;
238 cursor: pointer;
239 margin-top: 8px;
240 ">Reset Search Position</button>
241 </div>
242
243 <div id="search-status" style="
244 padding: 10px;
245 background: #252525;
246 border-radius: 4px;
247 margin-bottom: 15px;
248 font-size: 13px;
249 display: none;
250 ">
251 <div id="status-text">Ready to search...</div>
252 <div id="status-found" style="color: #4caf50; margin-top: 5px;">Found: 0 users</div>
253 </div>
254
255 <div id="search-results" style="margin-top: 15px;"></div>
256 `;
257
258 document.body.appendChild(panel);
259
260 // Add hover effect to start button
261 const startBtn = document.getElementById('start-search-btn');
262 startBtn.addEventListener('mouseenter', () => {
263 startBtn.style.background = '#b00000';
264 });
265 startBtn.addEventListener('mouseleave', () => {
266 startBtn.style.background = '#d50000';
267 });
268
269 // Event listeners
270 let searchAborted = false;
271
272 document.getElementById('close-search-panel').addEventListener('click', () => {
273 panel.remove();
274 });
275
276 document.getElementById('start-search-btn').addEventListener('click', async () => {
277 searchAborted = false;
278 document.getElementById('start-search-btn').style.display = 'none';
279 document.getElementById('stop-search-btn').style.display = 'block';
280 document.getElementById('search-status').style.display = 'block';
281 document.getElementById('search-results').innerHTML = '';
282
283 await searchUsers(
284 (statusText, foundCount) => {
285 if (searchAborted) return;
286 document.getElementById('status-text').textContent = statusText;
287 document.getElementById('status-found').textContent = `Found: ${foundCount} users`;
288 },
289 (results) => {
290 document.getElementById('start-search-btn').style.display = 'block';
291 document.getElementById('stop-search-btn').style.display = 'none';
292 document.getElementById('status-text').textContent = 'Search complete!';
293 displayResults(results);
294 }
295 );
296 });
297
298 document.getElementById('stop-search-btn').addEventListener('click', () => {
299 searchAborted = true;
300 document.getElementById('start-search-btn').style.display = 'block';
301 document.getElementById('stop-search-btn').style.display = 'none';
302 document.getElementById('status-text').textContent = 'Search stopped.';
303 });
304
305 document.getElementById('reset-search-btn').addEventListener('click', async () => {
306 await GM.setValue('lastSearchedUserId', CONFIG.startUserId);
307 alert('Search position reset to beginning.');
308 });
309 }
310
311 // Display search results
312 function displayResults(results) {
313 const resultsContainer = document.getElementById('search-results');
314
315 if (results.length === 0) {
316 resultsContainer.innerHTML = '<p style="color: #999; text-align: center;">No users found matching criteria.</p>';
317 return;
318 }
319
320 resultsContainer.innerHTML = '<h4 style="margin: 0 0 10px 0; color: #fff; font-size: 16px;">Results:</h4>';
321
322 results.forEach(user => {
323 const userCard = document.createElement('div');
324 userCard.style.cssText = `
325 background: #252525;
326 border-radius: 4px;
327 padding: 12px;
328 margin-bottom: 10px;
329 display: flex;
330 gap: 12px;
331 align-items: center;
332 transition: background 0.2s;
333 `;
334
335 userCard.addEventListener('mouseenter', () => {
336 userCard.style.background = '#2a2a2a';
337 });
338 userCard.addEventListener('mouseleave', () => {
339 userCard.style.background = '#252525';
340 });
341
342 userCard.innerHTML = `
343 ${user.avatarUrl ? `<img src="${user.avatarUrl}" style="width: 50px; height: 50px; border-radius: 4px; object-fit: cover;">` : ''}
344 <div style="flex: 1;">
345 <div style="font-weight: 600; color: #fff; margin-bottom: 4px;">
346 <a href="${user.profileUrl}" target="_blank" style="color: #d50000; text-decoration: none;">${user.nickname}</a>
347 </div>
348 <div style="font-size: 12px; color: #999;">
349 ${user.age ? `${user.age} years old • ` : ''}${user.location || 'Location unknown'}
350 </div>
351 </div>
352 `;
353
354 resultsContainer.appendChild(userCard);
355 });
356 }
357
358 // Add search button to navigation
359 function addSearchButton() {
360 const nav = document.querySelector('#main-nav');
361 if (!nav || document.getElementById('fetlife-search-btn')) {
362 return;
363 }
364
365 const buttonContainer = document.createElement('div');
366 buttonContainer.style.cssText = `
367 position: fixed;
368 bottom: 20px;
369 right: 20px;
370 z-index: 9999;
371 `;
372
373 const button = document.createElement('button');
374 button.id = 'fetlife-search-btn';
375 button.textContent = '🔍 Search New England';
376 button.style.cssText = `
377 padding: 12px 20px;
378 background: #d50000;
379 color: white;
380 border: none;
381 border-radius: 25px;
382 font-size: 14px;
383 font-weight: 600;
384 cursor: pointer;
385 box-shadow: 0 4px 12px rgba(213, 0, 0, 0.4);
386 transition: all 0.2s;
387 `;
388
389 button.addEventListener('mouseenter', () => {
390 button.style.background = '#b00000';
391 button.style.transform = 'translateY(-2px)';
392 button.style.boxShadow = '0 6px 16px rgba(213, 0, 0, 0.5)';
393 });
394
395 button.addEventListener('mouseleave', () => {
396 button.style.background = '#d50000';
397 button.style.transform = 'translateY(0)';
398 button.style.boxShadow = '0 4px 12px rgba(213, 0, 0, 0.4)';
399 });
400
401 button.addEventListener('click', () => {
402 createSearchUI();
403 });
404
405 buttonContainer.appendChild(button);
406 document.body.appendChild(buttonContainer);
407 }
408
409 // Initialize
410 function init() {
411 console.log('FetLife New England Female User Search extension loaded');
412
413 // Wait for page to be ready
414 if (document.readyState === 'loading') {
415 document.addEventListener('DOMContentLoaded', addSearchButton);
416 } else {
417 addSearchButton();
418 }
419
420 // Re-add button on navigation changes (for SPA behavior)
421 const observer = new MutationObserver(debounce(() => {
422 if (!document.getElementById('fetlife-search-btn')) {
423 addSearchButton();
424 }
425 }, 1000));
426
427 observer.observe(document.body, {
428 childList: true,
429 subtree: true
430 });
431 }
432
433 init();
434})();