Using Python and Selenium to automate Facebook group invites
Daniel Ireson

Using Python and Selenium to automate Facebook group invites

Sat Jun 17 2017

At the start of the academic year whilst working with a student society at university I ran into a problem — how do you invite people to a Facebook group in bulk? It turns out that with a little bit of Python it’s easy. For the source code see the Github repository, keep reading for a walkthrough.

Why Python?

Python is a great choice of language for this kind of task because it’s lightweight and easy to write. I personally love how well it reads. Consider the following piece of code where you can understand the logic even if you don’t know Python syntax. It reads like English.

secret = ''
while secret != 'medium':
    secret = input('What's the secret?')
    if secret == 'medium':
        print('Correct')
    else:
        print('Incorrect')

But Python is really just one suitable option — the script could quite have easily have been written in many other languages. Selenium (which we will use to automate the browser) also has client APIs available for C#, Java, JavaScript and Ruby. There’s other non-Selenium solutions that could have been used as well.

The task

View a Facebook group that you’re a member of and look on the right hand-side, you’ll see an add members section. Here you can enter an email address and it will invite the person to join the group.

Example of a facebook group

It does this whether the user has a Facebook account or not. Try inviting yourself to the group and see what happens. You should shortly receive an email in your inbox.

Facebook group invite email

Broken down the process involves five steps:

  1. Log in to Facebook.
  2. Navigate to the Facebook group page.
  3. Enter the email address of a user to invite.
  4. Submit the input.
  5. Repeat steps 3 and 4 for a list of email addresses.

Create the CLI

The script has to be able to login as a user who has access to the Facebook group, it therefore needs access to personal login details. For this reason it’s a good idea to design it as a CLI. One of the benefits of Python is its huge standard library. There’s a module called argparse that we can use to parse CLI arguments and one called getpass which can be used to prompt the user for a password. Create a new file main.py with the following code (and install Python beforehand if you don’t already have it installed).

# main.py

import argparse
import getpass

def main():
  parser = argparse.ArgumentParser(description='This tool lets you invite people in bulk to your Facebook group')
  parser.add_argument('-e','--email', help='Your personal Facebook account email', required=True)
  parser.add_argument('-g','--group', help='The Facebook group name', required=True)
  args = vars(parser.parse_args())
  args['password'] = getpass.getpass()

if __name__ == '__main__':
  main()

Open up your terminal, navigate to the directory where you saved the file and enter python main.py. You should receive an error asking you to pass the two required arguments — email and group name. These are passed via flags. You should then be prompted to enter a password.

python main.py -e johndoe@example.com -g groupname

Load emails from a CSV

CSV is a widely supported file format that stores a list of comma separated values. I had my email addresses stored in a CSV in the following structure.

john@example.com,
jane@example.com

Using Python we can easily write some code to parse this file. Let’s create an EmailLoader class.

# email_loader.py

import csv
import os
import sys

class EmailLoader:
  filename = 'emails.csv'
  emails = []

  def __init__(self):
    dir_path = os.path.dirname(os.path.realpath(__file__))
    file_path = dir_path + '/' + self.filename
    if not os.path.isfile(file_path):
      sys.exit('File does not exist: ' + self.filename)

    with open(file_path, 'rb') as file:
      csv_reader = csv.reader(file)
      for email in csv_reader:
        self.emails.append(email[0])

    if len(self.emails) < 1:
      sys.exit('There are no emails in your supplied file')
    else:
      print('Loaded ' + str(len(self.emails)))

It presumes the CSV has a filename of emails.csv and is located in the same directory. It loads email addresses into the emails array property.

from email_loader import EmailLoader

email_loader = EmailLoader()
print(email_loader.emails)

Setup Selenium

To control the web browser we're going to use Selenium, a powerful open-source browser automation tool primarily used for testing. You can install it using Python’s package manager pip by entering pip install selenium in the terminal. Selenium requires a vendor driver to work. We’ll be using the PhantomJS driver which allows us to control a PhantomJS headless browser. A headless browser is a browser without a visible user interface. Download the correct PhantomJS driver for your operating system from their downloads page and copy it your working directory (you will need to extract the driver from the bin folder of the zip download). Continuing with an object-oriented pattern, let’s create a Browser class to encapsulate working with Selenium.

# browser.py

import os

from selenium import webdriver

class Browser:
  def __init__(self):
    dir_path = os.path.dirname(os.path.realpath(__file__))
    driver_path = dir_path + '/phantomjs-driver'
    self.browser = webdriver.PhantomJS(executable_path=driver_path)

This class sets up Selenium using the PhantomJS driver on its instantiation. It assumes the driver is in the working directory with a filename of phantomjs-driver.

Let’s add a navigate method to the Browser class.

# browser.py

import os

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait

class Browser:
  def __init__(self):
    dir_path = os.path.dirname(os.path.realpath(__file__))
    driver_path = dir_path + '/phantomjs-driver'
    self.browser = webdriver.PhantomJS(executable_path=driver_path)

  def navigate(self, url, wait_for, error):
    try:
      print('Navigating to: ' + url)
      self.browser.get(url)
      element_present = expected_conditions.presence_of_element_located((By.ID, wait_for))
      WebDriverWait(self.browser, self.delay).until(element_present)
    except TimeoutException:
      sys.exit(error)

The page to navigate to should be supplied as the first parameter. The second parameter is a div ID that can be found on the navigating page, this is used to ensure it has successfully loaded. The last parameter is an error message that is shown if the navigation is unsuccessful. This will only be shown if the try/catch block fails, in which case it will exit the CLI and print the error message. Using this method it becomes simple to navigate to the Facebook homepage.

from browser import Browser

browser = Browser()
browser.navigate(
  url='<a href="https://www.facebook.com%27">https://www.facebook.com'</a>,
  wait_for='facebook',
  error='Unable to load the Facebook website'
)

Logging in to Facebook

Our script needs to be able to automate the login. This is done on the Facebook homepage by input fields in the top right hand corner of the page. Simply enough, the email field has an ID of email and the password field has an ID of pass. We can use these IDs to lookup the inputs. Let’s create a method on our Browser class to automate the login.

def enter_login_details(self, email, password):
  try:
    print('Entering login details')
    email_field = self.browser.find_element_by_id('email')
    pass_field = self.browser.find_element_by_id('pass')
    email_field.send_keys(email)
    pass_field.send_keys(password)
    pass_field.submit()
    element_present = expected_conditions.presence_of_element_located((By.ID, 'userNavigationLabel'))
    WebDriverWait(self.browser, self.delay).until(element_present)
  except TimeoutException:
    sys.exit('Login with your credentials unsuccessful')

The method finds each field by an ID lookup and enters the appropriate value passed as a parameter.

browser.enter_login_details(args['email'], args['password'])

After logging in we need to navigate to the Facebook group page. This can be done using our navigate method from earlier.

browser.navigate(
  url='<a href="https://www.facebook.com/groups/%27">https://www.facebook.com/groups/'</a> + args['group'],
  wait_for='pagelet_group_',
  error='Couldn\'t navigate to the group\'s members page'
)

Automating the imports

Now on the group page we need to loop through the imported email addresses and enter them one at a time into the add members input box in the sidebar. To do this we’ll add a new import_members method on Browser.

# browser.py

import os
import random
import sys
import time
import unicodedata

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait

class Browser:
  delay = 3

  def __init__(self):
    dir_path = os.path.dirname(os.path.realpath(__file__))
    driver_path = dir_path + '/phantomjs-driver'
    self.browser = webdriver.PhantomJS(executable_path=driver_path)

  def navigate(self, url, wait_for, error):
    try:
      print('Navigating to: ' + url)
      self.browser.get(url)
      element_present = expected_conditions.presence_of_element_located((By.ID, wait_for))
      WebDriverWait(self.browser, self.delay).until(element_present)
    except TimeoutException:
      sys.exit(error)

  def enter_login_details(self, email, password):
    try:
      print('Entering login details')
      email_field = self.browser.find_element_by_id('email')
      pass_field = self.browser.find_element_by_id('pass')
      email_field.send_keys(email)
      pass_field.send_keys(password)
      pass_field.submit()
      element_present = expected_conditions.presence_of_element_located((By.ID, 'userNavigationLabel'))
      WebDriverWait(self.browser, self.delay).until(element_present)
    except TimeoutException:
      sys.exit('Login with your credentials unsuccessful')

  def import_members(self, emails):
    print('Attempting to import email addresses')
    xpath = "//input[<a href="http://twitter.com/placeholder">@placeholder</a>='Enter name or email address...']"
    add_members_field = self.browser.find_element_by_xpath(xpath)
    for email in emails:
      for c in email:
        add_members_field.send_keys(self._get_base_character(c))
      add_members_field.send_keys(Keys.RETURN)
      time.sleep(random.randint(1,self.delay))

  @staticmethod
  def _get_base_character(c):
    desc = unicodedata.name(unicode(c))
    cutoff = desc.find(' WITH ')
    if cutoff != -1:
        desc = desc[:cutoff]
    return unicodedata.lookup(desc)

This uses XPath which is a query language for working with HTML documents. We search for the add members input by looking up the placeholder. We then loop through the array of email addresses to invite, entering each email address character by character. This prevents the browser from locking up on the input. After each email address submission the script waits for at least one second before continuing. Add usage of import_members() to main.py and our automation script is complete.

# main.py

import argparse
import getpass

from browser import Browser
from email_loader import EmailLoader

def main():
  parser = argparse.ArgumentParser(description='This tool lets you invite people in bulk to your Facebook group')
  parser.add_argument('-e','--email', help='Your personal Facebook account email', required=True)
  parser.add_argument('-g','--group', help='The Facebook group name', required=True)
  args = vars(parser.parse_args())
  args['password'] = getpass.getpass()

  email_loader = EmailLoader()
  browser = Browser()
  browser.navigate(
    url='<a href="https://www.facebook.com%27">https://www.facebook.com'</a>,
    wait_for='facebook',
    error='Unable to load the Facebook website'
  )
  browser.enter_login_details(args['email'], args['password'])
  browser.navigate(
    url='<a href="https://www.facebook.com/groups/%27">https://www.facebook.com/groups/'</a> + args['group'],
    wait_for='pagelet_group_',
    error='Couldn\'t navigate to the group\'s members page'
  )
  browser.import_members(email_loader.emails)
  print('Import complete')

if __name__ == '__main__':
  main()

Wrap-up

Python and Selenium make it painless to automate simple web tasks. Today we’ve been through just one use case but the possibilities are almost endless. Happy automating! For the source code check out the repository on Github.

The cover image for this post uses graphics from SAP Scenes.

How to build a Serverless URL shortener using AWS Lambda and S3

How to build a Serverless URL shortener using AWS Lambda and S3

Creating a form forwarding service that deploys to AWS Lambda

Creating a form forwarding service that deploys to AWS Lambda