diff --git a/README.md b/README.md index eea8211..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. @@ -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. diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index db7dc25..20db6e3 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 @@ -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,11 +107,14 @@ 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) + unlike_pages(driver, args.profile_url) else: print("Please enter a valid mode") sys.exit(1) diff --git a/deletefb/tools/common.py b/deletefb/tools/common.py index fe97881..c107abf 100644 --- a/deletefb/tools/common.py +++ b/deletefb/tools/common.py @@ -2,29 +2,35 @@ import json import logging import logging.config import os -from os.path import abspath, relpath, split, isfile 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, 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: - 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): """ @@ -56,9 +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 os.environ.get("DELETEFB_ARCHIVE", "true") == "false": + if not settings["ARCHIVE"]: return + + if content in bfilter: + # This was already archived + return + structured_content = { "category" : category, "content" : content, @@ -67,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 new file mode 100644 index 0000000..078efca --- /dev/null +++ b/deletefb/tools/config.py @@ -0,0 +1,4 @@ +settings = { + "ARCHIVE" : True, + "MAX_POSTS" : 5000 +} diff --git a/deletefb/tools/likes.py b/deletefb/tools/likes.py index cbb8886..9533eaa 100644 --- a/deletefb/tools/likes.py +++ b/deletefb/tools/likes.py @@ -3,12 +3,11 @@ 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 +from .common import SELENIUM_EXCEPTIONS, archiver, logger, click_button LOG = logger(__name__) - -def load_likes(driver): +def load_likes(driver, profile_url): """ Loads the page that lists all pages you like @@ -18,68 +17,107 @@ def load_likes(driver): Returns: None """ - driver.get("https://www.facebook.com/pages/?category=liked") - wait = WebDriverWait(driver, 20) + driver.get("{0}/likes_all".format(profile_url)) - try: - wait.until( - EC.presence_of_element_located((By.XPATH, "//button/div/div[text()='Liked']")) - ) + wait = WebDriverWait(driver, 30) + try: 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 get_page_links(driver): """ - Unlike all pages + 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) + + 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 + Args: + driver: seleniumrequests.Chrome Driver instance + url: url string pointing to a page + archive: archiver instance Returns: None + """ - like_log, archive_likes = archiver("likes") + driver.get(url) + + print("Unliking {0}".format(url)) + + wait = WebDriverWait(driver, 60) actions = ActionChains(driver) - load_likes(driver) + 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']") - pages_list = driver.find_element_by_css_selector("#all_liked_pages") + # Click the "Liked" button to open up "Unlike" + click_button(driver, button) - actions.move_to_element(pages_list).perform() + wait.until( + EC.presence_of_element_located((By.XPATH, "//*[text()='Unlike']")) + ) - unlike_buttons = pages_list.find_elements_by_xpath("//button/div/div[text()='Liked']/../..") + # There should be an "Unlike" button now, click it + unlike_button = driver.find_element_by_xpath("//*[text()='Unlike']/..") - 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] + click_button(driver, unlike_button) - driver.execute_script("arguments[0].click();", button) + if archive: + archive(url) - archive_likes(page_name) +def unlike_pages(driver, profile_url): + """ + Unlike all pages + + Args: + driver: seleniumrequests.Chrome Driver instance + + Returns: + None + """ + + like_log, archive_likes = archiver("likes") - print("{0} was unliked".format(page_name)) + load_likes(driver, profile_url) - except SELENIUM_EXCEPTIONS: - continue + urls = get_page_links(driver) - load_likes(driver) + while urls: + for url in urls: + unlike_page(driver, url, archive=archive_likes) 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 + 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 diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 9983e41..5dcd6ec 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -1,12 +1,11 @@ import time - from selenium.webdriver.common.action_chains import ActionChains -from .common import SELENIUM_EXCEPTIONS, archiver +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, @@ -59,8 +58,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: 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",