Various improvements #31

Merged
weskerfoot merged 15 commits from various-improvements into master 6 years ago
  1. 13
      README.md
  2. 49
      deletefb/deletefb.py
  3. 51
      deletefb/tools/common.py
  4. 65
      deletefb/tools/likes.py
  5. 68
      deletefb/tools/login.py
  6. 26
      deletefb/tools/wall.py

13
README.md

@ -49,11 +49,24 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the
* It is recommended that you disable Two-Factor Authentication tempoprarily * It is recommended that you disable Two-Factor Authentication tempoprarily
while you are running the script, in order to get the best experience. while you are running the script, in order to get the best experience.
* If you run into issues with Facebook complaining about your browser,
currently the only workaround is to manually click through them.
* If you do have 2-Factor Auth configured then the script will pause for 20 * If you do have 2-Factor Auth configured then the script will pause for 20
seconds to allow you to enter your code and log in. seconds to allow you to enter your code and log in.
* You may also pass in a code by using the `-F` argument, e.g. `-F 111111`. * You may also pass in a code by using the `-F` argument, e.g. `-F 111111`.
## Delete by year
* The tool supports passing the `--year` flag in order to delete wall posts by
year. It is incompatible with any mode other than `wall`.
## Archival
* The tool will archive everything being deleted by default in `.log` files.
Currently they are simply stored as JSON objects for each line in the log. It
will archive the content, and a timestamp if it is available. You may disable
this feature by using `--no-archive`.
## Headless mode ## Headless mode
* The tool supports running Chrome in headless mode with the `--headless` * The tool supports running Chrome in headless mode with the `--headless`
option, which may be preferable if you plan on running it in the background. option, which may be preferable if you plan on running it in the background.

49
deletefb/deletefb.py

@ -3,11 +3,26 @@
import argparse import argparse
import getpass import getpass
from sys import exit
from os import environ
from .tools.login import login from .tools.login import login
from .tools.wall import delete_posts from .tools.wall import delete_posts
from .tools.likes import unlike_pages
def run_delete(): def run_delete():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument(
"-M",
"--mode",
required=False,
default="wall",
dest="mode",
type=str,
choices=["wall", "unlike_pages"],
help="The mode you want to run in. Default is `wall' which deletes wall posts"
)
parser.add_argument( parser.add_argument(
"-E", "-E",
"--email", "--email",
@ -53,19 +68,49 @@ def run_delete():
help="Run browser in headless mode (no gui)" help="Run browser in headless mode (no gui)"
) )
parser.add_argument(
"--no-archive",
action="store_true",
dest="archive_off",
default=True,
help="Turn off archiving (on by default)"
)
parser.add_argument(
"-Y",
"--year",
required=False,
dest="year",
type=str,
help="The year(s) you want posts deleted."
)
args = parser.parse_args() args = parser.parse_args()
if args.archive_off:
environ["DELETEFB_ARCHIVE"] = "false" if args.archive_off else "true"
if args.year and args.mode != "wall":
parser.error("The --year option is only supported in wall mode")
args_user_password = args.password or getpass.getpass('Enter your password: ') args_user_password = args.password or getpass.getpass('Enter your password: ')
driver = login( driver = login(
user_email_address=args.email, user_email_address=args.email,
user_password=args_user_password, user_password=args_user_password,
user_profile_url=args.profile_url,
is_headless=args.is_headless, is_headless=args.is_headless,
two_factor_token=args.two_factor_token two_factor_token=args.two_factor_token
) )
delete_posts(driver) if args.mode == "wall":
delete_posts(driver,
args.profile_url,
year=args.year)
elif args.mode == "unlike_pages":
unlike_pages(driver)
else:
print("Please enter a valid mode")
exit(1)
if __name__ == "__main__": if __name__ == "__main__":
run_delete() run_delete()

51
deletefb/tools/common.py

@ -1,3 +1,50 @@
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException from selenium.common.exceptions import (NoSuchElementException,
StaleElementReferenceException,
TimeoutException)
from time import sleep
from json import dumps
from os.path import abspath, relpath, split
from os import environ
SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) SELENIUM_EXCEPTIONS = (NoSuchElementException,
StaleElementReferenceException,
TimeoutException)
def try_move(actions, el):
for _ in range(10):
try:
actions.move_to_element(el).perform()
except StaleElementReferenceException:
sleep(5)
continue
def archiver(category):
"""
category: the category of logs you want to log
return values: (log_file_handle, archiver)
call archiver like archive("some content")
"""
log_path = "{0}.log".format(abspath(relpath(split(category)[-1], ".")))
log_file = open(log_path, mode="wt", buffering=1)
def log(content, timestamp=False):
if environ.get("DELETEFB_ARCHIVE", "true") == "false":
return
structured_content = {
"category" : category,
"content" : content,
"timestamp" : timestamp
}
log_file.write("{0}\n".format(dumps(structured_content)))
return (log_file, log)
no_chrome_driver = """
You need to install the chromedriver for Selenium\n
Please see this link https://github.com/weskerfoot/DeleteFB#how-to-use-it\n
"""

65
deletefb/tools/likes.py

