Compare commits

...

21 Commits

Author SHA1 Message Date
dependabot[bot] ed96dd7e6a
Bump urllib3 from 1.25.2 to 1.25.8 (#162) 3 years ago
Sean Leavey b93f528f99
Ensure protocol is provided in mobile URLs (fixes #160) (#161) 3 years ago
dependabot[bot] ddf358bed2
Bump pygments from 2.4.2 to 2.7.4 (#158) 3 years ago
dependabot[bot] 262c7f84c6
Bump lxml from 4.6.2 to 4.6.3 (#159) 3 years ago
Ian Hunter a92fd3eb35
Update Required Python version (#157) 3 years ago
Wesley Kerfoot d64ed5a3db
Update README.md 3 years ago
Wesley Kerfoot 39b2437f1d
Update README.md 3 years ago
dependabot[bot] 72eb1bdcde
Bump bleach from 3.1.4 to 3.3.0 (#155) 3 years ago
Wesley Kerfoot 86403534b9
bump versions of cattrs and attrs (#151) 3 years ago
dependabot[bot] 11c21f6d2c
Bump lxml from 4.4.0 to 4.6.2 (#150) 3 years ago
tklam a5ed694a46
Add "Hide from profile" (#145) 4 years ago
Wesley Kerfoot ede08a42a9 bump version 4 years ago
Thomas c1af411453
WebDriverException not defined (#139) 4 years ago
Wesley Kerfoot 7d24450b0f
add FUNDING.yml (#137) 4 years ago
Wesley Kerfoot 1a846f1dac bump version 4 years ago
Wesley Kerfoot a182eaaaa3
fixes https://github.com/weskerfoot/DeleteFB/issues/135 (#136) 4 years ago
Wesley Kerfoot 50162ba996
fix unliking (#134) 4 years ago
Wesley Kerfoot 9747f0a00d bump version 4 years ago
Wesley Kerfoot 5830cfeecd
force url to mobile site for deleting wall posts (#130) 4 years ago
Wesley Kerfoot 1875b754ab
Fixes for new facebook site (#129) 4 years ago
dependabot[bot] af949c2d24
Bump bleach from 3.1.2 to 3.1.4 (#124) 4 years ago
  1. 2
      FUNDING.yml
  2. 8
      README.md
  3. 1
      deletefb/tools/chrome_driver.py
  4. 31
      deletefb/tools/common.py
  5. 19
      deletefb/tools/conversations.py
  6. 42
      deletefb/tools/likes.py
  7. 55
      deletefb/tools/wall.py
  8. 12
      requirements.txt
  9. 6
      setup.py

2
FUNDING.yml

@ -0,0 +1,2 @@
github: [weskerfoot]
custom: "39qHYvjVcMCNFr3RPAVetci9mKjzYGTQPz"

8
README.md

@ -1,3 +1,7 @@
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? ## Why?
I needed a simple and reliable way to delete Facebook posts. There are I needed a simple and reliable way to delete Facebook posts. There are
@ -18,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). (and hence feel the need to use it less).
## Dependencies ## 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` - A recent copy of Chrome or Chromium installed and available in your `$PATH`
## Installation ## Installation
@ -57,7 +61,7 @@ optional arguments:
``` ```
* Make sure that you have a recent version of Python 3.x installed (preferably * 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 * 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. * 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 * On Linux, it will be called something like `chromium-chromedriver` or just

1
deletefb/tools/chrome_driver.py

@ -2,6 +2,7 @@ from ..exceptions import UnknownOSException, ChromeError
from .common import NO_CHROME_DRIVER from .common import NO_CHROME_DRIVER
from clint.textui import puts, colored from clint.textui import puts, colored
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from shutil import which from shutil import which
from subprocess import check_output from subprocess import check_output
from urllib.request import urlretrieve from urllib.request import urlretrieve

31
deletefb/tools/common.py

@ -12,6 +12,7 @@ import json
import logging import logging
import logging.config import logging.config
import os import os
import urllib.parse as urlparse
SELENIUM_EXCEPTIONS = ( SELENIUM_EXCEPTIONS = (
NoSuchElementException, NoSuchElementException,
@ -19,13 +20,20 @@ SELENIUM_EXCEPTIONS = (
TimeoutException TimeoutException
) )
def click_button(driver, el): def click_button(driver, el):
""" """
Click a button using Javascript Click a button using Javascript
""" """
driver.execute_script("arguments[0].click();", el) 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): def scroll_to(driver, el):
""" """
@ -36,7 +44,6 @@ def scroll_to(driver, el):
except SELENIUM_EXCEPTIONS: except SELENIUM_EXCEPTIONS:
return return
def logger(name): def logger(name):
""" """
Args: Args:
@ -56,17 +63,29 @@ def logger(name):
logging.config.dictConfig(config["logging"]) logging.config.dictConfig(config["logging"])
return logging.getLogger(name) return logging.getLogger(name)
def wait_xpath(driver, expr, timeout=20):
def wait_xpath(driver, expr):
""" """
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: try:
wait.until(EC.presence_of_element_located((By.XPATH, expr))) wait.until(EC.presence_of_element_located((By.XPATH, expr)))
except SELENIUM_EXCEPTIONS: except SELENIUM_EXCEPTIONS:
return return
def force_mobile(url):
"""
Force a url to use the mobile site.
"""
parsed = urlparse.urlparse(url)
# Ensure a protocol is given (needed by selenium).
scheme = parsed.scheme or "https"
return urlparse.urlunparse((scheme,
"mobile.facebook.com",
parsed.path,
parsed.params,
parsed.query,
parsed.fragment))
NO_CHROME_DRIVER = """ NO_CHROME_DRIVER = """
You need to manually install the chromedriver for Selenium\n You need to manually install the chromedriver for Selenium\n

19
deletefb/tools/conversations.py

@ -6,6 +6,7 @@ from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select from selenium.webdriver.support.ui import Select
from pendulum import now from pendulum import now
from json import loads from json import loads
from time import sleep
import lxml.html as lxh import lxml.html as lxh
@ -29,12 +30,13 @@ def get_conversations(driver):
date = None date = None
if url and "messages/read" in url: if url and "messages/read" in url:
try:
date = 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() conversation_name = convo.find_element_by_xpath("../../../div/div/header/h3").text.strip()
assert(conversation_name)
assert(conversation_name) assert(url)
assert(url) except (SELENIUM_EXCEPTIONS + (AssertionError,)):
continue
conversations.append( conversations.append(
Conversation( Conversation(
@ -49,7 +51,10 @@ def get_conversations(driver):
find_element_by_xpath("a"). find_element_by_xpath("a").
get_attribute("href")) get_attribute("href"))
except SELENIUM_EXCEPTIONS: print("next_url", next_url)
except SELENIUM_EXCEPTIONS as e:
print(e)
break break
if not next_url: if not next_url:
break break

42
deletefb/tools/likes.py

@ -1,6 +1,6 @@
from .archive import archiver from .archive import archiver
from ..types import Page 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.common.by import By
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
@ -18,17 +18,25 @@ def load_likes(driver, profile_url):
None 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: driver.get(force_mobile("{0}/about".format(profile_url)))
wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".PageLikeButton")) scroll_until_element_exists(driver, "//div[text()='Likes']")
)
except SELENIUM_EXCEPTIONS: likes_link_el = driver.find_element_by_xpath(likes_link_xpath)
LOG.exception("Traceback of load_likes")
return 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): def get_page_links(driver):
""" """
@ -39,8 +47,7 @@ def get_page_links(driver):
Returns: Returns:
List of URLs to pages 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] return [page.get_attribute("href").replace("www", "mobile") for page in pages]
def unlike_page(driver, url, archive=None): def unlike_page(driver, url, archive=None):
@ -60,7 +67,7 @@ def unlike_page(driver, url, archive=None):
print("Unliking {0}".format(url)) print("Unliking {0}".format(url))
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 5)
try: try:
wait.until( wait.until(
@ -70,19 +77,14 @@ def unlike_page(driver, url, archive=None):
# Something went wrong with this page, so skip it # Something went wrong with this page, so skip it
return return
button = driver.find_element_by_xpath("//*[text()='Liked']") driver.find_element_by_xpath("//*[text()='Liked']/../../../..").click()
# Click the "Liked" button to open up "Unlike"
click_button(driver, button)
wait.until( wait.until(
EC.presence_of_element_located((By.XPATH, "//*[text()='Unlike']")) EC.presence_of_element_located((By.XPATH, "//*[text()='Unlike']"))
) )
# There should be an "Unlike" button now, click it # There should be an "Unlike" button now, click it
unlike_button = driver.find_element_by_xpath("//*[text()='Unlike']/..") driver.find_element_by_xpath("//*[text()='Unlike']/..").click()
click_button(driver, unlike_button)
if archive: if archive:
archive(Page(name=url)) archive(Page(name=url))

55
deletefb/tools/wall.py

@ -1,6 +1,6 @@
from ..types import Post from ..types import Post
from .archive import archiver 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 .config import settings
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
@ -24,23 +24,43 @@ def delete_posts(driver,
if year is not None: if year is not None:
user_profile_url = "{0}/timeline?year={1}".format(user_profile_url, year) 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) driver.get(user_profile_url)
for _ in range(MAX_POSTS): finished = False
post_button_sel = "_4xev"
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')]"
post_content_sel = "userContent" # Cannot return a text node, so it returns the parent.
post_timestamp_sel = "timestampContent" # Tries to be pretty resilient against DOM re-organizations
timestamp_exp = "//article//*/header//*/div/a[contains(@href, 'story_fbid')]//text()/.."
button_types = ["FeedDeleteOption", "HIDE_FROM_TIMELINE", "UNTAG"] button_types = ["Delete post", "Remove tag", "Hide from timeline", "Hide from profile"]
with archiver("wall") as archive_wall_post:
while True: while True:
try: 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_xpath("//article/div[@class='story_body_container']/div")
post_content_ts = driver.find_element_by_xpath(timestamp_exp)
post_content_element = driver.find_element_by_class_name(post_content_sel) if not (post_content_element or post_content_ts):
post_content_ts = driver.find_element_by_class_name(post_timestamp_sel) break
# Archive the post # Archive the post
archive_wall_post.archive( archive_wall_post.archive(
@ -53,15 +73,18 @@ def delete_posts(driver,
actions = ActionChains(driver) actions = ActionChains(driver)
actions.move_to_element(timeline_element).click().perform() actions.move_to_element(timeline_element).click().perform()
wait_xpath(driver, "//*[@id='feed_post_menu']/..") # Wait until the buttons show up
wait_xpath(driver, "//*[contains(@data-sigil, 'removeStoryButton')]")
menu = driver.find_element_by_xpath("//*[@id='feed_post_menu']/..")
delete_button = None delete_button = None
# Search for visible buttons in priority order
# Delete -> Untag -> Hide
for button_type in button_types: for button_type in button_types:
try: 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 break
except SELENIUM_EXCEPTIONS as e: except SELENIUM_EXCEPTIONS as e:
print(e) print(e)
@ -72,8 +95,10 @@ def delete_posts(driver,
break break
click_button(driver, delete_button) 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) click_button(driver, confirmation_button)
except SELENIUM_EXCEPTIONS as e: except SELENIUM_EXCEPTIONS as e:

12
requirements.txt

@ -1,20 +1,20 @@
appdirs==1.4.3 appdirs==1.4.3
args==0.1.0 args==0.1.0
attrs==19.1.0 attrs==20.3.0
bitarray==0.9.3 bitarray==0.9.3
bleach==3.1.2 bleach==3.3.0
cattrs-3.8==0.9.1 cattrs==1.1.2
certifi==2018.11.29 certifi==2018.11.29
chardet==3.0.4 chardet==3.0.4
clint==0.5.1 clint==0.5.1
docutils==0.14 docutils==0.14
idna==2.8 idna==2.8
lxml==4.4.0 lxml==4.6.3
pendulum==2.0.5 pendulum==2.0.5
pkginfo==1.5.0.1 pkginfo==1.5.0.1
progressbar==2.5 progressbar==2.5
pybloom-live==3.0.0 pybloom-live==3.0.0
Pygments==2.4.2 Pygments==2.7.4
python-dateutil==2.8.0 python-dateutil==2.8.0
pytzdata==2019.2 pytzdata==2019.2
readme-renderer==24.0 readme-renderer==24.0
@ -28,5 +28,5 @@ tldextract==2.2.0
tqdm==4.32.2 tqdm==4.32.2
twine==1.13.0 twine==1.13.0
typing==3.7.4 typing==3.7.4
urllib3==1.25.2 urllib3==1.25.8
webencodings==0.5.1 webencodings==0.5.1

6
setup.py

@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
setuptools.setup( setuptools.setup(
name="delete-facebook-posts", name="delete-facebook-posts",
version="1.1.13", version="1.1.17",
author="Wesley Kerfoot", author="Wesley Kerfoot",
author_email="wes@wesk.tech", author_email="wes@wesk.tech",
description="A Selenium Script to Delete Facebook Posts", description="A Selenium Script to Delete Facebook Posts",
@ -24,8 +24,8 @@ setuptools.setup(
"selenium-requests", "selenium-requests",
"requests", "requests",
"pybloom-live", "pybloom-live",
"attrs", "attrs>=20.3.0",
"cattrs-3.8", "cattrs>=1.1.2",
"lxml", "lxml",
"pendulum", "pendulum",
"clint", "clint",

Loading…
Cancel
Save