├── .env.example ├── .idea ├── vcs.xml ├── modules.xml └── misc.xml ├── Pipfile ├── start.py ├── fb-anti-unsend.iml ├── src ├── _settings.py └── _client.py ├── LICENSE ├── README.md ├── .gitignore └── Pipfile.lock /.env.example: -------------------------------------------------------------------------------- 1 | FB_EMAIL = you@example.com 2 | FB_PASSWORD = secret 3 | FB_USE_SESSION = false 4 | FB_SESSION_FILE = session.json 5 | 6 | IGNORE_SELF = true 7 | ALWAYS_ACTIVE = false 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | python-dotenv = "*" 10 | fbchat = "*" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from src._client import Client 4 | from src._settings import FB_EMAIL, FB_PASSWORD, FB_SESSION, ALWAYS_ACTIVE 5 | 6 | 7 | if __name__ == "__main__": 8 | client = Client(FB_EMAIL, FB_PASSWORD, session_cookies=FB_SESSION) 9 | client.listen(ALWAYS_ACTIVE) 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fb-anti-unsend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | 11 | def is_true(a): 12 | return str(a).lower() in ("true", "yes", "1") 13 | 14 | 15 | FB_EMAIL = os.getenv("FB_EMAIL") 16 | FB_PASSWORD = os.getenv("FB_PASSWORD") 17 | FB_USE_SESSION = is_true(os.getenv("FB_USE_SESSION")) 18 | FB_SESSION_FILE = os.getenv("FB_SESSION_FILE") 19 | 20 | if FB_USE_SESSION: 21 | with open(FB_SESSION_FILE) as f: 22 | FB_SESSION = json.loads(f.read()) 23 | 24 | IGNORE_SELF = is_true(os.getenv("IGNORE_SELF")) 25 | ALWAYS_ACTIVE = is_true(os.getenv("ALWAYS_ACTIVE")) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kacper Ziubryniewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facebook Messenger anti-unsend 2 | 3 | ## Description 4 | Are you getting angry when someone unsends the message and you can't see it? This project is for you! When someone will unsend the message, you'll get it immediately in your conversation with yourself. 5 | 6 | ## Installation 7 | Clone the repo 8 | ```console 9 | $ git clone https://github.com/kapi2289/fb-anti-unsend.git 10 | $ cd fb-anti-unsend 11 | ``` 12 | 13 | Create the `.env` file and edit it with your favourite text editor 14 | ```console 15 | $ cp .env.example .env 16 | $ nano .env 17 | ``` 18 | 19 | Install dependencies 20 | ```console 21 | $ pip install --user pipenv 22 | $ pipenv install 23 | ``` 24 | 25 | And now you can run it! 26 | ```console 27 | $ pipenv run python start.py 28 | ``` 29 | 30 | ## Tips 31 | This project is using `fbchat` library. You can get your session, turn it to JSON and save it to the `session.json` file 32 | ```python 33 | import fbchat 34 | import json 35 | client = fbchat.Client("you@example.com", "password") 36 | session = client.getSession() 37 | 38 | with open("session.json", 'w') as f: 39 | f.write(json.dumps(session)) 40 | ``` 41 | 42 | And then you need to set the `FB_USE_SESSION` option to `true` in your `.env` file 43 | -------------------------------------------------------------------------------- /src/_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import time 5 | 6 | import fbchat 7 | import requests 8 | from fbchat.models import * 9 | 10 | from ._settings import * 11 | 12 | 13 | class Client(fbchat.Client): 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(Client, self).__init__(*args, **kwargs) 17 | self.messages = list() 18 | self.setDefaultThread(self.uid, ThreadType.USER) 19 | 20 | def onMessage(self, mid, message_object, thread_id, **kwargs): 21 | message_object.uid = mid 22 | self.messages.append(message_object) 23 | for message in self.messages: 24 | ts = (time.time() - 10 * 60) * 1000 25 | if message.timestamp < ts: 26 | self.messages = list(filter(lambda x: x is not message, self.messages)) 27 | 28 | def onMessageUnsent(self, mid, author_id, **kwargs): 29 | if IGNORE_SELF and author_id == self.uid: 30 | return 31 | for message in self.messages: 32 | if message.uid == mid: 33 | files = [] 34 | unsendable_files = [] 35 | for a in message.attachments: 36 | if isinstance(a, ImageAttachment): 37 | if a.is_animated: 38 | files.append(a.animated_preview_url) 39 | else: 40 | url = a.large_preview_url or a.preview_url or a.thumbnail_url 41 | if url: 42 | files.append(url) 43 | elif isinstance(a, VideoAttachment): 44 | files.append(a.preview_url) 45 | elif isinstance(a, AudioAttachment): 46 | unsendable_files.append(a.url) 47 | elif isinstance(a, FileAttachment): 48 | r = requests.get(a.url) 49 | if r.status_code == 200: 50 | url = re.search("document\.location\.replace\(\"(.*)\"\);", r.text).group(1) 51 | url = url.replace('\\/', '/') 52 | files.append(url) 53 | 54 | author = self.fetchUserInfo(message.author)[message.author] 55 | message.reply_to_id = None 56 | self.send(Message("{} unsent the message:".format(author.name), 57 | mentions=[Mention(author.uid, length=len(author.name))])) 58 | if message.text or message.sticker: 59 | self.send(message) 60 | if unsendable_files: 61 | self.sendMessage("Attachments: \n{}".format("\n----------\n".join(unsendable_files))) 62 | if files: 63 | self.sendRemoteFiles(files) 64 | self.messages = list(filter(lambda x: x is not message, self.messages)) 65 | break 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,intellij 3 | # Edit at https://www.gitignore.io/?templates=python,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | # JetBrains templates 74 | **___jb_tmp___ 75 | 76 | ### Intellij Patch ### 77 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 78 | 79 | # *.iml 80 | # modules.xml 81 | # .idea/misc.xml 82 | # *.ipr 83 | 84 | # Sonarlint plugin 85 | .idea/sonarlint 86 | 87 | ### Python ### 88 | # Byte-compiled / optimized / DLL files 89 | __pycache__/ 90 | *.py[cod] 91 | *$py.class 92 | 93 | # C extensions 94 | *.so 95 | 96 | # Distribution / packaging 97 | .Python 98 | build/ 99 | develop-eggs/ 100 | dist/ 101 | downloads/ 102 | eggs/ 103 | .eggs/ 104 | lib/ 105 | lib64/ 106 | parts/ 107 | sdist/ 108 | var/ 109 | wheels/ 110 | pip-wheel-metadata/ 111 | share/python-wheels/ 112 | *.egg-info/ 113 | .installed.cfg 114 | *.egg 115 | MANIFEST 116 | 117 | # PyInstaller 118 | # Usually these files are written by a python script from a template 119 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 120 | *.manifest 121 | *.spec 122 | 123 | # Installer logs 124 | pip-log.txt 125 | pip-delete-this-directory.txt 126 | 127 | # Unit test / coverage reports 128 | htmlcov/ 129 | .tox/ 130 | .nox/ 131 | .coverage 132 | .coverage.* 133 | .cache 134 | nosetests.xml 135 | coverage.xml 136 | *.cover 137 | .hypothesis/ 138 | .pytest_cache/ 139 | 140 | # Translations 141 | *.mo 142 | *.pot 143 | 144 | # Django stuff: 145 | *.log 146 | local_settings.py 147 | db.sqlite3 148 | 149 | # Flask stuff: 150 | instance/ 151 | .webassets-cache 152 | 153 | # Scrapy stuff: 154 | .scrapy 155 | 156 | # Sphinx documentation 157 | docs/_build/ 158 | 159 | # PyBuilder 160 | target/ 161 | 162 | # Jupyter Notebook 163 | .ipynb_checkpoints 164 | 165 | # IPython 166 | profile_default/ 167 | ipython_config.py 168 | 169 | # pyenv 170 | .python-version 171 | 172 | # pipenv 173 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 174 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 175 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 176 | # install all needed dependencies. 177 | #Pipfile.lock 178 | 179 | # celery beat schedule file 180 | celerybeat-schedule 181 | 182 | # SageMath parsed files 183 | *.sage.py 184 | 185 | # Environments 186 | .env 187 | .venv 188 | env/ 189 | venv/ 190 | ENV/ 191 | env.bak/ 192 | venv.bak/ 193 | 194 | # Spyder project settings 195 | .spyderproject 196 | .spyproject 197 | 198 | # Rope project settings 199 | .ropeproject 200 | 201 | # mkdocs documentation 202 | /site 203 | 204 | # mypy 205 | .mypy_cache/ 206 | .dmypy.json 207 | dmypy.json 208 | 209 | # Pyre type checker 210 | .pyre/ 211 | 212 | # End of https://www.gitignore.io/api/python,intellij 213 | 214 | # Facebook session file 215 | session.json 216 | 217 | # nohup logs 218 | nohup.out 219 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5a273e33bf637b4d3ae366742c06c4e8e1c9fd465fe6987b832e6d40bbea8f46" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aenum": { 20 | "hashes": [ 21 | "sha256:260225470b49429f5893a195a8b99c73a8d182be42bf90c37c93e7b20e44eaae", 22 | "sha256:aaebe735508d9cbc72cd6adfb59660a5e676dfbeb6fb24fb090041e7ddb8d3b3", 23 | "sha256:f9d20f7302ce3dc3639b3f75c3b3e146f3b22409a6b4513c1f0bd6dbdfcbd8c1" 24 | ], 25 | "version": "==2.2.6" 26 | }, 27 | "attrs": { 28 | "hashes": [ 29 | "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", 30 | "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" 31 | ], 32 | "markers": "python_version >= '3.5'", 33 | "version": "==22.1.0" 34 | }, 35 | "beautifulsoup4": { 36 | "hashes": [ 37 | "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", 38 | "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==4.11.1" 42 | }, 43 | "certifi": { 44 | "hashes": [ 45 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 46 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 47 | ], 48 | "index": "pypi", 49 | "version": "==2022.12.7" 50 | }, 51 | "charset-normalizer": { 52 | "hashes": [ 53 | "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", 54 | "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" 55 | ], 56 | "markers": "python_version >= '3'", 57 | "version": "==2.0.12" 58 | }, 59 | "fbchat": { 60 | "hashes": [ 61 | "sha256:112fe2c20320a278258afacf7f466cd2fd34dc9da49fd0b999467464be6486b8", 62 | "sha256:18c21b0b86c697a16975a78033d4690083d8668235005c56a023de4f4f744394" 63 | ], 64 | "index": "pypi", 65 | "version": "==1.9.1" 66 | }, 67 | "idna": { 68 | "hashes": [ 69 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 70 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 71 | ], 72 | "markers": "python_version >= '3'", 73 | "version": "==3.4" 74 | }, 75 | "paho-mqtt": { 76 | "hashes": [ 77 | "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f" 78 | ], 79 | "version": "==1.6.1" 80 | }, 81 | "python-dotenv": { 82 | "hashes": [ 83 | "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", 84 | "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" 85 | ], 86 | "index": "pypi", 87 | "version": "==0.10.3" 88 | }, 89 | "requests": { 90 | "hashes": [ 91 | "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", 92 | "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" 93 | ], 94 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 95 | "version": "==2.27.1" 96 | }, 97 | "soupsieve": { 98 | "hashes": [ 99 | "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", 100 | "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" 101 | ], 102 | "markers": "python_version >= '3.6'", 103 | "version": "==2.3.2.post1" 104 | }, 105 | "urllib3": { 106 | "hashes": [ 107 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", 108 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" 109 | ], 110 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 111 | "version": "==1.26.13" 112 | } 113 | }, 114 | "develop": {} 115 | } 116 | --------------------------------------------------------------------------------