@ -1,8 +1,69 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from .common import SELENIUM_EXCEPTIONS from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from .common import SELENIUM_EXCEPTIONS, archiver
def load_likes(driver):
"""
Loads the page that lists all pages you like
"""
driver.get("https://www.facebook.com/pages/?category=liked")
wait = WebDriverWait(driver, 20)
try:
wait.until(
EC.presence_of_element_located((By.XPATH, "//button/div/div[text()='Liked']"))
)
wait.until(
EC.presence_of_element_located((By.XPATH, "//button/div/i[@aria-hidden=\"true\"]"))
)
except SELENIUM_EXCEPTIONS:
return
def unlike_pages(driver): def unlike_pages(driver):
""" """
Unlike all pages Unlike all pages
""" """
return
like_log, archive_likes = archiver("likes")
actions = ActionChains(driver)
load_likes(driver)
pages_list = driver.find_element_by_css_selector("#all_liked_pages")
actions.move_to_element(pages_list).perform()
unlike_buttons = pages_list.find_elements_by_xpath("//button/div/div[text()='Liked']/../..")
while unlike_buttons:
for button in unlike_buttons:
try:
if "Liked" in button.text:
page_name = button.find_element_by_xpath("./../..").text.split("\n")[0]
driver.execute_script("arguments[0].click();", button)
archive_likes(page_name)
print("{0} was unliked".format(page_name))
except SELENIUM_EXCEPTIONS as e:
continue
load_likes(driver)
try:
pages_list = driver.find_element_by_css_selector("#all_liked_pages")
actions.move_to_element(pages_list).perform()
unlike_buttons = pages_list.find_elements_by_xpath("//button")
if not unlike_buttons:
break
except SELENIUM_EXCEPTIONS:
break
# Explicitly close the log file when we're done with it
like_log.close()

68
deletefb/tools/login.py

@ -1,11 +1,13 @@
import time import time
from sys import stderr, exit
from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.options import Options
from seleniumrequests import Chrome from seleniumrequests import Chrome
from selenium.common.exceptions import NoSuchElementException
from .common import no_chrome_driver
def login(user_email_address, def login(user_email_address,
user_password, user_password,
user_profile_url,
is_headless, is_headless,
two_factor_token): two_factor_token):
""" """
@ -28,7 +30,15 @@ def login(user_email_address,
chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('log-level=2') chrome_options.add_argument('log-level=2')
driver = Chrome(options=chrome_options) try:
driver = Chrome(options=chrome_options)
except Exception as e:
# The user does not have chromedriver installed
# Tell them to install it
stderr.write(str(e))
stderr.write(no_chrome_driver)
exit(1)
driver.implicitly_wait(10) driver.implicitly_wait(10)
driver.get("https://facebook.com") driver.get("https://facebook.com")
@ -47,24 +57,40 @@ def login(user_email_address,
loginelement = driver.find_element_by_id(login) loginelement = driver.find_element_by_id(login)
loginelement.click() loginelement.click()
if "two-factor authentication" in driver.page_source.lower(): # Defaults to no 2fa
if two_factor_token: has_2fa = False
twofactorelement = driver.find_element_by_name(approvals_code) try:
twofactorelement.send_keys(two_factor_token) # If this element exists, we've reached a 2FA page
driver.find_element_by_xpath("//form[@class=\"checkpoint\"]")
# Submits after the code is passed into the form, does not validate 2FA code. driver.find_element_by_xpath("//input[@name=\"approvals_code\"]")
contelement = driver.find_element_by_id("checkpointSubmitButton") has_2fa = True
contelement.click() except NoSuchElementException:
has_2fa = "two-factor authentication" in driver.page_source.lower() or has_2fa
# Defaults to saving this new browser, this occurs on each new automated login.
save_browser = driver.find_element_by_id("checkpointSubmitButton") if has_2fa:
save_browser.click() print("""
else: Two-Factor Auth is enabled.
# Allow time to enter 2FA code Please file an issue at https://github.com/weskerfoot/DeleteFB/issues if you run into any problems
print("Pausing to enter 2FA code") """)
time.sleep(20)
print("Continuing execution") if two_factor_token and has_2fa:
twofactorelement = driver.find_element_by_name(approvals_code)
twofactorelement.send_keys(two_factor_token)
# Submits after the code is passed into the form, does not validate 2FA code.
contelement = driver.find_element_by_id("checkpointSubmitButton")
contelement.click()
# Defaults to saving this new browser, this occurs on each new automated login.
save_browser = driver.find_element_by_id("checkpointSubmitButton")
save_browser.click()
elif has_2fa:
# Allow time to enter 2FA code
print("Pausing to enter 2FA code")
time.sleep(35)
print("Continuing execution")
else:
pass
driver.get(user_profile_url)
return driver return driver

26
deletefb/tools/wall.py

@ -1,20 +1,40 @@
import time import time
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from .common import SELENIUM_EXCEPTIONS from .common import SELENIUM_EXCEPTIONS, archiver
MAX_POSTS = 5000 # Used as a threshold to avoid running forever
MAX_POSTS = 15000
def delete_posts(driver): def delete_posts(driver,
user_profile_url,
year=None):
""" """
Deletes or hides all posts from the wall Deletes or hides all posts from the wall
""" """
if not year is None:
user_profile_url = "{0}/timeline?year={1}".format(user_profile_url, year)
driver.get(user_profile_url)
wall_log, archive_wall_post = archiver("wall")
for _ in range(MAX_POSTS): for _ in range(MAX_POSTS):
post_button_sel = "_4xev" post_button_sel = "_4xev"
post_content_sel = "_5_jv"
post_timestamp_sel = "timestamp"
while True: while True:
try: try:
timeline_element = driver.find_element_by_class_name(post_button_sel) timeline_element = driver.find_element_by_class_name(post_button_sel)
post_content_element = driver.find_element_by_class_name(post_content_sel)
post_content_ts = driver.find_element_by_class_name(post_timestamp_sel)
archive_wall_post(post_content_element.text, timestamp=post_content_ts.text)
actions = ActionChains(driver) actions = ActionChains(driver)
actions.move_to_element(timeline_element).click().perform() actions.move_to_element(timeline_element).click().perform()

Loading…
Cancel
Save