From c9c680cd6ab002dee42c1f0a555f557b151a116b Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 31 Oct 2020 19:51:41 -0400 Subject: [PATCH 01/10] fix issue with year argument --- 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..3b92b8a 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -22,7 +22,7 @@ def delete_posts(driver, """ if year is not None: - user_profile_url = "{0}/timeline?year={1}".format(user_profile_url, year) + user_profile_url = "{0}/?year={1}".format(force_mobile(user_profile_url), year) user_profile_url = force_mobile(user_profile_url) -- 2.30.2 From d6b3b240f4743ae42e4b20e72a5a3bfce1549b57 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 15 Nov 2020 14:29:33 -0500 Subject: [PATCH 02/10] activity log (not working yet) --- deletefb/deletefb.py | 11 ++-- deletefb/tools/activity.py | 107 +++++++++++++++++++++++++++++++++++++ deletefb/tools/wall.py | 6 +-- 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 deletefb/tools/activity.py diff --git a/deletefb/deletefb.py b/deletefb/deletefb.py index e1d0557..5c38fa1 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 .tools.activity import delete_activity from .quit_driver import quit_driver_and_reap_children import argparse @@ -24,8 +25,8 @@ def run_delete(): default="wall", dest="mode", type=str, - choices=["wall", "unlike_pages", "conversations"], - help="The mode you want to run in. Default is `wall' which deletes wall posts" + choices=["wall", "unlike_pages", "conversations", "activity"], + help="The mode you want to run in. Default is `wall' which deletes all wall posts" ) parser.add_argument( @@ -104,7 +105,7 @@ def run_delete(): settings["ARCHIVE"] = not args.archive_off - if args.year and args.mode not in ("wall", "conversations"): + if args.year and args.mode not in ("activity", "conversations"): parser.error("The --year option is not supported in this mode") args_user_password = args.password or getpass.getpass('Enter your password: ') @@ -122,9 +123,11 @@ def run_delete(): delete_posts( driver, args.profile_url, - year=args.year ) + elif args.mode == "activity": + delete_activity(driver, year=args.year) + elif args.mode == "unlike_pages": unlike_pages(driver, args.profile_url) diff --git a/deletefb/tools/activity.py b/deletefb/tools/activity.py new file mode 100644 index 0000000..4eebcc3 --- /dev/null +++ b/deletefb/tools/activity.py @@ -0,0 +1,107 @@ +from ..types import Post +from .archive import archiver +from .common import SELENIUM_EXCEPTIONS, click_button, wait_xpath, force_mobile +from .config import settings +from selenium.webdriver.common.action_chains import ActionChains + +import time + +# Used as a threshold to avoid running forever +MAX_POSTS = settings["MAX_POSTS"] + +def delete_activity(driver, + year=None): + """ + Deletes or hides all activity related to posts. + + Args: + driver: seleniumrequests.Chrome Driver instance + year: optional int YYYY year + """ + + driver.get("https://m.facebook.com/allactivity/?category_key=statuscluster") + + time.sleep(1000) + + finished = False + + with archiver("wall") as archive_wall_post: + for _ in range(MAX_POSTS): + if finished: + break + post_button_sel = "_4s19" + + 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"] + + while True: + try: + 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_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( + Post( + content=post_content_element.text, + date=post_content_ts.text + ) + ) + + actions = ActionChains(driver) + actions.move_to_element(timeline_element).click().perform() + + # 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 = 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) + continue + + if not delete_button: + print("Could not find anything to delete") + break + + click_button(driver, delete_button) + 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: + print(e) + continue + else: + break + + # Required to sleep the thread for a bit after using JS to click this button + time.sleep(5) + driver.refresh() diff --git a/deletefb/tools/wall.py b/deletefb/tools/wall.py index 3b92b8a..70c1894 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -10,8 +10,7 @@ import time MAX_POSTS = settings["MAX_POSTS"] def delete_posts(driver, - user_profile_url, - year=None): + user_profile_url): """ Deletes or hides all posts from the wall @@ -21,9 +20,6 @@ def delete_posts(driver, year: optional int YYYY year """ - if year is not None: - user_profile_url = "{0}/?year={1}".format(force_mobile(user_profile_url), year) - user_profile_url = force_mobile(user_profile_url) driver.get(user_profile_url) -- 2.30.2 From f92ec7ef6567abfa1528337815c18f8ab66263b0 Mon Sep 17 00:00:00 2001 From: wes Date: Sun, 15 Nov 2020 14:31:15 -0500 Subject: [PATCH 03/10] passing MODE in run script --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 6a6897b..0fc3586 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -/usr/bin/python3 -m deletefb.deletefb -E $EMAIL -P $PASS -U $URL \ No newline at end of file +/usr/bin/python3 -m deletefb.deletefb -E $EMAIL -P $PASS -U $URL -M $MODE -- 2.30.2 From cb777d4c6d2559664be48623b1a226a42e9b819b Mon Sep 17 00:00:00 2001 From: wes Date: Mon, 16 Nov 2020 00:12:00 -0500 Subject: [PATCH 04/10] wip activity log deletion --- deletefb/tools/activity.py | 130 ++++++++++++++----------------------- 1 file changed, 48 insertions(+), 82 deletions(-) diff --git a/deletefb/tools/activity.py b/deletefb/tools/activity.py index 4eebcc3..37cbaaa 100644 --- a/deletefb/tools/activity.py +++ b/deletefb/tools/activity.py @@ -3,12 +3,49 @@ from .archive import archiver from .common import SELENIUM_EXCEPTIONS, click_button, wait_xpath, force_mobile from .config import settings from selenium.webdriver.common.action_chains import ActionChains +from calendar import month_name + +months = [m for m in month_name if m] import time # Used as a threshold to avoid running forever MAX_POSTS = settings["MAX_POSTS"] +def get_load_more(driver): + """ + Click the "Load more from X" button repeatedly + """ + + button_expr = f"//div[contains(text(), 'Load more from')]" + + print("Trying to load more") + + while True: + try: + wait_xpath(driver, button_expr) + driver.find_element_by_xpath(button_expr).click() + except SELENIUM_EXCEPTIONS: + break + +def get_timeslices(driver): + """ + Get a list of the time slices Facebook is going to let us click. + """ + + slice_expr = "//header" + + wait_xpath(driver, slice_expr) + for ts in driver.find_elements_by_xpath(slice_expr): + if any(w in months for w in ts.text.strip().split()): + yield ts + try: + int(ts.text.strip()) + if len(ts.text.strip()) == 4: # it's a year + yield ts + except ValueError: + continue + def delete_activity(driver, year=None): """ @@ -21,87 +58,16 @@ def delete_activity(driver, driver.get("https://m.facebook.com/allactivity/?category_key=statuscluster") + #print(get_load_more(driver)) + + actions = ActionChains(driver) + + for ts in get_timeslices(driver): + # Need to figure out how to ignore the ones with nothing in them + print(ts.text) + actions.move_to_element(ts) + get_load_more(driver) + time.sleep(1000) - finished = False - - with archiver("wall") as archive_wall_post: - for _ in range(MAX_POSTS): - if finished: - break - post_button_sel = "_4s19" - - 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"] - - while True: - try: - 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_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( - Post( - content=post_content_element.text, - date=post_content_ts.text - ) - ) - - actions = ActionChains(driver) - actions.move_to_element(timeline_element).click().perform() - - # 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 = 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) - continue - - if not delete_button: - print("Could not find anything to delete") - break - - click_button(driver, delete_button) - 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: - print(e) - continue - else: - break - - # Required to sleep the thread for a bit after using JS to click this button - time.sleep(5) - driver.refresh() + #with archiver("activity") as archive_wall_post: -- 2.30.2 From 6b7334ac634baf63c5bcfe3dd3393bd220869846 Mon Sep 17 00:00:00 2001 From: tklam Date: Tue, 1 Dec 2020 23:45:30 +0000 Subject: [PATCH 05/10] 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 70c1894..9673d77 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -41,7 +41,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 7e67dabb2b3610434e200061f0238b9a322be14a 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 06/10] 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 1b8491b57af43ef956dbcbe8ba3f75ced099752d 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 07/10] 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 5ce35aa75784ee0352cbe215793c64be7cb707e3 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 24 Jan 2021 18:49:35 -0500 Subject: [PATCH 08/10] fix run.sh --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 0fc3586..7ba8645 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -/usr/bin/python3 -m deletefb.deletefb -E $EMAIL -P $PASS -U $URL -M $MODE +python3 -m deletefb.deletefb -E $EMAIL -P $PASS -U $URL -- 2.30.2 From a91bfb9cb7213db5b20fd804939401c2dc962fd4 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 24 Jan 2021 18:59:05 -0500 Subject: [PATCH 09/10] fix "Remove tag" to "Remove Tag" --- 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 9673d77..92f5a67 100644 --- a/deletefb/tools/wall.py +++ b/deletefb/tools/wall.py @@ -41,7 +41,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", "Hide from profile"] + button_types = ["Delete post", "Remove Tag", "Hide from timeline", "Hide from profile"] while True: try: -- 2.30.2 From b4ea9729e023a7a487781ed525bd7a58040579f6 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sun, 24 Jan 2021 19:38:00 -0500 Subject: [PATCH 10/10] refactoring activity --- deletefb/tools/activity.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/deletefb/tools/activity.py b/deletefb/tools/activity.py index 37cbaaa..1c5a5c0 100644 --- a/deletefb/tools/activity.py +++ b/deletefb/tools/activity.py @@ -36,6 +36,9 @@ def get_timeslices(driver): slice_expr = "//header" wait_xpath(driver, slice_expr) + + print("Loaded") + for ts in driver.find_elements_by_xpath(slice_expr): if any(w in months for w in ts.text.strip().split()): yield ts @@ -58,16 +61,35 @@ def delete_activity(driver, driver.get("https://m.facebook.com/allactivity/?category_key=statuscluster") - #print(get_load_more(driver)) + if year is None: + print("Deleting all years") + years = [elem.text.strip() for elem in get_timeslices(driver)] + else: + years = [elem.text.strip() for elem in get_timeslices(driver) if year in elem.text.strip()] actions = ActionChains(driver) - for ts in get_timeslices(driver): + print(years) + + for year in years: + year_elems = [y for y in get_timeslices(driver) if y.text.strip() == year] + + if not year_elems: + raise ValueError("Non-existent year") # FIXME use a better exception + + year_elem = year_elems[0] + # Need to figure out how to ignore the ones with nothing in them - print(ts.text) - actions.move_to_element(ts) + print(f"Deleting activity from {year_elem.text}") + + actions.move_to_element(year_elem) + year_elem.click() + get_load_more(driver) - time.sleep(1000) + time.sleep(10) + + driver.refresh() + #with archiver("activity") as archive_wall_post: -- 2.30.2