diff --git a/main.py b/main.py index b06eec4..6e0bc0d 100644 --- a/main.py +++ b/main.py @@ -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 @@ -10,7 +10,7 @@ Disclaimer: Educational purposes only. Use at your own risk. License: MIT -""" +"" import argparse import logging @@ -18,8 +18,8 @@ import re import signal import sys -import time -import random +time +random from bs4 import BeautifulSoup from selenium import webdriver @@ -81,7 +81,6 @@ # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- - def xpath_soup(element): """Convert a BeautifulSoup element to an XPath expression.""" components = [] @@ -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 @@ -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 @@ -166,7 +163,6 @@ def get_selected_count(driver, labels): # --------------------------------------------------------------------------- # Core automation # --------------------------------------------------------------------------- - class FacebookGroupInviter: """Encapsulates the full invite-automation workflow.""" @@ -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()