Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 33 additions & 192 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""
""
Facebook Group Invite Automation
Automates the process of inviting Facebook friends to join a group.
Project by SoClose Society — https://soclose.com
Expand All @@ -10,16 +10,16 @@

Disclaimer: Educational purposes only. Use at your own risk.
License: MIT
"""
""

import argparse
import logging
import os
import re
import signal
import sys
import time
import random
time
random

from bs4 import BeautifulSoup
from selenium import webdriver
Expand Down Expand Up @@ -81,7 +81,6 @@
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def xpath_soup(element):
"""Convert a BeautifulSoup element to an XPath expression."""
components = []
Expand Down Expand Up @@ -131,7 +130,6 @@ def find_element_with_retry(driver, soup_finder, click=False, retries=RETRY_LIMI
time.sleep(1)
return None


def refresh_friend_list(driver, labels):
"""Re-parse the invitation dialog and return the friend-item list."""
html = driver.page_source
Expand All @@ -144,7 +142,6 @@ def refresh_friend_list(driver, labels):
)
return items, soup


def get_selected_count(driver, labels):
"""Parse the 'N FRIENDS SELECTED' text and return N as int."""
html = driver.page_source
Expand All @@ -166,7 +163,6 @@ def get_selected_count(driver, labels):
# ---------------------------------------------------------------------------
# Core automation
# ---------------------------------------------------------------------------

class FacebookGroupInviter:
"""Encapsulates the full invite-automation workflow."""

Expand Down Expand Up @@ -298,199 +294,44 @@ def _select_friends(self, target_count):
selected += 1
consecutive_errors = 0
logger.info(
"Selected friend %d/%d (index %d)",
"%d friends selected so far.",
selected,
target_count,
idx,
)
except (
NoSuchElementException,
StaleElementReferenceException,
ElementClickInterceptedException,
) as exc:
except (ElementClickInterceptedException, StaleElementReferenceException) as exc:
logger.warning("Failed to select friend: %s", exc)
consecutive_errors += 1
logger.warning("Error selecting friend at index %d: %s", idx, exc)
if consecutive_errors >= 5:
logger.error("Too many consecutive errors, stopping selection.")
break

if consecutive_errors >= RETRY_LIMIT:
raise RuntimeError("Too many errors selecting friends.")
time.sleep(1)
idx += 1
time.sleep(0.5)

return selected
logger.info("Selected %d friends in total.", selected)

def _send_invitations(self):
"""Click the 'Send invitations' button."""
def finder(soup):
return soup.find("div", attrs={"aria-label": self.labels["send_invitations"]})

el = find_element_with_retry(self.driver, finder, click=True)
if el is None:
raise RuntimeError("Could not find the 'Send invitations' button.")
logger.info("Clicked 'Send invitations'.")
"""Send the collected invitations."""
try:
send_button = self.driver.find_element(By.XPATH, xpath_soup(find_element_with_retry(self.driver, lambda soup: soup.find("button", attrs={"aria-label": self.labels["send_invitations"]}))))
send_button.click()
logger.info("Invitations sent.")
except NoSuchElementException:
logger.warning("Send button not found. Assuming no more friends to invite.")

# ------------------------------------------------------------------
# Main loop
# ------------------------------------------------------------------
def run(self):
"""Execute the full invitation loop."""
batch_number = 0

while not self._shutdown:
batch_number += 1
batch_size = random.randint(self.batch_min, self.batch_max)
logger.info(
"=== Batch #%d — targeting %d friends ===",
batch_number,
batch_size,
)

try:
# Navigate to group
self.driver.get(self.group_url)
time.sleep(2)

# Open invite flow
self._click_invite_button()
time.sleep(1)
self._click_invite_friends_menu()
self._wait_for_dialog()

# Select friends
selected = self._select_friends(batch_size)
confirmed = get_selected_count(self.driver, self.labels)
logger.info(
"Batch #%d: clicked %d, confirmed selected = %d",
batch_number,
selected,
confirmed,
)

if confirmed == 0:
logger.info("No friends left to invite. Stopping.")
"""Run the automation workflow."""
try:
self.start_browser()
self.navigate_to_facebook()
time.sleep(10) # Wait for user to log in manually
self._click_invite_button()
self._click_invite_friends_menu()
self._wait_for_dialog()
while True:
selected_count = get_selected_count(self.driver, self.labels)
if selected_count >= self.batch_max or self.max_invites > 0 and self.total_invited >= self.max_invites:
break

# Send
self._select_friends(self.batch_min)
self._send_invitations()
self.total_invited += confirmed
logger.info(
"Batch #%d sent. Total invited so far: %d",
batch_number,
self.total_invited,
)

# Check max invites limit
if self.max_invites > 0 and self.total_invited >= self.max_invites:
logger.info(
"Reached max invites limit (%d). Stopping.",
self.max_invites,
)
break

# Delay between batches
logger.info("Waiting %ds before next batch…", POST_INVITE_DELAY)
time.sleep(POST_INVITE_DELAY)

except RuntimeError as exc:
logger.error("Batch #%d failed: %s", batch_number, exc)
logger.info("Retrying in 5 seconds…")
time.sleep(5)
except WebDriverException as exc:
logger.error("Browser error during batch #%d: %s", batch_number, exc)
break

logger.info("Finished. Total friends invited: %d", self.total_invited)

# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def parse_args(argv=None):
parser = argparse.ArgumentParser(
description="Facebook Group Invite Automation — SoClose Society",
epilog="More info: https://soclose.com",
)
parser.add_argument(
"--group-url",
type=str,
default=None,
help="Facebook group URL (will prompt interactively if omitted)",
)
parser.add_argument(
"--lang",
choices=LABELS.keys(),
default=DEFAULT_LANG,
help="Facebook UI language (default: fr)",
)
parser.add_argument(
"--batch-min",
type=int,
default=DEFAULT_BATCH_MIN,
help="Minimum friends per batch (default: 5)",
)
parser.add_argument(
"--batch-max",
type=int,
default=DEFAULT_BATCH_MAX,
help="Maximum friends per batch (default: 10)",
)
parser.add_argument(
"--max-invites",
type=int,
default=DEFAULT_MAX_INVITES,
help="Stop after N invites total (0 = unlimited, default: 0)",
)
parser.add_argument(
"--headless",
action="store_true",
help="Run Chrome in headless mode (login will not be possible interactively)",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable debug logging",
)
return parser.parse_args(argv)


def main():
args = parse_args()

if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)

# Get group URL
group_url = args.group_url
if not group_url:
group_url = input("Enter the Facebook group URL: ").strip()

if not validate_facebook_group_url(group_url):
logger.error("Invalid Facebook group URL: %s", group_url)
logger.error("Expected format: https://www.facebook.com/groups/YOUR_GROUP")
sys.exit(1)

# Create inviter
inviter = FacebookGroupInviter(
group_url=group_url,
lang=args.lang,
batch_min=args.batch_min,
batch_max=args.batch_max,
max_invites=args.max_invites,
headless=args.headless,
)

try:
inviter.start_browser()
inviter.navigate_to_facebook()

input('\nLog in to Facebook in the browser, then press Enter to start…')

inviter.run()
except KeyboardInterrupt:
logger.info("Interrupted by user.")
finally:
inviter.quit()


if __name__ == "__main__":
main()
logger.info("Automation completed. %d friends invited.", self.total_invited)
finally:
self.quit()