From 2c73a529b3b66dfc06e88305b479275d5d2c0c37 Mon Sep 17 00:00:00 2001 From: wes Date: Wed, 22 May 2019 19:05:48 -0400 Subject: [PATCH 001/186] Start refactoring things into separate modules --- deletefb/deletefb.py | 89 +++----------------------------------- deletefb/tools/__init__.py | 0 deletefb/tools/wall.py | 79 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 82 deletions(-) create mode 100644 deletefb/tools/__init__.py create mode 100644 deletefb/tools/wall.py diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 83b6434..4247b9f 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -4,6 +4,8 @@ import argparse import time import getpass +import tools.wall as wall + from seleniumrequests import Chrome from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.chrome.options import Options @@ -54,89 +56,12 @@ def run_delete(): args_user_password = args.password or getpass.getpass('Enter your password: ') - delete_posts( - user_email_address=args.email, - user_password=args_user_password, - user_profile_url=args.profile_url, - is_headless=args.is_headless + wall.delete_posts( + user_email_address=args.email, + user_password=args_user_password, + user_profile_url=args.profile_url, + is_headless=args.is_headless ) -def delete_posts(user_email_address, - user_password, - user_profile_url, - is_headless): - """ - user_email_address: str Your Email - user_password: str Your password - user_profile_url: str Your profile URL - """ - # The Chrome driver is required because Gecko was having issues - chrome_options = Options() - prefs = {"profile.default_content_setting_values.notifications": 2, 'disk-cache-size': 4096} - chrome_options.add_experimental_option("prefs", prefs) - chrome_options.add_argument("start-maximized") - - if is_headless: - chrome_options.add_argument('--headless') - chrome_options.add_argument('--disable-gpu') - chrome_options.add_argument('log-level=2') - - driver = Chrome(options=chrome_options) - driver.implicitly_wait(10) - - driver.get("https://facebook.com") - - email = "email" - password = "pass" - login = "loginbutton" - - emailelement = driver.find_element_by_name(email) - passwordelement = driver.find_element_by_name(password) - - emailelement.send_keys(user_email_address) - passwordelement.send_keys(user_password) - - loginelement = driver.find_element_by_id(login) - loginelement.click() - - if "Two-factor authentication" in driver.page_source: - # Allow time to enter 2FA code - print("Pausing to enter 2FA code") - time.sleep(20) - print("Continuing execution") - driver.get(user_profile_url) - - for _ in range(MAX_POSTS): - post_button_sel = "_4xev" - - while True: - try: - timeline_element = driver.find_element_by_class_name(post_button_sel) - actions = ActionChains(driver) - actions.move_to_element(timeline_element).click().perform() - - menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") - actions.move_to_element(menu).perform() - - try: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"FeedDeleteOption\"]") - except SELENIUM_EXCEPTIONS: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"HIDE_FROM_TIMELINE\"]") - - actions.move_to_element(delete_button).click().perform() - confirmation_button = driver.find_element_by_class_name("layerConfirm") - - # Facebook would not let me get focus on this button without some custom JS - driver.execute_script("arguments[0].click();", confirmation_button) - except SELENIUM_EXCEPTIONS: - continue - else: - break - - # Required to sleep the thread for a bit after using JS to click this button - time.sleep(5) - driver.refresh() - - if __name__ == "__main__": run_delete() diff --git a/deletefb/tools/__init__.py b/deletefb/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py new file mode 100644 index 0000000..50b5222 --- /dev/null +++ b/deletefb/tools/wall.py @@ -0,0 +1,79 @@ + +def delete_posts(user_email_address, + user_password, + user_profile_url, + is_headless): + """ + user_email_address: str Your Email + user_password: str Your password + user_profile_url: str Your profile URL + """ + # The Chrome driver is required because Gecko was having issues + chrome_options = Options() + prefs = {"profile.default_content_setting_values.notifications": 2, 'disk-cache-size': 4096} + chrome_options.add_experimental_option("prefs", prefs) + chrome_options.add_argument("start-maximized") + + if is_headless: + chrome_options.add_argument('--headless') + chrome_options.add_argument('--disable-gpu') + chrome_options.add_argument('log-level=2') + + driver = Chrome(options=chrome_options) + driver.implicitly_wait(10) + + driver.get("https://facebook.com") + + email = "email" + password = "pass" + login = "loginbutton" + + emailelement = driver.find_element_by_name(email) + passwordelement = driver.find_element_by_name(password) + + emailelement.send_keys(user_email_address) + passwordelement.send_keys(user_password) + + loginelement = driver.find_element_by_id(login) + loginelement.click() + + if "Two-factor authentication" in driver.page_source: + # Allow time to enter 2FA code + print("Pausing to enter 2FA code") + time.sleep(20) + print("Continuing execution") + driver.get(user_profile_url) + + for _ in range(MAX_POSTS): + post_button_sel = "_4xev" + + while True: + try: + timeline_element = driver.find_element_by_class_name(post_button_sel) + actions = ActionChains(driver) + actions.move_to_element(timeline_element).click().perform() + + menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") + actions.move_to_element(menu).perform() + + try: + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"FeedDeleteOption\"]") + except SELENIUM_EXCEPTIONS: + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"HIDE_FROM_TIMELINE\"]") + + actions.move_to_element(delete_button).click().perform() + confirmation_button = driver.find_element_by_class_name("layerConfirm") + + # Facebook would not let me get focus on this button without some custom JS + driver.execute_script("arguments[0].click();", confirmation_button) + except SELENIUM_EXCEPTIONS: + continue + else: + break + + # Required to sleep the thread for a bit after using JS to click this button + time.sleep(5) + driver.refresh() + + + -- 2.30.2 From 8cc9074934daa67792f3109f25019ee7dc56d55d Mon Sep 17 00:00:00 2001 From: wes Date: Wed, 22 May 2019 19:11:21 -0400 Subject: [PATCH 002/186] Refactoring --- deletefb/deletefb.py | 11 +---------- deletefb/tools/wall.py | 7 +++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 4247b9f..a2a1aa0 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -3,16 +3,7 @@ import argparse import time import getpass - -import tools.wall as wall - -from seleniumrequests import Chrome -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.chrome.options import Options -from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException - -MAX_POSTS = 5000 -SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) +import deletefb.tools.wall as wall def run_delete(): parser = argparse.ArgumentParser() diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 50b5222..bb23e16 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,3 +1,10 @@ +from selenium.webdriver.chrome.options import Options +from seleniumrequests import Chrome +from selenium.webdriver.common.action_chains import ActionChains +from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException + +MAX_POSTS = 5000 +SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) def delete_posts(user_email_address, user_password, -- 2.30.2 From 81fd9329f9b537df5cabf22cf548c6749af0f07b Mon Sep 17 00:00:00 2001 From: wes Date: Wed, 22 May 2019 19:21:17 -0400 Subject: [PATCH 003/186] More refactoring into modules --- deletefb/deletefb.py | 15 ++++++----- deletefb/tools/login.py | 57 +++++++++++++++++++++++++++++++++++++++++ deletefb/tools/wall.py | 50 +++--------------------------------- 3 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 deletefb/tools/login.py diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index a2a1aa0..84a9c75 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -1,8 +1,9 @@ #! /usr/bin/env python import argparse -import time import getpass + +from deletefb.tools.login import login import deletefb.tools.wall as wall def run_delete(): @@ -47,12 +48,14 @@ def run_delete(): args_user_password = args.password or getpass.getpass('Enter your password: ') - wall.delete_posts( - user_email_address=args.email, - user_password=args_user_password, - user_profile_url=args.profile_url, - is_headless=args.is_headless + driver = login( + user_email_address=args.email, + user_password=args_user_password, + user_profile_url=args.profile_url, + is_headless=args.is_headless ) + wall.delete_posts(driver) + if __name__ == "__main__": run_delete() diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py new file mode 100644 index 0000000..d1c81df --- /dev/null +++ b/deletefb/tools/login.py @@ -0,0 +1,57 @@ +import time + +from selenium.webdriver.chrome.options import Options +from seleniumrequests import Chrome +from selenium.webdriver.common.action_chains import ActionChains +from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException + +def login(user_email_address, + user_password, + user_profile_url, + is_headless): + """ + Attempts to log into Facebook + Returns a driver object + + user_email_address: str Your Email + user_password: str Your password + user_profile_url: str Your profile URL + + """ + # The Chrome driver is required because Gecko was having issues + chrome_options = Options() + prefs = {"profile.default_content_setting_values.notifications": 2, 'disk-cache-size': 4096} + chrome_options.add_experimental_option("prefs", prefs) + chrome_options.add_argument("start-maximized") + + if is_headless: + chrome_options.add_argument('--headless') + chrome_options.add_argument('--disable-gpu') + chrome_options.add_argument('log-level=2') + + driver = Chrome(options=chrome_options) + driver.implicitly_wait(10) + + driver.get("https://facebook.com") + + email = "email" + password = "pass" + login = "loginbutton" + + emailelement = driver.find_element_by_name(email) + passwordelement = driver.find_element_by_name(password) + + emailelement.send_keys(user_email_address) + passwordelement.send_keys(user_password) + + loginelement = driver.find_element_by_id(login) + loginelement.click() + + if "Two-factor authentication" in driver.page_source: + # Allow time to enter 2FA code + print("Pausing to enter 2FA code") + time.sleep(20) + print("Continuing execution") + driver.get(user_profile_url) + + return driver diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index bb23e16..0093fce 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,3 +1,5 @@ +import time + from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome from selenium.webdriver.common.action_chains import ActionChains @@ -6,51 +8,10 @@ from selenium.common.exceptions import NoSuchElementException, StaleElementRefer MAX_POSTS = 5000 SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) -def delete_posts(user_email_address, - user_password, - user_profile_url, - is_headless): +def delete_posts(driver): """ - user_email_address: str Your Email - user_password: str Your password - user_profile_url: str Your profile URL + Deletes or hides all posts from the wall """ - # The Chrome driver is required because Gecko was having issues - chrome_options = Options() - prefs = {"profile.default_content_setting_values.notifications": 2, 'disk-cache-size': 4096} - chrome_options.add_experimental_option("prefs", prefs) - chrome_options.add_argument("start-maximized") - - if is_headless: - chrome_options.add_argument('--headless') - chrome_options.add_argument('--disable-gpu') - chrome_options.add_argument('log-level=2') - - driver = Chrome(options=chrome_options) - driver.implicitly_wait(10) - - driver.get("https://facebook.com") - - email = "email" - password = "pass" - login = "loginbutton" - - emailelement = driver.find_element_by_name(email) - passwordelement = driver.find_element_by_name(password) - - emailelement.send_keys(user_email_address) - passwordelement.send_keys(user_password) - - loginelement = driver.find_element_by_id(login) - loginelement.click() - - if "Two-factor authentication" in driver.page_source: - # Allow time to enter 2FA code - print("Pausing to enter 2FA code") - time.sleep(20) - print("Continuing execution") - driver.get(user_profile_url) - for _ in range(MAX_POSTS): post_button_sel = "_4xev" @@ -81,6 +42,3 @@ def delete_posts(user_email_address, # Required to sleep the thread for a bit after using JS to click this button time.sleep(5) driver.refresh() - - - -- 2.30.2 From 9f9fcc1ba633955b660362d03e70c0c93446dcb5 Mon Sep 17 00:00:00 2001 From: wes Date: Wed, 22 May 2019 19:25:00 -0400 Subject: [PATCH 004/186] Remove unnecessary imports --- deletefb/tools/login.py | 2 -- deletefb/tools/wall.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index d1c81df..62ad2a3 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -2,8 +2,6 @@ import time from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome -from selenium.webdriver.common.action_chains import ActionChains -from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException def login(user_email_address, user_password, diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0093fce..d187bb6 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,7 +1,5 @@ import time -from selenium.webdriver.chrome.options import Options -from seleniumrequests import Chrome from selenium.webdriver.common.action_chains import ActionChains from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException -- 2.30.2 From 1502feef4ba703bca98aa7b7a9d8e2803b3c25ca Mon Sep 17 00:00:00 2001 From: wes Date: Wed, 22 May 2019 21:52:04 -0400 Subject: [PATCH 005/186] More cleanup --- deletefb/tools/common.py | 3 +++ deletefb/tools/likes.py | 8 ++++++++ deletefb/tools/login.py | 12 ++++++------ deletefb/tools/wall.py | 3 +-- 4 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 deletefb/tools/common.py create mode 100644 deletefb/tools/likes.py diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py new file mode 100644 index 0000000..92405c4 --- /dev/null +++ b/deletefb/tools/common.py @@ -0,0 +1,3 @@ +from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException + +SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py new file mode 100644 index 0000000..80c6b0a --- /dev/null +++ b/deletefb/tools/likes.py @@ -0,0 +1,8 @@ +from selenium.webdriver.common.action_chains import ActionChains +from .common import SELENIUM_EXCEPTIONS + +def unlike_pages(driver): + """ + Unlike all pages + """ + return diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 62ad2a3..d3d7663 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -36,14 +36,14 @@ def login(user_email_address, password = "pass" login = "loginbutton" - emailelement = driver.find_element_by_name(email) - passwordelement = driver.find_element_by_name(password) + email_element = driver.find_element_by_name(email) + password_element = driver.find_element_by_name(password) - emailelement.send_keys(user_email_address) - passwordelement.send_keys(user_password) + email_element.send_keys(user_email_address) + password_element.send_keys(user_password) - loginelement = driver.find_element_by_id(login) - loginelement.click() + login_element = driver.find_element_by_id(login) + login_element.click() if "Two-factor authentication" in driver.page_source: # Allow time to enter 2FA code diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index d187bb6..3005dd1 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,10 +1,9 @@ import time from selenium.webdriver.common.action_chains import ActionChains -from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException +from .common import SELENIUM_EXCEPTIONS MAX_POSTS = 5000 -SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) def delete_posts(driver): """ -- 2.30.2 From ccce59e200d5bf9ff61ee188b744cd22ec1466dc Mon Sep 17 00:00:00 2001 From: Alex Franco Date: Fri, 24 May 2019 18:46:02 -0500 Subject: [PATCH 006/186] Added --year argument Will add "/timeline?year=" year to user_profile_url before navigating --- deletefb/deletefb.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 7d260ce..5efd5e0 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -59,6 +59,15 @@ def run_delete(): help="Run browser in headless mode (no gui)" ) + parser.add_argument( + "-Y", + "--year", + required=False, + dest="year", + type=str, + help="The year(s) you want posts deleted." + ) + args = parser.parse_args() args_user_password = args.password or getpass.getpass('Enter your password: ') @@ -68,14 +77,16 @@ def run_delete(): user_password=args_user_password, user_profile_url=args.profile_url, is_headless=args.is_headless, - two_factor_token=args.two_factor_token + two_factor_token=args.two_factor_token, + year=args.year ) def delete_posts(user_email_address, user_password, user_profile_url, is_headless, - two_factor_token): + two_factor_token, + year): """ user_email_address: str Your Email user_password: str Your password @@ -131,6 +142,9 @@ def delete_posts(user_email_address, time.sleep(20) print("Continuing execution") + if year: + user_profile_url += "/timeline?year=" + year + driver.get(user_profile_url) for _ in range(MAX_POSTS): -- 2.30.2 From 1770fa47aabd93774e96e4bf9c7309fa5b7391a4 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 24 May 2019 20:49:42 -0400 Subject: [PATCH 007/186] Clean up README --- README.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 41730e5..fca93c9 100644 --- a/README.md +++ b/README.md @@ -38,27 +38,19 @@ would if you had installed it from PyPI. everything. You may safely minimize the chrome window without breaking it. ## 2FA -* If you have 2-Factor Auth configured then the script will pause for 20 +* It is recommended that you disable Two-Factor Authentication tempoprarily + while you are running the script, in order to get the best experience. + +* 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. +* You may also pass in a code by using the `-F` argument, e.g. `-F 111111`. + ## Headless mode * 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. -## How To Install Python - -### MacOS -See [this link](https://docs.python-guide.org/starting/install3/osx/) for -instructions on installing with Brew. - -### Linux -Use your native package manager - -### Windows -See [this link](https://www.howtogeek.com/197947/how-to-install-python-on-windows/), but I make no guarantees that Selenium will actually work as I have not tested it. - - -### Bugs +## Bugs If it stops working or otherwise crashes, delete the latest post manually and start it again after waiting a minute. I make no guarantees that it will work -- 2.30.2 From 5c111631804929f3e020ec3248863f5033b424e5 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 24 May 2019 21:02:52 -0400 Subject: [PATCH 008/186] Integrate changes that allow selecting year --- deletefb/deletefb.py | 14 ++++++++++++-- deletefb/tools/login.py | 2 -- deletefb/tools/wall.py | 10 +++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index b6f3715..d3d3ddb 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -53,6 +53,15 @@ def run_delete(): help="Run browser in headless mode (no gui)" ) + parser.add_argument( + "-Y", + "--year", + required=False, + dest="year", + type=str, + help="The year(s) you want posts deleted." + ) + args = parser.parse_args() args_user_password = args.password or getpass.getpass('Enter your password: ') @@ -60,12 +69,13 @@ def run_delete(): driver = login( user_email_address=args.email, user_password=args_user_password, - user_profile_url=args.profile_url, is_headless=args.is_headless, two_factor_token=args.two_factor_token ) - wall.delete_posts(driver) + wall.delete_posts(driver, + args.profile_url, + year=args.year) if __name__ == "__main__": run_delete() diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 38aa5bf..012253d 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -5,7 +5,6 @@ from seleniumrequests import Chrome def login(user_email_address, user_password, - user_profile_url, is_headless, two_factor_token): """ @@ -66,5 +65,4 @@ def login(user_email_address, time.sleep(20) print("Continuing execution") - driver.get(user_profile_url) return driver diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 3005dd1..0017a4c 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -5,10 +5,18 @@ from .common import SELENIUM_EXCEPTIONS MAX_POSTS = 5000 -def delete_posts(driver): +def delete_posts(driver, + user_profile_url, + year=None): """ 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) + for _ in range(MAX_POSTS): post_button_sel = "_4xev" -- 2.30.2 From d0be69bbbb6c0249d6124c5a27aad04e6d7476a3 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 24 May 2019 21:32:51 -0400 Subject: [PATCH 009/186] (Hopefully) less buggy 2FA --- deletefb/tools/login.py | 48 +++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 012253d..ce0f424 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -2,6 +2,7 @@ import time from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome +from selenium.common.exceptions import NoSuchElementException def login(user_email_address, user_password, @@ -46,23 +47,34 @@ def login(user_email_address, loginelement = driver.find_element_by_id(login) loginelement.click() - if "two-factor authentication" in driver.page_source.lower(): - if two_factor_token: - - 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() - else: - # Allow time to enter 2FA code - print("Pausing to enter 2FA code") - time.sleep(20) - print("Continuing execution") + # Defaults to no 2fa + has_2fa = False + + try: + # If this element exists, we've reached a 2FA page + driver.find_element_by_xpath("//form[@class=\"checkpoint\"]") + driver.find_element_by_xpath("//input[@name=\"approvals_code\"]") + has_2fa = True + except NoSuchElementException: + has_2fa = "two-factor authentication" in driver.page_source.lower() or has_2fa + + 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 return driver -- 2.30.2 From 266b5697731660336e6e8e01b2d6d7dc4593a73c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 24 May 2019 22:09:25 -0400 Subject: [PATCH 010/186] Add a warning when 2FA is enabled --- deletefb/tools/login.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index ce0f424..2dea088 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -58,6 +58,12 @@ def login(user_email_address, except NoSuchElementException: has_2fa = "two-factor authentication" in driver.page_source.lower() or has_2fa + if has_2fa: + print(""" + Two-Factor Auth is enabled. + Please file an issue at https://github.com/weskerfoot/DeleteFB/issues if you run into any problems + """) + if two_factor_token and has_2fa: twofactorelement = driver.find_element_by_name(approvals_code) twofactorelement.send_keys(two_factor_token) -- 2.30.2 From db015cc28183545185c2e2ed48ab788e7d48d3b1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 25 May 2019 14:07:12 -0400 Subject: [PATCH 011/186] Add new `mode` parameter, work on unliking pages --- deletefb/deletefb.py | 26 +++++++++++++++++++++++--- deletefb/tools/likes.py | 20 ++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index d3d3ddb..b51915f 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -3,11 +3,25 @@ import argparse import getpass +from sys import exit from deletefb.tools.login import login import deletefb.tools.wall as wall +import deletefb.tools.likes as likes def run_delete(): 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( "-E", "--email", @@ -73,9 +87,15 @@ def run_delete(): two_factor_token=args.two_factor_token ) - wall.delete_posts(driver, - args.profile_url, - year=args.year) + if args.mode == "wall": + wall.delete_posts(driver, + args.profile_url, + year=args.year) + elif args.mode == "unlike_pages": + likes.unlike_pages(driver, args.profile_url) + else: + print("Please enter a valid mode") + exit(1) if __name__ == "__main__": run_delete() diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 80c6b0a..edc1669 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,8 +1,24 @@ +from time import sleep from selenium.webdriver.common.action_chains import ActionChains from .common import SELENIUM_EXCEPTIONS -def unlike_pages(driver): +def unlike_pages(driver, + user_profile_url): """ Unlike all pages """ - return + + actions = ActionChains(driver) + + driver.get("https://www.facebook.com/pages/?category=liked") + + pages_list = driver.find_element_by_css_selector("#all_liked_pages") + + actions.move_to_element(pages_list) + + unlike_buttons = pages_list.find_elements_by_xpath("//button") + + for button in unlike_buttons: + print(button) + + sleep(1000) -- 2.30.2 From e10f57751dcf0e28c8d7cf83ac1339a444c20a24 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 12:42:24 -0400 Subject: [PATCH 012/186] Updates to unlikes (not quite working yet) --- deletefb/tools/common.py | 29 +++++++++++++++++++++++++++++ deletefb/tools/likes.py | 28 ++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 92405c4..0bf3df1 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,3 +1,32 @@ from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException +from time import sleep +from json import dumps +from os.path import abspath, relpath, split SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) + +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 = abspath(relpath(split(category)[-1], ".")) + + log_file = open(log_path, mode="wt", buffering=1) + + def log(content): + structured_content = {"category" : category, "content" : content} + log_file.write("{0}\n".format(dumps(structured_content))) + + return (log_file, log) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index edc1669..ed01fff 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,6 +1,9 @@ from time import sleep +from selenium.webdriver.common.by import By 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 unlike_pages(driver, user_profile_url): @@ -8,17 +11,34 @@ def unlike_pages(driver, Unlike all pages """ + like_log, archive_likes = archiver("likes") + actions = ActionChains(driver) driver.get("https://www.facebook.com/pages/?category=liked") - pages_list = driver.find_element_by_css_selector("#all_liked_pages") + wait = WebDriverWait(driver, 20) - actions.move_to_element(pages_list) + wait.until( + EC.presence_of_element_located((By.XPATH, "//div[text()='Liked']")) + ) + sleep(10) + + 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") for button in unlike_buttons: + try: + actions.move_to_element(button).perform() + page_name = button.find_element_by_xpath("./../..").text.split("\n")[0] + archive_likes(page_name) + + except SELENIUM_EXCEPTIONS as e: + print(e) + continue print(button) - sleep(1000) + # Explicitly close the log file when we're done with it + like_log.close() -- 2.30.2 From cc6886112c0a5508fc8d47125e6e989c10239cff Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 12:47:24 -0400 Subject: [PATCH 013/186] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 745c8c5..eee8a36 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.0.4", + version="1.0.5", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From fddd373d142a36f0161406b7975fb920632bf88f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 15:08:32 -0400 Subject: [PATCH 014/186] Bug the user if they do not have chromedriver installed --- deletefb/tools/common.py | 6 ++++++ deletefb/tools/login.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 0bf3df1..4b6cbea 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -30,3 +30,9 @@ def archiver(category): 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 +""" diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 2dea088..0a1ddd7 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,8 +1,10 @@ import time +from sys import stderr, exit from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome from selenium.common.exceptions import NoSuchElementException +from .common import no_chrome_driver def login(user_email_address, user_password, @@ -28,7 +30,15 @@ def login(user_email_address, chrome_options.add_argument('--disable-gpu') 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.get("https://facebook.com") -- 2.30.2 From a26fcf42f3e6a4407c864088848d33341b9e74d0 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 16:41:31 -0400 Subject: [PATCH 015/186] Unliking pages working --- deletefb/tools/common.py | 8 +++-- deletefb/tools/likes.py | 66 ++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 4b6cbea..29d9341 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,9 +1,13 @@ -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 -SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException) +SELENIUM_EXCEPTIONS = (NoSuchElementException, + StaleElementReferenceException, + TimeoutException) def try_move(actions, el): for _ in range(10): diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index ed01fff..e74ec7a 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,10 +1,28 @@ -from time import sleep from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains 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, user_profile_url): """ @@ -15,30 +33,40 @@ def unlike_pages(driver, actions = ActionChains(driver) - driver.get("https://www.facebook.com/pages/?category=liked") + load_likes(driver) - wait = WebDriverWait(driver, 20) + pages_list = driver.find_element_by_css_selector("#all_liked_pages") - wait.until( - EC.presence_of_element_located((By.XPATH, "//div[text()='Liked']")) - ) + actions.move_to_element(pages_list).perform() - sleep(10) + unlike_buttons = pages_list.find_elements_by_xpath("//button/div/div[text()='Liked']/../..") - 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") + 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] + + print("Trying to unlike {0}".format(page_name)) + + driver.execute_script("arguments[0].click();", button) + + archive_likes(page_name) + + print("{0} was unliked".format(page_name)) + + except SELENIUM_EXCEPTIONS as e: + continue - for button in unlike_buttons: + load_likes(driver) try: - actions.move_to_element(button).perform() - page_name = button.find_element_by_xpath("./../..").text.split("\n")[0] - archive_likes(page_name) - - except SELENIUM_EXCEPTIONS as e: - print(e) - continue - print(button) + 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() -- 2.30.2 From 6bfd72912539ef7298c55d807de40fe2e9127c98 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 16:45:02 -0400 Subject: [PATCH 016/186] Remove unnecessary parameter --- deletefb/deletefb.py | 2 +- deletefb/tools/likes.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index b51915f..4507c68 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -92,7 +92,7 @@ def run_delete(): args.profile_url, year=args.year) elif args.mode == "unlike_pages": - likes.unlike_pages(driver, args.profile_url) + likes.unlike_pages(driver) else: print("Please enter a valid mode") exit(1) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index e74ec7a..6914145 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -23,8 +23,7 @@ def load_likes(driver): except SELENIUM_EXCEPTIONS: return -def unlike_pages(driver, - user_profile_url): +def unlike_pages(driver): """ Unlike all pages """ @@ -47,8 +46,6 @@ def unlike_pages(driver, if "Liked" in button.text: page_name = button.find_element_by_xpath("./../..").text.split("\n")[0] - print("Trying to unlike {0}".format(page_name)) - driver.execute_script("arguments[0].click();", button) archive_likes(page_name) -- 2.30.2 From 9bb624eaa5d89c614b6a79753a2abbe4fab0a0a3 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 26 May 2019 16:51:26 -0400 Subject: [PATCH 017/186] Guard against invalid combos of options --- deletefb/deletefb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 4507c68..da96208 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -78,6 +78,9 @@ def run_delete(): args = parser.parse_args() + 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: ') driver = login( -- 2.30.2 From 89a6c2de1deb2109fb5e844d3082fe5af3cc7fc3 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:28:47 -0400 Subject: [PATCH 018/186] Use relative imports to allow direct execution --- deletefb/deletefb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index b6f3715..b52f956 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -3,8 +3,8 @@ import argparse import getpass -from deletefb.tools.login import login -import deletefb.tools.wall as wall +from .tools.login import login +from .tools.wall import delete_posts def run_delete(): parser = argparse.ArgumentParser() @@ -65,7 +65,7 @@ def run_delete(): two_factor_token=args.two_factor_token ) - wall.delete_posts(driver) + delete_posts(driver) if __name__ == "__main__": run_delete() -- 2.30.2 From 06d7c5a45da666e109fd7ee1c2465ca613dc5f1c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:29:35 -0400 Subject: [PATCH 019/186] Change how it is executed directly in a venv --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index fca93c9..9db2e87 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,7 @@ You have several options to run it. 2) Clone this repo and run `pip install --user .` or do `pip install --user git+https://github.com/weskerfoot/DeleteFB.git` 3) Set up a Python virtualenv, activate it, and run `pip install -r -requirements.txt`, then you can just run `python deletefb/deletefb.py` as you -would if you had installed it from PyPI. +requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the project directory, as you would if you had installed it from PyPI. ## How To Use It -- 2.30.2 From f20c4c6283cda14637b8d2119f91fd2bab6adc30 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:44:03 -0400 Subject: [PATCH 020/186] Clarify chromedriver instructions --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9db2e87..54f2497 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,16 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the ## How To Use It * Make sure that you have Google Chrome installed and that it is up to date -* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/downloads). On Arch Linux you can find this in the `chromium` package, and on Ubuntu it is `chromium-chromedriver`. +* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/downloads). + * On Linux, it will be called something like `chromium-chromedriver` or just + `chromium`. + * On MacOS, it will be available via brew, with the following commands: + + ``` + brew tap homebrew/cask; + brew cask install chromedriver + ``` + * Run `deletefb -E "youremail@example.org" -P "yourfacebookpassword" -U "https://www.facebook.com/your.profile.url"` * The script will log into your Facebook account, go to your profile page, and start deleting posts. If it cannot delete something, then it will "hide" it -- 2.30.2 From 6fcb0adb0bf9914af77e14a142d2891eb5f3e83a Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:44:53 -0400 Subject: [PATCH 021/186] Clarify chromedriver more --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54f2497..79cc5ea 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the ## How To Use It * Make sure that you have Google Chrome installed and that it is up to date -* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/downloads). +* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/downloads) for an explanation of what the chromedriver does. * On Linux, it will be called something like `chromium-chromedriver` or just `chromium`. * On MacOS, it will be available via brew, with the following commands: -- 2.30.2 From 565e01f6d0499f2b405ef50c006d772068e253fe Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:46:08 -0400 Subject: [PATCH 022/186] Update link to chromedriver to point to about page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79cc5ea..aa9e64a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the ## How To Use It * Make sure that you have Google Chrome installed and that it is up to date -* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/downloads) for an explanation of what the chromedriver does. +* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/home) for an explanation of what the chromedriver does. * On Linux, it will be called something like `chromium-chromedriver` or just `chromium`. * On MacOS, it will be available via brew, with the following commands: -- 2.30.2 From 99a30df3de327ec44672b704c0e992c0f614db11 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 27 May 2019 01:52:25 -0400 Subject: [PATCH 023/186] Remove stray import --- deletefb/deletefb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 72d223a..0e91240 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -4,7 +4,6 @@ import argparse import getpass from sys import exit -from deletefb.tools.login import login from .tools.login import login from .tools.wall import delete_posts from .tools.likes import unlike_pages -- 2.30.2 From 643929d3dd96048ef3e15d89d124f1f4293aa3a0 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 01:21:51 -0400 Subject: [PATCH 024/186] Archive wall posts, include timestamp --- deletefb/tools/common.py | 11 ++++++++--- deletefb/tools/wall.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 29d9341..4b95ea1 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -25,12 +25,17 @@ def archiver(category): call archiver like archive("some content") """ - log_path = abspath(relpath(split(category)[-1], ".")) + log_path = "{0}.log".format(abspath(relpath(split(category)[-1], "."))) log_file = open(log_path, mode="wt", buffering=1) - def log(content): - structured_content = {"category" : category, "content" : content} + def log(content, timestamp=False): + structured_content = { + "category" : category, + "content" : content, + "timestamp" : timestamp + } + log_file.write("{0}\n".format(dumps(structured_content))) return (log_file, log) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0017a4c..a318c54 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,9 +1,10 @@ import time 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, user_profile_url, @@ -17,12 +18,23 @@ def delete_posts(driver, driver.get(user_profile_url) + wall_log, archive_wall_post = archiver("wall") + for _ in range(MAX_POSTS): post_button_sel = "_4xev" + post_content_sel = "_5_jv" + + post_timestamp_sel = "timestamp" while True: try: 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.move_to_element(timeline_element).click().perform() -- 2.30.2 From 056fd75aae9983285d27bfd3d5449c311d566ff7 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 01:46:01 -0400 Subject: [PATCH 025/186] Add feature to disable logging --- README.md | 13 +++++++++++++ deletefb/deletefb.py | 12 ++++++++++++ deletefb/tools/common.py | 3 +++ 3 files changed, 28 insertions(+) diff --git a/README.md b/README.md index aa9e64a..b6eaa6b 100644 --- a/README.md +++ b/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 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 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`. +## 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 * 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. diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 0e91240..23e275d 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -4,6 +4,7 @@ import argparse import getpass from sys import exit +from os import environ from .tools.login import login from .tools.wall import delete_posts from .tools.likes import unlike_pages @@ -67,6 +68,14 @@ def run_delete(): 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", @@ -78,6 +87,9 @@ def run_delete(): 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") diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 4b95ea1..9dc9e38 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -4,6 +4,7 @@ from selenium.common.exceptions import (NoSuchElementException, from time import sleep from json import dumps from os.path import abspath, relpath, split +from os import environ SELENIUM_EXCEPTIONS = (NoSuchElementException, StaleElementReferenceException, @@ -30,6 +31,8 @@ def archiver(category): 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, -- 2.30.2 From 39ddca80c55fd3f8b4ae2c71f3f00b2d8f796f77 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 01:48:35 -0400 Subject: [PATCH 026/186] Update readme with info about unliking pages --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b6eaa6b..d210890 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,11 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the * The tool supports passing the `--year` flag in order to delete wall posts by year. It is incompatible with any mode other than `wall`. +## Unlike Pages +* You may use `-M unlike_pages` to unlike all of your pages. The names of the + pages will be archived (unless archival is turned off), and this option + conflicts with the year option. + ## 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 -- 2.30.2 From 75dbf645db5825de3f010b2a50d9332f03e7502b Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 01:50:03 -0400 Subject: [PATCH 027/186] Clean up README some more --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d210890..da85a81 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the * You may also pass in a code by using the `-F` argument, e.g. `-F 111111`. -## Delete by year +## 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`. + year. E.g. `-Y 2010` would delete posts from the year 2010. It is incompatible with any mode other than `wall`. ## Unlike Pages * You may use `-M unlike_pages` to unlike all of your pages. The names of the -- 2.30.2 From 24adaa47a9312f1bd08704de44273bb0f91c5d29 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 01:50:22 -0400 Subject: [PATCH 028/186] New minor release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index eee8a36..9ff96c8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.0.5", + version="1.1.0", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From a469b8f5f7dc94ad2b189900efbfae80e047238d Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 28 May 2019 02:06:22 -0400 Subject: [PATCH 029/186] correct 2fa timeout --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da85a81..4b2b4c3 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the * 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 35 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`. -- 2.30.2 From 1241d6e7e89863d95f037c350722dd52a6aa44f8 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 29 May 2019 00:08:31 -0400 Subject: [PATCH 030/186] Fix bugs with logging and bump version --- deletefb/deletefb.py | 2 +- deletefb/tools/common.py | 2 +- deletefb/tools/wall.py | 9 +++++---- setup.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 23e275d..e7cc186 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -72,7 +72,7 @@ def run_delete(): "--no-archive", action="store_true", dest="archive_off", - default=True, + default=False, help="Turn off archiving (on by default)" ) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 9dc9e38..50fb71d 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -28,7 +28,7 @@ def archiver(category): """ log_path = "{0}.log".format(abspath(relpath(split(category)[-1], "."))) - log_file = open(log_path, mode="wt", buffering=1) + log_file = open(log_path, mode="ta", buffering=1) def log(content, timestamp=False): if environ.get("DELETEFB_ARCHIVE", "true") == "false": diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index a318c54..0264841 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -18,13 +18,13 @@ def delete_posts(driver, driver.get(user_profile_url) - wall_log, archive_wall_post = archiver("wall") - for _ in range(MAX_POSTS): post_button_sel = "_4xev" - post_content_sel = "_5_jv" - post_timestamp_sel = "timestamp" + post_content_sel = "userContent" + post_timestamp_sel = "timestampContent" + + wall_log, archive_wall_post = archiver("wall") while True: try: @@ -55,6 +55,7 @@ def delete_posts(driver, continue else: break + wall_log.close() # Required to sleep the thread for a bit after using JS to click this button time.sleep(5) diff --git a/setup.py b/setup.py index 9ff96c8..7e3f445 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.0", + version="1.1.1", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 4401318b2a5b2463c3f787c4ad9d4a0e476f1060 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 29 May 2019 00:14:32 -0400 Subject: [PATCH 031/186] Clarify archiving logic in setup --- deletefb/deletefb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index e7cc186..6f670ca 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -88,7 +88,10 @@ def run_delete(): args = parser.parse_args() if args.archive_off: - environ["DELETEFB_ARCHIVE"] = "false" if args.archive_off else "true" + environ["DELETEFB_ARCHIVE"] = "false" + else: + environ["DELETEFB_ARCHIVE"] = "true" + if args.year and args.mode != "wall": parser.error("The --year option is only supported in wall mode") -- 2.30.2 From e76266f966eb4ddbdcc5768552603fc56b358d6e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 30 May 2019 11:06:06 -0400 Subject: [PATCH 032/186] Try to use untag button if delete or hide is not an option --- deletefb/tools/wall.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0264841..4af01d0 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -44,7 +44,10 @@ def delete_posts(driver, try: delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"FeedDeleteOption\"]") except SELENIUM_EXCEPTIONS: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"HIDE_FROM_TIMELINE\"]") + try: + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"HIDE_FROM_TIMELINE\"]") + except SELENIUM_EXCEPTIONS: + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"UNTAG\"]") actions.move_to_element(delete_button).click().perform() confirmation_button = driver.find_element_by_class_name("layerConfirm") -- 2.30.2 From 8bf8617c15f4d6a0b4cff7edc1fcb93bae115111 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Fri, 31 May 2019 19:57:55 -0400 Subject: [PATCH 033/186] Refactor --- deletefb/deletefb.py | 12 +++++----- deletefb/tools/common.py | 49 ++++++++++++++++++++++++---------------- deletefb/tools/likes.py | 16 ++++++++++++- deletefb/tools/login.py | 28 ++++++++++++++--------- deletefb/tools/wall.py | 9 +++++++- 5 files changed, 75 insertions(+), 39 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 6f670ca..b6cc483 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -1,10 +1,10 @@ -#! /usr/bin/env python +#!/usr/bin/env python import argparse import getpass +import sys +import os -from sys import exit -from os import environ from .tools.login import login from .tools.wall import delete_posts from .tools.likes import unlike_pages @@ -88,9 +88,9 @@ def run_delete(): args = parser.parse_args() if args.archive_off: - environ["DELETEFB_ARCHIVE"] = "false" + os.environ["DELETEFB_ARCHIVE"] = "false" else: - environ["DELETEFB_ARCHIVE"] = "true" + os.environ["DELETEFB_ARCHIVE"] = "true" if args.year and args.mode != "wall": @@ -113,7 +113,7 @@ def run_delete(): unlike_pages(driver) else: print("Please enter a valid mode") - exit(1) + sys.exit(1) if __name__ == "__main__": run_delete() diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 50fb71d..e086df6 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,50 +1,59 @@ -from selenium.common.exceptions import (NoSuchElementException, - StaleElementReferenceException, - TimeoutException) -from time import sleep -from json import dumps +import json +import os from os.path import abspath, relpath, split -from os import environ +import time -SELENIUM_EXCEPTIONS = (NoSuchElementException, - StaleElementReferenceException, - TimeoutException) +from selenium.common.exceptions import ( + NoSuchElementException, + StaleElementReferenceException, + TimeoutException +) + + +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) + time.sleep(5) continue def archiver(category): """ - category: the category of logs you want to log - return values: (log_file_handle, archiver) + Log content to file. Call using `archive("some content")` + + Args: + category: str The category of logs you want to log - call archiver like archive("some content") + Returns: + (log_file_handle, archiver) """ log_path = "{0}.log".format(abspath(relpath(split(category)[-1], "."))) log_file = open(log_path, mode="ta", buffering=1) def log(content, timestamp=False): - if environ.get("DELETEFB_ARCHIVE", "true") == "false": + if os.environ.get("DELETEFB_ARCHIVE", "true") == "false": return structured_content = { - "category" : category, - "content" : content, - "timestamp" : timestamp - } + "category" : category, + "content" : content, + "timestamp" : timestamp + } - log_file.write("{0}\n".format(dumps(structured_content))) + log_file.write("{0}\n".format(json.dumps(structured_content))) return (log_file, log) -no_chrome_driver = """ +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 """ diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 6914145..abef381 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -2,11 +2,19 @@ from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains 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 + + Args: + driver: seleniumrequests.Chrome Driver instance + + Returns: + None """ driver.get("https://www.facebook.com/pages/?category=liked") @@ -26,6 +34,12 @@ def load_likes(driver): def unlike_pages(driver): """ Unlike all pages + + Args: + driver: seleniumrequests.Chrome Driver instance + + Returns: + None """ like_log, archive_likes = archiver("likes") @@ -52,7 +66,7 @@ def unlike_pages(driver): print("{0} was unliked".format(page_name)) - except SELENIUM_EXCEPTIONS as e: + except SELENIUM_EXCEPTIONS: continue load_likes(driver) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 0a1ddd7..847649d 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,10 +1,12 @@ import time +import sys -from sys import stderr, exit +from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome -from selenium.common.exceptions import NoSuchElementException -from .common import no_chrome_driver + +from .common import NO_CHROME_DRIVER + def login(user_email_address, user_password, @@ -14,9 +16,13 @@ def login(user_email_address, Attempts to log into Facebook Returns a driver object - user_email_address: str Your Email - user_password: str Your password - user_profile_url: str Your profile URL + Args: + user_email_address: str Your email + user_password: str Your password + user_profile_url: str Your profile URL + + Returns: + seleniumrequests.Chrome instance """ # The Chrome driver is required because Gecko was having issues @@ -35,9 +41,9 @@ def login(user_email_address, 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) + sys.stderr.write(str(e)) + sys.stderr.write(NO_CHROME_DRIVER) + sys.exit(1) driver.implicitly_wait(10) @@ -45,7 +51,7 @@ def login(user_email_address, email = "email" password = "pass" - login = "loginbutton" + login_button = "loginbutton" approvals_code = "approvals_code" emailelement = driver.find_element_by_name(email) @@ -54,7 +60,7 @@ def login(user_email_address, emailelement.send_keys(user_email_address) passwordelement.send_keys(user_password) - loginelement = driver.find_element_by_id(login) + loginelement = driver.find_element_by_id(login_button) loginelement.click() # Defaults to no 2fa diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0264841..26da422 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,19 +1,26 @@ import time from selenium.webdriver.common.action_chains import ActionChains + from .common import SELENIUM_EXCEPTIONS, archiver # Used as a threshold to avoid running forever MAX_POSTS = 15000 + def delete_posts(driver, user_profile_url, year=None): """ Deletes or hides all posts from the wall + + Args: + driver: seleniumrequests.Chrome Driver instance + user_profile_url: str + year: optional int YYYY year """ - if not year is None: + if year is not None: user_profile_url = "{0}/timeline?year={1}".format(user_profile_url, year) driver.get(user_profile_url) -- 2.30.2 From c89f7a1d6cfd1fa9264483321f55bdedc905e28c Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sat, 1 Jun 2019 10:05:39 -0400 Subject: [PATCH 034/186] Add basic logging --- deletefb/deletefb.log | 0 deletefb/deletefb.py | 14 ++++++--- deletefb/logging_conf.json | 62 ++++++++++++++++++++++++++++++++++++++ deletefb/tools/common.py | 13 +++++++- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 deletefb/deletefb.log create mode 100644 deletefb/logging_conf.json diff --git a/deletefb/deletefb.log b/deletefb/deletefb.log new file mode 100644 index 0000000..e69de29 diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index b6cc483..832280a 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -2,12 +2,18 @@ import argparse import getpass -import sys +import json import os +import sys + +from tools.common import logger +from tools.login import login +from tools.wall import delete_posts +from tools.likes import unlike_pages + + +LOG = logger(__name__) -from .tools.login import login -from .tools.wall import delete_posts -from .tools.likes import unlike_pages def run_delete(): parser = argparse.ArgumentParser() diff --git a/deletefb/logging_conf.json b/deletefb/logging_conf.json new file mode 100644 index 0000000..2078e80 --- /dev/null +++ b/deletefb/logging_conf.json @@ -0,0 +1,62 @@ +{ + "logging": { + "version": 1, + "disable_existing_loggers": true, + "formatters": { + "brief": { + "class": "logging.Formatter", + "style": "{", + "datefmt": "%H:%M:%S", + "format": "{name:s}-{levelname:s}-{asctime:s}-{message:s}" + }, + "verbose": { + "class": "logging.Formatter", + "style": "{", + "datefmt": "%Y-%m-%dT%H:%M:%S", + "format": "{name:s}:{levelname:s}:L{lineno:d} {asctime:s} {message:s}" + } + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "brief", + "stream": "ext://sys.stdout" + }, + "file_handler": { + "level": "INFO", + "class": "logging.handlers.WatchedFileHandler", + "formatter": "verbose", + "filename": "deletefb/deletefb.log", + "mode": "a", + "encoding": "utf-8" + } + }, + "loggers": { + "root": { + "level": "DEBUG", + "handlers": ["console", "file_handler"] + }, + "deletefb": { + "level": "DEBUG", + "handlers": ["console"], + "propagate": false + }, + "login": { + "level": "DEBUG", + "handlers": ["file_handler"], + "propagate": false + }, + "likes": { + "level": "DEBUG", + "handlers": ["file_handler"], + "propagate": false + }, + "wall": { + "level": "DEBUG", + "handlers": ["file_handler"], + "propagate": false + } + } + } +} diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index e086df6..b50dc90 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,6 +1,8 @@ import json +import logging +import logging.config import os -from os.path import abspath, relpath, split +from os.path import abspath, relpath, split, isfile import time from selenium.common.exceptions import ( @@ -24,6 +26,15 @@ def try_move(actions, el): time.sleep(5) continue +def logger(name: str): + # called from directory (__main__.py) + config_path = "deletefb/logging_conf.json" + if not isfile(config_path): # called from file (deletefb.py) + os.chdir("..") + with open(config_path, "r", encoding="utf-8") as config_file: + config = json.load(config_file) + logging.config.dictConfig(config["logging"]) + return logging.getLogger("deletefb") def archiver(category): """ -- 2.30.2 From 5a42296b306576f7a94eaaab2599ae6bf89ff80d Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sat, 1 Jun 2019 10:06:09 -0400 Subject: [PATCH 035/186] Create __main__.py Allow for it to be run from outer directory with `python deletefb` --- deletefb/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 deletefb/__main__.py diff --git a/deletefb/__main__.py b/deletefb/__main__.py new file mode 100644 index 0000000..c735177 --- /dev/null +++ b/deletefb/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import deletefb + +if __name__ == "__main__": + deletefb.run_delete() -- 2.30.2 From 6884c12c404b5225f112336f708cb08a8c192d52 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sat, 1 Jun 2019 11:01:22 -0400 Subject: [PATCH 036/186] Add docstring and fix hardcoded strings --- deletefb/deletefb.py | 3 +-- deletefb/tools/common.py | 12 +++++++++--- deletefb/tools/likes.py | 5 ++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 832280a..54d0c66 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -11,8 +11,7 @@ from tools.login import login from tools.wall import delete_posts from tools.likes import unlike_pages - -LOG = logger(__name__) +LOG = logger("deletefb") def run_delete(): diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index b50dc90..fe97881 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -26,15 +26,21 @@ def try_move(actions, el): time.sleep(5) continue -def logger(name: str): - # called from directory (__main__.py) +def logger(name): + """ + Args: + name (str): Logger name + + Returns: + logging.Logger + """ config_path = "deletefb/logging_conf.json" if not isfile(config_path): # called from file (deletefb.py) os.chdir("..") with open(config_path, "r", encoding="utf-8") as config_file: config = json.load(config_file) logging.config.dictConfig(config["logging"]) - return logging.getLogger("deletefb") + return logging.getLogger(name) def archiver(category): """ diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index abef381..cbb8886 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -3,7 +3,9 @@ from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from .common import SELENIUM_EXCEPTIONS, archiver +from .common import SELENIUM_EXCEPTIONS, archiver, logger + +LOG = logger(__name__) def load_likes(driver): @@ -29,6 +31,7 @@ def load_likes(driver): EC.presence_of_element_located((By.XPATH, "//button/div/i[@aria-hidden=\"true\"]")) ) except SELENIUM_EXCEPTIONS: + LOG.exception("Traceback of load_likes") return def unlike_pages(driver): -- 2.30.2 From b7287634de00770d03aed4e4c9d38572d73993d3 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Sat, 1 Jun 2019 12:06:24 -0400 Subject: [PATCH 037/186] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b2b4c3..c882e17 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,7 @@ You have several options to run it. 1) Install from PyPI with `pip install --user delete-facebook-posts` 2) Clone this repo and run `pip install --user .` or do `pip install --user git+https://github.com/weskerfoot/DeleteFB.git` -3) Set up a Python virtualenv, activate it, and run `pip install -r -requirements.txt`, then you can just run `python -m deletefb.deletefb.py` in the project directory, as you would if you had installed it from PyPI. +3) Set up a Python virtualenv, activate it, and run `pip install -r requirements.txt`, then you can just run `python deletefb` in the DeleteFB directory. ## How To Use It -- 2.30.2 From 69313de81152ee5f20d0a1078cc6b2b6d83a31bb Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 17:12:01 -0400 Subject: [PATCH 038/186] Refactor settings --- deletefb/config.py | 3 +++ deletefb/deletefb.py | 17 ++++++++--------- deletefb/tools/common.py | 7 +++++-- 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 deletefb/config.py diff --git a/deletefb/config.py b/deletefb/config.py new file mode 100644 index 0000000..16fb42a --- /dev/null +++ b/deletefb/config.py @@ -0,0 +1,3 @@ +settings = { + "ARCHIVE" : False +} diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 54d0c66..c69d42b 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -6,6 +6,7 @@ import json import os import sys +from config import settings from tools.common import logger from tools.login import login from tools.wall import delete_posts @@ -13,7 +14,6 @@ from tools.likes import unlike_pages LOG = logger("deletefb") - def run_delete(): parser = argparse.ArgumentParser() @@ -92,11 +92,7 @@ def run_delete(): args = parser.parse_args() - if args.archive_off: - os.environ["DELETEFB_ARCHIVE"] = "false" - else: - os.environ["DELETEFB_ARCHIVE"] = "true" - + settings["ARCHIVE"] = not args.archive_off if args.year and args.mode != "wall": parser.error("The --year option is only supported in wall mode") @@ -111,9 +107,12 @@ def run_delete(): ) if args.mode == "wall": - delete_posts(driver, - args.profile_url, - year=args.year) + delete_posts( + driver, + args.profile_url, + year=args.year + ) + elif args.mode == "unlike_pages": unlike_pages(driver) else: diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index fe97881..e9b99af 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -2,9 +2,11 @@ import json import logging import logging.config import os -from os.path import abspath, relpath, split, isfile import time +from config import settings + +from os.path import abspath, relpath, split, isfile from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, @@ -57,8 +59,9 @@ def archiver(category): log_file = open(log_path, mode="ta", buffering=1) def log(content, timestamp=False): - if os.environ.get("DELETEFB_ARCHIVE", "true") == "false": + if not settings["ARCHIVE"]: return + structured_content = { "category" : category, "content" : content, -- 2.30.2 From e52dc89e07a5f0e1e03a209b5cac64f41d937ce6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 17:20:17 -0400 Subject: [PATCH 039/186] Default should be 'True' for archive --- deletefb/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/config.py b/deletefb/config.py index 16fb42a..8ca2556 100644 --- a/deletefb/config.py +++ b/deletefb/config.py @@ -1,3 +1,3 @@ settings = { - "ARCHIVE" : False + "ARCHIVE" : True } -- 2.30.2 From e8e9a3434f54fbcc2ee6b8bb4f4963c12374729f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 17:41:35 -0400 Subject: [PATCH 040/186] See if a driver.refresh() helps with unliking pages --- deletefb/tools/common.py | 1 - deletefb/tools/likes.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index e9b99af..79815ec 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -13,7 +13,6 @@ from selenium.common.exceptions import ( TimeoutException ) - SELENIUM_EXCEPTIONS = ( NoSuchElementException, StaleElementReferenceException, diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index cbb8886..c332ba0 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -18,6 +18,8 @@ def load_likes(driver): Returns: None """ + + driver.refresh() driver.get("https://www.facebook.com/pages/?category=liked") wait = WebDriverWait(driver, 20) -- 2.30.2 From 88e4703889c7b273f459fa1c1c9c6a183863ea52 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 23:21:24 -0400 Subject: [PATCH 041/186] Fix imports --- deletefb/deletefb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 54d0c66..db7dc25 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -6,10 +6,10 @@ import json import os import sys -from tools.common import logger -from tools.login import login -from tools.wall import delete_posts -from tools.likes import unlike_pages +from .tools.common import logger +from .tools.login import login +from .tools.wall import delete_posts +from .tools.likes import unlike_pages LOG = logger("deletefb") -- 2.30.2 From ab81a7f9d67e30014576176ab57053e5eed2ace4 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 23:30:31 -0400 Subject: [PATCH 042/186] Fix docs on how to call package manually --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c882e17..eea8211 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ You have several options to run it. 1) Install from PyPI with `pip install --user delete-facebook-posts` 2) Clone this repo and run `pip install --user .` or do `pip install --user git+https://github.com/weskerfoot/DeleteFB.git` -3) Set up a Python virtualenv, activate it, and run `pip install -r requirements.txt`, then you can just run `python deletefb` in the DeleteFB directory. +3) Set up a Python virtualenv, activate it, and run `pip install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. ## How To Use It -- 2.30.2 From 169321e9cc4c2931a8a427e0b8ca293d70ad2fb9 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 1 Jun 2019 23:39:15 -0400 Subject: [PATCH 043/186] Refactor settings --- deletefb/deletefb.py | 1 + deletefb/tools/common.py | 2 +- deletefb/{ => tools}/config.py | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename deletefb/{ => tools}/config.py (100%) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 0e4f546..484b5f9 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -6,6 +6,7 @@ import json import os import sys +from .tools.config import settings from .tools.common import logger from .tools.login import login from .tools.wall import delete_posts diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 79815ec..06df691 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -4,7 +4,7 @@ import logging.config import os import time -from config import settings +from .config import settings from os.path import abspath, relpath, split, isfile from selenium.common.exceptions import ( diff --git a/deletefb/config.py b/deletefb/tools/config.py similarity index 100% rename from deletefb/config.py rename to deletefb/tools/config.py -- 2.30.2 From 23ba502b657ef51b7f3c9de217f6a99096ac34ef Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 01:59:41 -0400 Subject: [PATCH 044/186] Grab all URLs from page like page --- deletefb/deletefb.py | 2 +- deletefb/tools/likes.py | 48 +++++++++++------------------------------ 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 484b5f9..20db6e3 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -114,7 +114,7 @@ def run_delete(): ) elif args.mode == "unlike_pages": - unlike_pages(driver) + unlike_pages(driver, args.profile_url) else: print("Please enter a valid mode") sys.exit(1) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index c332ba0..be86926 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -2,13 +2,14 @@ from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +from time import sleep from .common import SELENIUM_EXCEPTIONS, archiver, logger LOG = logger(__name__) -def load_likes(driver): +def load_likes(driver, profile_url): """ Loads the page that lists all pages you like @@ -19,24 +20,19 @@ def load_likes(driver): None """ - driver.refresh() - driver.get("https://www.facebook.com/pages/?category=liked") + driver.get("{0}/likes_all".format(profile_url)) 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\"]")) + EC.presence_of_element_located((By.CSS_SELECTOR, ".PageLikeButton")) ) except SELENIUM_EXCEPTIONS: LOG.exception("Traceback of load_likes") return -def unlike_pages(driver): +def unlike_pages(driver, profile_url): """ Unlike all pages @@ -49,40 +45,20 @@ def unlike_pages(driver): like_log, archive_likes = archiver("likes") + wait = WebDriverWait(driver, 20) actions = ActionChains(driver) - load_likes(driver) - - pages_list = driver.find_element_by_css_selector("#all_liked_pages") + load_likes(driver, profile_url) - actions.move_to_element(pages_list).perform() + pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") - 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) + actions = ActionChains(driver) - print("{0} was unliked".format(page_name)) + page_urls = [page.get_attribute("href").replace("www", "mobile") for page in pages] - except SELENIUM_EXCEPTIONS: - continue + for url in page_urls: + driver.get(url) - 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() -- 2.30.2 From 2b1346720d5d32c5de50ee7c59cf56f2cc2b5b10 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 02:24:45 -0400 Subject: [PATCH 045/186] Refactor page links --- deletefb/tools/likes.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index be86926..c1cb508 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -32,6 +32,24 @@ def load_likes(driver, profile_url): LOG.exception("Traceback of load_likes") return + +def get_page_links(driver): + pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") + + actions = ActionChains(driver) + + return [page.get_attribute("href").replace("www", "mobile") for page in pages] + + +def unlike_page(driver, url): + driver.get(url) + + actions = ActionChains(driver) + + button_element = find_element_by_xpath("//span[text()='Unlike']/..") + actions.move_to_element(button_element).click().perform() + + def unlike_pages(driver, profile_url): """ Unlike all pages @@ -45,20 +63,14 @@ def unlike_pages(driver, profile_url): like_log, archive_likes = archiver("likes") - wait = WebDriverWait(driver, 20) - actions = ActionChains(driver) - load_likes(driver, profile_url) - pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") - - actions = ActionChains(driver) - - page_urls = [page.get_attribute("href").replace("www", "mobile") for page in pages] - - for url in page_urls: - driver.get(url) + urls = get_page_links(driver) + while urls: + for url in urls: + print(unlike_page(driver, url)) + urls = get_page_links(driver) # Explicitly close the log file when we're done with it like_log.close() -- 2.30.2 From 0d1e047aedd0f969a2ffe44bfc1994932f10d5c6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 15:21:26 -0400 Subject: [PATCH 046/186] Page unliking working using mobile pages --- deletefb/tools/likes.py | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index c1cb508..1677979 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -8,7 +8,6 @@ from .common import SELENIUM_EXCEPTIONS, archiver, logger LOG = logger(__name__) - def load_likes(driver, profile_url): """ Loads the page that lists all pages you like @@ -22,7 +21,7 @@ def load_likes(driver, profile_url): driver.get("{0}/likes_all".format(profile_url)) - wait = WebDriverWait(driver, 20) + wait = WebDriverWait(driver, 30) try: wait.until( @@ -34,6 +33,14 @@ def load_likes(driver, profile_url): def get_page_links(driver): + """ + Gets all of the links to the pages you like + + Args: + driver: seleniumrequests.Chrome Driver instance + Returns: + List of URLs to pages + """ pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") actions = ActionChains(driver) @@ -42,12 +49,36 @@ def get_page_links(driver): def unlike_page(driver, url): + """ + Unlikes a page given the URL to it + Args: + driver: seleniumrequests.Chrome Driver instance + url: url string pointing to a page + + Returns: + None + + """ driver.get(url) + wait = WebDriverWait(driver, 30) actions = ActionChains(driver) - button_element = find_element_by_xpath("//span[text()='Unlike']/..") - actions.move_to_element(button_element).click().perform() + wait.until( + EC.presence_of_element_located((By.XPATH, "//*[text()='Liked']")) + ) + + button = driver.find_element_by_xpath("//*[text()='Liked']") + + driver.execute_script("arguments[0].click();", button) + + wait.until( + EC.presence_of_element_located((By.XPATH, "//a/span[text()='Unlike']")) + ) + + unlike_button = driver.find_element_by_xpath("//a/span[text()='Unlike']/..") + + driver.execute_script("arguments[0].click();", unlike_button) def unlike_pages(driver, profile_url): @@ -69,8 +100,13 @@ def unlike_pages(driver, profile_url): while urls: for url in urls: - print(unlike_page(driver, url)) - urls = get_page_links(driver) + unlike_page(driver, url) + load_likes(driver, profile_url) + try: + urls = get_page_links(driver) + except SELENIUM_EXCEPTIONS: + # We're done + break # Explicitly close the log file when we're done with it like_log.close() -- 2.30.2 From 940a94afed1e7ae2de97bb8b06db551b521b0fe9 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 15:29:07 -0400 Subject: [PATCH 047/186] Clean up --- deletefb/tools/common.py | 16 +++++++++------- deletefb/tools/likes.py | 9 +++++---- deletefb/tools/wall.py | 8 +++----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 06df691..cc9e95f 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -19,13 +19,15 @@ SELENIUM_EXCEPTIONS = ( TimeoutException ) -def try_move(actions, el): - for _ in range(10): - try: - actions.move_to_element(el).perform() - except StaleElementReferenceException: - time.sleep(5) - continue +def click_button(driver, el): + """ + Click a button using Javascript + Args: + driver: seleniumrequests.Chrome Driver instance + Returns: + None + """ + driver.execute_script("arguments[0].click();", el) def logger(name): """ diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 1677979..9498f2e 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -2,9 +2,8 @@ from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from time import sleep -from .common import SELENIUM_EXCEPTIONS, archiver, logger +from .common import SELENIUM_EXCEPTIONS, archiver, logger, click_button LOG = logger(__name__) @@ -70,15 +69,17 @@ def unlike_page(driver, url): button = driver.find_element_by_xpath("//*[text()='Liked']") - driver.execute_script("arguments[0].click();", button) + # Click the "Liked" button to open up "Unlike" + click_button(driver, button) wait.until( EC.presence_of_element_located((By.XPATH, "//a/span[text()='Unlike']")) ) + # There should be an "Unlike" button now, click it unlike_button = driver.find_element_by_xpath("//a/span[text()='Unlike']/..") - driver.execute_script("arguments[0].click();", unlike_button) + click_button(driver, unlike_button) def unlike_pages(driver, profile_url): diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 9983e41..f164305 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,13 +1,11 @@ import time - from selenium.webdriver.common.action_chains import ActionChains -from .common import SELENIUM_EXCEPTIONS, archiver +from .common import SELENIUM_EXCEPTIONS, archiver, click_button # Used as a threshold to avoid running forever MAX_POSTS = 15000 - def delete_posts(driver, user_profile_url, year=None): @@ -59,8 +57,8 @@ def delete_posts(driver, actions.move_to_element(delete_button).click().perform() confirmation_button = driver.find_element_by_class_name("layerConfirm") - # Facebook would not let me get focus on this button without some custom JS - driver.execute_script("arguments[0].click();", confirmation_button) + click_button(driver, confirmation_button) + except SELENIUM_EXCEPTIONS: continue else: -- 2.30.2 From bf6ce1d615deef413975f801212b68730e54a945 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 15:38:40 -0400 Subject: [PATCH 048/186] Match anything with "Unlike" for pages because FB keeps changing the elements --- deletefb/tools/likes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 9498f2e..a245961 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -59,6 +59,7 @@ def unlike_page(driver, url): """ driver.get(url) + print(url) wait = WebDriverWait(driver, 30) actions = ActionChains(driver) @@ -73,11 +74,11 @@ def unlike_page(driver, url): click_button(driver, button) wait.until( - EC.presence_of_element_located((By.XPATH, "//a/span[text()='Unlike']")) + EC.presence_of_element_located((By.XPATH, "//*[text()='Unlike']")) ) # There should be an "Unlike" button now, click it - unlike_button = driver.find_element_by_xpath("//a/span[text()='Unlike']/..") + unlike_button = driver.find_element_by_xpath("//*[text()='Unlike']/..") click_button(driver, unlike_button) -- 2.30.2 From 139801c37cf0dfc20b5a7c67d1554d84c4d99876 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 15:50:44 -0400 Subject: [PATCH 049/186] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eea8211..ddc4fc5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ git+https://github.com/weskerfoot/DeleteFB.git` ## Unlike Pages * You may use `-M unlike_pages` to unlike all of your pages. The names of the pages will be archived (unless archival is turned off), and this option - conflicts with the year option. + conflicts with the year option. This will only unlike your *pages* that you + have liked. It will *not* unlike anything else (like books or movies). ## Archival * The tool will archive everything being deleted by default in `.log` files. -- 2.30.2 From 2580f4594e57b1e3eff7488537a7418f1e310198 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 16:26:13 -0400 Subject: [PATCH 050/186] Bump version and refactor to use bloom filters for avoiding dups --- deletefb/tools/common.py | 14 ++++++++++++++ deletefb/tools/config.py | 3 ++- deletefb/tools/likes.py | 14 ++++++++++---- deletefb/tools/wall.py | 3 ++- setup.py | 5 +++-- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index cc9e95f..c107abf 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -6,6 +6,9 @@ import time from .config import settings +# Used to avoid duplicates in the log +from pybloom_live import BloomFilter + from os.path import abspath, relpath, split, isfile from selenium.common.exceptions import ( NoSuchElementException, @@ -59,10 +62,19 @@ def archiver(category): log_file = open(log_path, mode="ta", buffering=1) + bfilter = BloomFilter( + capacity=settings["MAX_POSTS"], + error_rate=0.001 + ) + def log(content, timestamp=False): if not settings["ARCHIVE"]: return + if content in bfilter: + # This was already archived + return + structured_content = { "category" : category, "content" : content, @@ -71,6 +83,8 @@ def archiver(category): log_file.write("{0}\n".format(json.dumps(structured_content))) + bfilter.add(content) + return (log_file, log) diff --git a/deletefb/tools/config.py b/deletefb/tools/config.py index 8ca2556..078efca 100644 --- a/deletefb/tools/config.py +++ b/deletefb/tools/config.py @@ -1,3 +1,4 @@ settings = { - "ARCHIVE" : True + "ARCHIVE" : True, + "MAX_POSTS" : 5000 } diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index a245961..3adb151 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -47,19 +47,23 @@ def get_page_links(driver): return [page.get_attribute("href").replace("www", "mobile") for page in pages] -def unlike_page(driver, url): +def unlike_page(driver, url, archive=None): """ Unlikes a page given the URL to it Args: driver: seleniumrequests.Chrome Driver instance url: url string pointing to a page + archive: archiver instance Returns: None """ + driver.get(url) - print(url) + + print("Unliking {0}".format(url)) + wait = WebDriverWait(driver, 30) actions = ActionChains(driver) @@ -82,6 +86,8 @@ def unlike_page(driver, url): click_button(driver, unlike_button) + if archive: + archive(url) def unlike_pages(driver, profile_url): """ @@ -102,9 +108,9 @@ def unlike_pages(driver, profile_url): while urls: for url in urls: - unlike_page(driver, url) - load_likes(driver, profile_url) + unlike_page(driver, url, archive=archive_likes) try: + load_likes(driver, profile_url) urls = get_page_links(driver) except SELENIUM_EXCEPTIONS: # We're done diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index f164305..5dcd6ec 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,10 +1,11 @@ import time from selenium.webdriver.common.action_chains import ActionChains +from .config import settings from .common import SELENIUM_EXCEPTIONS, archiver, click_button # Used as a threshold to avoid running forever -MAX_POSTS = 15000 +MAX_POSTS = settings["MAX_POSTS"] def delete_posts(driver, user_profile_url, diff --git a/setup.py b/setup.py index 7e3f445..ce75ca6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.1", + version="1.1.2", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", @@ -16,7 +16,8 @@ setuptools.setup( install_requires = [ "selenium", "selenium-requests", - "requests" + "requests", + "pybloom-live" ], classifiers= [ "Programming Language :: Python :: 3", -- 2.30.2 From 5cc653b2d5ebded2e4ac4d2f818eb39020def776 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 16:30:18 -0400 Subject: [PATCH 051/186] Use single quotes in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ddc4fc5..5263b14 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ git+https://github.com/weskerfoot/DeleteFB.git` brew cask install chromedriver ``` -* Run `deletefb -E "youremail@example.org" -P "yourfacebookpassword" -U "https://www.facebook.com/your.profile.url"` +* Run `deletefb -E 'youremail@example.org' -P 'yourfacebookpassword' -U 'https://www.facebook.com/your.profile.url'` * The script will log into your Facebook account, go to your profile page, and start deleting posts. If it cannot delete something, then it will "hide" it from your timeline instead. -- 2.30.2 From f4b015b95f0fb1bf55b6451dae0087fdc4aece23 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 16:42:20 -0400 Subject: [PATCH 052/186] Try to move elegantly handle broken pages --- deletefb/tools/likes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 3adb151..9533eaa 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -64,13 +64,17 @@ def unlike_page(driver, url, archive=None): print("Unliking {0}".format(url)) - wait = WebDriverWait(driver, 30) + wait = WebDriverWait(driver, 60) actions = ActionChains(driver) - wait.until( - EC.presence_of_element_located((By.XPATH, "//*[text()='Liked']")) - ) + try: + wait.until( + EC.presence_of_element_located((By.XPATH, "//*[text()='Liked']")) + ) + except SELENIUM_EXCEPTIONS: + # Something went wrong with this page, so skip it + return button = driver.find_element_by_xpath("//*[text()='Liked']") -- 2.30.2 From b9db5d0eb2eae6d5fb2aa155a5f056efd1c8d714 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 16:55:22 -0400 Subject: [PATCH 053/186] Cleanup --- deletefb/tools/likes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 9533eaa..daa8789 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -20,7 +20,7 @@ def load_likes(driver, profile_url): driver.get("{0}/likes_all".format(profile_url)) - wait = WebDriverWait(driver, 30) + wait = WebDriverWait(driver, 20) try: wait.until( @@ -30,7 +30,6 @@ def load_likes(driver, profile_url): LOG.exception("Traceback of load_likes") return - def get_page_links(driver): """ Gets all of the links to the pages you like @@ -46,7 +45,6 @@ def get_page_links(driver): return [page.get_attribute("href").replace("www", "mobile") for page in pages] - def unlike_page(driver, url, archive=None): """ Unlikes a page given the URL to it @@ -64,7 +62,7 @@ def unlike_page(driver, url, archive=None): print("Unliking {0}".format(url)) - wait = WebDriverWait(driver, 60) + wait = WebDriverWait(driver, 20) actions = ActionChains(driver) -- 2.30.2 From e1ec0a89a6da35dbd0469bae36a00745048e27dc Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 2 Jun 2019 17:09:40 -0400 Subject: [PATCH 054/186] Try to refactor delete options --- deletefb/tools/wall.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 5dcd6ec..0b854ea 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -30,6 +30,8 @@ def delete_posts(driver, post_content_sel = "userContent" post_timestamp_sel = "timestampContent" + button_types = ["FeedDeleteOption", "HIDE_FROM_TIMELINE", "UNTAG"] + wall_log, archive_wall_post = archiver("wall") while True: @@ -47,13 +49,18 @@ def delete_posts(driver, menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") actions.move_to_element(menu).perform() - try: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"FeedDeleteOption\"]") - except SELENIUM_EXCEPTIONS: + delete_button = None + + for button_type in button_types: try: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"HIDE_FROM_TIMELINE\"]") + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"{0}\"]".format(button_type)) + break except SELENIUM_EXCEPTIONS: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"UNTAG\"]") + continue + + if not delete_button: + print("Could not find anything to delete") + break actions.move_to_element(delete_button).click().perform() confirmation_button = driver.find_element_by_class_name("layerConfirm") -- 2.30.2 From 63beee5154abb5bb5f6f07cecb0d279ac8667550 Mon Sep 17 00:00:00 2001 From: Arshad Ahmad Date: Mon, 3 Jun 2019 13:03:22 +0800 Subject: [PATCH 055/186] Corrected typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5263b14..5a6c430 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ git+https://github.com/weskerfoot/DeleteFB.git` everything. You may safely minimize the chrome window without breaking it. ## 2FA -* It is recommended that you disable Two-Factor Authentication tempoprarily +* It is recommended that you disable Two-Factor Authentication temporarily while you are running the script, in order to get the best experience. * If you run into issues with Facebook complaining about your browser, -- 2.30.2 From f8804931838e9fadbb74cd7e86c9683e77595de2 Mon Sep 17 00:00:00 2001 From: wes Date: Mon, 3 Jun 2019 10:30:12 -0400 Subject: [PATCH 056/186] Fix error with missing data file --- deletefb/tools/common.py | 2 +- setup.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index c107abf..a33f32b 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -40,7 +40,7 @@ def logger(name): Returns: logging.Logger """ - config_path = "deletefb/logging_conf.json" + config_path = "../logging_conf.json" if not isfile(config_path): # called from file (deletefb.py) os.chdir("..") with open(config_path, "r", encoding="utf-8") as config_file: diff --git a/setup.py b/setup.py index ce75ca6..1a4eedf 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,11 @@ setuptools.setup( long_description_content_type="text/markdown", url="https://github.com/weskerfoot/DeleteFB", packages=setuptools.find_packages(), + include_package_data=True, + package_data={ + # If any package contains *.txt or *.rst files, include them: + '': ['*.json'], + }, install_requires = [ "selenium", "selenium-requests", -- 2.30.2 From 73e98905c0d8263fe404c8976f3d25e6605b2729 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 3 Jun 2019 10:40:20 -0400 Subject: [PATCH 057/186] Construct path to logging conf using __file__ variable --- deletefb/tools/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index a33f32b..8cc4385 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -40,7 +40,11 @@ def logger(name): Returns: logging.Logger """ - config_path = "../logging_conf.json" + + # Make sure the path always points to the correct directory + config_path = os.path.dirname( + os.path.realpath(__file__)) + "/../logging_conf.json" + if not isfile(config_path): # called from file (deletefb.py) os.chdir("..") with open(config_path, "r", encoding="utf-8") as config_file: -- 2.30.2 From 9bc86fa644735d06dd346b3b6fa3b522e3fc6942 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 3 Jun 2019 10:40:48 -0400 Subject: [PATCH 058/186] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a4eedf..19c796c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.2", + version="1.1.3", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 2ac334c70f59c236b37ea1dd32b6c780e3c61e82 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 3 Jun 2019 10:48:27 -0400 Subject: [PATCH 059/186] Change logging confg path --- deletefb/logging_conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/logging_conf.json b/deletefb/logging_conf.json index 2078e80..e8037ec 100644 --- a/deletefb/logging_conf.json +++ b/deletefb/logging_conf.json @@ -27,7 +27,7 @@ "level": "INFO", "class": "logging.handlers.WatchedFileHandler", "formatter": "verbose", - "filename": "deletefb/deletefb.log", + "filename": "./deletefb.log", "mode": "a", "encoding": "utf-8" } -- 2.30.2 From 238e6ef69aba5d8dfba5e2bfbf336c43e0571ca6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 3 Jun 2019 10:48:54 -0400 Subject: [PATCH 060/186] Bump minor version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 19c796c..87e82db 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.3", + version="1.1.4", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 04ea30917bd76d14958af05932d028c0b71d55df Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 3 Jun 2019 11:09:14 -0400 Subject: [PATCH 061/186] Fix comment in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 87e82db..22f6959 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setuptools.setup( packages=setuptools.find_packages(), include_package_data=True, package_data={ - # If any package contains *.txt or *.rst files, include them: + # Include *json files in the package: '': ['*.json'], }, install_requires = [ -- 2.30.2 From dbf3649583d39f2c5993dc08528ab30373c2fe9f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 7 Jun 2019 01:07:05 -0400 Subject: [PATCH 062/186] Refactor in progress --- deletefb/tools/archive.py | 57 +++++++++++++++++++++++++++++++++++++++ deletefb/tools/common.py | 42 ----------------------------- 2 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 deletefb/tools/archive.py diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py new file mode 100644 index 0000000..107ee38 --- /dev/null +++ b/deletefb/tools/archive.py @@ -0,0 +1,57 @@ +import attr +import datetime +import uuid +import json + +from contextlib import contextmanager +from pathlib import Path + + +# Used to avoid duplicates in the log +from pybloom_live import BloomFilter + +def acquire_path(): + + log_file = open(log_path, mode="ta", buffering=1) + + bfilter = BloomFilter( + capacity=settings["MAX_POSTS"], + error_rate=0.001 + ) + + return + +@attr.s +class Post: + content = attr.ib() + comments = attr.ib(default=[]) + date = attr.ib(factory=datetime.datetime.now) + name = attr.ib(factory=uuid.uuid4) + +@attr.s +class Comment: + commenter = attr.ib() + content = attr.ib() + date = attr.ib(factory=datetime.datetime.now) + name = attr.ib(factory=uuid.uuid4) + +@attr.s +class Archive: + archive_type = attr.ib() + + # We give the Archive class a file handle + # This is better because the archive function + # should not know about anything related to filesystem paths + archive_file = attr.ib() + + def archive(self, content): + # do something + print("Archiving type {0} with content {1} to directory {2}".format(self.archive_type, content, self.archive_dir)) + self.archive_file.write(json.dumps(content.asdict())) + +wall_archive = Archive(archive_type="wall") + +comments = [Comment(commenter="Bob", content="Nice post!"), + Comment(commenter="Alice", content="I hate this")] + +wall_archive.archive(Post(content="A post!", comments=comments)) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 8cc4385..a831fc3 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -6,9 +6,6 @@ import time from .config import settings -# Used to avoid duplicates in the log -from pybloom_live import BloomFilter - from os.path import abspath, relpath, split, isfile from selenium.common.exceptions import ( NoSuchElementException, @@ -52,45 +49,6 @@ def logger(name): logging.config.dictConfig(config["logging"]) return logging.getLogger(name) -def archiver(category): - """ - Log content to file. Call using `archive("some content")` - - Args: - category: str The category of logs you want to log - - Returns: - (log_file_handle, archiver) - """ - log_path = "{0}.log".format(abspath(relpath(split(category)[-1], "."))) - - log_file = open(log_path, mode="ta", buffering=1) - - bfilter = BloomFilter( - capacity=settings["MAX_POSTS"], - error_rate=0.001 - ) - - def log(content, timestamp=False): - if not settings["ARCHIVE"]: - return - - if content in bfilter: - # This was already archived - return - - structured_content = { - "category" : category, - "content" : content, - "timestamp" : timestamp - } - - log_file.write("{0}\n".format(json.dumps(structured_content))) - - bfilter.add(content) - - return (log_file, log) - NO_CHROME_DRIVER = """ You need to install the chromedriver for Selenium\n -- 2.30.2 From 8bc51b88d7598755979a644b53f0eae47b929d5d Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 16:47:21 -0400 Subject: [PATCH 063/186] New archiver semi-working --- deletefb/tools/archive.py | 29 +++++++++++----- deletefb/tools/likes.py | 30 ++++++++-------- deletefb/tools/wall.py | 72 +++++++++++++++++++++------------------ 3 files changed, 73 insertions(+), 58 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 107ee38..c8f3e4c 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -11,7 +11,6 @@ from pathlib import Path from pybloom_live import BloomFilter def acquire_path(): - log_file = open(log_path, mode="ta", buffering=1) bfilter = BloomFilter( @@ -26,14 +25,15 @@ class Post: content = attr.ib() comments = attr.ib(default=[]) date = attr.ib(factory=datetime.datetime.now) - name = attr.ib(factory=uuid.uuid4) + name = attr.ib(factory=lambda: uuid.uuid4().hex) + @attr.s class Comment: commenter = attr.ib() content = attr.ib() date = attr.ib(factory=datetime.datetime.now) - name = attr.ib(factory=uuid.uuid4) + name = attr.ib(factory=lambda: uuid.uuid4().hex) @attr.s class Archive: @@ -46,12 +46,23 @@ class Archive: def archive(self, content): # do something - print("Archiving type {0} with content {1} to directory {2}".format(self.archive_type, content, self.archive_dir)) - self.archive_file.write(json.dumps(content.asdict())) + print("Archiving type {0} with content {1} to file".format(self.archive_type, content)) + self.archive_file.write(json.dumps(attr.asdict(content))) -wall_archive = Archive(archive_type="wall") + def close(self): + self.archive_file.close() -comments = [Comment(commenter="Bob", content="Nice post!"), - Comment(commenter="Alice", content="I hate this")] +@contextmanager +def archiver(archive_type): + archiver_instance = Archive(archive_type=archive_type, + archive_file=open( + "./%s.log" % archive_type, + mode="ta", + buffering=1 + ) + ) -wall_archive.archive(Post(content="A post!", comments=comments)) + try: + yield archiver_instance + finally: + archiver_instance.close() diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index daa8789..3246d85 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -3,7 +3,8 @@ from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from .common import SELENIUM_EXCEPTIONS, archiver, logger, click_button +from .common import SELENIUM_EXCEPTIONS, logger, click_button +from .archive import archiver LOG = logger(__name__) @@ -102,21 +103,18 @@ def unlike_pages(driver, profile_url): None """ - like_log, archive_likes = archiver("likes") + with archiver("likes") as archive_likes: + load_likes(driver, profile_url) - load_likes(driver, profile_url) + urls = get_page_links(driver) - urls = get_page_links(driver) + while urls: + for url in urls: + unlike_page(driver, url, archive=archive_likes.archive) + try: + load_likes(driver, profile_url) + urls = get_page_links(driver) + except SELENIUM_EXCEPTIONS: + # We're done + break - while urls: - for url in urls: - unlike_page(driver, url, archive=archive_likes) - try: - load_likes(driver, profile_url) - urls = get_page_links(driver) - except SELENIUM_EXCEPTIONS: - # We're done - break - - # Explicitly close the log file when we're done with it - like_log.close() diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0b854ea..f95d17e 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -2,7 +2,8 @@ import time from selenium.webdriver.common.action_chains import ActionChains from .config import settings -from .common import SELENIUM_EXCEPTIONS, archiver, click_button +from .common import SELENIUM_EXCEPTIONS, click_button +from .archive import archiver, Post # Used as a threshold to avoid running forever MAX_POSTS = settings["MAX_POSTS"] @@ -32,47 +33,52 @@ def delete_posts(driver, button_types = ["FeedDeleteOption", "HIDE_FROM_TIMELINE", "UNTAG"] - wall_log, archive_wall_post = archiver("wall") + with archiver("wall") as archive_wall_post: + while True: + try: + timeline_element = driver.find_element_by_class_name(post_button_sel) - while True: - try: - 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) - 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) + # Archive the post + archive_wall_post.archive( + Post( + content=post_content_element.text, + date=post_content_ts.text + ) + ) - actions = ActionChains(driver) - actions.move_to_element(timeline_element).click().perform() + actions = ActionChains(driver) + actions.move_to_element(timeline_element).click().perform() - menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") - actions.move_to_element(menu).perform() + menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") + actions.move_to_element(menu).perform() - delete_button = None + delete_button = None - for button_type in button_types: - try: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"{0}\"]".format(button_type)) - break - except SELENIUM_EXCEPTIONS: - continue + for button_type in button_types: + try: + delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"{0}\"]".format(button_type)) + break + except SELENIUM_EXCEPTIONS: + continue - if not delete_button: - print("Could not find anything to delete") - break + if not delete_button: + print("Could not find anything to delete") + break - actions.move_to_element(delete_button).click().perform() - confirmation_button = driver.find_element_by_class_name("layerConfirm") + actions.move_to_element(delete_button).click().perform() + confirmation_button = driver.find_element_by_class_name("layerConfirm") - click_button(driver, confirmation_button) + click_button(driver, confirmation_button) - except SELENIUM_EXCEPTIONS: - continue - else: - break - wall_log.close() + except SELENIUM_EXCEPTIONS: + continue + else: + break - # Required to sleep the thread for a bit after using JS to click this button - time.sleep(5) - driver.refresh() + # Required to sleep the thread for a bit after using JS to click this button + time.sleep(5) + driver.refresh() -- 2.30.2 From 8ac0351ab279815f1ffd4c2025fd6526dd3a8a9d Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 17:02:54 -0400 Subject: [PATCH 064/186] Cleanup & refactoring --- deletefb/deletefb.py | 12 ++++---- deletefb/tools/archive.py | 62 ++++++++++++++------------------------- deletefb/tools/common.py | 13 ++++---- deletefb/tools/likes.py | 18 ++++++++---- deletefb/tools/login.py | 8 ++--- deletefb/tools/wall.py | 26 +++++++++++++--- 6 files changed, 71 insertions(+), 68 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 20db6e3..a69dfea 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -1,17 +1,17 @@ #!/usr/bin/env python +from .tools.common import logger +from .tools.config import settings +from .tools.likes import unlike_pages +from .tools.login import login +from .tools.wall import delete_posts + import argparse import getpass import json import os import sys -from .tools.config import settings -from .tools.common import logger -from .tools.login import login -from .tools.wall import delete_posts -from .tools.likes import unlike_pages - LOG = logger("deletefb") def run_delete(): diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index c8f3e4c..c2e0e04 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -1,39 +1,17 @@ -import attr -import datetime -import uuid -import json - from contextlib import contextmanager from pathlib import Path +import attr +import json # Used to avoid duplicates in the log from pybloom_live import BloomFilter -def acquire_path(): - log_file = open(log_path, mode="ta", buffering=1) - - bfilter = BloomFilter( +def make_filter(): + return BloomFilter( capacity=settings["MAX_POSTS"], error_rate=0.001 - ) - - return - -@attr.s -class Post: - content = attr.ib() - comments = attr.ib(default=[]) - date = attr.ib(factory=datetime.datetime.now) - name = attr.ib(factory=lambda: uuid.uuid4().hex) - - -@attr.s -class Comment: - commenter = attr.ib() - content = attr.ib() - date = attr.ib(factory=datetime.datetime.now) - name = attr.ib(factory=lambda: uuid.uuid4().hex) + ) @attr.s class Archive: @@ -44,25 +22,29 @@ class Archive: # should not know about anything related to filesystem paths archive_file = attr.ib() + _bloom_filter = attr.ib(factory=make_filter) + def archive(self, content): - # do something - print("Archiving type {0} with content {1} to file".format(self.archive_type, content)) - self.archive_file.write(json.dumps(attr.asdict(content))) + """ + Archive an object + """ + print("Archiving {0}".format(content)) - def close(self): - self.archive_file.close() + if content.name not in self._bloom_filter: + self.archive_file.write(json.dumps(attr.asdict(content))) + self._bloom_filter.add(content.name) + return @contextmanager def archiver(archive_type): - archiver_instance = Archive(archive_type=archive_type, - archive_file=open( - "./%s.log" % archive_type, - mode="ta", - buffering=1 - ) - ) + archive_file = open("./%s.log" % archive_type, mode="ta", buffering=1) + + archiver_instance = Archive( + archive_type=archive_type, + archive_file=archive_file + ) try: yield archiver_instance finally: - archiver_instance.close() + archive_file.close() diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index a831fc3..b508afc 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,11 +1,4 @@ -import json -import logging -import logging.config -import os -import time - from .config import settings - from os.path import abspath, relpath, split, isfile from selenium.common.exceptions import ( NoSuchElementException, @@ -13,6 +6,12 @@ from selenium.common.exceptions import ( TimeoutException ) +import json +import logging +import logging.config +import os +import time + SELENIUM_EXCEPTIONS = ( NoSuchElementException, StaleElementReferenceException, diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 3246d85..9f8ef05 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,13 +1,19 @@ -from selenium.webdriver.common.by import By +from .archive import archiver +from .common import SELENIUM_EXCEPTIONS, logger, click_button from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC - -from .common import SELENIUM_EXCEPTIONS, logger, click_button -from .archive import archiver +from selenium.webdriver.support.ui import WebDriverWait +import attr LOG = logger(__name__) +# Data type definitions of posts and comments +@attr.s +class Page: + date = attr.ib(factory=datetime.datetime.now) + name = attr.ib() + def load_likes(driver, profile_url): """ Loads the page that lists all pages you like @@ -90,7 +96,7 @@ def unlike_page(driver, url, archive=None): click_button(driver, unlike_button) if archive: - archive(url) + archive(Page(name=url)) def unlike_pages(driver, profile_url): """ diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 847649d..2bca993 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,12 +1,10 @@ -import time -import sys - +from .common import NO_CHROME_DRIVER from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options from seleniumrequests import Chrome -from .common import NO_CHROME_DRIVER - +import sys +import time def login(user_email_address, user_password, diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index f95d17e..0fc94f2 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,9 +1,27 @@ -import time +from .archive import archiver, Post +from .common import SELENIUM_EXCEPTIONS, click_button +from .config import settings from selenium.webdriver.common.action_chains import ActionChains -from .config import settings -from .common import SELENIUM_EXCEPTIONS, click_button -from .archive import archiver, Post +import attr +import datetime +import time +import uuid + +# Data type definitions of posts and comments +@attr.s +class Post: + content = attr.ib() + comments = attr.ib(default=[]) + date = attr.ib(factory=datetime.datetime.now) + name = attr.ib(factory=lambda: uuid.uuid4().hex) + +@attr.s +class Comment: + commenter = attr.ib() + content = attr.ib() + date = attr.ib(factory=datetime.datetime.now) + name = attr.ib(factory=lambda: uuid.uuid4().hex) # Used as a threshold to avoid running forever MAX_POSTS = settings["MAX_POSTS"] -- 2.30.2 From f4ba552fc4a07b985beb73485ff7a9b86369599f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 17:06:21 -0400 Subject: [PATCH 065/186] Fix missing imports --- deletefb/deletefb.py | 1 - deletefb/tools/archive.py | 1 + deletefb/tools/likes.py | 4 +++- deletefb/tools/wall.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index a69dfea..befd51e 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - from .tools.common import logger from .tools.config import settings from .tools.likes import unlike_pages diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index c2e0e04..43a8f2b 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -1,3 +1,4 @@ +from .config import settings from contextlib import contextmanager from pathlib import Path diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 9f8ef05..afeed8e 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -4,15 +4,17 @@ from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait + import attr +import datetime LOG = logger(__name__) # Data type definitions of posts and comments @attr.s class Page: - date = attr.ib(factory=datetime.datetime.now) name = attr.ib() + date = attr.ib(factory=datetime.datetime.now) def load_likes(driver, profile_url): """ diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 0fc94f2..46a92c8 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,4 +1,4 @@ -from .archive import archiver, Post +from .archive import archiver from .common import SELENIUM_EXCEPTIONS, click_button from .config import settings from selenium.webdriver.common.action_chains import ActionChains -- 2.30.2 From f159937e1deeaee8614ebf32395687f64528b6eb Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 17:16:34 -0400 Subject: [PATCH 066/186] Fix bugs with archiver --- deletefb/tools/archive.py | 2 +- deletefb/tools/common.py | 7 +++++++ deletefb/tools/likes.py | 5 ++--- deletefb/tools/wall.py | 7 +++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 43a8f2b..831bf6e 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -32,7 +32,7 @@ class Archive: print("Archiving {0}".format(content)) if content.name not in self._bloom_filter: - self.archive_file.write(json.dumps(attr.asdict(content))) + self.archive_file.write(json.dumps(attr.asdict(content)) + "\n") self._bloom_filter.add(content.name) return diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index b508afc..fb08daf 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -6,6 +6,7 @@ from selenium.common.exceptions import ( TimeoutException ) +import datetime import json import logging import logging.config @@ -18,6 +19,12 @@ SELENIUM_EXCEPTIONS = ( TimeoutException ) +def timestamp_now(): + """ + Returns: a timestamp for this instant, in ISO 8601 format + """ + return datetime.datetime.isoformat(datetime.datetime.now()) + def click_button(driver, el): """ Click a button using Javascript diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index afeed8e..0ebce33 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,12 +1,11 @@ from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, logger, click_button +from .common import SELENIUM_EXCEPTIONS, logger, click_button, timestamp_now from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait import attr -import datetime LOG = logger(__name__) @@ -14,7 +13,7 @@ LOG = logger(__name__) @attr.s class Page: name = attr.ib() - date = attr.ib(factory=datetime.datetime.now) + date = attr.ib(factory=timestamp_now) def load_likes(driver, profile_url): """ diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 46a92c8..2b01816 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,10 +1,9 @@ from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, click_button +from .common import SELENIUM_EXCEPTIONS, click_button, timestamp_now from .config import settings from selenium.webdriver.common.action_chains import ActionChains import attr -import datetime import time import uuid @@ -13,14 +12,14 @@ import uuid class Post: content = attr.ib() comments = attr.ib(default=[]) - date = attr.ib(factory=datetime.datetime.now) + date = attr.ib(factory=timestamp_now) name = attr.ib(factory=lambda: uuid.uuid4().hex) @attr.s class Comment: commenter = attr.ib() content = attr.ib() - date = attr.ib(factory=datetime.datetime.now) + date = attr.ib(factory=timestamp_now) name = attr.ib(factory=lambda: uuid.uuid4().hex) # Used as a threshold to avoid running forever -- 2.30.2 From 91594a6cd47c36e6128302eb0abe1daf7262ee98 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 18:57:41 -0400 Subject: [PATCH 067/186] Refactor types into separate module --- deletefb/tools/common.py | 7 ------- deletefb/tools/likes.py | 11 ++--------- deletefb/tools/wall.py | 20 ++------------------ deletefb/types.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 deletefb/types.py diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index fb08daf..b508afc 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -6,7 +6,6 @@ from selenium.common.exceptions import ( TimeoutException ) -import datetime import json import logging import logging.config @@ -19,12 +18,6 @@ SELENIUM_EXCEPTIONS = ( TimeoutException ) -def timestamp_now(): - """ - Returns: a timestamp for this instant, in ISO 8601 format - """ - return datetime.datetime.isoformat(datetime.datetime.now()) - def click_button(driver, el): """ Click a button using Javascript diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 0ebce33..63ade1e 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,20 +1,13 @@ from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, logger, click_button, timestamp_now +from ..types import Page +from .common import SELENIUM_EXCEPTIONS, logger, click_button from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait -import attr - LOG = logger(__name__) -# Data type definitions of posts and comments -@attr.s -class Page: - name = attr.ib() - date = attr.ib(factory=timestamp_now) - def load_likes(driver, profile_url): """ Loads the page that lists all pages you like diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 2b01816..890bae0 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,26 +1,10 @@ +from ..types import Post from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, click_button, timestamp_now +from .common import SELENIUM_EXCEPTIONS, click_button from .config import settings from selenium.webdriver.common.action_chains import ActionChains -import attr import time -import uuid - -# Data type definitions of posts and comments -@attr.s -class Post: - content = attr.ib() - comments = attr.ib(default=[]) - date = attr.ib(factory=timestamp_now) - name = attr.ib(factory=lambda: uuid.uuid4().hex) - -@attr.s -class Comment: - commenter = attr.ib() - content = attr.ib() - date = attr.ib(factory=timestamp_now) - name = attr.ib(factory=lambda: uuid.uuid4().hex) # Used as a threshold to avoid running forever MAX_POSTS = settings["MAX_POSTS"] diff --git a/deletefb/types.py b/deletefb/types.py new file mode 100644 index 0000000..f171a9c --- /dev/null +++ b/deletefb/types.py @@ -0,0 +1,30 @@ +import attr +import time +import uuid +import datetime + +def timestamp_now(): + """ + Returns: a timestamp for this instant, in ISO 8601 format + """ + return datetime.datetime.isoformat(datetime.datetime.now()) + +# Data type definitions of posts and comments +@attr.s +class Post: + content = attr.ib() + comments = attr.ib(default=[]) + date = attr.ib(factory=timestamp_now) + name = attr.ib(factory=lambda: uuid.uuid4().hex) + +@attr.s +class Comment: + commenter = attr.ib() + content = attr.ib() + date = attr.ib(factory=timestamp_now) + name = attr.ib(factory=lambda: uuid.uuid4().hex) + +@attr.s +class Page: + name = attr.ib() + date = attr.ib(factory=timestamp_now) -- 2.30.2 From 0cebd82af8172d5690d714785e96ed47ccb3bdc4 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 19:03:08 -0400 Subject: [PATCH 068/186] Remove unused imports --- deletefb/deletefb.py | 2 -- deletefb/tools/archive.py | 1 - deletefb/tools/common.py | 4 +--- deletefb/tools/likes.py | 5 ----- deletefb/types.py | 1 - 5 files changed, 1 insertion(+), 12 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index befd51e..6688f4c 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -7,8 +7,6 @@ from .tools.wall import delete_posts import argparse import getpass -import json -import os import sys LOG = logger("deletefb") diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 831bf6e..36692d8 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -1,6 +1,5 @@ from .config import settings from contextlib import contextmanager -from pathlib import Path import attr import json diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index b508afc..6b6c74e 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,5 +1,4 @@ -from .config import settings -from os.path import abspath, relpath, split, isfile +from os.path import isfile from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, @@ -10,7 +9,6 @@ import json import logging import logging.config import os -import time SELENIUM_EXCEPTIONS = ( NoSuchElementException, diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 63ade1e..5a8497f 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,7 +1,6 @@ from .archive import archiver from ..types import Page from .common import SELENIUM_EXCEPTIONS, logger, click_button -from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -42,8 +41,6 @@ def get_page_links(driver): """ pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") - actions = ActionChains(driver) - return [page.get_attribute("href").replace("www", "mobile") for page in pages] def unlike_page(driver, url, archive=None): @@ -65,8 +62,6 @@ def unlike_page(driver, url, archive=None): wait = WebDriverWait(driver, 20) - actions = ActionChains(driver) - try: wait.until( EC.presence_of_element_located((By.XPATH, "//*[text()='Liked']")) diff --git a/deletefb/types.py b/deletefb/types.py index f171a9c..a771c67 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -1,5 +1,4 @@ import attr -import time import uuid import datetime -- 2.30.2 From 07cc35ce50e17cc57f4aef9bc7e28dba5674592c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 9 Jun 2019 19:07:04 -0400 Subject: [PATCH 069/186] Cleanup --- deletefb/tools/archive.py | 18 +++++++++++------- deletefb/tools/common.py | 4 +--- deletefb/tools/likes.py | 1 - 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 36692d8..cb4c41f 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -9,9 +9,9 @@ from pybloom_live import BloomFilter def make_filter(): return BloomFilter( - capacity=settings["MAX_POSTS"], - error_rate=0.001 - ) + capacity=settings["MAX_POSTS"], + error_rate=0.001 + ) @attr.s class Archive: @@ -33,15 +33,19 @@ class Archive: if content.name not in self._bloom_filter: self.archive_file.write(json.dumps(attr.asdict(content)) + "\n") self._bloom_filter.add(content.name) - return + return @contextmanager def archiver(archive_type): - archive_file = open("./%s.log" % archive_type, mode="ta", buffering=1) + archive_file = open( + "./{0}.log".format(archive_type), + mode="ta", + buffering=1 + ) archiver_instance = Archive( - archive_type=archive_type, - archive_file=archive_file + archive_type=archive_type, + archive_file=archive_file ) try: diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 6b6c74e..7f6364a 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -36,8 +36,7 @@ def logger(name): """ # Make sure the path always points to the correct directory - config_path = os.path.dirname( - os.path.realpath(__file__)) + "/../logging_conf.json" + config_path = os.path.dirname(os.path.realpath(__file__)) + "/../logging_conf.json" if not isfile(config_path): # called from file (deletefb.py) os.chdir("..") @@ -46,7 +45,6 @@ def logger(name): logging.config.dictConfig(config["logging"]) return logging.getLogger(name) - 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 diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 5a8497f..091a144 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -112,4 +112,3 @@ def unlike_pages(driver, profile_url): except SELENIUM_EXCEPTIONS: # We're done break - -- 2.30.2 From 17f00abff779d071c29158a3def2bf883bfc6232 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 10 Jun 2019 18:46:41 -0400 Subject: [PATCH 070/186] More robust handling of paths in archiver --- deletefb/tools/archive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index cb4c41f..1bc7364 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -1,5 +1,6 @@ from .config import settings from contextlib import contextmanager +from pathlib import Path import attr import json @@ -18,8 +19,6 @@ class Archive: archive_type = attr.ib() # We give the Archive class a file handle - # This is better because the archive function - # should not know about anything related to filesystem paths archive_file = attr.ib() _bloom_filter = attr.ib(factory=make_filter) @@ -37,8 +36,9 @@ class Archive: @contextmanager def archiver(archive_type): + archive_file = open( - "./{0}.log".format(archive_type), + (Path(".") / Path(archive_type).name).with_suffix(".log"), mode="ta", buffering=1 ) -- 2.30.2 From fc96e60c3a87516c7cea59bfbfc4a4fdec1447f0 Mon Sep 17 00:00:00 2001 From: wikijm Date: Thu, 13 Jun 2019 11:47:22 +0200 Subject: [PATCH 071/186] Add 'attr' as requirement Add 'attr' as requirement to avoid "ModuleNotFoundError: No module named 'attr'" error message. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0511c3e..fe3c239 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ selenium-requests==1.3 six==1.12.0 tldextract==2.2.0 urllib3==1.25.2 +attr -- 2.30.2 From 008f0c57ac08e84e849f1db54bba6e313d2a122c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 13 Jun 2019 10:55:51 -0400 Subject: [PATCH 072/186] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 22f6959..28d81fd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.4", + version="1.1.5", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From ad683182a3d41ab6d824a6b51054ada4f6c8d9be Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 13 Jun 2019 11:01:01 -0400 Subject: [PATCH 073/186] Update requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fe3c239..0780145 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ +attrs==19.1.0 +bitarray==0.9.3 certifi==2018.11.29 chardet==3.0.4 idna==2.8 +pybloom-live==3.0.0 requests==2.22.0 requests-file==1.4.3 selenium==3.141.0 @@ -8,4 +11,3 @@ selenium-requests==1.3 six==1.12.0 tldextract==2.2.0 urllib3==1.25.2 -attr -- 2.30.2 From cd95b0fe75b8937fcfc9ecd1ce496948cd03d680 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 13 Jun 2019 11:01:46 -0400 Subject: [PATCH 074/186] Update setup install requires --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28d81fd..896271a 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ setuptools.setup( "selenium", "selenium-requests", "requests", - "pybloom-live" + "pybloom-live", + "attrs" ], classifiers= [ "Programming Language :: Python :: 3", -- 2.30.2 From e3314f0d914411aa2959386b6337f8821c35b90c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 15 Jun 2019 11:54:43 -0400 Subject: [PATCH 075/186] Update README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a6c430..1ba5ba4 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,15 @@ Personally, I did this so I would feel less attached to my Facebook profile ## Installation You have several options to run it. -1) Install from PyPI with `pip install --user delete-facebook-posts` -2) Clone this repo and run `pip install --user .` or do `pip install --user +1) Install from PyPI with `pip3 install --user delete-facebook-posts` +2) Clone this repo and run `pip3 install --user .` or do `pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git` -3) Set up a Python virtualenv, activate it, and run `pip install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. +3) Set up a Python virtualenv, activate it, and run `pip3 install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. ## How To Use It +* Make sure that you have a recent version of Python 3.x installed (preferably + 3.6 or greater) * Make sure that you have Google Chrome installed and that it is up to date * Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/home) for an explanation of what the chromedriver does. * On Linux, it will be called something like `chromium-chromedriver` or just -- 2.30.2 From 469f9e1f5ce5f201434ecfb07cf05a9ef026bb5e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 15 Jun 2019 12:12:10 -0400 Subject: [PATCH 076/186] Force python 3.6 or greater in setuptools --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 896271a..76df64f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.5", + version="1.1.6", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", @@ -14,6 +14,7 @@ setuptools.setup( url="https://github.com/weskerfoot/DeleteFB", packages=setuptools.find_packages(), include_package_data=True, + requires_python=">=3.6", package_data={ # Include *json files in the package: '': ['*.json'], -- 2.30.2 From 41ab642f326add38b5ea763032107f6155ed09e9 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 16 Jun 2019 01:00:16 -0400 Subject: [PATCH 077/186] More robust login procedure --- deletefb/tools/login.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 2bca993..772b53e 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -45,7 +45,7 @@ def login(user_email_address, driver.implicitly_wait(10) - driver.get("https://facebook.com") + driver.get("https://www.facebook.com/login/device-based/regular/login/?login_attempt=1&lwv=110") email = "email" password = "pass" @@ -97,4 +97,10 @@ def login(user_email_address, else: pass + # block until we have reached the main page + # print a message warning the user + while driver.current_url != "https://www.facebook.com/": + print("Execution blocked: Please navigate to https://www.facebook.com to continue") + time.sleep(5) + return driver -- 2.30.2 From ceb29e0344512aa5ff1d1f115ab6374c8051fde2 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 16 Jun 2019 01:02:28 -0400 Subject: [PATCH 078/186] Update readme with info about login --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1ba5ba4..1f059fe 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ git+https://github.com/weskerfoot/DeleteFB.git` * Be patient as it will take a very long time, but it will eventually clear everything. You may safely minimize the chrome window without breaking it. +## Login +* The tool will log in using the credentials passed to it. It will wait until + the page "https://www.facebook.com/" is loaded in order to avoid any issues + with logging in. If you pass a 2FA token explicitly with the `-F` option, + then it will try to enter that for you. If there are any issues, it simply + pauses indefinitely to allow the user to resolve the problems, and then + continues execution. + ## 2FA * It is recommended that you disable Two-Factor Authentication temporarily while you are running the script, in order to get the best experience. -- 2.30.2 From 46f8324eb276e51067792cecf410bbbf90b4fe0f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 16 Jun 2019 01:07:28 -0400 Subject: [PATCH 079/186] New minor release 1.6.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 76df64f..a9b692d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.6", + version="1.1.7", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 0f06567c7f91c3dd810b257a11ab056a311227e3 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 16 Jun 2019 01:14:08 -0400 Subject: [PATCH 080/186] Clean up README --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 1f059fe..856e1f3 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,7 @@ git+https://github.com/weskerfoot/DeleteFB.git` everything. You may safely minimize the chrome window without breaking it. ## Login -* The tool will log in using the credentials passed to it. It will wait until - the page "https://www.facebook.com/" is loaded in order to avoid any issues - with logging in. If you pass a 2FA token explicitly with the `-F` option, - then it will try to enter that for you. If there are any issues, it simply - pauses indefinitely to allow the user to resolve the problems, and then - continues execution. +* The tool will log in using the credentials passed to it. It will wait until the page `https://www.facebook.com/` is loaded in order to avoid any issues with logging in. If you pass a 2FA token explicitly with the `-F` option, then it will try to enter that for you. If there are any issues, it simply pauses indefinitely to allow the user to resolve the problems, and then continues execution. ## 2FA * It is recommended that you disable Two-Factor Authentication temporarily -- 2.30.2 From 4fd7a5b32b67b260e8e7aa92378a69c970fc8665 Mon Sep 17 00:00:00 2001 From: Gregory Gundersen Date: Wed, 19 Jun 2019 15:40:39 +0100 Subject: [PATCH 081/186] Don't version control log output files. --- deletefb/deletefb.log | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 deletefb/deletefb.log diff --git a/deletefb/deletefb.log b/deletefb/deletefb.log deleted file mode 100644 index e69de29..0000000 -- 2.30.2 From bee0c958a2d21681505186e9363abcd4e3c60481 Mon Sep 17 00:00:00 2001 From: Gregory Gundersen Date: Wed, 19 Jun 2019 15:41:46 +0100 Subject: [PATCH 082/186] Don't version control log output files. --- deletefb/logging_conf.json | 62 -------------------------------------- 1 file changed, 62 deletions(-) delete mode 100644 deletefb/logging_conf.json diff --git a/deletefb/logging_conf.json b/deletefb/logging_conf.json deleted file mode 100644 index 2078e80..0000000 --- a/deletefb/logging_conf.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "logging": { - "version": 1, - "disable_existing_loggers": true, - "formatters": { - "brief": { - "class": "logging.Formatter", - "style": "{", - "datefmt": "%H:%M:%S", - "format": "{name:s}-{levelname:s}-{asctime:s}-{message:s}" - }, - "verbose": { - "class": "logging.Formatter", - "style": "{", - "datefmt": "%Y-%m-%dT%H:%M:%S", - "format": "{name:s}:{levelname:s}:L{lineno:d} {asctime:s} {message:s}" - } - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "brief", - "stream": "ext://sys.stdout" - }, - "file_handler": { - "level": "INFO", - "class": "logging.handlers.WatchedFileHandler", - "formatter": "verbose", - "filename": "deletefb/deletefb.log", - "mode": "a", - "encoding": "utf-8" - } - }, - "loggers": { - "root": { - "level": "DEBUG", - "handlers": ["console", "file_handler"] - }, - "deletefb": { - "level": "DEBUG", - "handlers": ["console"], - "propagate": false - }, - "login": { - "level": "DEBUG", - "handlers": ["file_handler"], - "propagate": false - }, - "likes": { - "level": "DEBUG", - "handlers": ["file_handler"], - "propagate": false - }, - "wall": { - "level": "DEBUG", - "handlers": ["file_handler"], - "propagate": false - } - } - } -} -- 2.30.2 From 6e8970b23f0b3a1cc24ef1b8aab4f3077a83a484 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 3 Jul 2019 19:25:26 -0400 Subject: [PATCH 083/186] Stubs for removing messages and comments, new type for convos --- deletefb/tools/comments.py | 15 +++++++++++++++ deletefb/tools/messages.py | 15 +++++++++++++++ deletefb/types.py | 6 ++++++ 3 files changed, 36 insertions(+) create mode 100644 deletefb/tools/comments.py create mode 100644 deletefb/tools/messages.py diff --git a/deletefb/tools/comments.py b/deletefb/tools/comments.py new file mode 100644 index 0000000..a167845 --- /dev/null +++ b/deletefb/tools/comments.py @@ -0,0 +1,15 @@ +from .archive import archiver +from ..types import Comment +from .common import SELENIUM_EXCEPTIONS, logger, click_button +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +LOG = logger(__name__) + +def delete_comments(driver, profile_url): + """ + Remove all comments on posts + """ + + driver.get("{0}/allactivity?privacy_source=activity_log&category_key=commentscluster".format(profile_url)) diff --git a/deletefb/tools/messages.py b/deletefb/tools/messages.py new file mode 100644 index 0000000..be4a745 --- /dev/null +++ b/deletefb/tools/messages.py @@ -0,0 +1,15 @@ +from .archive import archiver +from ..types import Conversation +from .common import SELENIUM_EXCEPTIONS, logger, click_button +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +LOG = logger(__name__) + +def delete_comments(driver): + """ + Remove all conversations within a specified range + """ + + driver.get("https://www.facebook.com/messages/t/" diff --git a/deletefb/types.py b/deletefb/types.py index a771c67..8cae509 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -23,6 +23,12 @@ class Comment: date = attr.ib(factory=timestamp_now) name = attr.ib(factory=lambda: uuid.uuid4().hex) +@attr.s +class Conversation: + recipient = attr.ib() + last_message_time = attr.ib(factory=timestamp_now) + name = attr.ib() + @attr.s class Page: name = attr.ib() -- 2.30.2 From fbc18058bdf4c0e4e0d5298c915ca9ac47dd3498 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 3 Jul 2019 19:33:57 -0400 Subject: [PATCH 084/186] Clean up new modules --- deletefb/deletefb.py | 11 ++++++++++- deletefb/tools/{messages.py => conversations.py} | 4 ++-- deletefb/types.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) rename deletefb/tools/{messages.py => conversations.py} (81%) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 6688f4c..ec82c95 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -4,6 +4,8 @@ from .tools.config import settings from .tools.likes import unlike_pages from .tools.login import login from .tools.wall import delete_posts +from .tools.conversations import delete_conversations +from .tools.comments import delete_comments import argparse import getpass @@ -21,7 +23,7 @@ def run_delete(): default="wall", dest="mode", type=str, - choices=["wall", "unlike_pages"], + choices=["wall", "unlike_pages", "comments", "conversations"], help="The mode you want to run in. Default is `wall' which deletes wall posts" ) @@ -112,6 +114,13 @@ def run_delete(): elif args.mode == "unlike_pages": unlike_pages(driver, args.profile_url) + + elif args.mode == "comments": + delete_comments(driver, args.profile_url) + + elif args.mode == "conversations": + delete_conversations(driver) + else: print("Please enter a valid mode") sys.exit(1) diff --git a/deletefb/tools/messages.py b/deletefb/tools/conversations.py similarity index 81% rename from deletefb/tools/messages.py rename to deletefb/tools/conversations.py index be4a745..a1870f0 100644 --- a/deletefb/tools/messages.py +++ b/deletefb/tools/conversations.py @@ -7,9 +7,9 @@ from selenium.webdriver.support.ui import WebDriverWait LOG = logger(__name__) -def delete_comments(driver): +def delete_conversations(driver): """ Remove all conversations within a specified range """ - driver.get("https://www.facebook.com/messages/t/" + driver.get("https://www.facebook.com/messages/t/") diff --git a/deletefb/types.py b/deletefb/types.py index 8cae509..44672bb 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -26,8 +26,8 @@ class Comment: @attr.s class Conversation: recipient = attr.ib() - last_message_time = attr.ib(factory=timestamp_now) name = attr.ib() + last_message_time = attr.ib(factory=timestamp_now) @attr.s class Page: -- 2.30.2 From 3939c8642dd1abe498893c08b02613ff072f71a1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 3 Jul 2019 20:17:04 -0400 Subject: [PATCH 085/186] (almost) working list of convo URLs --- deletefb/tools/comments.py | 2 ++ deletefb/tools/conversations.py | 19 +++++++++++++++++++ deletefb/tools/wall.py | 1 - 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/comments.py b/deletefb/tools/comments.py index a167845..ebf9de7 100644 --- a/deletefb/tools/comments.py +++ b/deletefb/tools/comments.py @@ -13,3 +13,5 @@ def delete_comments(driver, profile_url): """ driver.get("{0}/allactivity?privacy_source=activity_log&category_key=commentscluster".format(profile_url)) + + wait = WebDriverWait(driver, 20) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index a1870f0..cb023ae 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -4,12 +4,31 @@ from .common import SELENIUM_EXCEPTIONS, logger, click_button from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.action_chains import ActionChains LOG = logger(__name__) +def get_conversation_list(driver): + """ + Get a list of conversations + """ + + actions = ActionChains(driver) + + convos = driver.find_elements_by_xpath("//ul[@aria-label=\"Conversation list\"]/li") + + for convo in convos: + actions.move_to_element(convo).perform() + yield convo.find_element_by_xpath("//a") + def delete_conversations(driver): """ Remove all conversations within a specified range """ driver.get("https://www.facebook.com/messages/t/") + + wait = WebDriverWait(driver, 20) + + for convo_url in get_conversation_list(driver): + print(convo_url.get_property("data-href")) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 890bae0..1d9d232 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -42,7 +42,6 @@ def delete_posts(driver, 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 the post archive_wall_post.archive( Post( -- 2.30.2 From d17919f6752500b425d91ef6435ab47579367317 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 5 Jul 2019 03:57:08 -0400 Subject: [PATCH 086/186] Gathering conversations --- deletefb/tools/common.py | 16 ++++++++----- deletefb/tools/conversations.py | 40 +++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 7f6364a..5c14118 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -2,7 +2,8 @@ from os.path import isfile from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, - TimeoutException + TimeoutException, + JavascriptException ) import json @@ -19,13 +20,18 @@ SELENIUM_EXCEPTIONS = ( def click_button(driver, el): """ Click a button using Javascript - Args: - driver: seleniumrequests.Chrome Driver instance - Returns: - None """ driver.execute_script("arguments[0].click();", el) +def scroll_to(driver, el): + """ + Scroll an element into view, using JS + """ + try: + driver.execute_script("arguments[0].scrollIntoView();", el) + except SELENIUM_EXCEPTIONS: + return + def logger(name): """ Args: diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index cb023ae..45ebb7b 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,6 +1,6 @@ from .archive import archiver from ..types import Conversation -from .common import SELENIUM_EXCEPTIONS, logger, click_button +from .common import SELENIUM_EXCEPTIONS, logger, scroll_to from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -8,18 +8,44 @@ from selenium.webdriver.common.action_chains import ActionChains LOG = logger(__name__) -def get_conversation_list(driver): +def get_conversation_list(driver, offset=0): """ Get a list of conversations """ actions = ActionChains(driver) - convos = driver.find_elements_by_xpath("//ul[@aria-label=\"Conversation list\"]/li") + convos = driver.find_elements_by_xpath("//ul[@aria-label=\"Conversation list\"]/li/div/a[@role=\"link\"]") - for convo in convos: + for convo in convos[offset:]: actions.move_to_element(convo).perform() - yield convo.find_element_by_xpath("//a") + yield convo + actions.move_to_element(current_convo).perform() + +def get_all_conversations(driver): + conversation_urls = set() + + current_convo = None + + while True: + l = len(conversation_urls) + + for convo in get_conversation_list(driver, offset=l): + url = convo.get_attribute("data-href") + conversation_urls.add(url) + current_convo = convo + + if current_convo: + scroll_to(driver, current_convo) + + print(l) + print(len(conversation_urls)) + if len(conversation_urls) == l: + # no more conversations left + break + + return list(conversation_urls) + def delete_conversations(driver): """ @@ -30,5 +56,5 @@ def delete_conversations(driver): wait = WebDriverWait(driver, 20) - for convo_url in get_conversation_list(driver): - print(convo_url.get_property("data-href")) + for convo_url in get_all_conversations(driver): + print(convo_url) -- 2.30.2 From d4d9420fc3f498444e9a00ecc240666d6b48b5c5 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Mon, 8 Jul 2019 08:54:29 +0000 Subject: [PATCH 087/186] saving status --- Dockerfile | 41 +++++++++++++++++++++++++++++++++++++++++ sources.list | 13 +++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Dockerfile create mode 100644 sources.list diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2fc8cab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# docker build -t DeleteFB . && \ +# docker run -ti --rm \ +# -e DISPLAY=$DISPLAY \ +# -v /tmp/.X11-unix:/tmp/.X11-unix \ +# DeleteFB + + +FROM ubuntu:bionic + +# Update and apt install + # add your own sources.list file here in order to speed up the build + ADD sources.list /etc/apt/sources.list + + RUN apt-get update && \ + apt-get install -y firefox \ + git \ + python3 \ + python3-pip \ + libcanberra-gtk-module \ + curl \ + sudo \ + vim + +# creating user + ENV user username + RUN export uid=1000 gid=1000 && \ + mkdir -p /home/${user} && \ + echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ + echo "${user}:x:${uid}:" >> /etc/group && \ + echo "${user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${user} && \ + chmod 0440 /etc/sudoers.d/${user} && \ + chown ${uid}:${gid} -R /home/${user} && \ + usermod -aG sudo ${user} + +# delete FB repo + RUN pip3 install --user delete-facebook-posts + RUN pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git + RUN git clone https://github.com/weskerfoot/DeleteFB.git + WORKDIR ./DeleteFB + RUN pip3 install -r requirements.txt + CMD python3 -m ./deletefb/deletefb.py \ No newline at end of file diff --git a/sources.list b/sources.list new file mode 100644 index 0000000..e81c8f6 --- /dev/null +++ b/sources.list @@ -0,0 +1,13 @@ +###### Ubuntu Main Repos +deb http://pl.archive.ubuntu.com/ubuntu/ bionic main restricted universe +deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic main restricted universe + +###### Ubuntu Update Repos +deb http://pl.archive.ubuntu.com/ubuntu/ bionic-security main restricted universe +deb http://pl.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe +deb http://pl.archive.ubuntu.com/ubuntu/ bionic-proposed main restricted universe +deb http://pl.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe +deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-security main restricted universe +deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe +deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-proposed main restricted universe +deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe -- 2.30.2 From 9d28d097646a889bdfb986264d73d741a7f236c4 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Mon, 8 Jul 2019 12:59:46 +0000 Subject: [PATCH 088/186] fixing current status --- Dockerfile | 37 +++++++++++++++++++++++++++++++++---- local.conf | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 local.conf diff --git a/Dockerfile b/Dockerfile index 2fc8cab..d930022 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -# docker build -t DeleteFB . && \ +# docker build -t deletefb . && \ # docker run -ti --rm \ # -e DISPLAY=$DISPLAY \ # -v /tmp/.X11-unix:/tmp/.X11-unix \ -# DeleteFB +# deletefb FROM ubuntu:bionic @@ -32,10 +32,39 @@ FROM ubuntu:bionic chown ${uid}:${gid} -R /home/${user} && \ usermod -aG sudo ${user} -# delete FB repo +# Install Chrome +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + hicolor-icon-theme \ + libcanberra-gtk* \ + libgl1-mesa-dri \ + libgl1-mesa-glx \ + libpango1.0-0 \ + libpulse0 \ + libv4l-0 \ + fonts-symbola \ + --no-install-recommends \ + && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list \ + && apt-get update && apt-get install -y \ + google-chrome-stable \ + --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + +COPY local.conf /etc/fonts/local.conf + + +# delete FB repo install + + RUN pip3 install --user delete-facebook-posts RUN pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git RUN git clone https://github.com/weskerfoot/DeleteFB.git WORKDIR ./DeleteFB RUN pip3 install -r requirements.txt - CMD python3 -m ./deletefb/deletefb.py \ No newline at end of file + CMD python3 -m ./deletefb/deletefb.py + + USER ${user} diff --git a/local.conf b/local.conf new file mode 100644 index 0000000..f6a5f5c --- /dev/null +++ b/local.conf @@ -0,0 +1,34 @@ + + + + + +rgb + + + + +true + + + + +hintslight + + + + +true + + + + +lcddefault + + + + +false + + + -- 2.30.2 From 1cccf4a2c5eb2371fcba88966c81235d726715b0 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Mon, 8 Jul 2019 15:01:18 +0000 Subject: [PATCH 089/186] updating latest changes --- Dockerfile | 11 ++++++++++- run.sh | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 run.sh diff --git a/Dockerfile b/Dockerfile index d930022..077184b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,15 @@ # docker run -ti --rm \ # -e DISPLAY=$DISPLAY \ # -v /tmp/.X11-unix:/tmp/.X11-unix \ + # --cap-add=SYS_ADMIN \ + # --cap-add=NET_ADMIN \ + # --cpuset-cpus 0 \ + # --memory 4GB \ + # -v /tmp/.X11-unix:/tmp/.X11-unix \ + # -e DISPLAY=unix:0 \ + # --device /dev/snd \ + # --device /dev/dri \ + # -v /dev/shm:/dev/shm \ # deletefb @@ -67,4 +76,4 @@ COPY local.conf /etc/fonts/local.conf RUN pip3 install -r requirements.txt CMD python3 -m ./deletefb/deletefb.py - USER ${user} + USER ${user} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..a9cc385 --- /dev/null +++ b/run.sh @@ -0,0 +1,14 @@ +docker build -t deletefb . && \ +docker run -ti --rm \ + -e DISPLAY=$DISPLAY \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + --cap-add=SYS_ADMIN \ + --cap-add=NET_ADMIN \ + --cpuset-cpus 0 \ + --memory 4GB \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + -e DISPLAY=unix:0 \ + --device /dev/snd \ + --device /dev/dri \ + -v /dev/shm:/dev/shm \ + deletefb \ No newline at end of file -- 2.30.2 From 95751423be8c622001f7884cfc075fc2acd5a768 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Tue, 9 Jul 2019 12:26:04 +0000 Subject: [PATCH 090/186] working files --- Dockerfile | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 077184b..5175b3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,15 +31,15 @@ FROM ubuntu:bionic vim # creating user - ENV user username - RUN export uid=1000 gid=1000 && \ - mkdir -p /home/${user} && \ - echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ - echo "${user}:x:${uid}:" >> /etc/group && \ - echo "${user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${user} && \ - chmod 0440 /etc/sudoers.d/${user} && \ - chown ${uid}:${gid} -R /home/${user} && \ - usermod -aG sudo ${user} + # ENV user username + # RUN export uid=1000 gid=1000 && \ + # mkdir -p /home/${user} && \ + # echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ + # echo "${user}:x:${uid}:" >> /etc/group && \ + # echo "${user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${user} && \ + # chmod 0440 /etc/sudoers.d/${user} && \ + # chown ${uid}:${gid} -R /home/${user} && \ + # usermod -aG sudo ${user} # Install Chrome RUN apt-get update && apt-get install -y \ @@ -74,6 +74,7 @@ COPY local.conf /etc/fonts/local.conf RUN git clone https://github.com/weskerfoot/DeleteFB.git WORKDIR ./DeleteFB RUN pip3 install -r requirements.txt - CMD python3 -m ./deletefb/deletefb.py - - USER ${user} +# RUN pip3 install selenium oathlib attrs pybloom_live + RUN pip3 install attrs pybloom_live + RUN pip3 install --user selenium + CMD python3 -m deletefb.deletefb \ No newline at end of file -- 2.30.2 From 67085d198b58c25c2206ebd10f440259b00325b7 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Tue, 9 Jul 2019 15:12:43 +0000 Subject: [PATCH 091/186] updating working files --- Dockerfile | 94 ++++++++++++++++-------------------------------------- run.sh | 2 +- 2 files changed, 29 insertions(+), 67 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5175b3d..c0d6034 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,80 +1,42 @@ -# docker build -t deletefb . && \ -# docker run -ti --rm \ -# -e DISPLAY=$DISPLAY \ -# -v /tmp/.X11-unix:/tmp/.X11-unix \ - # --cap-add=SYS_ADMIN \ - # --cap-add=NET_ADMIN \ - # --cpuset-cpus 0 \ - # --memory 4GB \ - # -v /tmp/.X11-unix:/tmp/.X11-unix \ - # -e DISPLAY=unix:0 \ - # --device /dev/snd \ - # --device /dev/dri \ - # -v /dev/shm:/dev/shm \ -# deletefb - - -FROM ubuntu:bionic - -# Update and apt install - # add your own sources.list file here in order to speed up the build - ADD sources.list /etc/apt/sources.list +# to build and launch, edit ./run.sh +# with your values, then execute it +FROM debian:stable-slim RUN apt-get update && \ - apt-get install -y firefox \ + apt-get install -y \ git \ python3 \ python3-pip \ libcanberra-gtk-module \ curl \ sudo \ - vim - -# creating user - # ENV user username - # RUN export uid=1000 gid=1000 && \ - # mkdir -p /home/${user} && \ - # echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ - # echo "${user}:x:${uid}:" >> /etc/group && \ - # echo "${user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${user} && \ - # chmod 0440 /etc/sudoers.d/${user} && \ - # chown ${uid}:${gid} -R /home/${user} && \ - # usermod -aG sudo ${user} - -# Install Chrome -RUN apt-get update && apt-get install -y \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg \ - hicolor-icon-theme \ - libcanberra-gtk* \ - libgl1-mesa-dri \ - libgl1-mesa-glx \ - libpango1.0-0 \ - libpulse0 \ - libv4l-0 \ - fonts-symbola \ - --no-install-recommends \ - && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list \ - && apt-get update && apt-get install -y \ - google-chrome-stable \ - --no-install-recommends && \ - rm -rf /var/lib/apt/lists/* - -COPY local.conf /etc/fonts/local.conf - + vim \ + unzip \ + chromium \ + chromium-driver + +#creating user + ENV user username + RUN export uid=1000 gid=1000 && \ + mkdir -p /home/${user} && \ + echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ + echo "${user}:x:${uid}:" >> /etc/group && \ + echo "${user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${user} && \ + chmod 0440 /etc/sudoers.d/${user} && \ + chown ${uid}:${gid} -R /home/${user} && \ + usermod -aG sudo ${user} # delete FB repo install + USER ${user} + WORKDIR /home/${user} + ARG mail + ARG pass + ARG url RUN pip3 install --user delete-facebook-posts RUN pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git - RUN git clone https://github.com/weskerfoot/DeleteFB.git - WORKDIR ./DeleteFB - RUN pip3 install -r requirements.txt -# RUN pip3 install selenium oathlib attrs pybloom_live - RUN pip3 install attrs pybloom_live - RUN pip3 install --user selenium - CMD python3 -m deletefb.deletefb \ No newline at end of file + RUN git clone https://github.com/weskerfoot/DeleteFB.git + RUN pip3 install -r ./DeleteFB/requirements.txt + RUN pip3 install --user selenium attrs pybloom_live + CMD python3 -m deletefb.deletefb -E ${mail} -P ${pass} -U ${url} \ No newline at end of file diff --git a/run.sh b/run.sh index a9cc385..bf48132 100755 --- a/run.sh +++ b/run.sh @@ -11,4 +11,4 @@ docker run -ti --rm \ --device /dev/snd \ --device /dev/dri \ -v /dev/shm:/dev/shm \ - deletefb \ No newline at end of file + deletefb -e mail="your@email.com" -e pass="Y0Ur*P4ss" -e url="http://facebook.com/your-username" deletefb:latest -- 2.30.2 From 237aaa60c1381132ad934e7cd346096ccbb114c4 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Tue, 9 Jul 2019 15:13:03 +0000 Subject: [PATCH 092/186] cleaning trash --- local.conf | 34 ---------------------------------- sources.list | 13 ------------- 2 files changed, 47 deletions(-) delete mode 100644 local.conf delete mode 100644 sources.list diff --git a/local.conf b/local.conf deleted file mode 100644 index f6a5f5c..0000000 --- a/local.conf +++ /dev/null @@ -1,34 +0,0 @@ - - - - - -rgb - - - - -true - - - - -hintslight - - - - -true - - - - -lcddefault - - - - -false - - - diff --git a/sources.list b/sources.list deleted file mode 100644 index e81c8f6..0000000 --- a/sources.list +++ /dev/null @@ -1,13 +0,0 @@ -###### Ubuntu Main Repos -deb http://pl.archive.ubuntu.com/ubuntu/ bionic main restricted universe -deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic main restricted universe - -###### Ubuntu Update Repos -deb http://pl.archive.ubuntu.com/ubuntu/ bionic-security main restricted universe -deb http://pl.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe -deb http://pl.archive.ubuntu.com/ubuntu/ bionic-proposed main restricted universe -deb http://pl.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe -deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-security main restricted universe -deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe -deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-proposed main restricted universe -deb-src http://pl.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe -- 2.30.2 From 08305ac8b5c34e146b509fac59cd6bff12cac513 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Thu, 11 Jul 2019 08:33:50 +0000 Subject: [PATCH 093/186] saving makefile status --- Makefile | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55b7eb1 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# Makefile + +NAME:= deletefb +url := https://facebook.com/$$username + +.PHONY: all build run + +all: build run + +build: + @docker build -t $(NAME) . + +run: + @read -p "Enter your Facebook email: " email; \ + + @read -p "Enter your Facebook password: " password; \ + + @read -p "Enter your Facebook username: " username; + + @docker run -ti --rm \ + -e DISPLAY=$DISPLAY \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + --cap-add=SYS_ADMIN \ + --cap-add=NET_ADMIN \ + --cpuset-cpus 0 \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + --device /dev/dri \ + -v /dev/shm:/dev/shm \ + $(NAME):latest "`which python3` -m deletefb.deletefb -e mail="$$email" -e pass=$$password -e url=$$url" + -- 2.30.2 From 65e6286234750a214f730e3d24606e862a21c51f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 13 Jul 2019 20:12:26 -0400 Subject: [PATCH 094/186] Iterating through all conversations working! --- deletefb/tools/conversations.py | 59 +++++++++++++++------------------ 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 45ebb7b..e51790e 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,60 +1,53 @@ from .archive import archiver from ..types import Conversation -from .common import SELENIUM_EXCEPTIONS, logger, scroll_to +from .common import SELENIUM_EXCEPTIONS, logger, scroll_to, click_button from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.action_chains import ActionChains +from time import sleep LOG = logger(__name__) -def get_conversation_list(driver, offset=0): +def get_conversations(driver): """ Get a list of conversations """ actions = ActionChains(driver) - convos = driver.find_elements_by_xpath("//ul[@aria-label=\"Conversation list\"]/li/div/a[@role=\"link\"]") - - for convo in convos[offset:]: - actions.move_to_element(convo).perform() - yield convo - actions.move_to_element(current_convo).perform() - -def get_all_conversations(driver): - conversation_urls = set() + wait = WebDriverWait(driver, 20) - current_convo = None + try: + wait.until( + EC.presence_of_element_located((By.XPATH, "//div[@id=\"threadlist_rows\"]")) + ) + except SELENIUM_EXCEPTIONS: + LOG.exception("No conversations") + return while True: - l = len(conversation_urls) - - for convo in get_conversation_list(driver, offset=l): - url = convo.get_attribute("data-href") - conversation_urls.add(url) - current_convo = convo - - if current_convo: - scroll_to(driver, current_convo) - - print(l) - print(len(conversation_urls)) - if len(conversation_urls) == l: - # no more conversations left + for convo in driver.find_elements_by_xpath("//a"): + url = convo.get_attribute("href") + if url and "messages/read" in url: + yield url + + try: + next_url = driver.find_element_by_id("see_older_threads").find_element_by_xpath("a").get_attribute("href") + except SELENIUM_EXCEPTIONS: break - - return list(conversation_urls) - + if not next_url: + break + driver.get(next_url) def delete_conversations(driver): """ Remove all conversations within a specified range """ - driver.get("https://www.facebook.com/messages/t/") + driver.get("https://mobile.facebook.com/messages/?pageNum=1&selectable&see_older_newer=1") - wait = WebDriverWait(driver, 20) + convos = list(get_conversations(driver)) - for convo_url in get_all_conversations(driver): - print(convo_url) + for convo in convos: + driver.get(convo) -- 2.30.2 From 723d90981d55b99cc01ee1252d65f7a19908ad81 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 16 Jul 2019 23:24:05 -0400 Subject: [PATCH 095/186] Try parsing conversation timestamps, if they exist --- deletefb/tools/common.py | 5 +++++ deletefb/tools/conversations.py | 15 ++++++++++----- deletefb/types.py | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 5c14118..301d5f0 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -5,11 +5,13 @@ from selenium.common.exceptions import ( TimeoutException, JavascriptException ) +from arrow.parser import ParserError import json import logging import logging.config import os +import arrow SELENIUM_EXCEPTIONS = ( NoSuchElementException, @@ -32,6 +34,9 @@ def scroll_to(driver, el): except SELENIUM_EXCEPTIONS: return +def parse_ts(text): + return arrow.get(text, "DD/M/YYYY") + def logger(name): """ Args: diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index e51790e..7ea3ee5 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,11 +1,10 @@ from .archive import archiver from ..types import Conversation -from .common import SELENIUM_EXCEPTIONS, logger, scroll_to, click_button +from .common import SELENIUM_EXCEPTIONS, logger, parse_ts, ParserError from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.action_chains import ActionChains -from time import sleep LOG = logger(__name__) @@ -30,10 +29,16 @@ def get_conversations(driver): for convo in driver.find_elements_by_xpath("//a"): url = convo.get_attribute("href") if url and "messages/read" in url: - yield url - + try: + print(parse_ts(convo.find_element_by_xpath("../../..//abbr").text)) + except ParserError: + print("Failed to parse timestamp") + continue try: - next_url = driver.find_element_by_id("see_older_threads").find_element_by_xpath("a").get_attribute("href") + next_url = (driver.find_element_by_id("see_older_threads"). + find_element_by_xpath("a"). + get_attribute("href")) + except SELENIUM_EXCEPTIONS: break if not next_url: diff --git a/deletefb/types.py b/deletefb/types.py index 44672bb..429f3ba 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -25,6 +25,7 @@ class Comment: @attr.s class Conversation: + url = attr.ib() recipient = attr.ib() name = attr.ib() last_message_time = attr.ib(factory=timestamp_now) -- 2.30.2 From 442fc142960e0d588f95bb8ad5a84ea19748f1da Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 17 Jul 2019 19:21:29 -0400 Subject: [PATCH 096/186] Fixes #75 --- deletefb/tools/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 1bc7364..7a54cd0 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -38,7 +38,7 @@ class Archive: def archiver(archive_type): archive_file = open( - (Path(".") / Path(archive_type).name).with_suffix(".log"), + str((Path(".") / Path(archive_type).name).with_suffix(".log")), mode="ta", buffering=1 ) -- 2.30.2 From 79ed40c1325555d9390718aafc0e302750d19a24 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 17 Jul 2019 19:24:06 -0400 Subject: [PATCH 097/186] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a9b692d..c05d8a3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.7", + version="1.1.8", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From aa9e3672c348aebc53e301722df76c5f2d7d662e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 20 Jul 2019 15:06:56 -0400 Subject: [PATCH 098/186] Package up conversations into its own type now --- deletefb/tools/conversations.py | 17 +++++++++++++++-- deletefb/types.py | 3 +-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 7ea3ee5..fb4a992 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -28,12 +28,23 @@ def get_conversations(driver): while True: for convo in driver.find_elements_by_xpath("//a"): url = convo.get_attribute("href") + timestamp = None + if url and "messages/read" in url: try: - print(parse_ts(convo.find_element_by_xpath("../../..//abbr").text)) + timestamp = parse_ts(convo.find_element_by_xpath("../../..//abbr").text) except ParserError: print("Failed to parse timestamp") continue + + conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip() + + assert(conversation_name) + assert(url) + + yield Conversation(url=url, + name=conversation_name, + timestamp=timestamp) try: next_url = (driver.find_element_by_id("see_older_threads"). find_element_by_xpath("a"). @@ -55,4 +66,6 @@ def delete_conversations(driver): convos = list(get_conversations(driver)) for convo in convos: - driver.get(convo) + print(convo.url) + print(convo.name) + print(convo.timestamp) diff --git a/deletefb/types.py b/deletefb/types.py index 429f3ba..3d0c2c4 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -26,9 +26,8 @@ class Comment: @attr.s class Conversation: url = attr.ib() - recipient = attr.ib() name = attr.ib() - last_message_time = attr.ib(factory=timestamp_now) + timestamp = attr.ib(default=None) @attr.s class Page: -- 2.30.2 From 0355ebcc66d2d57da750e8791691b659db32041b Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 20 Jul 2019 16:05:50 -0400 Subject: [PATCH 099/186] Refactor timestamp parsing --- deletefb/tools/common.py | 6 +----- deletefb/tools/conversations.py | 34 +++++++++++++++++++-------------- deletefb/types.py | 24 +++++++++++++---------- requirements.txt | 12 ++++++++++++ setup.py | 3 ++- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 301d5f0..18c74cd 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -5,13 +5,12 @@ from selenium.common.exceptions import ( TimeoutException, JavascriptException ) -from arrow.parser import ParserError import json import logging import logging.config import os -import arrow +import pendulum SELENIUM_EXCEPTIONS = ( NoSuchElementException, @@ -34,9 +33,6 @@ def scroll_to(driver, el): except SELENIUM_EXCEPTIONS: return -def parse_ts(text): - return arrow.get(text, "DD/M/YYYY") - def logger(name): """ Args: diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index fb4a992..75f13d0 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,6 +1,6 @@ from .archive import archiver from ..types import Conversation -from .common import SELENIUM_EXCEPTIONS, logger, parse_ts, ParserError +from .common import SELENIUM_EXCEPTIONS, logger from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -25,26 +25,32 @@ def get_conversations(driver): LOG.exception("No conversations") return + # This function *cannot* be a generator + # Otherwise elements will become stale + conversations = [] + while True: for convo in driver.find_elements_by_xpath("//a"): url = convo.get_attribute("href") + timestamp = None if url and "messages/read" in url: - try: - timestamp = parse_ts(convo.find_element_by_xpath("../../..//abbr").text) - except ParserError: - print("Failed to parse timestamp") - continue + timestamp = convo.find_element_by_xpath("../../..//abbr").text conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip() assert(conversation_name) assert(url) - yield Conversation(url=url, - name=conversation_name, - timestamp=timestamp) + conversations.append( + Conversation( + url=url, + timestamp=timestamp, + name=conversation_name + ) + ) + try: next_url = (driver.find_element_by_id("see_older_threads"). find_element_by_xpath("a"). @@ -56,16 +62,16 @@ def get_conversations(driver): break driver.get(next_url) -def delete_conversations(driver): + return conversations + +def delete_conversations(driver, older_than=None): """ Remove all conversations within a specified range """ driver.get("https://mobile.facebook.com/messages/?pageNum=1&selectable&see_older_newer=1") - convos = list(get_conversations(driver)) + convos = get_conversations(driver) for convo in convos: - print(convo.url) - print(convo.name) - print(convo.timestamp) + print(convo) diff --git a/deletefb/types.py b/deletefb/types.py index 3d0c2c4..d2d8f7e 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -1,35 +1,39 @@ import attr import uuid -import datetime +import pendulum -def timestamp_now(): - """ - Returns: a timestamp for this instant, in ISO 8601 format - """ - return datetime.datetime.isoformat(datetime.datetime.now()) +def convert_timestamp(text): + try: + return pendulum.from_format(text, "DD/M/YYYY") + except ValueError: + try: + return (pendulum.from_format(text, "DD MMM") + .set(year=pendulum.now().year)) + except ValueError: + return None # Data type definitions of posts and comments @attr.s class Post: content = attr.ib() comments = attr.ib(default=[]) - date = attr.ib(factory=timestamp_now) + date = attr.ib(factory=pendulum.now) name = attr.ib(factory=lambda: uuid.uuid4().hex) @attr.s class Comment: commenter = attr.ib() content = attr.ib() - date = attr.ib(factory=timestamp_now) + date = attr.ib(factory=pendulum.now) name = attr.ib(factory=lambda: uuid.uuid4().hex) @attr.s class Conversation: url = attr.ib() name = attr.ib() - timestamp = attr.ib(default=None) + timestamp = attr.ib(converter=convert_timestamp) @attr.s class Page: name = attr.ib() - date = attr.ib(factory=timestamp_now) + date = attr.ib(factory=pendulum.now) diff --git a/requirements.txt b/requirements.txt index 0780145..eb788e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,25 @@ attrs==19.1.0 bitarray==0.9.3 +bleach==3.1.0 certifi==2018.11.29 chardet==3.0.4 +docutils==0.14 idna==2.8 +pendulum==2.0.5 +pkginfo==1.5.0.1 pybloom-live==3.0.0 +Pygments==2.4.2 +python-dateutil==2.8.0 +pytzdata==2019.2 +readme-renderer==24.0 requests==2.22.0 requests-file==1.4.3 +requests-toolbelt==0.9.1 selenium==3.141.0 selenium-requests==1.3 six==1.12.0 tldextract==2.2.0 +tqdm==4.32.2 +twine==1.13.0 urllib3==1.25.2 +webencodings==0.5.1 diff --git a/setup.py b/setup.py index a9b692d..fc1cef0 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ setuptools.setup( "selenium-requests", "requests", "pybloom-live", - "attrs" + "attrs", + "pendulum" ], classifiers= [ "Programming Language :: Python :: 3", -- 2.30.2 From b74dd81e0d15ecfabdcaed56505671379feba45e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 20 Jul 2019 17:05:00 -0400 Subject: [PATCH 100/186] Support for filtering conversations by year --- deletefb/deletefb.py | 6 +++--- deletefb/tools/conversations.py | 15 +++++++++++++-- deletefb/types.py | 4 ++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index ec82c95..c2f8c2e 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -93,8 +93,8 @@ def run_delete(): settings["ARCHIVE"] = not args.archive_off - if args.year and args.mode != "wall": - parser.error("The --year option is only supported in wall mode") + if args.year and args.mode not in ("wall", "conversations"): + parser.error("The --year option is not supported in this mode") args_user_password = args.password or getpass.getpass('Enter your password: ') @@ -119,7 +119,7 @@ def run_delete(): delete_comments(driver, args.profile_url) elif args.mode == "conversations": - delete_conversations(driver) + delete_conversations(driver, year=args.year) else: print("Please enter a valid mode") diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 75f13d0..4f39a38 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -5,6 +5,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.action_chains import ActionChains +from pendulum import now LOG = logger(__name__) @@ -64,7 +65,7 @@ def get_conversations(driver): return conversations -def delete_conversations(driver, older_than=None): +def delete_conversations(driver, year=None): """ Remove all conversations within a specified range """ @@ -74,4 +75,14 @@ def delete_conversations(driver, older_than=None): convos = get_conversations(driver) for convo in convos: - print(convo) + # If the year is set and there is a timestamp + # Then we want to only look at convos from this year + + if year and convo.timestamp: + if convo.timestamp.year == int(year): + print(convo) + + # Otherwise we're looking at all convos + elif not year: + print(convo) + diff --git a/deletefb/types.py b/deletefb/types.py index d2d8f7e..b04e6ab 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -3,6 +3,10 @@ import uuid import pendulum def convert_timestamp(text): + """ + Tries to parse a timestamp into a DateTime instance + Returns `None` if it cannot be parsed + """ try: return pendulum.from_format(text, "DD/M/YYYY") except ValueError: -- 2.30.2 From 015f57a17f59ef617217c94992247bde8da6ddcc Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 27 Jul 2019 13:23:15 -0400 Subject: [PATCH 101/186] Can now get entire conversations --- deletefb/tools/conversations.py | 35 ++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 4f39a38..253b281 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,6 +1,6 @@ from .archive import archiver from ..types import Conversation -from .common import SELENIUM_EXCEPTIONS, logger +from .common import SELENIUM_EXCEPTIONS, logger, click_button from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -65,6 +65,35 @@ def get_conversations(driver): return conversations + +def archive_conversation(driver, convo): + print(convo) + driver.get(convo.url) + + wait = WebDriverWait(driver, 20) + try: + wait.until( + EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'See Older Messages')]")) + ) + except SELENIUM_EXCEPTIONS: + LOG.exception("Could not load more messages") + return + + while True: + try: + see_older = driver.find_element_by_xpath("//*[contains(text(), 'See Older Messages')]") + except SELENIUM_EXCEPTIONS: + break + + if not see_older: + break + + try: + click_button(driver, see_older) + except SELENIUM_EXCEPTIONS: + continue + driver.find_element_by_xpath("html").save_screenshot("%s.png" % convo.name) + def delete_conversations(driver, year=None): """ Remove all conversations within a specified range @@ -80,9 +109,9 @@ def delete_conversations(driver, year=None): if year and convo.timestamp: if convo.timestamp.year == int(year): - print(convo) + archive_conversation(driver, convo) # Otherwise we're looking at all convos elif not year: - print(convo) + archive_conversation(driver, convo) -- 2.30.2 From d3064257e0898b4c4336cf8a61d1320122a9e7e0 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 27 Jul 2019 13:37:10 -0400 Subject: [PATCH 102/186] Depend on lxml --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index eb788e5..24001f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ certifi==2018.11.29 chardet==3.0.4 docutils==0.14 idna==2.8 +lxml==4.4.0 pendulum==2.0.5 pkginfo==1.5.0.1 pybloom-live==3.0.0 diff --git a/setup.py b/setup.py index fc1cef0..0e5681f 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ setuptools.setup( "requests", "pybloom-live", "attrs", + "lxml", "pendulum" ], classifiers= [ -- 2.30.2 From 7697af2481598bc4df0bfa7c0969b5797fe481b6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 28 Jul 2019 12:22:58 -0400 Subject: [PATCH 103/186] Construct Message instances for each message in a convo --- deletefb/tools/conversations.py | 36 ++++++++++++++++++++++++++++++--- deletefb/types.py | 9 +++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 253b281..c6f57fb 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,11 +1,14 @@ from .archive import archiver -from ..types import Conversation +from ..types import Conversation, Message from .common import SELENIUM_EXCEPTIONS, logger, click_button from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.action_chains import ActionChains from pendulum import now +from json import loads + +import lxml.html as lxh LOG = logger(__name__) @@ -65,6 +68,28 @@ def get_conversations(driver): return conversations +def get_convo_images(driver): + """ + Gets all links to images in a messenger conversation + Removes duplicates + """ + for img in set(lxh.fromstring(driver.page_source).xpath("//img")): + yield img.get("src") + +def get_convo_messages(driver): + """ + Gets all messages in a conversation + """ + + for msg in lxh.fromstring(driver.page_source).xpath("//div[@class='msg']/div"): + data_store = loads(msg.get("data-store")) + msg_text = msg.text_content() + + yield Message( + name=data_store.get("author"), + content=msg_text, + timestamp=data_store.get("timestamp") + ) def archive_conversation(driver, convo): print(convo) @@ -79,6 +104,7 @@ def archive_conversation(driver, convo): LOG.exception("Could not load more messages") return + # Expand conversation until we've reached the beginning while True: try: see_older = driver.find_element_by_xpath("//*[contains(text(), 'See Older Messages')]") @@ -92,7 +118,11 @@ def archive_conversation(driver, convo): click_button(driver, see_older) except SELENIUM_EXCEPTIONS: continue - driver.find_element_by_xpath("html").save_screenshot("%s.png" % convo.name) + + #for img in get_convo_images(driver): + #print(img) + + convo.messages = list(get_convo_messages(driver)) def delete_conversations(driver, year=None): """ @@ -110,8 +140,8 @@ def delete_conversations(driver, year=None): if year and convo.timestamp: if convo.timestamp.year == int(year): archive_conversation(driver, convo) + print(convo.messages) # Otherwise we're looking at all convos elif not year: archive_conversation(driver, convo) - diff --git a/deletefb/types.py b/deletefb/types.py index b04e6ab..0e589d7 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -36,6 +36,15 @@ class Conversation: url = attr.ib() name = attr.ib() timestamp = attr.ib(converter=convert_timestamp) + messages = attr.ib(default=[]) + +@attr.s +class Message: + name = attr.ib() + content = attr.ib() + + # Remove the last 3 digits from FB's timestamps. They are not standard. + timestamp = attr.ib(converter=lambda t: pendulum.from_timestamp(int(str(t)[0:-3]))) @attr.s class Page: -- 2.30.2 From b2e4a92e82e3a98288bac766ffd3f3e945d99112 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 28 Jul 2019 19:01:03 -0400 Subject: [PATCH 104/186] Rename `timestamp` to `date` to be consistent --- deletefb/tools/conversations.py | 14 +++++++------- deletefb/types.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index c6f57fb..19d82ab 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -37,11 +37,11 @@ def get_conversations(driver): for convo in driver.find_elements_by_xpath("//a"): url = convo.get_attribute("href") - timestamp = None + date = None if url and "messages/read" in url: - timestamp = convo.find_element_by_xpath("../../..//abbr").text + date = convo.find_element_by_xpath("../../..//abbr").text conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip() assert(conversation_name) @@ -50,7 +50,7 @@ def get_conversations(driver): conversations.append( Conversation( url=url, - timestamp=timestamp, + date=date, name=conversation_name ) ) @@ -88,7 +88,7 @@ def get_convo_messages(driver): yield Message( name=data_store.get("author"), content=msg_text, - timestamp=data_store.get("timestamp") + date=data_store.get("timestamp") ) def archive_conversation(driver, convo): @@ -134,11 +134,11 @@ def delete_conversations(driver, year=None): convos = get_conversations(driver) for convo in convos: - # If the year is set and there is a timestamp + # If the year is set and there is a date # Then we want to only look at convos from this year - if year and convo.timestamp: - if convo.timestamp.year == int(year): + if year and convo.date: + if convo.date.year == int(year): archive_conversation(driver, convo) print(convo.messages) diff --git a/deletefb/types.py b/deletefb/types.py index 0e589d7..16a5c60 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -2,9 +2,9 @@ import attr import uuid import pendulum -def convert_timestamp(text): +def convert_date(text): """ - Tries to parse a timestamp into a DateTime instance + Tries to parse a date into a DateTime instance Returns `None` if it cannot be parsed """ try: @@ -35,7 +35,7 @@ class Comment: class Conversation: url = attr.ib() name = attr.ib() - timestamp = attr.ib(converter=convert_timestamp) + date = attr.ib(converter=convert_date) messages = attr.ib(default=[]) @attr.s @@ -43,8 +43,8 @@ class Message: name = attr.ib() content = attr.ib() - # Remove the last 3 digits from FB's timestamps. They are not standard. - timestamp = attr.ib(converter=lambda t: pendulum.from_timestamp(int(str(t)[0:-3]))) + # Remove the last 3 digits from FB's dates. They are not standard. + date = attr.ib(converter=lambda t: pendulum.from_timestamp(int(str(t)[0:-3]))) @attr.s class Page: -- 2.30.2 From 01647262c31cbeac01b92032fa83d4f6019e7649 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 29 Jul 2019 00:39:06 -0400 Subject: [PATCH 105/186] Use cattrs to serialize conversations in archive --- deletefb/deletefb.py | 4 +-- deletefb/tools/archive.py | 10 ++++++- deletefb/tools/conversations.py | 53 +++++++++++++++++---------------- deletefb/types.py | 6 ++-- setup.py | 1 + 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index c2f8c2e..21c5f47 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -4,7 +4,7 @@ from .tools.config import settings from .tools.likes import unlike_pages from .tools.login import login from .tools.wall import delete_posts -from .tools.conversations import delete_conversations +from .tools.conversations import traverse_conversations from .tools.comments import delete_comments import argparse @@ -119,7 +119,7 @@ def run_delete(): delete_comments(driver, args.profile_url) elif args.mode == "conversations": - delete_conversations(driver, year=args.year) + traverse_conversations(driver, year=args.year) else: print("Please enter a valid mode") diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 1bc7364..1352da0 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -1,13 +1,21 @@ from .config import settings from contextlib import contextmanager from pathlib import Path +from datetime import datetime import attr +import cattr import json +TIME_FORMAT = "%Y-%m-%d %H:%M:%S" + # Used to avoid duplicates in the log from pybloom_live import BloomFilter +cattr.register_unstructure_hook( + datetime, lambda dt: datetime.strftime(dt, format=TIME_FORMAT) +) + def make_filter(): return BloomFilter( capacity=settings["MAX_POSTS"], @@ -30,7 +38,7 @@ class Archive: print("Archiving {0}".format(content)) if content.name not in self._bloom_filter: - self.archive_file.write(json.dumps(attr.asdict(content)) + "\n") + self.archive_file.write(json.dumps(cattr.unstructure(content)) + "\n") self._bloom_filter.add(content.name) return diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 19d82ab..1c1c557 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -68,17 +68,9 @@ def get_conversations(driver): return conversations -def get_convo_images(driver): +def parse_conversation(driver): """ - Gets all links to images in a messenger conversation - Removes duplicates - """ - for img in set(lxh.fromstring(driver.page_source).xpath("//img")): - yield img.get("src") - -def get_convo_messages(driver): - """ - Gets all messages in a conversation + Extracts all messages in a conversation """ for msg in lxh.fromstring(driver.page_source).xpath("//div[@class='msg']/div"): @@ -91,8 +83,10 @@ def get_convo_messages(driver): date=data_store.get("timestamp") ) -def archive_conversation(driver, convo): - print(convo) +def get_messages(driver, convo): + """ + Get all of the messages for a given conversation + """ driver.get(convo.url) wait = WebDriverWait(driver, 20) @@ -119,12 +113,16 @@ def archive_conversation(driver, convo): except SELENIUM_EXCEPTIONS: continue - #for img in get_convo_images(driver): - #print(img) + return list(parse_conversation(driver)) - convo.messages = list(get_convo_messages(driver)) +def delete_conversation(driver, convo): + """ + Deletes a conversation + """ -def delete_conversations(driver, year=None): + return + +def traverse_conversations(driver, year=None): """ Remove all conversations within a specified range """ @@ -133,15 +131,18 @@ def delete_conversations(driver, year=None): convos = get_conversations(driver) - for convo in convos: - # If the year is set and there is a date - # Then we want to only look at convos from this year + with archiver("conversations") as archive_convo: + for convo in convos: + # If the year is set and there is a date + # Then we want to only look at convos from this year + + if year and convo.date: + if convo.date.year == int(year): + convo.messages = get_messages(driver, convo) + archive_convo.archive(convo) - if year and convo.date: - if convo.date.year == int(year): - archive_conversation(driver, convo) - print(convo.messages) + # Otherwise we're looking at all convos + elif not year: + convo.messages = get_messages(driver, convo) + archive_convo.archive(convo) - # Otherwise we're looking at all convos - elif not year: - archive_conversation(driver, convo) diff --git a/deletefb/types.py b/deletefb/types.py index 16a5c60..43ee24c 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -2,6 +2,8 @@ import attr import uuid import pendulum +from datetime import datetime + def convert_date(text): """ Tries to parse a date into a DateTime instance @@ -35,7 +37,7 @@ class Comment: class Conversation: url = attr.ib() name = attr.ib() - date = attr.ib(converter=convert_date) + date : datetime = attr.ib(converter=convert_date) messages = attr.ib(default=[]) @attr.s @@ -44,7 +46,7 @@ class Message: content = attr.ib() # Remove the last 3 digits from FB's dates. They are not standard. - date = attr.ib(converter=lambda t: pendulum.from_timestamp(int(str(t)[0:-3]))) + date : datetime = attr.ib(converter=lambda t: pendulum.from_timestamp(int(str(t)[0:-3]))) @attr.s class Page: diff --git a/setup.py b/setup.py index 0e5681f..5edd7de 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ setuptools.setup( "requests", "pybloom-live", "attrs", + "cattrs", "lxml", "pendulum" ], -- 2.30.2 From 3b46b987636d4b0d31d14e85ccc1283f082221a1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 2 Aug 2019 01:46:39 -0400 Subject: [PATCH 106/186] Cleaning up README --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 856e1f3..f847b48 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Personally, I did this so I would feel less attached to my Facebook profile ## Installation You have several options to run it. -1) Install from PyPI with `pip3 install --user delete-facebook-posts` +1) Install from PyPI with `pip3 install --user delete-facebook-posts` (recommended) 2) Clone this repo and run `pip3 install --user .` or do `pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git` 3) Set up a Python virtualenv, activate it, and run `pip3 install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. @@ -62,14 +62,7 @@ git+https://github.com/weskerfoot/DeleteFB.git` * 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. E.g. `-Y 2010` would delete posts from the year 2010. It is incompatible with any mode other than `wall`. - -## Unlike Pages -* You may use `-M unlike_pages` to unlike all of your pages. The names of the - pages will be archived (unless archival is turned off), and this option - conflicts with the year option. This will only unlike your *pages* that you - have liked. It will *not* unlike anything else (like books or movies). +* The tool supports passing the `--year` flag in order to delete/archive by year. E.g. `-Y 2010` would only affect posts from 2010. ## Archival * The tool will archive everything being deleted by default in `.log` files. -- 2.30.2 From 9ea47f3e46a5d94e4513c0f72bdf246254e79c2b Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 2 Aug 2019 01:56:40 -0400 Subject: [PATCH 107/186] Add contribution guidelines --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ce5fde5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +### How to contribute + +## Dependencies +If you are adding any new dependencies, please make sure that both `requirements.txt` and `setup.py` have been updated. Please read [this](https://caremad.io/posts/2013/07/setup-vs-requirement/) if you are confused about the difference between `requirements.txt` and the `install_requires` section. + +## Virtualenv +Always develop with virtualenv, as well as test with `pip install --user .`. This helps make sure implicit dependencies aren't accidentally introduced, and makes sure the average user will be more likely to run it without issues. + +## Pull requests +Feel free to make a pull request! Make sure to give a brief overview of what you did, and why you think it is useful. If you are fixing a specific bug or resolving an issue, then make sure to reference it in your PR. + +## Coding style +Try to be consistent with the existing codebase as much as possible. Things should be modularized. Don't repeat yourself if possible, but don't add needless complexity. Straightforward is often better than clever and optimized. -- 2.30.2 From 5377b48850997a5c852f7e3f0171fcb14cbe5d17 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 2 Aug 2019 02:32:29 -0400 Subject: [PATCH 108/186] Prettify archives, log image links in convos --- deletefb/deletefb.log | 0 deletefb/tools/archive.py | 6 ++++- deletefb/tools/conversations.py | 39 ++++++++++++++++++++++++++++----- deletefb/types.py | 1 + 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 deletefb/deletefb.log diff --git a/deletefb/deletefb.log b/deletefb/deletefb.log new file mode 100644 index 0000000..e69de29 diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 1352da0..72a6e01 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -6,6 +6,7 @@ from datetime import datetime import attr import cattr import json +import typing TIME_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -38,7 +39,10 @@ class Archive: print("Archiving {0}".format(content)) if content.name not in self._bloom_filter: - self.archive_file.write(json.dumps(cattr.unstructure(content)) + "\n") + self.archive_file.write(json.dumps(cattr.unstructure(content), + indent=4, + sort_keys=True) + "\n") + self._bloom_filter.add(content.name) return diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 1c1c557..003924c 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -83,9 +83,18 @@ def parse_conversation(driver): date=data_store.get("timestamp") ) -def get_messages(driver, convo): +def get_images(driver): """ - Get all of the messages for a given conversation + Gets all links to images in a messenger conversation + Removes duplicates + """ + for img in set(lxh.fromstring(driver.page_source).xpath("//img")): + yield img.get("src") + +def get_convo(driver, convo): + """ + Get all of the messages/images for a given conversation + Returns a list of messages and a list of image links """ driver.get(convo.url) @@ -113,7 +122,9 @@ def get_messages(driver, convo): except SELENIUM_EXCEPTIONS: continue - return list(parse_conversation(driver)) + messages = list(parse_conversation(driver)) + image_links = list(set(get_images(driver))) + return (messages, image_links) def delete_conversation(driver, convo): """ @@ -122,6 +133,24 @@ def delete_conversation(driver, convo): return +def extract_convo(driver, convo): + """ + Extract messages and image links from a conversation + Return a new Conversation instance + """ + result = get_convo(driver, convo) + + if not result: + return None + + messages, image_links = result + + convo.messages = messages + convo.image_links = image_links + + return convo + + def traverse_conversations(driver, year=None): """ Remove all conversations within a specified range @@ -138,11 +167,11 @@ def traverse_conversations(driver, year=None): if year and convo.date: if convo.date.year == int(year): - convo.messages = get_messages(driver, convo) + extract_convo(driver, convo) archive_convo.archive(convo) # Otherwise we're looking at all convos elif not year: - convo.messages = get_messages(driver, convo) + extract_convo(driver, convo) archive_convo.archive(convo) diff --git a/deletefb/types.py b/deletefb/types.py index 43ee24c..c1a0148 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -39,6 +39,7 @@ class Conversation: name = attr.ib() date : datetime = attr.ib(converter=convert_date) messages = attr.ib(default=[]) + image_links = attr.ib(default=[]) @attr.s class Message: -- 2.30.2 From 81262fe4d7f5e760817e29ebb01738f2e6135827 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 2 Aug 2019 03:16:41 -0400 Subject: [PATCH 109/186] Add timestamps to archive files --- deletefb/tools/archive.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 72a6e01..052a05a 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -2,6 +2,7 @@ from .config import settings from contextlib import contextmanager from pathlib import Path from datetime import datetime +from time import time import attr import cattr @@ -36,7 +37,7 @@ class Archive: """ Archive an object """ - print("Archiving {0}".format(content)) + print("Archiving content") if content.name not in self._bloom_filter: self.archive_file.write(json.dumps(cattr.unstructure(content), @@ -50,7 +51,7 @@ class Archive: def archiver(archive_type): archive_file = open( - (Path(".") / Path(archive_type).name).with_suffix(".log"), + (Path(".") / Path(archive_type).name).with_suffix(".log.{0}".format(time())), mode="ta", buffering=1 ) -- 2.30.2 From e1c7e822f3b0e94ba036d10f7d6ce3921931bfee Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 3 Aug 2019 23:11:37 -0400 Subject: [PATCH 110/186] Refactoring, almost support deleting --- deletefb/tools/archive.py | 4 +++- deletefb/tools/common.py | 14 ++++++++++++ deletefb/tools/conversations.py | 40 +++++++++++++-------------------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 052a05a..1fca7c3 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -37,7 +37,9 @@ class Archive: """ Archive an object """ - print("Archiving content") + + if hasattr(content, 'name'): + print("Archiving {0}".format(content.name)) if content.name not in self._bloom_filter: self.archive_file.write(json.dumps(cattr.unstructure(content), diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 18c74cd..de0d679 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -1,4 +1,7 @@ from os.path import isfile +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, @@ -52,6 +55,17 @@ def logger(name): logging.config.dictConfig(config["logging"]) return logging.getLogger(name) + +def wait_xpath(driver, expr): + """ + Takes an XPath expression, and waits at most 20 seconds until it exists + """ + wait = WebDriverWait(driver, 20) + try: + wait.until(EC.presence_of_element_located((By.XPATH, expr))) + except SELENIUM_EXCEPTIONS: + return + 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 diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 003924c..d94923d 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,12 +1,10 @@ from .archive import archiver from ..types import Conversation, Message -from .common import SELENIUM_EXCEPTIONS, logger, click_button -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait +from .common import SELENIUM_EXCEPTIONS, logger, click_button, wait_xpath from selenium.webdriver.common.action_chains import ActionChains from pendulum import now from json import loads +from time import sleep import lxml.html as lxh @@ -17,17 +15,7 @@ def get_conversations(driver): Get a list of conversations """ - actions = ActionChains(driver) - - wait = WebDriverWait(driver, 20) - - try: - wait.until( - EC.presence_of_element_located((By.XPATH, "//div[@id=\"threadlist_rows\"]")) - ) - except SELENIUM_EXCEPTIONS: - LOG.exception("No conversations") - return + wait_xpath(driver, "//div[@id=\"threadlist_rows\"]") # This function *cannot* be a generator # Otherwise elements will become stale @@ -98,14 +86,7 @@ def get_convo(driver, convo): """ driver.get(convo.url) - wait = WebDriverWait(driver, 20) - try: - wait.until( - EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'See Older Messages')]")) - ) - except SELENIUM_EXCEPTIONS: - LOG.exception("Could not load more messages") - return + wait_xpath(driver, "//*[contains(text(), 'See Older Messages')]") # Expand conversation until we've reached the beginning while True: @@ -131,6 +112,12 @@ def delete_conversation(driver, convo): Deletes a conversation """ + actions = ActionChains(driver) + + delete_button = driver.find_element_by_xpath("//select/option[contains(text(), 'Delete')]") + + actions.move_to_element(delete_button).click().perform() + return def extract_convo(driver, convo): @@ -150,8 +137,7 @@ def extract_convo(driver, convo): return convo - -def traverse_conversations(driver, year=None): +def traverse_conversations(driver, year=None, delete=False): """ Remove all conversations within a specified range """ @@ -169,9 +155,13 @@ def traverse_conversations(driver, year=None): if convo.date.year == int(year): extract_convo(driver, convo) archive_convo.archive(convo) + if delete: + delete_conversation(driver, convo) # Otherwise we're looking at all convos elif not year: extract_convo(driver, convo) archive_convo.archive(convo) + if delete: + delete_conversation(driver, convo) -- 2.30.2 From c91bec3367f52e3dbbf931a007c99a4a8f522823 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 4 Aug 2019 16:25:22 -0400 Subject: [PATCH 111/186] Conversation deletion almost working --- deletefb/tools/conversations.py | 34 +++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index d94923d..4c7c638 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -1,7 +1,9 @@ from .archive import archiver from ..types import Conversation, Message from .common import SELENIUM_EXCEPTIONS, logger, click_button, wait_xpath +from .config import settings from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.support.ui import Select from pendulum import now from json import loads from time import sleep @@ -114,10 +116,22 @@ def delete_conversation(driver, convo): actions = ActionChains(driver) - delete_button = driver.find_element_by_xpath("//select/option[contains(text(), 'Delete')]") + menu_select = Select(driver.find_element_by_xpath("//select/option[contains(text(), 'Delete')]/..")) + + for i, option in enumerate(menu_select.options): + print(option.text) + if option.text.strip() == "Delete": + menu_select.select_by_index(i) + + + wait_xpath(driver, "//h2[contains(text(), 'Delete conversation')]") + + delete_button = driver.find_element_by_xpath("//a[contains(text(), 'Delete')][@role='button']") actions.move_to_element(delete_button).click().perform() + sleep(10000) + return def extract_convo(driver, convo): @@ -137,7 +151,7 @@ def extract_convo(driver, convo): return convo -def traverse_conversations(driver, year=None, delete=False): +def traverse_conversations(driver, year=None): """ Remove all conversations within a specified range """ @@ -154,14 +168,18 @@ def traverse_conversations(driver, year=None, delete=False): if year and convo.date: if convo.date.year == int(year): extract_convo(driver, convo) - archive_convo.archive(convo) - if delete: - delete_conversation(driver, convo) + + if settings["ARCHIVE"]: + archive_convo.archive(convo) + + delete_conversation(driver, convo) # Otherwise we're looking at all convos elif not year: extract_convo(driver, convo) - archive_convo.archive(convo) - if delete: - delete_conversation(driver, convo) + + if settings["ARCHIVE"]: + archive_convo.archive(convo) + + delete_conversation(driver, convo) -- 2.30.2 From 06da87fba61f78ca67abadcd9230f2982c255ec1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 4 Aug 2019 19:44:31 -0400 Subject: [PATCH 112/186] Deletion of conversations working --- deletefb/tools/conversations.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index 4c7c638..bcbd089 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -6,7 +6,6 @@ from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import Select from pendulum import now from json import loads -from time import sleep import lxml.html as lxh @@ -119,19 +118,14 @@ def delete_conversation(driver, convo): menu_select = Select(driver.find_element_by_xpath("//select/option[contains(text(), 'Delete')]/..")) for i, option in enumerate(menu_select.options): - print(option.text) if option.text.strip() == "Delete": menu_select.select_by_index(i) - + break wait_xpath(driver, "//h2[contains(text(), 'Delete conversation')]") - delete_button = driver.find_element_by_xpath("//a[contains(text(), 'Delete')][@role='button']") - actions.move_to_element(delete_button).click().perform() - sleep(10000) - return def extract_convo(driver, convo): -- 2.30.2 From 63f7109c22554cc9c23baa03a68ebecc69c31f31 Mon Sep 17 00:00:00 2001 From: wes Date: Tue, 6 Aug 2019 10:15:01 -0400 Subject: [PATCH 113/186] Fixes #79 --- deletefb/tools/wall.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 1d9d232..568c516 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,6 +1,6 @@ from ..types import Post from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, click_button +from .common import SELENIUM_EXCEPTIONS, click_button, wait_xpath from .config import settings from selenium.webdriver.common.action_chains import ActionChains @@ -53,8 +53,9 @@ def delete_posts(driver, actions = ActionChains(driver) actions.move_to_element(timeline_element).click().perform() - menu = driver.find_element_by_css_selector("#globalContainer > div.uiContextualLayerPositioner.uiLayer > div") - actions.move_to_element(menu).perform() + wait_xpath(driver, "//*[@id='feed_post_menu']/..") + + menu = driver.find_element_by_xpath("//*[@id='feed_post_menu']/..") delete_button = None @@ -69,7 +70,7 @@ def delete_posts(driver, print("Could not find anything to delete") break - actions.move_to_element(delete_button).click().perform() + click_button(driver, delete_button) confirmation_button = driver.find_element_by_class_name("layerConfirm") click_button(driver, confirmation_button) -- 2.30.2 From 0fb8a5d789b5c651fb8c6a2219562abccc06eca6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 6 Aug 2019 10:23:57 -0400 Subject: [PATCH 114/186] Update requirements.txt --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 24001f7..9b578c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ attrs==19.1.0 bitarray==0.9.3 bleach==3.1.0 +cattrs==0.9.0 certifi==2018.11.29 chardet==3.0.4 docutils==0.14 @@ -22,5 +23,6 @@ six==1.12.0 tldextract==2.2.0 tqdm==4.32.2 twine==1.13.0 +typing==3.7.4 urllib3==1.25.2 webencodings==0.5.1 -- 2.30.2 From f25b48b84603a484960de1f3a7171e744c9ecc64 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 6 Aug 2019 10:24:46 -0400 Subject: [PATCH 115/186] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 04fd8dc..b83d23b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.8", + version="1.1.9", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 51454a993264b1b64beb3b71a0cd3d1bd4b9e767 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Thu, 8 Aug 2019 10:17:21 +0000 Subject: [PATCH 116/186] updating working files --- Dockerfile | 35 ++++++++++++++++++++++++----------- Makefile | 17 ++++++----------- run.sh | 16 ++-------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index c0d6034..a1a9a5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,19 @@ -# to build and launch, edit ./run.sh -# with your values, then execute it +# docker run -ti --rm \ +# -e DISPLAY=$DISPLAY \ +# -v /tmp/.X11-unix:/tmp/.X11-unix \ +# --cap-add=SYS_ADMIN \ +# --cap-add=NET_ADMIN \ +# --cpuset-cpus 0 \ +# --memory 4GB \ +# -v /tmp/.X11-unix:/tmp/.X11-unix \ +# -e DISPLAY=unix:0 \ +# --device /dev/snd \ +# --device /dev/dri \ +# -v /dev/shm:/dev/shm \ +# deletefb -e mail="your@email.com" -e pass="Y0Ur*P4ss" -e url="http://facebook.com/your-username" deletefb:latest FROM debian:stable-slim + RUN apt-get update && \ apt-get install -y \ git \ @@ -15,8 +27,8 @@ FROM debian:stable-slim chromium \ chromium-driver -#creating user - ENV user username +#creating new user + ENV user deletefb RUN export uid=1000 gid=1000 && \ mkdir -p /home/${user} && \ echo "${user}:x:${uid}:${gid}:${user},,,:/home/${user}:/bin/bash" >> /etc/passwd && \ @@ -26,17 +38,18 @@ FROM debian:stable-slim chown ${uid}:${gid} -R /home/${user} && \ usermod -aG sudo ${user} -# delete FB repo install + +# deletefb install USER ${user} WORKDIR /home/${user} - - ARG mail + + ARG email ARG pass ARG url + #ARG --conversations RUN pip3 install --user delete-facebook-posts - RUN pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git - RUN git clone https://github.com/weskerfoot/DeleteFB.git - RUN pip3 install -r ./DeleteFB/requirements.txt RUN pip3 install --user selenium attrs pybloom_live - CMD python3 -m deletefb.deletefb -E ${mail} -P ${pass} -U ${url} \ No newline at end of file + + ADD run.sh /tmp/run.sh + ENTRYPOINT [ "/tmp/run.sh" ] diff --git a/Makefile b/Makefile index 55b7eb1..976fdb9 100644 --- a/Makefile +++ b/Makefile @@ -11,20 +11,15 @@ build: @docker build -t $(NAME) . run: - @read -p "Enter your Facebook email: " email; \ - - @read -p "Enter your Facebook password: " password; \ - - @read -p "Enter your Facebook username: " username; - - @docker run -ti --rm \ - -e DISPLAY=$DISPLAY \ + @read -p "Enter your Facebook email: " email && read -p "Enter your Facebook password: " password && read -p "Enter your Facebook username: " username && docker run -ti --rm \ + -e DISPLAY=$$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ --cap-add=SYS_ADMIN \ --cap-add=NET_ADMIN \ --cpuset-cpus 0 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ --device /dev/dri \ -v /dev/shm:/dev/shm \ - $(NAME):latest "`which python3` -m deletefb.deletefb -e mail="$$email" -e pass=$$password -e url=$$url" - + -e EMAIL="$$email" \ + -e PASS="$$password" \ + -e URL="https://facebook.com/$$username" \ + $(NAME):latest \ No newline at end of file diff --git a/run.sh b/run.sh index bf48132..6a6897b 100755 --- a/run.sh +++ b/run.sh @@ -1,14 +1,2 @@ -docker build -t deletefb . && \ -docker run -ti --rm \ - -e DISPLAY=$DISPLAY \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - --cap-add=SYS_ADMIN \ - --cap-add=NET_ADMIN \ - --cpuset-cpus 0 \ - --memory 4GB \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e DISPLAY=unix:0 \ - --device /dev/snd \ - --device /dev/dri \ - -v /dev/shm:/dev/shm \ - deletefb -e mail="your@email.com" -e pass="Y0Ur*P4ss" -e url="http://facebook.com/your-username" deletefb:latest +#!/bin/bash +/usr/bin/python3 -m deletefb.deletefb -E $EMAIL -P $PASS -U $URL \ No newline at end of file -- 2.30.2 From d2ce6b5ab85dea7e0d116804b22ed0f378b0ea47 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Thu, 8 Aug 2019 10:35:42 +0000 Subject: [PATCH 117/186] updating dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index a1a9a5d..f7c2177 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,6 @@ +# To run, just type "make", or + +# docker build -t deletefb . # docker run -ti --rm \ # -e DISPLAY=$DISPLAY \ # -v /tmp/.X11-unix:/tmp/.X11-unix \ -- 2.30.2 From ee34c119750d002d6753c25105f409a136618f34 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Thu, 8 Aug 2019 10:41:34 +0000 Subject: [PATCH 118/186] fixing makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 976fdb9..f342b0c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ # Makefile NAME:= deletefb -url := https://facebook.com/$$username .PHONY: all build run -- 2.30.2 From e761cc236b8550ba399c13384889b80711bb2853 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Thu, 8 Aug 2019 12:59:50 +0000 Subject: [PATCH 119/186] fixing spaces --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f342b0c..87bf189 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build: @docker build -t $(NAME) . run: - @read -p "Enter your Facebook email: " email && read -p "Enter your Facebook password: " password && read -p "Enter your Facebook username: " username && docker run -ti --rm \ + @read -p "Enter your Facebook email: " email && read -p "Enter your Facebook password: " password && read -p "Enter your Facebook username: " username && docker run -ti --rm \ -e DISPLAY=$$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ --cap-add=SYS_ADMIN \ -- 2.30.2 From 6cd7d6aaf7ba291b56b4932e453de5a9a5874e98 Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Sat, 17 Aug 2019 16:09:10 +0000 Subject: [PATCH 120/186] travis test --- .travis.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..11aac95 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +sudo: required +services: + - docker +env: + global: + # - REGISTRY_USER=${REGISTRY_USER} + # - REGISTRY_PASS=${REGISTRY_PASS} + - IMAGE_NAME=${REGISTRY_USER}/deletefb + +# before_script: + +script: + - docker build --pull --no-cache --tag "$IMAGE_NAME" . + # - docker build --pull --cache-from "$IMAGE_NAME" --tag "$IMAGE_NAME" . + +after_script: + - docker images + +before_deploy: + - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" + - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:latest" + # - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:${version}" +deploy: + provider: script + script: docker push "${IMAGE_NAME}:latest" + on: + branch: master \ No newline at end of file -- 2.30.2 From 0a726d135e5d74a5261fba0df9d0720a645c405e Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Sat, 17 Aug 2019 16:25:02 +0000 Subject: [PATCH 121/186] shorter travis-ci --- .travis.yml | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 11aac95..7fbe130 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,23 +5,29 @@ env: global: # - REGISTRY_USER=${REGISTRY_USER} # - REGISTRY_PASS=${REGISTRY_PASS} - - IMAGE_NAME=${REGISTRY_USER}/deletefb + # - IMAGE_NAME=${REGISTRY_USER}/deletefb # before_script: script: - - docker build --pull --no-cache --tag "$IMAGE_NAME" . + # - docker build --pull --no-cache --tag "$IMAGE_NAME" . # - docker build --pull --cache-from "$IMAGE_NAME" --tag "$IMAGE_NAME" . - -after_script: + - echo "${REGISTRY_PASS}" | docker login -u "${REGISTRY_USER}" --password-stdin + - docker build -t ${IMAGE_NAME} . - docker images + - docker push ${REGISTRY_USER}/${IMAGE_NAME} +on: + branch: master + +# after_script: +# - docker images -before_deploy: - - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" - - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:latest" - # - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:${version}" -deploy: - provider: script - script: docker push "${IMAGE_NAME}:latest" - on: - branch: master \ No newline at end of file +# before_deploy: +# - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" +# - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:latest" +# # - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:${version}" +# deploy: +# provider: script +# script: docker push "${IMAGE_NAME}:latest" +# on: +# branch: master \ No newline at end of file -- 2.30.2 From 5f073cb97156e6e87310854661f28fd76711d83e Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Sat, 17 Aug 2019 16:33:21 +0000 Subject: [PATCH 122/186] fixing image name --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7fbe130..7bc94c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: # - docker build --pull --no-cache --tag "$IMAGE_NAME" . # - docker build --pull --cache-from "$IMAGE_NAME" --tag "$IMAGE_NAME" . - echo "${REGISTRY_PASS}" | docker login -u "${REGISTRY_USER}" --password-stdin - - docker build -t ${IMAGE_NAME} . + - docker build -t ${REGISTRY_USER}/${IMAGE_NAME} . - docker images - docker push ${REGISTRY_USER}/${IMAGE_NAME} on: -- 2.30.2 From 111fc52fc3a4e7021d444cfeef38f89edc5027bb Mon Sep 17 00:00:00 2001 From: Marco Matos Date: Sat, 17 Aug 2019 16:51:31 +0000 Subject: [PATCH 123/186] final version --- .travis.yml | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bc94c3..7d6a34f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,31 +3,15 @@ services: - docker env: global: + # setup these vars under settings at https://travis-ci.com # - REGISTRY_USER=${REGISTRY_USER} # - REGISTRY_PASS=${REGISTRY_PASS} # - IMAGE_NAME=${REGISTRY_USER}/deletefb - -# before_script: - + script: - # - docker build --pull --no-cache --tag "$IMAGE_NAME" . - # - docker build --pull --cache-from "$IMAGE_NAME" --tag "$IMAGE_NAME" . - echo "${REGISTRY_PASS}" | docker login -u "${REGISTRY_USER}" --password-stdin - docker build -t ${REGISTRY_USER}/${IMAGE_NAME} . - docker images - docker push ${REGISTRY_USER}/${IMAGE_NAME} on: - branch: master - -# after_script: -# - docker images - -# before_deploy: -# - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" -# - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:latest" -# # - docker tag "$IMAGE_NAME" "${IMAGE_NAME}:${version}" -# deploy: -# provider: script -# script: docker push "${IMAGE_NAME}:latest" -# on: -# branch: master \ No newline at end of file + branch: master \ No newline at end of file -- 2.30.2 From 4b59fde84eee42a96883642090a7e7ef13c545e4 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sun, 25 Aug 2019 12:29:26 -0400 Subject: [PATCH 125/186] Remove comments option until work is completed --- deletefb/deletefb.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 21c5f47..ee6a163 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -23,7 +23,7 @@ def run_delete(): default="wall", dest="mode", type=str, - choices=["wall", "unlike_pages", "comments", "conversations"], + choices=["wall", "unlike_pages", "conversations"], help="The mode you want to run in. Default is `wall' which deletes wall posts" ) @@ -115,9 +115,6 @@ def run_delete(): elif args.mode == "unlike_pages": unlike_pages(driver, args.profile_url) - elif args.mode == "comments": - delete_comments(driver, args.profile_url) - elif args.mode == "conversations": traverse_conversations(driver, year=args.year) -- 2.30.2 From cef901354798cf162b2e8c3a34a1ef1a7cf97efc Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 30 Sep 2019 02:18:12 -0400 Subject: [PATCH 126/186] Update .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d6a34f..2066438 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ env: script: - echo "${REGISTRY_PASS}" | docker login -u "${REGISTRY_USER}" --password-stdin - - docker build -t ${REGISTRY_USER}/${IMAGE_NAME} . + - docker build -t "${REGISTRY_USER}/${IMAGE_NAME}" . - docker images - docker push ${REGISTRY_USER}/${IMAGE_NAME} on: - branch: master \ No newline at end of file + branch: master -- 2.30.2 From 8896549318b6b381ee747f4410ba5b95478b9e71 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 30 Sep 2019 02:29:31 -0400 Subject: [PATCH 127/186] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f847b48..a8a4aed 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ You have several options to run it. 2) Clone this repo and run `pip3 install --user .` or do `pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git` 3) Set up a Python virtualenv, activate it, and run `pip3 install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. +4) Use the docker image (experimental) by running `make` after checking this repository out with git. There is also an image built and published automatically at `wjak56/deletefb:latest` ## How To Use It -- 2.30.2 From 5384c082d92ffda71f74fb88748290c52cca5817 Mon Sep 17 00:00:00 2001 From: wes Date: Mon, 4 Nov 2019 18:28:44 -0500 Subject: [PATCH 128/186] Add new arguments for headless option #89 --- deletefb/tools/login.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 772b53e..73fe08b 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -32,6 +32,9 @@ def login(user_email_address, if is_headless: chrome_options.add_argument('--headless') chrome_options.add_argument('--disable-gpu') + chrome_options.add_argument('--disabled-features=VizDisplayCompositor') + chrome_options.add_argument('--dump-dom') + chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('log-level=2') try: -- 2.30.2 From 511b43a9f14c648f3e6a55fe13b983b32b92b378 Mon Sep 17 00:00:00 2001 From: esir Date: Sat, 9 Nov 2019 09:50:50 +0300 Subject: [PATCH 129/186] Automatically downloads chrome driver for users according the their os platform --- deletefb/tools/chrome_driver.py | 111 ++++++++++++++++++++++++++++++++ deletefb/tools/common.py | 10 +-- deletefb/tools/login.py | 17 ++--- 3 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 deletefb/tools/chrome_driver.py diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py new file mode 100644 index 0000000..ddd8fc1 --- /dev/null +++ b/deletefb/tools/chrome_driver.py @@ -0,0 +1,111 @@ +import re +import zipfile +import os, sys, stat, platform +from urllib.request import urlretrieve + +from clint.textui import puts, colored +import progressbar + +from selenium import webdriver +from selenium.webdriver.chrome.options import Options + +from .common import NO_CHROME_DRIVER + + +def extract_zip(filename): + """ + Uses zipfile package to extract a single zipfile + :param filename: + :return: new filename + """ + try: + file = zipfile.ZipFile(filename, 'r') + except FileNotFoundError: + puts(colored.red(f"{filename} Does not exist")) + sys.exit() + + # Save the name of the new file + new_file_name = file.namelist()[0] + + # Extract the file and make it executable + file.extractall() + + driver_stat = os.stat(new_file_name) + os.chmod(new_file_name, driver_stat.st_mode | stat.S_IEXEC) + + file.close() + os.remove(filename) + return new_file_name + + +def setup_selenium(driver_path, options): + # Configures selenium to use a custom path + driver = webdriver.Chrome(executable_path=driver_path, options=options) + return driver + + +def get_webdriver(): + """ + Ensure a webdriver is available + If Not, Download it. + """ + cwd = os.listdir(os.getcwd()) + webdriver_regex = re.compile('chromedriver') + web_driver = list(filter(webdriver_regex.match, cwd)) + + if web_driver: + # check if a extracted copy already exists + if not os.path.isfile('chromedriver'): + # Extract file + extract_zip(web_driver[0]) + + return os.getcwd() + '/chromedriver' + + else: + # Download it according to the current machine + webdrivers = ['https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip', + 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip', + 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip' + ] + os_platform = platform.system() + if os_platform == 'Darwin': + chrome_webdriver = webdrivers[0] + elif os_platform == 'Linux': + chrome_webdriver = webdrivers[1] + elif os_platform == 'Windows': + chrome_webdriver = webdrivers[2] + else: + raise Exception("Unknown Operating system platform") + + global total_size + + def show_progress(*res): + global total_size + pbar = None + downloaded = 0 + block_num, block_size, total_size = res + + if not pbar: + pbar = progressbar.ProgressBar(maxval=total_size) + pbar.start() + downloaded += block_num * block_size + + if downloaded < total_size: + pbar.update(downloaded) + else: + pbar.finish() + + puts(colored.yellow("Downloading Chrome Webdriver")) + file_name = chrome_webdriver.split('/')[-1] + response = urlretrieve(chrome_webdriver, file_name, show_progress) + + if int(response[1].get('Content-Length')) == total_size: + puts(colored.green(f"DONE!")) + + return os.getcwd() + '/' + extract_zip(file_name) + else: + puts(colored.red("An error Occurred While trying to download the driver.")) + # remove the downloaded file and exit + os.remove(file_name) + sys.stderr.write(NO_CHROME_DRIVER) + sys.exit(1) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index de0d679..bb24989 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -5,15 +5,13 @@ from selenium.webdriver.common.by import By from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, - TimeoutException, - JavascriptException + TimeoutException ) import json import logging import logging.config import os -import pendulum SELENIUM_EXCEPTIONS = ( NoSuchElementException, @@ -21,12 +19,14 @@ SELENIUM_EXCEPTIONS = ( TimeoutException ) + def click_button(driver, el): """ Click a button using Javascript """ driver.execute_script("arguments[0].click();", el) + def scroll_to(driver, el): """ Scroll an element into view, using JS @@ -36,6 +36,7 @@ def scroll_to(driver, el): except SELENIUM_EXCEPTIONS: return + def logger(name): """ Args: @@ -66,7 +67,8 @@ def wait_xpath(driver, expr): except SELENIUM_EXCEPTIONS: return + NO_CHROME_DRIVER = """ -You need to install the chromedriver for Selenium\n +You need to manually install the chromedriver for Selenium\n Please see this link https://github.com/weskerfoot/DeleteFB#how-to-use-it\n """ diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 772b53e..2d3f39c 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,11 +1,11 @@ -from .common import NO_CHROME_DRIVER from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options -from seleniumrequests import Chrome -import sys import time +from .chrome_driver import get_webdriver, setup_selenium + + def login(user_email_address, user_password, is_headless, @@ -34,15 +34,8 @@ def login(user_email_address, chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('log-level=2') - try: - driver = Chrome(options=chrome_options) - except Exception as e: - # The user does not have chromedriver installed - # Tell them to install it - sys.stderr.write(str(e)) - sys.stderr.write(NO_CHROME_DRIVER) - sys.exit(1) - + driver_path = get_webdriver() + driver = setup_selenium(driver_path, chrome_options) driver.implicitly_wait(10) driver.get("https://www.facebook.com/login/device-based/regular/login/?login_attempt=1&lwv=110") -- 2.30.2 From 6ce333780d3f58ba54318a3e6351f771fa88896c Mon Sep 17 00:00:00 2001 From: esir Date: Sat, 9 Nov 2019 11:14:07 +0300 Subject: [PATCH 130/186] Include dependencies added --- requirements.txt | 2 ++ setup.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b578c6..25df0c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,13 @@ bleach==3.1.0 cattrs==0.9.0 certifi==2018.11.29 chardet==3.0.4 +clint==0.5.1 docutils==0.14 idna==2.8 lxml==4.4.0 pendulum==2.0.5 pkginfo==1.5.0.1 +progressbar==2.5 pybloom-live==3.0.0 Pygments==2.4.2 python-dateutil==2.8.0 diff --git a/setup.py b/setup.py index b83d23b..49e119d 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,9 @@ setuptools.setup( "attrs", "cattrs", "lxml", - "pendulum" + "pendulum", + "clint", + "progressbar" ], classifiers= [ "Programming Language :: Python :: 3", -- 2.30.2 From 6340d2c50acd6e71b914a4cd61dfd7457ee83103 Mon Sep 17 00:00:00 2001 From: esir Date: Sun, 10 Nov 2019 16:41:38 +0300 Subject: [PATCH 131/186] Adds a custom exceptions class --- deletefb/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 deletefb/exceptions.py diff --git a/deletefb/exceptions.py b/deletefb/exceptions.py new file mode 100644 index 0000000..6f22085 --- /dev/null +++ b/deletefb/exceptions.py @@ -0,0 +1,2 @@ +class UnknownOSException(Exception): + pass -- 2.30.2 From d08d8861d4f398856e8df4c6c5caa221f3e661e9 Mon Sep 17 00:00:00 2001 From: esir Date: Sun, 10 Nov 2019 16:42:45 +0300 Subject: [PATCH 132/186] Code cleanup --- deletefb/tools/chrome_driver.py | 42 +++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index ddd8fc1..d6549ba 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -2,14 +2,23 @@ import re import zipfile import os, sys, stat, platform from urllib.request import urlretrieve +from collections import namedtuple from clint.textui import puts, colored import progressbar from selenium import webdriver -from selenium.webdriver.chrome.options import Options from .common import NO_CHROME_DRIVER +from ..exceptions import UnknownOSException + + +_ = namedtuple('WebDrivers', 'mac linux windows') +drivers = ['https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip', + 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip', + 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip' + ] +WebDriver = _(drivers[0], drivers[1], drivers[2]) def extract_zip(filename): @@ -19,29 +28,28 @@ def extract_zip(filename): :return: new filename """ try: - file = zipfile.ZipFile(filename, 'r') + _file = zipfile.ZipFile(filename, 'r') except FileNotFoundError: puts(colored.red(f"{filename} Does not exist")) - sys.exit() + sys.exit(1) # Save the name of the new file - new_file_name = file.namelist()[0] + new_file_name = _file.namelist()[0] # Extract the file and make it executable - file.extractall() + _file.extractall() driver_stat = os.stat(new_file_name) os.chmod(new_file_name, driver_stat.st_mode | stat.S_IEXEC) - file.close() + _file.close() os.remove(filename) return new_file_name def setup_selenium(driver_path, options): # Configures selenium to use a custom path - driver = webdriver.Chrome(executable_path=driver_path, options=options) - return driver + return webdriver.Chrome(executable_path=driver_path, options=options) def get_webdriver(): @@ -59,23 +67,20 @@ def get_webdriver(): # Extract file extract_zip(web_driver[0]) - return os.getcwd() + '/chromedriver' + return "{0}/chromedriver".format(os.getcwd()) else: # Download it according to the current machine - webdrivers = ['https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip', - 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip', - 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip' - ] + os_platform = platform.system() if os_platform == 'Darwin': - chrome_webdriver = webdrivers[0] + chrome_webdriver = WebDriver.mac elif os_platform == 'Linux': - chrome_webdriver = webdrivers[1] + chrome_webdriver = WebDriver.linux elif os_platform == 'Windows': - chrome_webdriver = webdrivers[2] + chrome_webdriver = WebDriver.windows else: - raise Exception("Unknown Operating system platform") + raise UnknownOSException("Unknown Operating system platform") global total_size @@ -102,7 +107,8 @@ def get_webdriver(): if int(response[1].get('Content-Length')) == total_size: puts(colored.green(f"DONE!")) - return os.getcwd() + '/' + extract_zip(file_name) + return "{0}/{1}".format(os.getcwd(), extract_zip(file_name)) + else: puts(colored.red("An error Occurred While trying to download the driver.")) # remove the downloaded file and exit -- 2.30.2 From 928c593c981e6466a23412aeb96381d1f61aae16 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 10 Dec 2019 19:02:36 -0500 Subject: [PATCH 133/186] Update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 49e119d..137cbe1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.9", + version="1.1.10", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From a0c7929d393cd6b2f4b76160eb3a6d34fc7d6c67 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 10 Dec 2019 19:06:37 -0500 Subject: [PATCH 134/186] Update docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8a4aed..e95bea5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ git+https://github.com/weskerfoot/DeleteFB.git` * Make sure that you have a recent version of Python 3.x installed (preferably 3.6 or greater) * Make sure that you have Google Chrome installed and that it is up to date -* Also install the chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/home) for an explanation of what the chromedriver does. +* The tool will attempt to automatically install chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/home) for an explanation of what the chromedriver does. You may have to manually install it if auto-install fails. * On Linux, it will be called something like `chromium-chromedriver` or just `chromium`. * On MacOS, it will be available via brew, with the following commands: -- 2.30.2 From 4def8d6690524125d7593a0e1cd8c2e1660d6dea Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 12 Dec 2019 22:56:57 -0500 Subject: [PATCH 135/186] Fix broken cattrs --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 25df0c8..a29e288 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ attrs==19.1.0 bitarray==0.9.3 bleach==3.1.0 -cattrs==0.9.0 +cattrs-3.8==0.9.0 certifi==2018.11.29 chardet==3.0.4 clint==0.5.1 diff --git a/setup.py b/setup.py index 137cbe1..2c5d425 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setuptools.setup( "requests", "pybloom-live", "attrs", - "cattrs", + "cattrs-3.8", "lxml", "pendulum", "clint", -- 2.30.2 From 34b110f9ad320f7b3e403646c738791c8598ac4c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 12 Dec 2019 22:58:56 -0500 Subject: [PATCH 136/186] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 137cbe1..d8877e4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.10", + version="1.1.11", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From c1d9cc5934a2bf32187c80e4f55b04a44b6a920f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sat, 28 Dec 2019 23:44:50 -0500 Subject: [PATCH 137/186] Avoid using `default` for attrs classes with mutable objects --- deletefb/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deletefb/types.py b/deletefb/types.py index c1a0148..fc51b84 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -22,7 +22,7 @@ def convert_date(text): @attr.s class Post: content = attr.ib() - comments = attr.ib(default=[]) + comments = attr.ib(factory=[]) date = attr.ib(factory=pendulum.now) name = attr.ib(factory=lambda: uuid.uuid4().hex) @@ -38,8 +38,8 @@ class Conversation: url = attr.ib() name = attr.ib() date : datetime = attr.ib(converter=convert_date) - messages = attr.ib(default=[]) - image_links = attr.ib(default=[]) + messages = attr.ib(factory=[]) + image_links = attr.ib(factory=[]) @attr.s class Message: -- 2.30.2 From 3ace0acddd162f58c4ac53e4aae3b827e6936c37 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sat, 28 Dec 2019 23:46:36 -0500 Subject: [PATCH 138/186] factory=list, not [] --- deletefb/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deletefb/types.py b/deletefb/types.py index fc51b84..5e294ee 100644 --- a/deletefb/types.py +++ b/deletefb/types.py @@ -22,7 +22,7 @@ def convert_date(text): @attr.s class Post: content = attr.ib() - comments = attr.ib(factory=[]) + comments = attr.ib(factory=list) date = attr.ib(factory=pendulum.now) name = attr.ib(factory=lambda: uuid.uuid4().hex) @@ -38,8 +38,8 @@ class Conversation: url = attr.ib() name = attr.ib() date : datetime = attr.ib(converter=convert_date) - messages = attr.ib(factory=[]) - image_links = attr.ib(factory=[]) + messages = attr.ib(factory=list) + image_links = attr.ib(factory=list) @attr.s class Message: -- 2.30.2 From 7a92f80a2a9359db1774c62e184f0ff50ec31643 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 31 Dec 2019 17:14:32 -0500 Subject: [PATCH 139/186] add option to pass path to chrome binary --- deletefb/deletefb.py | 13 ++++++++++++- deletefb/tools/chrome_driver.py | 1 - deletefb/tools/login.py | 8 ++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index ee6a163..4d8c5a9 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -89,6 +89,16 @@ def run_delete(): help="The year(s) you want posts deleted." ) + parser.add_argument( + "-B", + "--chromebin", + required=False, + default=False, + dest="chromebin", + type=str, + help="Optional path to the Google Chrome (or Chromium) binary" + ) + args = parser.parse_args() settings["ARCHIVE"] = not args.archive_off @@ -102,7 +112,8 @@ def run_delete(): user_email_address=args.email, user_password=args_user_password, is_headless=args.is_headless, - two_factor_token=args.two_factor_token + two_factor_token=args.two_factor_token, + chrome_binary_path=args.chromebin ) if args.mode == "wall": diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index d6549ba..2069340 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -51,7 +51,6 @@ def setup_selenium(driver_path, options): # Configures selenium to use a custom path return webdriver.Chrome(executable_path=driver_path, options=options) - def get_webdriver(): """ Ensure a webdriver is available diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index dd3ce05..c1c67a5 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -5,11 +5,11 @@ import time from .chrome_driver import get_webdriver, setup_selenium - def login(user_email_address, user_password, is_headless, - two_factor_token): + two_factor_token, + chrome_binary_path=None): """ Attempts to log into Facebook Returns a driver object @@ -27,6 +27,10 @@ def login(user_email_address, chrome_options = Options() prefs = {"profile.default_content_setting_values.notifications": 2, 'disk-cache-size': 4096} chrome_options.add_experimental_option("prefs", prefs) + + if chrome_binary_path: + chrome_options.binary_location = chrome_binary_path + chrome_options.add_argument("start-maximized") if is_headless: -- 2.30.2 From a794388f8f5a96d8cf27daa7aa842a83b5e13413 Mon Sep 17 00:00:00 2001 From: Mohamed Barhdadi Date: Wed, 8 Jan 2020 19:41:20 +0100 Subject: [PATCH 140/186] Update cattrs package version Change cattrs package to 0.9.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a29e288..04533f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ attrs==19.1.0 bitarray==0.9.3 bleach==3.1.0 -cattrs-3.8==0.9.0 +cattrs-3.8==0.9.1 certifi==2018.11.29 chardet==3.0.4 clint==0.5.1 -- 2.30.2 From abe597db9b87f66e8d5a1ea49d611b298d3a721e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 18 Jan 2020 13:58:38 -0500 Subject: [PATCH 141/186] Refactor variable names --- deletefb/tools/login.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index c1c67a5..63d0156 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -52,14 +52,9 @@ def login(user_email_address, login_button = "loginbutton" approvals_code = "approvals_code" - emailelement = driver.find_element_by_name(email) - passwordelement = driver.find_element_by_name(password) - - emailelement.send_keys(user_email_address) - passwordelement.send_keys(user_password) - - loginelement = driver.find_element_by_id(login_button) - loginelement.click() + driver.find_element_by_name(email).send_keys(user_email_address) + driver.find_element_by_name(password).send_keys(user_password) + driver.find_element_by_id(login_button).click() # Defaults to no 2fa has_2fa = False -- 2.30.2 From 0e24537f42b703bc8782951c1de86a348ffc4f98 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 18 Jan 2020 14:04:41 -0500 Subject: [PATCH 142/186] More refactoring --- deletefb/tools/chrome_driver.py | 42 +++++++++++++-------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index 2069340..a0247d8 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -1,25 +1,21 @@ -import re -import zipfile -import os, sys, stat, platform -from urllib.request import urlretrieve -from collections import namedtuple - +from ..exceptions import UnknownOSException +from .common import NO_CHROME_DRIVER from clint.textui import puts, colored -import progressbar - +from collections import namedtuple from selenium import webdriver +from urllib.request import urlretrieve +from enum import Enum -from .common import NO_CHROME_DRIVER -from ..exceptions import UnknownOSException - - -_ = namedtuple('WebDrivers', 'mac linux windows') -drivers = ['https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip', - 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip', - 'https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip' - ] -WebDriver = _(drivers[0], drivers[1], drivers[2]) +import os, sys, stat, platform +import progressbar +import re +import zipfile +chrome_drivers = { + "Windows" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip", + "Darwin" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip", + "Linux" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip" +} def extract_zip(filename): """ @@ -71,14 +67,8 @@ def get_webdriver(): else: # Download it according to the current machine - os_platform = platform.system() - if os_platform == 'Darwin': - chrome_webdriver = WebDriver.mac - elif os_platform == 'Linux': - chrome_webdriver = WebDriver.linux - elif os_platform == 'Windows': - chrome_webdriver = WebDriver.windows - else: + chrome_webdriver = chrome_drivers.get(platform.system(), False) + if not chrome_webdriver: raise UnknownOSException("Unknown Operating system platform") global total_size -- 2.30.2 From bc210890e5edbf534c2f2d600e212d0cb31a0167 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 18 Jan 2020 14:05:54 -0500 Subject: [PATCH 143/186] Refactor imports --- deletefb/tools/chrome_driver.py | 2 -- deletefb/tools/login.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index a0247d8..c59361e 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -1,10 +1,8 @@ from ..exceptions import UnknownOSException from .common import NO_CHROME_DRIVER from clint.textui import puts, colored -from collections import namedtuple from selenium import webdriver from urllib.request import urlretrieve -from enum import Enum import os, sys, stat, platform import progressbar diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 63d0156..daae1d5 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,10 +1,9 @@ +from .chrome_driver import get_webdriver, setup_selenium from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options import time -from .chrome_driver import get_webdriver, setup_selenium - def login(user_email_address, user_password, is_headless, -- 2.30.2 From c4727c373bb8cdb971b84aa17f3b76525325917c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 20 Jan 2020 00:04:15 -0500 Subject: [PATCH 144/186] Starting a refactor of all chrome driver code --- deletefb/tools/chrome_driver.py | 46 ++++++++++++++++++++++++++------- deletefb/tools/login.py | 2 +- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index c59361e..07346e8 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -2,6 +2,8 @@ from ..exceptions import UnknownOSException from .common import NO_CHROME_DRIVER from clint.textui import puts, colored from selenium import webdriver +from shutil import which +from subprocess import check_output from urllib.request import urlretrieve import os, sys, stat, platform @@ -9,12 +11,6 @@ import progressbar import re import zipfile -chrome_drivers = { - "Windows" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip", - "Darwin" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip", - "Linux" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip" -} - def extract_zip(filename): """ Uses zipfile package to extract a single zipfile @@ -45,7 +41,36 @@ def setup_selenium(driver_path, options): # Configures selenium to use a custom path return webdriver.Chrome(executable_path=driver_path, options=options) -def get_webdriver(): +# TODO Merge these two into one +def get_chrome_version(chrome_binary_path=None): + driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chrome.exe"]] + + for location in driver_locations: + if location: + version = check_output([location, "--version"]).strip() + # TODO need to reliably parse this output somehow + +def construct_driver_url(chrome_binary_path=None): + """ + Construct a URL to download the Chrome Driver + """ + + platform_string = platform.system() + chrome_drivers = { + "Windows" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_win32.zip", + "Darwin" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_mac64.zip", + "Linux" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_linux64.zip" + } + + # First, construct a LATEST_RELEASE URL using Chrome's major version number. + # For example, with Chrome version 73.0.3683.86, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_73". + # Try to download a small file from this URL. If it successful, the file contains the ChromeDriver version to use. + # If the above step failed, reduce the Chrome major version by 1 and try again. + # For example, with Chrome version 75.0.3745.4, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_74" + # to download a small file, which contains the ChromeDriver version to use. + # You can also use ChromeDriver Canary build. + +def get_webdriver(chrome_binary_path): """ Ensure a webdriver is available If Not, Download it. @@ -65,7 +90,8 @@ def get_webdriver(): else: # Download it according to the current machine - chrome_webdriver = chrome_drivers.get(platform.system(), False) + chrome_webdriver = construct_driver_url(chrome_binary_path) + if not chrome_webdriver: raise UnknownOSException("Unknown Operating system platform") @@ -91,8 +117,8 @@ def get_webdriver(): file_name = chrome_webdriver.split('/')[-1] response = urlretrieve(chrome_webdriver, file_name, show_progress) - if int(response[1].get('Content-Length')) == total_size: - puts(colored.green(f"DONE!")) + if int(response[1].get("Content-Length")) == total_size: + puts(colored.green("Completed downloading the Chrome Driver.")) return "{0}/{1}".format(os.getcwd(), extract_zip(file_name)) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index daae1d5..17866cf 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -40,7 +40,7 @@ def login(user_email_address, chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('log-level=2') - driver_path = get_webdriver() + driver_path = get_webdriver(chrome_binary_path) driver = setup_selenium(driver_path, chrome_options) driver.implicitly_wait(10) -- 2.30.2 From 6ba365e124a5f9eb8f07ebf7fd5081c5adaba83f Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Tue, 21 Jan 2020 13:58:15 -0500 Subject: [PATCH 145/186] add help output to readme --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index e95bea5..98542b5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,28 @@ git+https://github.com/weskerfoot/DeleteFB.git` 4) Use the docker image (experimental) by running `make` after checking this repository out with git. There is also an image built and published automatically at `wjak56/deletefb:latest` ## How To Use It +``` +usage: deletefb [-h] [-M {wall,unlike_pages,conversations}] -E EMAIL [-P PASSWORD] -U PROFILE_URL [-F TWO_FACTOR_TOKEN] [-H] [--no-archive] [-Y YEAR] + [-B CHROMEBIN] + +optional arguments: + -h, --help show this help message and exit + -M {wall,unlike_pages,conversations}, --mode {wall,unlike_pages,conversations} + The mode you want to run in. Default is `wall' which deletes wall posts + -E EMAIL, --email EMAIL + Your email address associated with the account + -P PASSWORD, --password PASSWORD + Your Facebook password + -U PROFILE_URL, --profile-url PROFILE_URL + The link to your Facebook profile, e.g. https://www.facebook.com/your.name + -F TWO_FACTOR_TOKEN, --two-factor TWO_FACTOR_TOKEN + The code generated by your 2FA device for Facebook + -H, --headless Run browser in headless mode (no gui) + --no-archive Turn off archiving (on by default) + -Y YEAR, --year YEAR The year(s) you want posts deleted. + -B CHROMEBIN, --chromebin CHROMEBIN + Optional path to the Google Chrome (or Chromium) binary +``` * Make sure that you have a recent version of Python 3.x installed (preferably 3.6 or greater) -- 2.30.2 From 5da3367df63938efa532c0eb6808641abb180f38 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Tue, 4 Feb 2020 00:24:42 -0500 Subject: [PATCH 146/186] print exceptions (#108) --- deletefb/tools/wall.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 568c516..fb8ff69 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -75,7 +75,8 @@ def delete_posts(driver, click_button(driver, confirmation_button) - except SELENIUM_EXCEPTIONS: + except SELENIUM_EXCEPTIONS as e: + print(e) continue else: break -- 2.30.2 From 64c46e378b0ba4d66a2158c2c4f3898b2c89b023 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sun, 9 Feb 2020 02:52:36 -0500 Subject: [PATCH 147/186] Update chrome_driver.py (#111) --- deletefb/tools/chrome_driver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index c59361e..e048716 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -10,9 +10,9 @@ import re import zipfile chrome_drivers = { - "Windows" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_win32.zip", - "Darwin" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_mac64.zip", - "Linux" : "https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip" + "Windows" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_win32.zip", + "Darwin" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_mac64.zip", + "Linux" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_linux64.zip" } def extract_zip(filename): -- 2.30.2 From 428c354683bcf6abdbb868722a30540f551f9e0b Mon Sep 17 00:00:00 2001 From: as Date: Thu, 13 Feb 2020 21:56:45 +0100 Subject: [PATCH 148/186] respect --no-archive in wall mode (#112) --- deletefb/tools/archive.py | 41 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/deletefb/tools/archive.py b/deletefb/tools/archive.py index 718ee65..cd6c046 100644 --- a/deletefb/tools/archive.py +++ b/deletefb/tools/archive.py @@ -49,21 +49,32 @@ class Archive: self._bloom_filter.add(content.name) return -@contextmanager -def archiver(archive_type): - archive_file = open( - str((Path(".") / Path(archive_type).name).with_suffix(".log.{0}".format(time()))), - mode="ta", - buffering=1 - ) +class FakeArchive: + def archive(self, content): + """ + Do not archive an object + """ + return - archiver_instance = Archive( - archive_type=archive_type, - archive_file=archive_file - ) - try: - yield archiver_instance - finally: - archive_file.close() +@contextmanager +def archiver(archive_type): + if not settings["ARCHIVE"]: + yield FakeArchive() + else: + archive_file = open( + str((Path(".") / Path(archive_type).name).with_suffix(".log.{0}".format(time()))), + mode="ta", + buffering=1 + ) + + archiver_instance = Archive( + archive_type=archive_type, + archive_file=archive_file + ) + + try: + yield archiver_instance + finally: + archive_file.close() -- 2.30.2 From 95ac5252d4e8dd69f6b7838f7ec5bd1fb925f01d Mon Sep 17 00:00:00 2001 From: as Date: Thu, 13 Feb 2020 22:00:27 +0100 Subject: [PATCH 149/186] clean and gitignore deletefb.log (#113) --- .gitignore | 1 + deletefb/deletefb.log | 0 2 files changed, 1 insertion(+) delete mode 100644 deletefb/deletefb.log diff --git a/.gitignore b/.gitignore index c45468f..80c1faa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .envrc __pycache__ venv +deletefb.log diff --git a/deletefb/deletefb.log b/deletefb/deletefb.log deleted file mode 100644 index e69de29..0000000 -- 2.30.2 From 03f7aad731d65bca33ca53d9ec7168fdb6ca24bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:58:34 -0500 Subject: [PATCH 150/186] Bump bleach from 3.1.0 to 3.1.1 (#116) Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v3.1.0...v3.1.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 04533f9..6a0398b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ attrs==19.1.0 bitarray==0.9.3 -bleach==3.1.0 +bleach==3.1.1 cattrs-3.8==0.9.1 certifi==2018.11.29 chardet==3.0.4 -- 2.30.2 From e4967a677667fed45ab3984ebc7cd935ec711152 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Thu, 27 Feb 2020 17:02:24 -0500 Subject: [PATCH 151/186] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98542b5..5ff2a57 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Personally, I did this so I would feel less attached to my Facebook profile ## Installation You have several options to run it. -1) Install from PyPI with `pip3 install --user delete-facebook-posts` (recommended) +1) Install from PyPI with `pip3 install --user delete-facebook-posts` (recommended you do this in a virtualenv to avoid incompatibilities) 2) Clone this repo and run `pip3 install --user .` or do `pip3 install --user git+https://github.com/weskerfoot/DeleteFB.git` 3) Set up a Python virtualenv, activate it, and run `pip3 install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. -- 2.30.2 From 8ef207441ce4a2f16a0626c42d5e7ff0b962ff98 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Mon, 2 Mar 2020 23:11:18 -0500 Subject: [PATCH 152/186] proper chrome version extraction --- deletefb/exceptions.py | 3 +++ deletefb/tools/chrome_driver.py | 29 ++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/deletefb/exceptions.py b/deletefb/exceptions.py index 6f22085..c316d44 100644 --- a/deletefb/exceptions.py +++ b/deletefb/exceptions.py @@ -1,2 +1,5 @@ class UnknownOSException(Exception): pass + +class ChromeError(Exception): + pass diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index 07346e8..1f6081d 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -1,4 +1,4 @@ -from ..exceptions import UnknownOSException +from ..exceptions import UnknownOSException, ChromeError from .common import NO_CHROME_DRIVER from clint.textui import puts, colored from selenium import webdriver @@ -10,6 +10,7 @@ import os, sys, stat, platform import progressbar import re import zipfile +import requests def extract_zip(filename): """ @@ -41,18 +42,27 @@ def setup_selenium(driver_path, options): # Configures selenium to use a custom path return webdriver.Chrome(executable_path=driver_path, options=options) -# TODO Merge these two into one +def parse_version(output): + """ + Attempt to extract version number from chrome version string. + """ + return [c for c in re.split('([0-9]+)\.?', output.decode("utf-8")) if all(d.isdigit() for d in c) and c][0] + def get_chrome_version(chrome_binary_path=None): + """ + Extract the chrome major version. + """ driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chrome.exe"]] for location in driver_locations: + print(location) if location: - version = check_output([location, "--version"]).strip() - # TODO need to reliably parse this output somehow + return parse_version(check_output([location, "--version"]).strip()) + return None def construct_driver_url(chrome_binary_path=None): """ - Construct a URL to download the Chrome Driver + Construct a URL to download the Chrome Driver. """ platform_string = platform.system() @@ -62,6 +72,15 @@ def construct_driver_url(chrome_binary_path=None): "Linux" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_linux64.zip" } + version = get_chrome_version() + + if version is None: + raise ChromeError("Chrome version not found") + + latest_release_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{0}".format(version) + + return chrome_drivers.get(platform_string).format(requests.get(latest_release_url).text) + # First, construct a LATEST_RELEASE URL using Chrome's major version number. # For example, with Chrome version 73.0.3683.86, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_73". # Try to download a small file from this URL. If it successful, the file contains the ChromeDriver version to use. -- 2.30.2 From 82d749f261a15eaf9b009c08791f71d5bfad6c12 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 2 Mar 2020 23:12:53 -0500 Subject: [PATCH 153/186] Starting a refactor of all chrome driver code (#105) * Starting a refactor of all chrome driver code * proper chrome version extraction --- deletefb/exceptions.py | 3 ++ deletefb/tools/chrome_driver.py | 67 +++++++++++++++++++++++++++------ deletefb/tools/login.py | 2 +- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/deletefb/exceptions.py b/deletefb/exceptions.py index 6f22085..c316d44 100644 --- a/deletefb/exceptions.py +++ b/deletefb/exceptions.py @@ -1,2 +1,5 @@ class UnknownOSException(Exception): pass + +class ChromeError(Exception): + pass diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index e048716..1f6081d 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -1,19 +1,16 @@ -from ..exceptions import UnknownOSException +from ..exceptions import UnknownOSException, ChromeError from .common import NO_CHROME_DRIVER from clint.textui import puts, colored from selenium import webdriver +from shutil import which +from subprocess import check_output from urllib.request import urlretrieve import os, sys, stat, platform import progressbar import re import zipfile - -chrome_drivers = { - "Windows" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_win32.zip", - "Darwin" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_mac64.zip", - "Linux" : "https://chromedriver.storage.googleapis.com/80.0.3987.16/chromedriver_linux64.zip" -} +import requests def extract_zip(filename): """ @@ -45,7 +42,54 @@ def setup_selenium(driver_path, options): # Configures selenium to use a custom path return webdriver.Chrome(executable_path=driver_path, options=options) -def get_webdriver(): +def parse_version(output): + """ + Attempt to extract version number from chrome version string. + """ + return [c for c in re.split('([0-9]+)\.?', output.decode("utf-8")) if all(d.isdigit() for d in c) and c][0] + +def get_chrome_version(chrome_binary_path=None): + """ + Extract the chrome major version. + """ + driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chrome.exe"]] + + for location in driver_locations: + print(location) + if location: + return parse_version(check_output([location, "--version"]).strip()) + return None + +def construct_driver_url(chrome_binary_path=None): + """ + Construct a URL to download the Chrome Driver. + """ + + platform_string = platform.system() + chrome_drivers = { + "Windows" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_win32.zip", + "Darwin" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_mac64.zip", + "Linux" : "https://chromedriver.storage.googleapis.com/{0}/chromedriver_linux64.zip" + } + + version = get_chrome_version() + + if version is None: + raise ChromeError("Chrome version not found") + + latest_release_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{0}".format(version) + + return chrome_drivers.get(platform_string).format(requests.get(latest_release_url).text) + + # First, construct a LATEST_RELEASE URL using Chrome's major version number. + # For example, with Chrome version 73.0.3683.86, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_73". + # Try to download a small file from this URL. If it successful, the file contains the ChromeDriver version to use. + # If the above step failed, reduce the Chrome major version by 1 and try again. + # For example, with Chrome version 75.0.3745.4, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_74" + # to download a small file, which contains the ChromeDriver version to use. + # You can also use ChromeDriver Canary build. + +def get_webdriver(chrome_binary_path): """ Ensure a webdriver is available If Not, Download it. @@ -65,7 +109,8 @@ def get_webdriver(): else: # Download it according to the current machine - chrome_webdriver = chrome_drivers.get(platform.system(), False) + chrome_webdriver = construct_driver_url(chrome_binary_path) + if not chrome_webdriver: raise UnknownOSException("Unknown Operating system platform") @@ -91,8 +136,8 @@ def get_webdriver(): file_name = chrome_webdriver.split('/')[-1] response = urlretrieve(chrome_webdriver, file_name, show_progress) - if int(response[1].get('Content-Length')) == total_size: - puts(colored.green(f"DONE!")) + if int(response[1].get("Content-Length")) == total_size: + puts(colored.green("Completed downloading the Chrome Driver.")) return "{0}/{1}".format(os.getcwd(), extract_zip(file_name)) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index daae1d5..17866cf 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -40,7 +40,7 @@ def login(user_email_address, chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('log-level=2') - driver_path = get_webdriver() + driver_path = get_webdriver(chrome_binary_path) driver = setup_selenium(driver_path, chrome_options) driver.implicitly_wait(10) -- 2.30.2 From e41030677dd83d9928eab8c7138bd69d2a32f5a6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 7 Mar 2020 09:55:34 -0500 Subject: [PATCH 154/186] update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5ff2a57..25186bc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ git+https://github.com/weskerfoot/DeleteFB.git` 3) Set up a Python virtualenv, activate it, and run `pip3 install -r requirements.txt`, then you can just run `python -m deletefb.deletefb` in the DeleteFB directory. 4) Use the docker image (experimental) by running `make` after checking this repository out with git. There is also an image built and published automatically at `wjak56/deletefb:latest` +## Chromedriver +The tool will attempt to detect the version of Chrome that you have installed and download the appropriate chromedriver. It is possible that it might fail to find your chrome version if you are running on Windows. If that is the case, please try running the docker version. + ## How To Use It ``` usage: deletefb [-h] [-M {wall,unlike_pages,conversations}] -E EMAIL [-P PASSWORD] -U PROFILE_URL [-F TWO_FACTOR_TOKEN] [-H] [--no-archive] [-Y YEAR] -- 2.30.2 From e80b0dc3b2c57acafc3b64920ee7b2ea13f29773 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 7 Mar 2020 09:55:49 -0500 Subject: [PATCH 155/186] update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 80c1faa..aabdea0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__ venv deletefb.log +test.sh +chromedriver -- 2.30.2 From 5ee81855519328061927d1bd15033f71287603e5 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 7 Mar 2020 09:57:31 -0500 Subject: [PATCH 156/186] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4513129..816eafb 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.11", + version="1.1.12", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From cc5a5b8b32ea2a328b26d5a675466b969b5e7a97 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sat, 7 Mar 2020 11:14:50 -0500 Subject: [PATCH 157/186] Fix chromedriver (#120) * add appdirs for managing user data * use cache_dir for downloading chromedriver * cleanly shut down selenium when an exception happens --- deletefb/deletefb.py | 37 ++++++----- deletefb/quit_driver.py | 10 +++ deletefb/tools/chrome_driver.py | 29 +++++--- deletefb/tools/login.py | 114 +++++++++++++++++--------------- deletefb/version.py | 6 ++ requirements.txt | 2 + setup.py | 3 +- 7 files changed, 120 insertions(+), 81 deletions(-) create mode 100644 deletefb/quit_driver.py create mode 100644 deletefb/version.py diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index 4d8c5a9..efe9320 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -6,6 +6,7 @@ from .tools.login import login from .tools.wall import delete_posts from .tools.conversations import traverse_conversations from .tools.comments import delete_comments +from .quit_driver import quit_driver_and_reap_children import argparse import getpass @@ -116,22 +117,26 @@ def run_delete(): chrome_binary_path=args.chromebin ) - if args.mode == "wall": - delete_posts( - driver, - args.profile_url, - year=args.year - ) - - elif args.mode == "unlike_pages": - unlike_pages(driver, args.profile_url) - - elif args.mode == "conversations": - traverse_conversations(driver, year=args.year) - - else: - print("Please enter a valid mode") - sys.exit(1) + try: + if args.mode == "wall": + delete_posts( + driver, + args.profile_url, + year=args.year + ) + + elif args.mode == "unlike_pages": + unlike_pages(driver, args.profile_url) + + elif args.mode == "conversations": + traverse_conversations(driver, year=args.year) + + else: + print("Please enter a valid mode") + sys.exit(1) + except: + if driver: + quit_driver_and_reap_children(driver) if __name__ == "__main__": run_delete() diff --git a/deletefb/quit_driver.py b/deletefb/quit_driver.py new file mode 100644 index 0000000..18e6539 --- /dev/null +++ b/deletefb/quit_driver.py @@ -0,0 +1,10 @@ +import os + +def quit_driver_and_reap_children(driver): + driver.quit() + try: + pid = True + while pid: + pid = os.waitpid(-1, os.WNOHANG) + except ChildProcessError: + pass diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index 1f6081d..d19f0f6 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -5,12 +5,22 @@ from selenium import webdriver from shutil import which from subprocess import check_output from urllib.request import urlretrieve +from appdirs import AppDirs +from ..version import version import os, sys, stat, platform import progressbar import re import zipfile import requests +import pathlib + +cache_dir = AppDirs("DeleteFB", version=version).user_cache_dir + +try: + pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True) +except FileExistsError: + pass def extract_zip(filename): """ @@ -25,10 +35,10 @@ def extract_zip(filename): sys.exit(1) # Save the name of the new file - new_file_name = _file.namelist()[0] + new_file_name = f"{cache_dir}/{_file.namelist()[0]}" # Extract the file and make it executable - _file.extractall() + _file.extractall(path=cache_dir) driver_stat = os.stat(new_file_name) os.chmod(new_file_name, driver_stat.st_mode | stat.S_IEXEC) @@ -55,7 +65,6 @@ def get_chrome_version(chrome_binary_path=None): driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chrome.exe"]] for location in driver_locations: - print(location) if location: return parse_version(check_output([location, "--version"]).strip()) return None @@ -94,17 +103,19 @@ def get_webdriver(chrome_binary_path): Ensure a webdriver is available If Not, Download it. """ - cwd = os.listdir(os.getcwd()) + webdriver_regex = re.compile('chromedriver') - web_driver = list(filter(webdriver_regex.match, cwd)) + + web_driver = list(filter(webdriver_regex.match, cache_dir)) + if web_driver: # check if a extracted copy already exists - if not os.path.isfile('chromedriver'): + if not os.path.isfile(f"{cache_dir}/chromedriver"): # Extract file extract_zip(web_driver[0]) - return "{0}/chromedriver".format(os.getcwd()) + return "{0}/chromedriver".format(cache_dir) else: # Download it according to the current machine @@ -133,13 +144,13 @@ def get_webdriver(chrome_binary_path): pbar.finish() puts(colored.yellow("Downloading Chrome Webdriver")) - file_name = chrome_webdriver.split('/')[-1] + file_name = f"{cache_dir}/{chrome_webdriver.split('/')[-1]}" response = urlretrieve(chrome_webdriver, file_name, show_progress) if int(response[1].get("Content-Length")) == total_size: puts(colored.green("Completed downloading the Chrome Driver.")) - return "{0}/{1}".format(os.getcwd(), extract_zip(file_name)) + return extract_zip(file_name) else: puts(colored.red("An error Occurred While trying to download the driver.")) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 17866cf..65e44f4 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,6 +1,7 @@ from .chrome_driver import get_webdriver, setup_selenium from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options +from ..quit_driver import quit_driver_and_reap_children import time @@ -42,59 +43,62 @@ def login(user_email_address, driver_path = get_webdriver(chrome_binary_path) driver = setup_selenium(driver_path, chrome_options) - driver.implicitly_wait(10) - - driver.get("https://www.facebook.com/login/device-based/regular/login/?login_attempt=1&lwv=110") - - email = "email" - password = "pass" - login_button = "loginbutton" - approvals_code = "approvals_code" - - driver.find_element_by_name(email).send_keys(user_email_address) - driver.find_element_by_name(password).send_keys(user_password) - driver.find_element_by_id(login_button).click() - - # Defaults to no 2fa - has_2fa = False - try: - # If this element exists, we've reached a 2FA page - driver.find_element_by_xpath("//form[@class=\"checkpoint\"]") - driver.find_element_by_xpath("//input[@name=\"approvals_code\"]") - has_2fa = True - except NoSuchElementException: - has_2fa = "two-factor authentication" in driver.page_source.lower() or has_2fa - - if has_2fa: - print(""" - Two-Factor Auth is enabled. - Please file an issue at https://github.com/weskerfoot/DeleteFB/issues if you run into any problems - """) - - 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 - - # block until we have reached the main page - # print a message warning the user - while driver.current_url != "https://www.facebook.com/": - print("Execution blocked: Please navigate to https://www.facebook.com to continue") - time.sleep(5) - - return driver + driver.implicitly_wait(10) + + driver.get("https://www.facebook.com/login/device-based/regular/login/?login_attempt=1&lwv=110") + + email = "email" + password = "pass" + login_button = "loginbutton" + approvals_code = "approvals_code" + + driver.find_element_by_name(email).send_keys(user_email_address) + driver.find_element_by_name(password).send_keys(user_password) + driver.find_element_by_id(login_button).click() + + # Defaults to no 2fa + has_2fa = False + + try: + # If this element exists, we've reached a 2FA page + driver.find_element_by_xpath("//form[@class=\"checkpoint\"]") + driver.find_element_by_xpath("//input[@name=\"approvals_code\"]") + has_2fa = True + except NoSuchElementException: + has_2fa = "two-factor authentication" in driver.page_source.lower() or has_2fa + + if has_2fa: + print(""" + Two-Factor Auth is enabled. + Please file an issue at https://github.com/weskerfoot/DeleteFB/issues if you run into any problems + """) + + 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 + + # block until we have reached the main page + # print a message warning the user + while driver.current_url != "https://www.facebook.com/": + print("Execution blocked: Please navigate to https://www.facebook.com to continue") + time.sleep(5) + + return driver + except: + quit_driver_and_reap_children(driver) diff --git a/deletefb/version.py b/deletefb/version.py new file mode 100644 index 0000000..edbe5b0 --- /dev/null +++ b/deletefb/version.py @@ -0,0 +1,6 @@ +import pkg_resources # part of setuptools + +try: + version = pkg_resources.require("delete-facebook-posts")[0].version +except pkg_resources.DistributionNotFound: + version = "source" diff --git a/requirements.txt b/requirements.txt index 6a0398b..4e6b621 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +appdirs==1.4.3 +args==0.1.0 attrs==19.1.0 bitarray==0.9.3 bleach==3.1.1 diff --git a/setup.py b/setup.py index 816eafb..a0492ed 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,8 @@ setuptools.setup( "lxml", "pendulum", "clint", - "progressbar" + "progressbar", + "appdirs" ], classifiers= [ "Programming Language :: Python :: 3", -- 2.30.2 From 248bc0fdedc7914cb49b34be61accd68e64625d7 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 7 Mar 2020 11:18:23 -0500 Subject: [PATCH 158/186] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a0492ed..8cd39a3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.12", + version="1.1.13", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From b63a3799bfe802385660fd2e9be7d2027789bcc1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 7 Mar 2020 11:40:41 -0500 Subject: [PATCH 159/186] fix chromedriver to be versioned and check for that version --- deletefb/tools/chrome_driver.py | 95 +++++++++++++++++---------------- deletefb/tools/login.py | 5 +- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index d19f0f6..94d2c4d 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -7,6 +7,7 @@ from subprocess import check_output from urllib.request import urlretrieve from appdirs import AppDirs from ..version import version +from os.path import exists import os, sys, stat, platform import progressbar @@ -22,12 +23,19 @@ try: except FileExistsError: pass -def extract_zip(filename): +def extract_zip(filename, chrome_maj_version): """ Uses zipfile package to extract a single zipfile :param filename: :return: new filename """ + + # Remove any leftover unversioned chromedriver + try: + os.remove(f"{cache_dir}/chromedriver") + except FileNotFoundError: + pass + try: _file = zipfile.ZipFile(filename, 'r') except FileNotFoundError: @@ -35,11 +43,14 @@ def extract_zip(filename): sys.exit(1) # Save the name of the new file - new_file_name = f"{cache_dir}/{_file.namelist()[0]}" + new_file_name = f"{cache_dir}/{_file.namelist()[0] + chrome_maj_version}" # Extract the file and make it executable _file.extractall(path=cache_dir) + # Rename the filename to a versioned one + os.rename(f"{cache_dir}/chromedriver", f"{cache_dir}/chromedriver{chrome_maj_version}") + driver_stat = os.stat(new_file_name) os.chmod(new_file_name, driver_stat.st_mode | stat.S_IEXEC) @@ -48,8 +59,10 @@ def extract_zip(filename): return new_file_name -def setup_selenium(driver_path, options): +def setup_selenium(options, chrome_binary_path): # Configures selenium to use a custom path + driver_path = get_webdriver(chrome_binary_path) + return webdriver.Chrome(executable_path=driver_path, options=options) def parse_version(output): @@ -88,7 +101,7 @@ def construct_driver_url(chrome_binary_path=None): latest_release_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{0}".format(version) - return chrome_drivers.get(platform_string).format(requests.get(latest_release_url).text) + return version, chrome_drivers.get(platform_string).format(requests.get(latest_release_url).text) # First, construct a LATEST_RELEASE URL using Chrome's major version number. # For example, with Chrome version 73.0.3683.86, use URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_73". @@ -104,57 +117,47 @@ def get_webdriver(chrome_binary_path): If Not, Download it. """ - webdriver_regex = re.compile('chromedriver') + # Download it according to the current machine + chrome_maj_version, chrome_webdriver = construct_driver_url(chrome_binary_path) - web_driver = list(filter(webdriver_regex.match, cache_dir)) + driver_path = f"{cache_dir}/chromedriver{chrome_maj_version}" + if exists(driver_path): + return driver_path - if web_driver: - # check if a extracted copy already exists - if not os.path.isfile(f"{cache_dir}/chromedriver"): - # Extract file - extract_zip(web_driver[0]) + if not chrome_webdriver: + raise UnknownOSException("Unknown Operating system platform") - return "{0}/chromedriver".format(cache_dir) - - else: - # Download it according to the current machine - - chrome_webdriver = construct_driver_url(chrome_binary_path) - - if not chrome_webdriver: - raise UnknownOSException("Unknown Operating system platform") + global total_size + def show_progress(*res): global total_size + pbar = None + downloaded = 0 + block_num, block_size, total_size = res - def show_progress(*res): - global total_size - pbar = None - downloaded = 0 - block_num, block_size, total_size = res + if not pbar: + pbar = progressbar.ProgressBar(maxval=total_size) + pbar.start() + downloaded += block_num * block_size - if not pbar: - pbar = progressbar.ProgressBar(maxval=total_size) - pbar.start() - downloaded += block_num * block_size - - if downloaded < total_size: - pbar.update(downloaded) - else: - pbar.finish() + if downloaded < total_size: + pbar.update(downloaded) + else: + pbar.finish() - puts(colored.yellow("Downloading Chrome Webdriver")) - file_name = f"{cache_dir}/{chrome_webdriver.split('/')[-1]}" - response = urlretrieve(chrome_webdriver, file_name, show_progress) + puts(colored.yellow("Downloading Chrome Webdriver")) + file_name = f"{cache_dir}/{chrome_webdriver.split('/')[-1]}" + response = urlretrieve(chrome_webdriver, file_name, show_progress) - if int(response[1].get("Content-Length")) == total_size: - puts(colored.green("Completed downloading the Chrome Driver.")) + if int(response[1].get("Content-Length")) == total_size: + puts(colored.green("Completed downloading the Chrome Driver.")) - return extract_zip(file_name) + return extract_zip(file_name, chrome_maj_version) - else: - puts(colored.red("An error Occurred While trying to download the driver.")) - # remove the downloaded file and exit - os.remove(file_name) - sys.stderr.write(NO_CHROME_DRIVER) - sys.exit(1) + else: + puts(colored.red("An error Occurred While trying to download the driver.")) + # remove the downloaded file and exit + os.remove(file_name) + sys.stderr.write(NO_CHROME_DRIVER) + sys.exit(1) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index 65e44f4..c8e3e86 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -1,4 +1,4 @@ -from .chrome_driver import get_webdriver, setup_selenium +from .chrome_driver import setup_selenium from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.chrome.options import Options from ..quit_driver import quit_driver_and_reap_children @@ -41,8 +41,7 @@ def login(user_email_address, chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('log-level=2') - driver_path = get_webdriver(chrome_binary_path) - driver = setup_selenium(driver_path, chrome_options) + driver = setup_selenium(chrome_options, chrome_binary_path) try: driver.implicitly_wait(10) -- 2.30.2 From dbcc82b8294536ac96e067dd78cd6481d1eb3c9e Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sat, 7 Mar 2020 12:14:03 -0500 Subject: [PATCH 160/186] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 25186bc..1379443 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ online presence and not have to worry about what you wrote from years ago. Personally, I did this so I would feel less attached to my Facebook profile (and hence feel the need to use it less). +## Dependencies +- This tool requires at least Python 3.6 in order to run. +- A recent copy of Chrome or Chromium installed and available in your `$PATH` + ## Installation You have several options to run it. 1) Install from PyPI with `pip3 install --user delete-facebook-posts` (recommended you do this in a virtualenv to avoid incompatibilities) -- 2.30.2 From e245a87520550005baeaefe64a8984d9a1a1a9ac Mon Sep 17 00:00:00 2001 From: wes Date: Sat, 7 Mar 2020 21:38:32 -0500 Subject: [PATCH 161/186] add docstring --- deletefb/quit_driver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deletefb/quit_driver.py b/deletefb/quit_driver.py index 18e6539..4826c15 100644 --- a/deletefb/quit_driver.py +++ b/deletefb/quit_driver.py @@ -1,6 +1,9 @@ import os def quit_driver_and_reap_children(driver): + """ + Reaps child processes by waiting until they exit. + """ driver.quit() try: pid = True -- 2.30.2 From 178887aa0d3ce5eb3918bf7305ca66b56b4cff60 Mon Sep 17 00:00:00 2001 From: Jeff Carpenter Date: Sun, 8 Mar 2020 14:18:17 -0700 Subject: [PATCH 162/186] let Selenium try to find chromedriver (in PATH) (#121) --- deletefb/tools/chrome_driver.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index 94d2c4d..353c872 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -60,10 +60,13 @@ def extract_zip(filename, chrome_maj_version): def setup_selenium(options, chrome_binary_path): - # Configures selenium to use a custom path - driver_path = get_webdriver(chrome_binary_path) - - return webdriver.Chrome(executable_path=driver_path, options=options) + try: + # try letting Selenium find the driver (in PATH) + return webdriver.Chrome(options=options) + except WebDriverException: + # Configures selenium to use a custom path + driver_path = get_webdriver(chrome_binary_path) + return webdriver.Chrome(executable_path=driver_path, options=options) def parse_version(output): """ -- 2.30.2 From 6cdc7d4a710a775a4523e5262b65f355779d2427 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2020 22:14:01 -0400 Subject: [PATCH 163/186] Bump bleach from 3.1.1 to 3.1.2 (#122) Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v3.1.1...v3.1.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4e6b621..04ccc4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 args==0.1.0 attrs==19.1.0 bitarray==0.9.3 -bleach==3.1.1 +bleach==3.1.2 cattrs-3.8==0.9.1 certifi==2018.11.29 chardet==3.0.4 -- 2.30.2 From e8896b1a2c834b0b0cc9404a20225a138e37b303 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 31 Mar 2020 08:26:07 -0600 Subject: [PATCH 164/186] Report more exceptions (#125) * Report more exceptions This makes it much easier to debug chrome errors. Without reporting these errors things will silently fail. * Ignore pyc files * Don't pull chromedriver from cask --- .gitignore | 1 + README.md | 3 +-- deletefb/deletefb.py | 3 ++- deletefb/tools/login.py | 3 ++- deletefb/tools/wall.py | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index aabdea0..3709b15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .envrc __pycache__ +*.pyc venv deletefb.log test.sh diff --git a/README.md b/README.md index 1379443..b446e68 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,7 @@ optional arguments: * On MacOS, it will be available via brew, with the following commands: ``` - brew tap homebrew/cask; - brew cask install chromedriver + brew install chromedriver ``` * Run `deletefb -E 'youremail@example.org' -P 'yourfacebookpassword' -U 'https://www.facebook.com/your.profile.url'` diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index efe9320..e1d0557 100755 --- a/deletefb/deletefb.py +++ b/deletefb/deletefb.py @@ -134,7 +134,8 @@ def run_delete(): else: print("Please enter a valid mode") sys.exit(1) - except: + except BaseException as e: + print(e) if driver: quit_driver_and_reap_children(driver) diff --git a/deletefb/tools/login.py b/deletefb/tools/login.py index c8e3e86..765cb33 100644 --- a/deletefb/tools/login.py +++ b/deletefb/tools/login.py @@ -99,5 +99,6 @@ def login(user_email_address, time.sleep(5) return driver - except: + except BaseException as e: + print('An exception occurred: {}'.format(e)) quit_driver_and_reap_children(driver) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index fb8ff69..81d1002 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -63,7 +63,8 @@ def delete_posts(driver, try: delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"{0}\"]".format(button_type)) break - except SELENIUM_EXCEPTIONS: + except SELENIUM_EXCEPTIONS as e: + print(e) continue if not delete_button: -- 2.30.2 From bf93cf0b08ee082445ffcffe3485b950da4e9b08 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:35:46 -0400 Subject: [PATCH 165/186] Update chrome_driver.py (#127) --- deletefb/tools/chrome_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index 353c872..fcbb5d9 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -78,7 +78,7 @@ def get_chrome_version(chrome_binary_path=None): """ Extract the chrome major version. """ - driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chrome.exe"]] + driver_locations = [which(loc) for loc in ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome.exe"]] for location in driver_locations: if location: -- 2.30.2 From af949c2d24ae46ae00c5d93d74d815b6d035b0bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:42:57 -0400 Subject: [PATCH 166/186] Bump bleach from 3.1.2 to 3.1.4 (#124) Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.2 to 3.1.4. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v3.1.2...v3.1.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 04ccc4d..0a4968b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 args==0.1.0 attrs==19.1.0 bitarray==0.9.3 -bleach==3.1.2 +bleach==3.1.4 cattrs-3.8==0.9.1 certifi==2018.11.29 chardet==3.0.4 -- 2.30.2 From 1875b754ab946c575998198b206b3711d70bfc19 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 7 Sep 2020 17:09:25 -0400 Subject: [PATCH 167/186] Fixes for new facebook site (#129) * make work with the mobile site instead, bypasses changes to new facebook * fix timestamp selection * fix conversations mode --- deletefb/tools/conversations.py | 19 +++++++----- deletefb/tools/wall.py | 51 ++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/deletefb/tools/conversations.py b/deletefb/tools/conversations.py index bcbd089..c21b070 100644 --- a/deletefb/tools/conversations.py +++ b/deletefb/tools/conversations.py @@ -6,6 +6,7 @@ from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import Select from pendulum import now from json import loads +from time import sleep import lxml.html as lxh @@ -29,12 +30,13 @@ def get_conversations(driver): date = None if url and "messages/read" in url: - - date = convo.find_element_by_xpath("../../..//abbr").text - conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip() - - assert(conversation_name) - assert(url) + try: + date = convo.find_element_by_xpath("../../..//abbr").text + conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip() + assert(conversation_name) + assert(url) + except (SELENIUM_EXCEPTIONS + (AssertionError,)): + continue conversations.append( Conversation( @@ -49,7 +51,10 @@ def get_conversations(driver): find_element_by_xpath("a"). get_attribute("href")) - except SELENIUM_EXCEPTIONS: + print("next_url", next_url) + + except SELENIUM_EXCEPTIONS as e: + print(e) break if not next_url: break diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 81d1002..8cc4d27 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -26,21 +26,39 @@ def delete_posts(driver, driver.get(user_profile_url) - for _ in range(MAX_POSTS): - post_button_sel = "_4xev" + finished = False - post_content_sel = "userContent" - post_timestamp_sel = "timestampContent" + with archiver("wall") as archive_wall_post: + for _ in range(MAX_POSTS): + if finished: + break + post_button_sel = "_4s19" - button_types = ["FeedDeleteOption", "HIDE_FROM_TIMELINE", "UNTAG"] + post_content_sel = "userContent" + post_timestamp_sel = "timestampContent" + + confirmation_button_exp = "//div[contains(@data-sigil, 'undo-content')]//*/a[contains(@href, 'direct_action_execute')]" + + # Cannot return a text node, so it returns the parent. + # Tries to be pretty resilient against DOM re-organizations + timestamp_exp = "//article//*/header//*/div/a[contains(@href, 'story_fbid')]//text()/.." + + button_types = ["Delete post", "Remove Tag", "Hide from timeline"] - with archiver("wall") as archive_wall_post: while True: try: - timeline_element = driver.find_element_by_class_name(post_button_sel) + try: + timeline_element = driver.find_element_by_xpath("//div[@data-sigil='story-popup-causal-init']/a") + except SELENIUM_EXCEPTIONS: + print("Could not find any posts") + finished = True + break - post_content_element = driver.find_element_by_class_name(post_content_sel) - post_content_ts = driver.find_element_by_class_name(post_timestamp_sel) + post_content_element = driver.find_element_by_xpath("//article/div[@class='story_body_container']/div") + post_content_ts = driver.find_element_by_xpath(timestamp_exp) + + if not (post_content_element or post_content_ts): + break # Archive the post archive_wall_post.archive( @@ -53,15 +71,18 @@ def delete_posts(driver, actions = ActionChains(driver) actions.move_to_element(timeline_element).click().perform() - wait_xpath(driver, "//*[@id='feed_post_menu']/..") - - menu = driver.find_element_by_xpath("//*[@id='feed_post_menu']/..") + # Wait until the buttons show up + wait_xpath(driver, "//*[contains(@data-sigil, 'removeStoryButton')]") delete_button = None + # Search for visible buttons in priority order + # Delete -> Untag -> Hide for button_type in button_types: try: - delete_button = menu.find_element_by_xpath("//a[@data-feed-option-name=\"{0}\"]".format(button_type)) + delete_button = driver.find_element_by_xpath("//*[text()='{0}']".format(button_type)) + if not delete_button.is_displayed(): + continue break except SELENIUM_EXCEPTIONS as e: print(e) @@ -72,8 +93,10 @@ def delete_posts(driver, break click_button(driver, delete_button) - confirmation_button = driver.find_element_by_class_name("layerConfirm") + wait_xpath(driver, confirmation_button_exp) + confirmation_button = driver.find_element_by_xpath(confirmation_button_exp) + print(confirmation_button) click_button(driver, confirmation_button) except SELENIUM_EXCEPTIONS as e: -- 2.30.2 From 5830cfeecd544e71b84aaba78d65788d07dab8ea Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Mon, 7 Sep 2020 20:14:56 -0400 Subject: [PATCH 168/186] force url to mobile site for deleting wall posts (#130) * force url to mobile site for deleting wall posts * use urlunparse to force to mobile instead --- deletefb/tools/common.py | 12 ++++++++++++ deletefb/tools/wall.py | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index bb24989..fd5db1b 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -12,6 +12,7 @@ import json import logging import logging.config import os +import urllib.parse as urlparse SELENIUM_EXCEPTIONS = ( NoSuchElementException, @@ -67,6 +68,17 @@ def wait_xpath(driver, expr): except SELENIUM_EXCEPTIONS: return +def force_mobile(url): + """ + Force a url to use the mobile site. + """ + parsed = urlparse.urlparse(url) + return urlparse.urlunparse((parsed.scheme, + "mobile.facebook.com", + parsed.path, + parsed.params, + parsed.query, + parsed.fragment)) NO_CHROME_DRIVER = """ You need to manually install the chromedriver for Selenium\n diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 8cc4d27..fd97862 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,6 +1,6 @@ from ..types import Post from .archive import archiver -from .common import SELENIUM_EXCEPTIONS, click_button, wait_xpath +from .common import SELENIUM_EXCEPTIONS, click_button, wait_xpath, force_mobile from .config import settings from selenium.webdriver.common.action_chains import ActionChains @@ -24,6 +24,8 @@ def delete_posts(driver, if year is not None: user_profile_url = "{0}/timeline?year={1}".format(user_profile_url, year) + user_profile_url = force_mobile(user_profile_url) + driver.get(user_profile_url) finished = False -- 2.30.2 From 9747f0a00d9fc94c5603f61fce2cc0823e8476ea Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 16 Sep 2020 22:54:54 -0400 Subject: [PATCH 169/186] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8cd39a3..7685700 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.13", + version="1.1.15", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 50162ba9966e79dead3761af634d902b2ec73500 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Wed, 30 Sep 2020 21:46:35 -0400 Subject: [PATCH 170/186] fix unliking (#134) --- deletefb/tools/common.py | 17 ++++++++++------ deletefb/tools/likes.py | 42 +++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index fd5db1b..621e865 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -20,13 +20,20 @@ SELENIUM_EXCEPTIONS = ( TimeoutException ) - def click_button(driver, el): """ Click a button using Javascript """ driver.execute_script("arguments[0].click();", el) +def scroll_until_element_exists(driver, xpath_expr): + while True: + driver.execute_script("window.scrollTo(0, document.body.scrollHeight)") + try: + element = driver.find_element_by_xpath(xpath_expr) + except SELENIUM_EXCEPTIONS: + continue + break def scroll_to(driver, el): """ @@ -37,7 +44,6 @@ def scroll_to(driver, el): except SELENIUM_EXCEPTIONS: return - def logger(name): """ Args: @@ -57,12 +63,11 @@ def logger(name): logging.config.dictConfig(config["logging"]) return logging.getLogger(name) - -def wait_xpath(driver, expr): +def wait_xpath(driver, expr, timeout=20): """ - Takes an XPath expression, and waits at most 20 seconds until it exists + Takes an XPath expression, and waits at most `timeout` seconds until it exists """ - wait = WebDriverWait(driver, 20) + wait = WebDriverWait(driver, timeout) try: wait.until(EC.presence_of_element_located((By.XPATH, expr))) except SELENIUM_EXCEPTIONS: diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index 091a144..a90f44f 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -1,6 +1,6 @@ from .archive import archiver from ..types import Page -from .common import SELENIUM_EXCEPTIONS, logger, click_button +from .common import SELENIUM_EXCEPTIONS, logger, click_button, wait_xpath, force_mobile, scroll_to, scroll_until_element_exists from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -18,17 +18,25 @@ def load_likes(driver, profile_url): None """ - driver.get("{0}/likes_all".format(profile_url)) + likes_link_xpath = "//div[normalize-space(text())='Likes']/../..//a[contains(@href, 'app_section')]" - wait = WebDriverWait(driver, 20) + all_likes_link_xpath = "//div[normalize-space(text())='All Likes']/../../../..//a[contains(@href, 'app_collection')]" - try: - wait.until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".PageLikeButton")) - ) - except SELENIUM_EXCEPTIONS: - LOG.exception("Traceback of load_likes") - return + driver.get(force_mobile("{0}/about".format(profile_url))) + + scroll_until_element_exists(driver, "//div[text()='Likes']") + + likes_link_el = driver.find_element_by_xpath(likes_link_xpath) + + driver.get(likes_link_el.get_attribute("href")) + + wait_xpath(driver, all_likes_link_xpath) + + all_likes_link_el = driver.find_element_by_xpath(all_likes_link_xpath) + + all_likes_link = all_likes_link_el.get_attribute("href") + + driver.get(all_likes_link) def get_page_links(driver): """ @@ -39,8 +47,7 @@ def get_page_links(driver): Returns: List of URLs to pages """ - pages = driver.find_elements_by_xpath("//li//div/div/a[contains(@class, 'lfloat')]") - + pages = driver.find_elements_by_xpath("//header/..//a") return [page.get_attribute("href").replace("www", "mobile") for page in pages] def unlike_page(driver, url, archive=None): @@ -60,7 +67,7 @@ def unlike_page(driver, url, archive=None): print("Unliking {0}".format(url)) - wait = WebDriverWait(driver, 20) + wait = WebDriverWait(driver, 5) try: wait.until( @@ -70,19 +77,14 @@ def unlike_page(driver, url, archive=None): # Something went wrong with this page, so skip it return - button = driver.find_element_by_xpath("//*[text()='Liked']") - - # Click the "Liked" button to open up "Unlike" - click_button(driver, button) + driver.find_element_by_xpath("//*[text()='Liked']/../../../..").click() wait.until( EC.presence_of_element_located((By.XPATH, "//*[text()='Unlike']")) ) # There should be an "Unlike" button now, click it - unlike_button = driver.find_element_by_xpath("//*[text()='Unlike']/..") - - click_button(driver, unlike_button) + driver.find_element_by_xpath("//*[text()='Unlike']/..").click() if archive: archive(Page(name=url)) -- 2.30.2 From a182eaaaa312e5108dd6bf66bf680b2baa530872 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Wed, 30 Sep 2020 21:49:01 -0400 Subject: [PATCH 171/186] fixes https://github.com/weskerfoot/DeleteFB/issues/135 (#136) --- deletefb/tools/wall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index fd97862..6944f30 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -45,7 +45,7 @@ def delete_posts(driver, # Tries to be pretty resilient against DOM re-organizations timestamp_exp = "//article//*/header//*/div/a[contains(@href, 'story_fbid')]//text()/.." - button_types = ["Delete post", "Remove Tag", "Hide from timeline"] + button_types = ["Delete post", "Remove tag", "Hide from timeline"] while True: try: -- 2.30.2 From 1a846f1dac462cd5c54e9de0122c0197512d85ba Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Wed, 30 Sep 2020 21:49:42 -0400 Subject: [PATCH 172/186] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7685700..73b9c96 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.15", + version="1.1.16", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From 7d24450b0f8efb1d06f29c8ba1044e1ca539a70c Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Thu, 1 Oct 2020 09:05:15 -0400 Subject: [PATCH 173/186] add FUNDING.yml (#137) --- FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 FUNDING.yml diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 0000000..02b61c5 --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1,2 @@ +github: [weskerfoot] +custom: "39qHYvjVcMCNFr3RPAVetci9mKjzYGTQPz" -- 2.30.2 From c1af411453dd9f4f85027d7bc65ad1b42505539d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 18 Oct 2020 08:09:46 +1100 Subject: [PATCH 174/186] WebDriverException not defined (#139) I didn't have chromedriver installed initially - it caused a secondary exception. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/tcrha/.local/bin/deletefb", line 8, in sys.exit(run_delete()) File "/home/tcrha/.local/lib/python3.8/site-packages/deletefb/deletefb.py", line 112, in run_delete driver = login( File "/home/tcrha/.local/lib/python3.8/site-packages/deletefb/tools/login.py", line 44, in login driver = setup_selenium(chrome_options, chrome_binary_path) File "/home/tcrha/.local/lib/python3.8/site-packages/deletefb/tools/chrome_driver.py", line 66, in setup_selenium except WebDriverException: NameError: name 'WebDriverException' is not defined --- deletefb/tools/chrome_driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deletefb/tools/chrome_driver.py b/deletefb/tools/chrome_driver.py index fcbb5d9..50dbfdf 100644 --- a/deletefb/tools/chrome_driver.py +++ b/deletefb/tools/chrome_driver.py @@ -2,6 +2,7 @@ from ..exceptions import UnknownOSException, ChromeError from .common import NO_CHROME_DRIVER from clint.textui import puts, colored from selenium import webdriver +from selenium.common.exceptions import WebDriverException from shutil import which from subprocess import check_output from urllib.request import urlretrieve -- 2.30.2 From ede08a42a954678b7a5ad807e35994b489aa35de Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 17 Oct 2020 19:35:27 -0400 Subject: [PATCH 175/186] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73b9c96..a1bd58c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as fh: setuptools.setup( name="delete-facebook-posts", - version="1.1.16", + version="1.1.17", author="Wesley Kerfoot", author_email="wes@wesk.tech", description="A Selenium Script to Delete Facebook Posts", -- 2.30.2 From a5ed694a46e83779ebb6547c530532beaea637f1 Mon Sep 17 00:00:00 2001 From: tklam Date: Tue, 1 Dec 2020 23:45:30 +0000 Subject: [PATCH 176/186] Add "Hide from profile" (#145) Facebook has replaced "Hide from timeline" with "Hide from profile". The latter is added into button_types. --- deletefb/tools/wall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 6944f30..4883413 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -45,7 +45,7 @@ def delete_posts(driver, # Tries to be pretty resilient against DOM re-organizations timestamp_exp = "//article//*/header//*/div/a[contains(@href, 'story_fbid')]//text()/.." - button_types = ["Delete post", "Remove tag", "Hide from timeline"] + button_types = ["Delete post", "Remove tag", "Hide from timeline", "Hide from profile"] while True: try: -- 2.30.2 From 11c21f6d2ca04d801379e27301360477c592b715 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Jan 2021 21:55:33 -0500 Subject: [PATCH 177/186] Bump lxml from 4.4.0 to 4.6.2 (#150) Bumps [lxml](https://github.com/lxml/lxml) from 4.4.0 to 4.6.2. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.4.0...lxml-4.6.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0a4968b..18f8931 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ chardet==3.0.4 clint==0.5.1 docutils==0.14 idna==2.8 -lxml==4.4.0 +lxml==4.6.2 pendulum==2.0.5 pkginfo==1.5.0.1 progressbar==2.5 -- 2.30.2 From 86403534b9457477c0407946e92c630b583f77b2 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sun, 10 Jan 2021 06:00:48 -0500 Subject: [PATCH 178/186] bump versions of cattrs and attrs (#151) --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 18f8931..e985dfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ appdirs==1.4.3 args==0.1.0 -attrs==19.1.0 +attrs==20.3.0 bitarray==0.9.3 bleach==3.1.4 -cattrs-3.8==0.9.1 +cattrs==1.1.2 certifi==2018.11.29 chardet==3.0.4 clint==0.5.1 diff --git a/setup.py b/setup.py index a1bd58c..d50d938 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,8 @@ setuptools.setup( "selenium-requests", "requests", "pybloom-live", - "attrs", - "cattrs-3.8", + "attrs>=20.3.0", + "cattrs>=1.1.2", "lxml", "pendulum", "clint", -- 2.30.2 From 72eb1bdcdee26e4a8e6215692846cd99e36b37ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Feb 2021 13:18:30 -0500 Subject: [PATCH 179/186] Bump bleach from 3.1.4 to 3.3.0 (#155) Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.4 to 3.3.0. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v3.1.4...v3.3.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e985dfb..fa25339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 args==0.1.0 attrs==20.3.0 bitarray==0.9.3 -bleach==3.1.4 +bleach==3.3.0 cattrs==1.1.2 certifi==2018.11.29 chardet==3.0.4 -- 2.30.2 From 39b2437f1dac82bae77ddf1a153737c3c20f4e27 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Sun, 21 Feb 2021 21:42:15 -0500 Subject: [PATCH 180/186] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b446e68..26ba0f4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +Note: this currently only works for English language Facebook accounts, due to the lack of a usable API. Also, year by year deletion is currently broken. Feel free to fork or make pull requests. + ## Why? I needed a simple and reliable way to delete Facebook posts. There are -- 2.30.2 From d64ed5a3dbe30412d215be023c5a5323933b28b1 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot <378351+weskerfoot@users.noreply.github.com> Date: Fri, 12 Mar 2021 10:01:32 -0500 Subject: [PATCH 181/186] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26ba0f4..80110f2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -Note: this currently only works for English language Facebook accounts, due to the lack of a usable API. Also, year by year deletion is currently broken. Feel free to fork or make pull requests. +WARNING: +This currently only works for English language Facebook accounts, due to the lack of a usable API. +Also, year by year deletion is currently broken. Feel free to fork or make pull requests. ## Why? -- 2.30.2 From a92fd3eb3562a29409840ce7ae21772dcb13686a Mon Sep 17 00:00:00 2001 From: Ian Hunter Date: Mon, 29 Mar 2021 01:49:12 +0100 Subject: [PATCH 182/186] Update Required Python version (#157) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80110f2..96be1bd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Personally, I did this so I would feel less attached to my Facebook profile (and hence feel the need to use it less). ## Dependencies -- This tool requires at least Python 3.6 in order to run. +- This tool requires at least Python 3.7 in order to run. - A recent copy of Chrome or Chromium installed and available in your `$PATH` ## Installation @@ -61,7 +61,7 @@ optional arguments: ``` * Make sure that you have a recent version of Python 3.x installed (preferably - 3.6 or greater) + 3.7 or greater) * Make sure that you have Google Chrome installed and that it is up to date * The tool will attempt to automatically install chromedriver for Selenium. See [here](https://sites.google.com/a/chromium.org/chromedriver/home) for an explanation of what the chromedriver does. You may have to manually install it if auto-install fails. * On Linux, it will be called something like `chromium-chromedriver` or just -- 2.30.2 From 262c7f84c658f6ed132bddcb03f0a770b5530644 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Apr 2021 21:14:37 -0400 Subject: [PATCH 183/186] Bump lxml from 4.6.2 to 4.6.3 (#159) Bumps [lxml](https://github.com/lxml/lxml) from 4.6.2 to 4.6.3. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.2...lxml-4.6.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa25339..e376241 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ chardet==3.0.4 clint==0.5.1 docutils==0.14 idna==2.8 -lxml==4.6.2 +lxml==4.6.3 pendulum==2.0.5 pkginfo==1.5.0.1 progressbar==2.5 -- 2.30.2 From ddf358bed23275d6f862a031790d5e5dd5d7b380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Apr 2021 21:14:48 -0400 Subject: [PATCH 184/186] Bump pygments from 2.4.2 to 2.7.4 (#158) Bumps [pygments](https://github.com/pygments/pygments) from 2.4.2 to 2.7.4. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.4.2...2.7.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e376241..e873c18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pendulum==2.0.5 pkginfo==1.5.0.1 progressbar==2.5 pybloom-live==3.0.0 -Pygments==2.4.2 +Pygments==2.7.4 python-dateutil==2.8.0 pytzdata==2019.2 readme-renderer==24.0 -- 2.30.2 From b93f528f9992b2db5e9a942573607871a42f22e1 Mon Sep 17 00:00:00 2001 From: Sean Leavey Date: Tue, 6 Apr 2021 03:34:00 +0200 Subject: [PATCH 185/186] Ensure protocol is provided in mobile URLs (fixes #160) (#161) --- deletefb/tools/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index 621e865..e53e7e2 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -78,7 +78,9 @@ def force_mobile(url): Force a url to use the mobile site. """ parsed = urlparse.urlparse(url) - return urlparse.urlunparse((parsed.scheme, + # Ensure a protocol is given (needed by selenium). + scheme = parsed.scheme or "https" + return urlparse.urlunparse((scheme, "mobile.facebook.com", parsed.path, parsed.params, -- 2.30.2 From ed96dd7e6a71dee87a75722fa3bee2be074892b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 May 2021 18:20:13 -0400 Subject: [PATCH 186/186] Bump urllib3 from 1.25.2 to 1.25.8 (#162) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.2 to 1.25.8. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.2...1.25.8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e873c18..81e9609 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,5 +28,5 @@ tldextract==2.2.0 tqdm==4.32.2 twine==1.13.0 typing==3.7.4 -urllib3==1.25.2 +urllib3==1.25.8 webencodings==0.5.1 -- 2.30.2