// ==UserScript==
// @name FetLife Group Members Auto-Follow
// @namespace https://violentmonkey.github.io/
// @version 1.0
// @description Automatically follow all members in a FetLife group
// @author You
// @match https://fetlife.com/groups/*/members*
// @match https://www.fetlife.com/groups/*/members*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('👥 FetLife Group Auto-Follow script loaded!');
console.log('Current URL:', window.location.href);
let MIN_DELAY = 10;
let MAX_DELAY = 50;
const MAX_RETRIES = 2;
const TAB_PROCESS_DELAY = 1000; // Time to wait for tab to load
const SPEED_PRESETS = {
'fast': { min: 10, max: 50, label: '🚀 Fast' },
'normal': { min: 100, max: 300, label: '⚖️ Normal' },
'slow': { min: 500, max: 1500, label: '🐌 Slow' },
'stealth': { min: 2000, max: 5000, label: '🥷 Stealth' },
'human': { min: 8000, max: 15000, label: '👤 Human' }
};
let followCount = 0;
let isRunning = false;
let processedMembers = new Set();
let currentPage = 1;
let sessionKey = '';
let isProcessing = false; // Prevent double execution
// Session persistence functions
function saveSessionData() {
if (!sessionKey) return;
const data = {
followCount,
processedMembers: Array.from(processedMembers),
isRunning,
currentPage: getCurrentPageNumber(),
timestamp: Date.now()
};
localStorage.setItem(`fetlife_group_follow_${sessionKey}`, JSON.stringify(data));
console.log(`💾 Saved session data: ${followCount} follows, page ${data.currentPage}`);
}
function loadSessionData() {
if (!sessionKey) return false;
const saved = localStorage.getItem(`fetlife_group_follow_${sessionKey}`);
if (!saved) return false;
try {
const data = JSON.parse(saved);
// Check if session is less than 1 hour old
if (Date.now() - data.timestamp > 3600000) {
localStorage.removeItem(`fetlife_group_follow_${sessionKey}`);
return false;
}
followCount = data.followCount || 0;
processedMembers = new Set(data.processedMembers || []);
isRunning = data.isRunning || false;
currentPage = data.currentPage || 1;
console.log(`📂 Loaded session data: ${followCount} follows, page ${currentPage}`);
return true;
} catch (error) {
console.error('Error loading session data:', error);
return false;
}
}
function clearSessionData() {
if (sessionKey) {
localStorage.removeItem(`fetlife_group_follow_${sessionKey}`);
console.log('🗑️ Cleared session data');
}
}
function getGroupId() {
const match = window.location.pathname.match(/\/groups\/(\d+)/);
return match ? match[1] : 'unknown';
}
function getRandomDelay() {
return Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function randomDelay() {
const delay = getRandomDelay();
console.log(`💤 Waiting ${delay.toFixed(0)}ms before next follow...`);
await sleep(delay);
}
function createButton() {
console.log('Creating group follow button...');
const existingBtn = document.getElementById('auto-group-follow-btn');
if (existingBtn) {
existingBtn.remove();
}
const button = document.createElement('div');
button.id = 'auto-group-follow-btn';
button.innerHTML = `
<div class="btn-content">
<span class="btn-text">👥❤️ Follow All Members</span>
<div class="loading-spinner" style="display: none;">
<div class="spinner"></div>
</div>
<button class="pause-btn" style="display: none;" title="Pause">⏸️</button>
</div>
<div class="speed-dropdown" style="display: none;">
<div class="speed-header">Follow Speed:</div>
<select class="speed-select">
<option value="fast" selected>🚀 Fast</option>
<option value="normal">⚖️ Normal</option>
<option value="slow">🐌 Slow</option>
<option value="stealth">🥷 Stealth</option>
<option value="human">👤 Human</option>
</select>
</div>
`;
button.style.cssText = `
position: fixed !important;
top: 50% !important;
right: 20px !important;
transform: translateY(-50%) !important;
z-index: 999999 !important;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%) !important;
color: #e53e3e !important;
border: 1px solid #404040 !important;
padding: 12px 18px !important;
border-radius: 12px !important;
font-weight: 600 !important;
font-size: 13px !important;
cursor: pointer !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1) !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
backdrop-filter: blur(10px) !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.5) !important;
letter-spacing: 0.3px !important;
user-select: none !important;
min-width: 180px !important;
`;
const style = document.createElement('style');
style.textContent = `
#auto-group-follow-btn .btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
}
#auto-group-follow-btn .btn-text {
transition: opacity 0.3s ease;
}
#auto-group-follow-btn .loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
#auto-group-follow-btn .spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(229, 62, 62, 0.2);
border-top: 2px solid #e53e3e;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#auto-group-follow-btn .pause-btn {
background: rgba(229, 62, 62, 0.15) !important;
border: 1px solid rgba(229, 62, 62, 0.3) !important;
border-radius: 6px !important;
padding: 4px 6px !important;
font-size: 12px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
color: #e53e3e !important;
backdrop-filter: blur(5px) !important;
}
#auto-group-follow-btn .pause-btn:hover {
background: rgba(229, 62, 62, 0.25) !important;
border-color: rgba(229, 62, 62, 0.5) !important;
transform: scale(1.05) !important;
color: #ff6b6b !important;
}
#auto-group-follow-btn .speed-dropdown {
margin-top: 8px;
padding: 8px 12px;
background: rgba(26, 26, 26, 0.95) !important;
border: 1px solid rgba(64, 64, 64, 0.8) !important;
border-radius: 8px !important;
backdrop-filter: blur(10px) !important;
}
#auto-group-follow-btn .speed-header {
color: #e53e3e !important;
font-size: 11px !important;
font-weight: 600 !important;
margin-bottom: 6px !important;
text-align: center !important;
}
#auto-group-follow-btn .speed-select {
width: 100% !important;
background: rgba(45, 45, 45, 0.9) !important;
color: #e53e3e !important;
border: 1px solid rgba(64, 64, 64, 0.6) !important;
border-radius: 6px !important;
padding: 6px 8px !important;
font-size: 11px !important;
font-family: inherit !important;
cursor: pointer !important;
outline: none !important;
}
#auto-group-follow-btn .speed-select:hover {
background: rgba(61, 45, 45, 0.9) !important;
border-color: rgba(229, 62, 62, 0.4) !important;
}
#auto-group-follow-btn .speed-select:focus {
border-color: rgba(229, 62, 62, 0.6) !important;
box-shadow: 0 0 0 2px rgba(229, 62, 62, 0.1) !important;
}
#auto-group-follow-btn .speed-select option {
background: #2d2d2d !important;
color: #e53e3e !important;
}
`;
document.head.appendChild(style);
button.addEventListener('mouseenter', () => {
if (!isRunning) {
button.style.background = 'linear-gradient(135deg, #2d2d2d 0%, #3a3a3a 100%) !important';
button.style.color = '#ff6b6b !important';
button.style.transform = 'translateY(-50%) translateY(-2px) scale(1.02)';
button.style.boxShadow = '0 12px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15) !important';
button.style.borderColor = '#555555 !important';
const dropdown = button.querySelector('.speed-dropdown');
if (dropdown) dropdown.style.display = 'block';
}
});
button.addEventListener('mouseleave', () => {
if (!isRunning) {
button.style.background = 'linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%) !important';
button.style.color = '#e53e3e !important';
button.style.transform = 'translateY(-50%)';
button.style.boxShadow = '0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1) !important';
button.style.borderColor = '#404040 !important';
const dropdown = button.querySelector('.speed-dropdown');
if (dropdown) dropdown.style.display = 'none';
}
});
button.addEventListener('click', function(e) {
if (e.target.classList.contains('pause-btn')) return;
console.log('Group follow button clicked!');
if (isRunning) {
stopGroupFollow();
} else {
startGroupFollow();
}
});
const pauseBtn = button.querySelector('.pause-btn');
pauseBtn.addEventListener('click', function(e) {
e.stopPropagation();
console.log('Pause button clicked!');
stopGroupFollow();
});
const speedSelect = button.querySelector('.speed-select');
speedSelect.addEventListener('change', function(e) {
e.stopPropagation();
const preset = SPEED_PRESETS[e.target.value];
if (preset) {
MIN_DELAY = preset.min;
MAX_DELAY = preset.max;
console.log(`🎚️ Speed changed to: ${preset.label}`);
console.log(`New delays: ${MIN_DELAY}-${MAX_DELAY}ms`);
}
});
speedSelect.addEventListener('click', function(e) {
e.stopPropagation();
});
document.body.appendChild(button);
console.log('Group follow button added to page!');
}
function updateButton(text, isActive = false) {
const button = document.getElementById('auto-group-follow-btn');
if (!button) return;
const btnText = button.querySelector('.btn-text');
const spinner = button.querySelector('.loading-spinner');
const pauseBtn = button.querySelector('.pause-btn');
if (btnText) btnText.textContent = text;
if (isActive) {
button.style.setProperty('background', 'linear-gradient(135deg, #2a1a1a 0%, #3d2d2d 100%)', 'important');
button.style.setProperty('color', '#e53e3e', 'important');
button.style.setProperty('border-color', '#555555', 'important');
button.style.setProperty('box-shadow', '0 8px 32px rgba(229,62,62,0.15), inset 0 1px 0 rgba(255,255,255,0.1)', 'important');
if (spinner) {
spinner.style.setProperty('display', 'flex', 'important');
}
if (pauseBtn) {
pauseBtn.style.setProperty('display', 'block', 'important');
}
const dropdown = button.querySelector('.speed-dropdown');
if (dropdown) dropdown.style.display = 'none';
} else {
button.style.setProperty('background', 'linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)', 'important');
button.style.setProperty('color', '#e53e3e', 'important');
button.style.setProperty('border-color', '#404040', 'important');
button.style.setProperty('box-shadow', '0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1)', 'important');
if (spinner) {
spinner.style.setProperty('display', 'none', 'important');
}
if (pauseBtn) {
pauseBtn.style.setProperty('display', 'none', 'important');
}
}
}
function getCurrentPageNumber() {
const urlParams = new URLSearchParams(window.location.search);
return parseInt(urlParams.get('page')) || 1;
}
function getProfileLinks() {
const links = [];
// Find profile links - they are relative links like /StanleyM
document.querySelectorAll('a[href^="/"][class*="text-red-500"]').forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && !href.includes('/groups/') && !href.includes('/users/') && href.length > 1) {
// Make sure it's a full URL and looks like a username (no additional paths)
const pathOnly = href.split('?')[0]; // Remove query params
if (!pathOnly.includes('/', 1)) { // No additional slashes after the first one
const fullUrl = `https://fetlife.com${pathOnly}`;
if (!processedMembers.has(fullUrl)) {
links.push(fullUrl);
console.log(`Found profile: ${pathOnly} -> ${fullUrl}`);
}
}
}
});
// Fallback: look for any links that look like usernames
if (links.length === 0) {
document.querySelectorAll('a[href^="/"]').forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && href.length > 1) {
const pathOnly = href.split('?')[0];
// Check if it looks like a username (no slashes, not a known path)
if (!pathOnly.includes('/', 1) &&
!pathOnly.includes('groups') &&
!pathOnly.includes('users') &&
!pathOnly.includes('events') &&
!pathOnly.includes('writings') &&
pathOnly.match(/^\/[a-zA-Z0-9_-]+$/)) {
const fullUrl = `https://fetlife.com${pathOnly}`;
if (!processedMembers.has(fullUrl)) {
links.push(fullUrl);
console.log(`Found profile (fallback): ${pathOnly} -> ${fullUrl}`);
}
}
}
});
}
// Remove duplicates
const uniqueLinks = [...new Set(links)];
console.log(`Found ${uniqueLinks.length} unique profile links on page ${getCurrentPageNumber()}`);
return uniqueLinks;
}
async function followUserInTab(profileUrl, userIndex) {
return new Promise((resolve) => {
console.log(`🔗 Opening profile ${userIndex + 1}: ${profileUrl}`);
const newTab = window.open(profileUrl, '_blank');
setTimeout(() => {
try {
// Check if tab loaded and find follow button
const followButton = newTab.document.querySelector('button[type="submit"]');
const followButtons = newTab.document.querySelectorAll('button');
let targetButton = null;
for (const btn of followButtons) {
if (btn.textContent.trim() === 'Follow') {
targetButton = btn;
break;
}
}
if (targetButton) {
console.log(`👆 Clicking follow button for user ${userIndex + 1}`);
targetButton.click();
setTimeout(() => {
followCount++;
console.log(`✅ Successfully followed user ${userIndex + 1} (Total: ${followCount})`);
processedMembers.add(profileUrl);
saveSessionData(); // Save progress after each follow
newTab.close();
resolve(true);
}, 500);
} else {
console.log(`❌ No follow button found for user ${userIndex + 1}`);
processedMembers.add(profileUrl); // Mark as processed even if no follow button
saveSessionData();
newTab.close();
resolve(false);
}
} catch (error) {
console.error(`Error processing user ${userIndex + 1}:`, error);
processedMembers.add(profileUrl); // Mark as processed to avoid retry
saveSessionData();
newTab.close();
resolve(false);
}
}, TAB_PROCESS_DELAY);
});
}
function hasNextPage() {
// Look for next page link or pagination
const nextPageLink = document.querySelector('a[rel="next"]') ||
document.querySelector('a[href*="page=' + (getCurrentPageNumber() + 1) + '"]');
return !!nextPageLink;
}
function goToNextPage() {
const nextPage = getCurrentPageNumber() + 1;
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('page', nextPage);
window.location.href = currentUrl.toString();
}
async function continueGroupFollow() {
if (isProcessing) {
console.log('⚠️ Already processing, skipping duplicate call');
return;
}
if (!isRunning) {
console.log('❌ Session says running but isRunning is false, restarting...');
isRunning = true;
saveSessionData();
}
isProcessing = true;
console.log(`🔄 Continuing group follow from page ${getCurrentPageNumber()}, ${followCount} follows completed`);
updateButton(`👥❤️ Following... (${followCount})`, true);
await sleep(500);
try {
while (isRunning) {
console.log(`\n📄 Processing page ${getCurrentPageNumber()}...`);
const profileLinks = getProfileLinks();
if (profileLinks.length === 0) {
console.log('No profile links found on this page');
break;
}
for (let i = 0; i < profileLinks.length; i++) {
if (!isRunning) {
console.log('🛑 Group follow stopped by user');
break;
}
const profileUrl = profileLinks[i];
console.log(`\n--- Processing member ${i + 1}/${profileLinks.length} ---`);
await followUserInTab(profileUrl, i);
updateButton(`👥❤️ Following... (${followCount})`, true);
if (i < profileLinks.length - 1 && isRunning) {
await randomDelay();
}
}
if (!isRunning) break;
// Check if there's a next page
if (hasNextPage()) {
console.log(`📄 Moving to next page...`);
saveSessionData(); // Save before navigation
await sleep(1000); // Wait before navigating
isProcessing = false; // Reset before navigation
goToNextPage();
return; // Script will restart on new page
} else {
console.log('🏁 Reached last page');
break;
}
}
if (isRunning) {
console.log(`\n🎉 Group follow complete! Followed ${followCount} members total`);
updateButton(`✅ Done! (${followCount} members)`, false);
clearSessionData(); // Clear data when completely done
setTimeout(() => {
updateButton('👥❤️ Follow All Members', false);
}, 5000);
}
isRunning = false;
saveSessionData();
} finally {
isProcessing = false;
}
}
async function startGroupFollow() {
if (isRunning || isProcessing) {
console.log('⚠️ Already running or processing, ignoring click');
return;
}
console.log('👥 Starting group members auto-follow process...');
isRunning = true;
saveSessionData(); // Save that we're running
updateButton(`👥❤️ Following... (${followCount})`, true);
await continueGroupFollow();
}
function stopGroupFollow() {
console.log('🛑 Stopping group follow...');
isRunning = false;
isProcessing = false;
saveSessionData();
setTimeout(() => {
updateButton('👥❤️ Follow All Members', false);
}, 100);
}
function init() {
console.log('Document ready state:', document.readyState);
// Only show button on group members pages
if (window.location.pathname.includes('/groups/') && window.location.pathname.includes('/members')) {
sessionKey = getGroupId(); // Set session key based on group ID
// Try to load existing session data
const hasSession = loadSessionData();
createButton();
if (hasSession && isRunning) {
console.log(`🔄 Resuming session: ${followCount} follows completed, page ${getCurrentPageNumber()}`);
updateButton(`👥❤️ Following... (${followCount})`, true);
// Resume the process immediately but prevent user clicks during auto-resume
setTimeout(() => {
if (!isProcessing) { // Only resume if not already processing
continueGroupFollow();
}
}, 2000); // Longer delay to ensure page is fully loaded
} else if (hasSession) {
console.log(`📋 Previous session found: ${followCount} follows completed`);
updateButton(`👥❤️ Continue (${followCount} done)`, false);
}
console.log('💡 FetLife Group Auto-Follow Ready!');
console.log('📍 Navigate to a group members page and click the button');
console.log('⏱️ Speed control: Hover over button to adjust follow speed');
console.log('📄 Pagination support - will follow ALL members across all pages');
console.log('🔄 Click button again to stop mid-process');
console.log('💾 Progress is automatically saved and will resume on page changes');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
setTimeout(init, 1000);
setTimeout(init, 3000);
console.log('FetLife Group Auto-Follow script setup complete!');
})();