├── .github ├── emojis │ ├── package.png │ ├── pushpin.png │ ├── rocket.png │ └── sewing-needle.png ├── labtocat.png └── logo.jpg ├── .gitignore ├── .pylintrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── PRESERVED.md ├── README.md ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── tests ├── __init__.py ├── analysis_profile_replies.py ├── analysis_profile_threads.py ├── delete_thread.py ├── following_unfollowing.py ├── get_notifications.py ├── get_post_id_from_thread_id.py ├── get_post_id_from_url.py ├── get_suggested_users.py ├── get_thread_likers.py ├── get_threads.py ├── get_timeline.py ├── get_token.py ├── get_user_id_from_username.py ├── get_user_profile.py ├── get_user_profile_replies.py ├── get_user_profile_threads.py ├── like_unlike.py ├── publish.py ├── publish_with_image.py └── repost_unrepost.py └── threadspy ├── __init__.py ├── ai ├── __init__.py ├── agent.py └── templates │ ├── __init__.py │ └── qna.py ├── constants.py ├── threads_api.py └── types ├── __init__.py ├── candidate.py ├── caption.py ├── common.py ├── extensions.py ├── get_suggested_users_response.py ├── get_thread_likers_response.py ├── get_user_profile_replies_response.py ├── get_user_profile_response.py ├── get_user_profile_thread_response.py ├── get_user_profile_threads_response.py ├── image_versions2.py ├── media_data.py ├── post.py ├── quoted_post.py ├── reply_facepile_user.py ├── reposted_post.py ├── share_info.py ├── suggested_user.py ├── text_post_app_info.py ├── thread.py ├── thread_data.py ├── thread_item.py ├── threads_bio_link.py ├── threads_hd_profile_pic_version.py ├── threads_user.py ├── threads_user_summary.py ├── user_data.py ├── user_profile_data.py ├── users.py └── video_version.py /.github/emojis/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/emojis/package.png -------------------------------------------------------------------------------- /.github/emojis/pushpin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/emojis/pushpin.png -------------------------------------------------------------------------------- /.github/emojis/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/emojis/rocket.png -------------------------------------------------------------------------------- /.github/emojis/sewing-needle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/emojis/sewing-needle.png -------------------------------------------------------------------------------- /.github/labtocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/labtocat.png -------------------------------------------------------------------------------- /.github/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/.github/logo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | assets 131 | .idea 132 | .DS_Store 133 | models/*.pt 134 | models/*.bin 135 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | disable=( 3 | missing-module-docstring, 4 | missing-class-docstring 5 | ) 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.pylint", 4 | "ms-python.black-formatter" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.insertFinalNewline": true, 4 | "python.defaultInterpreterPath": ".venv/bin/python", 5 | "python.formatting.provider": "black", 6 | "python.linting.pylintEnabled": true, 7 | "python.linting.enabled": true, 8 | "[python]": { 9 | "editor.defaultFormatter": "ms-python.black-formatter" 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Junho Yeo 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 | -------------------------------------------------------------------------------- /PRESERVED.md: -------------------------------------------------------------------------------- 1 | # [](https://github.com/junhoyeo) threads-py 2 | 3 | [![pypi](https://img.shields.io/pypi/v/threads-py.svg?style=flat-square&labelColor=black)](https://pypi.org/project/threads-py) [![MIT License](https://img.shields.io/badge/license-MIT-blue?style=flat-square&labelColor=black)](https://github.com/junhoyeo/threads-py/blob/main/LICENSE) 4 | 5 | 6 | ### **Unofficial, Reverse-Engineered Python API for [Meta's Threads](https://www.threads.net/).** 7 | 8 | > #### **Looking for the TypeScript version?** _Check out **[junhoyeo/threads-api. ![](https://img.shields.io/github/stars/junhoyeo%2Fthreads-api?style=social)](https://github.com/junhoyeo/threads-api)**_ 9 | 10 | --- 11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install --no-cache-dir --upgrade threads-py 16 | ``` 17 | 18 | ## Initialization 19 | #### Public Data Usage: 20 | ```python3 21 | from threadspy import ThreadsAPI 22 | api = ThreadsAPI() 23 | ``` 24 | #### Private Data Usage: 25 | ```python3 26 | from threadspy import ThreadsAPI 27 | api = ThreadsAPI( 28 | username="Your-Username" 29 | password="Your-Password" 30 | token="You-Token" # optional (if you're already authenticated) 31 | ) 32 | ``` 33 | 34 | --- 35 | 36 | ## [](https://github.com/junhoyeo) Implementation Roadmap and Methodology Overview 37 | - [x] 📢 [Read public data](#read-public-data) 38 | - [x] ✅ [Fetch User ID](#fetch-user-id) 39 | - [x] ✅ [Read User Profile Info](#read-user-profile-info) 40 | - [x] ✅ [Read list of User Threads](#read-list-of-user-threads) 41 | - [ ] 🚧 With Pagination (If auth provided) 42 | - [x] ✅ [Read list of User Replies](#read-list-of-user-replies) 43 | - [ ] 🚧 With Pagination (If auth provided) 44 | - [x] ✅ [Fetch Post ID](#fetch-post-id) 45 | - [x] ✅ [Read A Single Thread](#read-a-single-thread) 46 | - [x] ✅ [Get Thread Likes](#get-thread-likes) 47 | - [x] 🔏 [Read user private data](#read-private-data) 48 | - [x] ✅ [Read User Followings](#read-user-followings) 49 | - [x] ✅ [Read User Followers](#read-user-followers) 50 | - [x] ✅ [Get suggested users](#get-suggested-users) 51 | - [x] ✅ [Search Query](#search-query) 52 | - [x] ✅ [Read User Timeline Feed](#read-user-timeline-feed) 53 | - [x] ✅ [Get Notifications](#get-notifications) 54 | - [ ] 🔏 [Write Private Data (Authentication Required)](#write-private-data-authentication-required) 55 | - [ ] ✅ [Create New Thrad Or Reply To Eexisting Thread](#create-new-thrad-or-reply-to-eexisting-thread) 56 | - [ ] 🚧 Make link previews to get shown 57 | - [ ] ✅ [Delete Thread](#delete-thread) 58 | - [ ] 🚧 Quote Thread 59 | - [x] 🔒 [Friendship Actions](#friendship-actions-authentication-required) 60 | - [x] ✅ [Follow User](#follow-user) 61 | - [x] ✅ [Unfollow User](#unfollow-user) 62 | - [x] ✅ [Block User](#block-user) 63 | - [x] ✅ [Unblock User](#unblock-user) 64 | - [x] ✅ [Mute User](#mute-user) 65 | - [x] ✅ [Unmute User](#unmute-user) 66 | - [x] ✅ [Restrict User](#restrict-user) 67 | - [x] ✅ [Unrestrict User](#unrestrict-user) 68 | - [x] ✅ [Check Friendship Status With Another Users](#check-friendship-status-with-another-users) 69 | - [x] 🔒 [Interactions (Authentication Required)](#interactions-authentication-required) 70 | - [x] ✅ [Like Thread](#like-thread) 71 | - [x] ✅ [Remove Like From Thread](#remove-like-from-thread) 72 | - [x] ✅ [Repost Thread](#repost-thread) 73 | - [x] ✅ [Delete Reposted Thread](#delete-reposted-thread) 74 | - [ ] 🚧 LangChain Agent (`threadspy.ai`) 75 | - [ ] 🚧 Threads Tool for LangChain 76 | - [ ] 📌 Link Threads & LLaMa ([@Mineru98](https://github.com/Mineru98)) 77 | - [ ] 📌 Provide statistical analysis of posts in Threads ([@Mineru98](https://github.com/Mineru98)) 78 | 79 | 80 | 81 | --- 82 | ## Read public data 83 | 84 | 85 | 86 | ### Fetch User ID: 87 | 88 | ```python3 89 | user_id = api.get_user_id_from_username(username) 90 | ``` 91 |
92 | 93 |

Parameters

94 |
95 | 96 | | Parameters | Description | Type | Required | 97 | |:----------:|:---------------:|:-------:|:--------:| 98 | | `username` | Target username | String | Yes | 99 |
100 | 101 | ### Read User Profile Info: 102 | 103 | ```python3 104 | user_profile = api.get_user_profile(username, user_id) 105 | ``` 106 |
107 | 108 |

Parameters

109 |
110 | 111 | | Parameters | Description | Type | Required | Default Value | 112 | |:----------:|:---------------:|:-------:|:--------:|:-------------:| 113 | | `username` | Target username | String | Yes | - | 114 | | `user_id` | Target User ID | String | No | None | 115 |
116 | 117 | 118 | 119 | ### Read list of User Threads: 120 | 121 | ```python3 122 | user_threads = api.get_user_profile_threads(username, user_id) 123 | ``` 124 |
125 | 126 |

Parameters

127 |
128 | 129 | | Parameters | Description | Type | Required | Default Value | 130 | |:----------:|:---------------:|:-------:|:--------:|:-------------:| 131 | | `username` | Target username | String | Yes | - | 132 | | `user_id` | Target User ID | String | No | None | 133 |
134 | 135 | 136 | 137 | ### Read list of User Replies: 138 | 139 | ```python3 140 | user_replies = api.get_user_profile_replies(username, user_id) 141 | ``` 142 |
143 | 144 |

Parameters

145 |
146 | 147 | | Parameters | Description | Type | Required | Default Value | 148 | |:----------:|:---------------:|:-------:|:--------:|:-------------:| 149 | | `username` | Target username | String | Yes | - | 150 | | `user_id` | Target User ID | String | No | None | 151 |
152 | 153 | 154 | 155 | ### Fetch Post ID: 156 | 157 | > #### via Thread ID E.g. "CuW6-7KyXme": 158 | ```python3 159 | post_id = api.get_post_id_from_thread_id(thread_id) 160 | ``` 161 |
162 | 163 |

Parameters

164 |
165 | 166 | | Parameters | Description | Type | Required | 167 | |:----------:|:---------------:|:-------:|:--------:| 168 | | `thread_id`| Last part of the thread URL | String | Yes | 169 |
170 |
171 | 172 |

Examples

173 |
174 | 175 | ``` 176 | thread_id = 'CugF-EjhQ3r' 177 | post_id = api.get_post_id_from_thread_id(thread_id) 178 | ``` 179 | 180 |
181 | 182 | 183 | #### via Post URL E.g."https://www.threads.net/t/CuW6-7KyXme": 184 | ```python3 185 | post_id = api.get_post_id_from_url(post_url) 186 | ``` 187 |
188 | 189 |

Parameters

190 |
191 | 192 | | Parameters | Description | Type | Required | 193 | |:----------:|:---------------:|:-------:|:--------:| 194 | | `post_url` | Thread URL | String | Yes | 195 |
196 |
197 | 198 |

Examples

199 |
200 | 201 | ``` 202 | post_url = 'https://www.threads.net/t/CugF-EjhQ3r' 203 | post_id = api.get_post_id_from_url(post_url) 204 | ``` 205 | 206 |
207 | 208 | 209 | 210 | ### Read A Single Thread: 211 | 212 | ```python3 213 | single_thread = api.get_threads(post_id) 214 | ``` 215 |
216 | 217 |

Parameters

218 |
219 | 220 | | Parameters | Description | Type | Required | 221 | |:----------:|:---------------:|:-------:|:--------:| 222 | | `post_id` | Target username | String | Yes | 223 |
224 | 225 | 226 | 227 | ### Get Thread Likes: 228 | 229 | ```python3 230 | thread_likes = api.get_thread_likers(post_id) 231 | ``` 232 |
233 | 234 |

Parameters

235 |
236 | 237 | | Parameters | Description | Type | Required | 238 | |:----------:|:---------------:|:-------:|:--------:| 239 | | `post_id` | Target username | String | Yes | 240 |
241 | 242 | 243 | 244 | 245 | 246 | 247 | --- 248 | ## Read Private Data 249 | 250 | 251 | 252 | ### Read User Followings: 253 | 254 | ```python3 255 | user_followers = api.get_followings(user_id) 256 | ``` 257 |
258 | 259 |

Parameters

260 |
261 | 262 | | Parameters | Description | Type | Required | 263 | |:----------:|:---------------:|:-------:|:--------:| 264 | | `user_id` | Target User ID | String | Yes | 265 |
266 | 267 | 268 | 269 | ### Read User Followers: 270 | 271 | ```python3 272 | user_followings = api.get_followers(user_id) 273 | ``` 274 |
275 | 276 |

Parameters

277 |
278 | 279 | | Parameters | Description | Type | Required | 280 | |:----------:|:---------------:|:-------:|:--------:| 281 | | `user_id` | Target User ID | String | Yes | 282 |
283 | 284 | 285 | 286 | ### Get Suggested Users: 287 | 288 | ```python3 289 | suggested_users = api.get_suggested_users(count, paging) 290 | ``` 291 |
292 | 293 |

Parameters

294 |
295 | 296 | | Parameters | Description | Type | Required | Default Value | 297 | |:----------:|:---------------:|:-------:|:--------:|:-------------:| 298 | | `count` | Number of suggested users | Integer | No | 15 | 299 | | `paging` | Page number | Integer | No | None | 300 |
301 | 302 | 303 | 304 | ### Search Query: 305 | 306 | ```python3 307 | thread_likes = api.search(search_parameter) 308 | ``` 309 |
310 | 311 |

Parameters

312 |
313 | 314 | | Parameters | Description | Type | Required | 315 | |:------------------:|:---------------:|:-------:|:--------:| 316 | | `search_parameter` | Search Query | String | Yes | 317 |
318 | 319 | 320 | 321 | ### Read User Timeline Feed: 322 | 323 | ```python3 324 | user_timeline = api.get_timeline(max_id) 325 | ``` 326 |
327 | 328 |

Parameters

329 |
330 | 331 | |Parameters| Description | Type | Required | 332 | |:--------:|:----------------------:|:-------:|:--------:| 333 | | `max_id` | Next Posts Batch ID | String | No | 334 |
335 | 336 | 337 | 338 | ### Get Notifications: 339 | 340 | ```python3 341 | user_timeline = api.get_timeline(max_id) 342 | ``` 343 |
344 | 345 |

Parameters

346 |
347 | 348 | | Parameters | Description | Type | Required | Default Value | 349 | |:---------------------:|:----------------------:|:-------:|:--------:|:-------------:| 350 | | `notification_filter` | Next Posts Batch ID | String | No | 'replies' | 351 | | `max_id` | Next Posts Batch ID | String | No | None | 352 | | `pagination` | Next Posts Batch ID | String | No | None | 353 | 354 | 'notification_filter' values: 'mentions', 'replies', 'verified' 355 |
356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | --- 364 | ## Write Private Data (Authentication Required) 365 | 366 | 367 | 368 | ### Create New Thrad Or Reply To Eexisting Thread: 369 | 370 | ```python3 371 | boolean_response = api.publish(count, image_path, url, parent_post_id) 372 | ``` 373 |
374 | 375 |

Parameters

376 |
377 | 378 | | Parameters | Description | Type | Required | Default Value | 379 | |:----------:|:---------------:|:-------:|:--------:|:-------------:| 380 | | `caption` | Text to post in Thread | String | Yes | 15 | 381 | | `image_path` | Image Path to post in Thread | String | No | None | 382 | | `url` | Link to post in Thread | String | No | None | 383 | | `parent_post_id` | Post ID | String | No | None | 384 |
385 |
386 | 387 |

Examples

388 |
389 | 390 | - Text Threads: 391 | 392 | ```python3 393 | api.publish(caption="🤖 Hello World!") 394 | ``` 395 | 396 | - Threads with Image: 397 | 398 | ```python3 399 | api.publish( 400 | caption= '🤖 Threads with Link Image', 401 | image_path="https://github.com/junhoyeo/threads-py/raw/main/.github/logo.jpg" 402 | ) 403 | ``` 404 | 405 | - Threads with Link Attachment: 406 | 407 | ```python3 408 | api.publish( 409 | caption= '🤖 Threads with Link Image', 410 | url="https://github.com/junhoyeo/threads-py" 411 | ) 412 | ``` 413 | 414 | Reply to Other Threads: 415 | 416 | ```python3 417 | parent_post_url = 'https://www.threads.net/t/CugF-EjhQ3r' 418 | parent_post_id = api.get_post_id_from_url(parent_post_url) # or use `get_post_id_from_thread_id` 419 | 420 | api.publish({ 421 | text: '🤖 Beep', 422 | link: 'https://github.com/junhoyeo/threads-py', 423 | parent_post_id: parent_post_id, 424 | }) 425 | ``` 426 | 427 |
428 | 429 | 430 | 431 | ### Delete Thread: 432 | 433 | ```python3 434 | boolean_response = api.delete_thread(post_id) 435 | ``` 436 |
437 | 438 |

Parameters

439 |
440 | 441 | | Parameters | Description | Type | Required | 442 | |:----------:|:---------------:|:-------:|:--------:| 443 | | `post_id` | Post Identifier | String | Yes | 444 |
445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | --- 453 | ## Friendship Actions (Authentication Required) 454 | 455 | 456 | 457 | ### Follow User: 458 | 459 | ```python3 460 | boolean_response = api.follow(user_id) 461 | ``` 462 |
463 | 464 |

Parameters

465 |
466 | 467 | | Parameters | Description | Type | Required | 468 | |:----------:|:---------------:|:-------:|:--------:| 469 | | `user_id` | User Identifier | String | Yes | 470 |
471 | 472 | 473 | 474 | ### Unfollow User: 475 | 476 | ```python3 477 | boolean_response = api.unfollow(user_id) 478 | ``` 479 |
480 | 481 |

Parameters

482 |
483 | 484 | | Parameters | Description | Type | Required | 485 | |:----------:|:---------------:|:-------:|:--------:| 486 | | `user_id` | User Identifier | String | Yes | 487 |
488 | 489 | 490 | 491 | ### Block User: 492 | 493 | ```python3 494 | boolean_response = api.block(user_id) 495 | ``` 496 |
497 | 498 |

Parameters

499 |
500 | 501 | | Parameters | Description | Type | Required | 502 | |:----------:|:---------------:|:-------:|:--------:| 503 | | `user_id` | User Identifier | String | Yes | 504 |
505 | 506 | 507 | 508 | ### Unblock User: 509 | 510 | ```python3 511 | boolean_response = api.unblock(user_id) 512 | ``` 513 |
514 | 515 |

Parameters

516 |
517 | 518 | | Parameters | Description | Type | Required | 519 | |:----------:|:---------------:|:-------:|:--------:| 520 | | `user_id` | User Identifier | String | Yes | 521 |
522 | 523 | 524 | 525 | ### Mute User: 526 | 527 | ```python3 528 | boolean_response = api.mute(user_id) 529 | ``` 530 |
531 | 532 |

Parameters

533 |
534 | 535 | | Parameters | Description | Type | Required | 536 | |:----------:|:---------------:|:-------:|:--------:| 537 | | `user_id` | User Identifier | String | Yes | 538 |
539 | 540 | 541 | 542 | ### Unmute User: 543 | 544 | ```python3 545 | boolean_response = api.unmute(user_id) 546 | ``` 547 |
548 | 549 |

Parameters

550 |
551 | 552 | | Parameters | Description | Type | Required | 553 | |:----------:|:---------------:|:-------:|:--------:| 554 | | `user_id` | User Identifier | String | Yes | 555 |
556 | 557 | 558 | 559 | ### Restrict User: 560 | 561 | ```python3 562 | boolean_response = api.restrict(user_id) 563 | ``` 564 |
565 | 566 |

Parameters

567 |
568 | 569 | | Parameters | Description | Type | Required | 570 | |:----------:|:---------------:|:-------:|:--------:| 571 | | `user_id` | User Identifier | String | Yes | 572 |
573 | 574 | 575 | 576 | ### Unrestrict User: 577 | 578 | ```python3 579 | boolean_response = api.unrestrict(user_id) 580 | ``` 581 |
582 | 583 |

Parameters

584 |
585 | 586 | | Parameters | Description | Type | Required | 587 | |:----------:|:---------------:|:-------:|:--------:| 588 | | `user_id` | User Identifier | String | Yes | 589 |
590 | 591 | 592 | 593 | ### Check Friendship Status With Another Users: 594 | 595 | ```python3 596 | friendship_status = api.friendship_status(user_id) 597 | ``` 598 |
599 | 600 |

Parameters

601 |
602 | 603 | | Parameters | Description | Type | Required | 604 | |:----------:|:---------------:|:-------:|:--------:| 605 | | `user_id` | User Identifier | String | Yes | 606 |
607 | 608 | 609 | 610 | 611 | --- 612 | ## Interactions (Authentication Required) 613 | 614 | 615 | 616 | ### Like Thread: 617 | 618 | ```python3 619 | boolean_response = api.like(post_id) 620 | ``` 621 |
622 | 623 |

Parameters

624 |
625 | 626 | | Parameters | Description | Type | Required | 627 | |:----------:|:---------------:|:-------:|:--------:| 628 | | `post_id` | Post Identifier | String | Yes | 629 |
630 | 631 | 632 | 633 | ### Remove Like From Thread: 634 | 635 | ```python3 636 | boolean_response = api.unlike(post_id) 637 | ``` 638 |
639 | 640 |

Parameters

641 |
642 | 643 | | Parameters | Description | Type | Required | 644 | |:----------:|:---------------:|:-------:|:--------:| 645 | | `post_id` | Post Identifier | String | Yes | 646 |
647 | 648 | 649 | 650 | ### Repost Thread: 651 | 652 | ```python3 653 | boolean_response = api.repost_thread(post_id) 654 | ``` 655 |
656 | 657 |

Parameters

658 |
659 | 660 | | Parameters | Description | Type | Required | 661 | |:----------:|:---------------:|:-------:|:--------:| 662 | | `post_id` | Post Identifier | String | Yes | 663 |
664 | 665 | 666 | ### Delete Reposted Thread: 667 | 668 | ```python3 669 | boolean_response = api.unrepost_thread(post_id) 670 | ``` 671 |
672 | 673 |

Parameters

674 |
675 | 676 | | Parameters | Description | Type | Required | 677 | |:----------:|:---------------:|:-------:|:--------:| 678 | | `post_id` | Post Identifier | String | Yes | 679 |
680 | 681 | 682 | 683 | 684 | --- 685 | ## Contributors 686 | 687 | 688 | 689 | 690 | 699 | 708 | 717 | 726 | 735 | 736 | 737 |
691 | 692 | Junho Yeo 693 |
694 | Junho Yeo 695 |
696 |
697 | 💻 698 |
700 | 701 | iamiks 702 |
703 | iamiks 704 |
705 |
706 | 💻 707 |
709 | 710 | DrunkLeen 711 |
712 | DrunkLeen 713 |
714 |
715 | 💻 716 |
718 | 719 | Asharaf Ali 720 |
721 | Asharaf Ali 722 |
723 |
724 | 💻 725 |
727 | 728 | mirageoasis 729 |
730 | mirageoasis 731 |
732 |
733 | 💻 734 |
738 | 739 | 740 | ## License 741 | 742 |

743 | 744 | 745 | 746 |

747 | 748 |

749 | MIT © Junho Yeo 750 |

751 | 752 | If you find this project intriguing, **please consider starring it(⭐)** or following me on [GitHub](https://github.com/junhoyeo) (I wouldn't say [Threads](https://www.threads.net/@_junhoyeo)). 753 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [](https://github.com/junhoyeo) threads-py 2 | 3 | [![pypi](https://img.shields.io/pypi/v/threads-py.svg?style=flat-square&labelColor=black)](https://pypi.org/project/threads-py) [![MIT License](https://img.shields.io/badge/license-MIT-blue?style=flat-square&labelColor=black)](https://github.com/junhoyeo/threads-py/blob/main/LICENSE) 4 | 5 | Unofficial, Reverse-Engineered Python API for [Meta's Threads](https://www.threads.net/). 6 | 7 | > **Warning**
8 | > **As of September 8, 2023, the development of the ["threads-api"](https://github.com/junhoyeo/threads-api) project have been halted and discontinued due to communication received from Meta Platforms, Inc. (“Meta,” previously known as Facebook, Inc.). This repository, which is related to the "threads-api" project, has also been archived and will no longer receive updates or maintenance.** The previous documentation related to this project has been moved to [PRESERVED.md](https://github.com/junhoyeo/threads-py/blob/main/PRESERVED.md) as requested. 9 | > 10 | > The "threads-api" was developed for educational and research purposes only. Based on the notification from Meta, it's clear that using or distributing the code might violate the terms of service of Meta Platforms, Inc. and its associated services, including but not limited to Instagram and Threads. Any actions or activities related to the material contained within this repository are solely the user's responsibility. The author and contributors of this repository do not support or condone any unethical or illegal activities. 11 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | [installer] 5 | modern-installation = false 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | packages = [{ include = "threadspy", from = "." }] 3 | name = "threads-py" 4 | version = "0.0.9" 5 | description = "Unofficial, Reverse-Engineered Python client for Meta's Threads." 6 | license = "MIT" 7 | authors = [ 8 | "Junho Yeo ", 9 | "iamiks ", 10 | "DrunkLeen ", 11 | "Ashrf ", 12 | "mirageoasis ", 13 | ] 14 | repository = "https://github.com/junhoyeo/threads-py" 15 | readme = ["README.md", "LICENSE"] 16 | keywords = ["threads", "instagram", "facebook", "meta", "api"] 17 | 18 | [tool.poetry.dependencies] 19 | python = ">=3.8.1,<4.0" 20 | requests = "^2.31.0" 21 | dataclasses = "^0.6" 22 | dataclasses-json = "^0.5.9" 23 | pycryptodome = "^3.18.0" 24 | 25 | [tool.poetry.group.ai] 26 | optional = true 27 | [tool.poetry.group.ai.dependencies] 28 | langchain = "^0.0.227" 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junhoyeo/threads-py/bceeb70906ed021565e0f625fdf1134c4772eadd/tests/__init__.py -------------------------------------------------------------------------------- /tests/analysis_profile_replies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAgent 3 | 4 | 5 | class TestAnalysisProfileReplies(unittest.TestCase): 6 | def setUp(self): 7 | self.agent = ThreadsAgent(mode="openai", OPENAI_API_KEY="OPENAI_API_KEY") 8 | 9 | def test_analysis_profile_replies(self): 10 | replies = self.agent.analysis_profile( 11 | username="zuck", boundary="replies", onlyText=True, sort="DESC" 12 | ) 13 | replies = list(map(lambda x: x["text"], replies)) 14 | print(replies) 15 | self.assertIsInstance(replies, list) 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /tests/analysis_profile_threads.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAgent 3 | 4 | 5 | class TestAnalysisProfileThreads(unittest.TestCase): 6 | def setUp(self): 7 | self.agent = ThreadsAgent(mode="openai", OPENAI_API_KEY="OPENAI_API_KEY") 8 | 9 | def test_analysis_profile_threads(self): 10 | threads = self.agent.analysis_profile( 11 | username="zuck", boundary="threads", onlyText=True, sort="DESC" 12 | ) 13 | threads = list(map(lambda x: x["text"], threads)) 14 | print(threads) 15 | self.assertIsInstance(threads, list) 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /tests/delete_thread.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestDeleteThread(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, 9 | username="username", 10 | password="password", 11 | # token="token" 12 | ) 13 | 14 | def test_delete_thread(self): 15 | post_id = self.threads_api.delete_thread( 16 | "CugF-EjhQ3r" 17 | ) # or use `get_post_id_from_url` 18 | check_sum = self.threads_api.repost_thread(post_id=post_id) 19 | self.assertEqual(check_sum, True) 20 | 21 | 22 | if __name__ == "__main__": 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /tests/following_unfollowing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestFollowingAndUnFollowing(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, 9 | username="username", 10 | password="password", 11 | ) 12 | self.username = "zuck" 13 | 14 | def test_follow(self): 15 | target_user_id = self.threads_api.get_user_id_from_username(self.username) 16 | check_sum = self.threads_api.follow(user_id=target_user_id) 17 | self.assertEqual(check_sum, True) 18 | 19 | def test_unfollow(self): 20 | target_user_id = self.threads_api.get_user_id_from_username(self.username) 21 | check_sum = self.threads_api.unfollow(user_id=target_user_id) 22 | self.assertEqual(check_sum, True) 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /tests/get_notifications.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | from time import sleep 4 | 5 | 6 | class TestGetNotifications(unittest.TestCase): 7 | def setUp(self): 8 | self.threads_api = ThreadsAPI( 9 | verbose=True, 10 | username="username", 11 | password="password", 12 | # token="token" 13 | ) 14 | 15 | def test_get_notifications_replies(self): 16 | notifications = self.threads_api.get_notifications() 17 | self.assertIsInstance(notifications, dict) 18 | 19 | def test_get_notifications_mentions(self): 20 | notifications = self.threads_api.get_notifications(notification_filter='mentions') 21 | self.assertIsInstance(notifications, dict) 22 | 23 | def test_get_notifications_verified(self): 24 | notifications = self.threads_api.get_notifications(notification_filter='verified') 25 | self.assertIsInstance(notifications, dict) 26 | 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/get_post_id_from_thread_id.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetPostIDfromThreadID(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.thread_id = "CuX_UYABrr7" 9 | self.post_id = None 10 | 11 | def test_fetching_post_id_with_thread_id(self): 12 | self.post_id = self.threads_api.get_post_id_from_thread_id(self.thread_id) 13 | self.assertEqual(self.post_id, "3141257742204189435") 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/get_post_id_from_url.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetPostIDfromURL(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.post_url = "https://www.threads.net/t/CuX_UYABrr7/?igshid=MzRlODBiNWFlZA==" 9 | self.post_id = None 10 | 11 | def test_fetching_post_id_with_post_url(self): 12 | self.post_id = self.threads_api.get_post_id_from_url(self.post_url) 13 | self.assertEqual(self.post_id, "3141257742204189435") 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/get_suggested_users.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetSuggestedUsers(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True, username="username", password="password") 8 | 9 | def test_get_suggested_users(self): 10 | token = self.threads_api.get_token() 11 | self.assertIsInstance(token, str) 12 | self.assertEqual(token, self.threads_api.token) 13 | suggest = self.threads_api.get_suggested_users() 14 | self.assertIsInstance(suggest, list) 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/get_thread_likers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetThreadLikers(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.post_id = "3141675920411513399" 9 | 10 | def test_get_thread_likers(self): 11 | likers = self.threads_api.get_thread_likers(self.post_id) 12 | 13 | self.assertIsInstance(likers.users, list) 14 | self.assertIn("pk", likers.users[0].to_dict().keys()) 15 | self.assertIn("full_name", likers.users[0].to_dict().keys()) 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /tests/get_threads.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetThreads(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.post_id = "3140957200974444958" # https://www.threads.net/t/CuW6-7KyXme 9 | 10 | def test_get_threads(self): 11 | thread = self.threads_api.get_threads(self.post_id) 12 | 13 | self.assertIn("reply_threads", thread.to_dict().keys()) 14 | self.assertIn("containing_thread", thread.to_dict().keys()) 15 | self.assertEqual(thread.containing_thread.id, self.post_id) 16 | self.assertIsInstance(thread.reply_threads, list) 17 | 18 | containing_thread_captions = [ 19 | item.post.caption.text for item in thread.containing_thread.thread_items 20 | ] 21 | self.assertCountEqual( 22 | containing_thread_captions, 23 | [ 24 | "This is fast. Could be made into a Mastodon API bridge like Skybridge (for Bluesky)", 25 | "For context, this is Skybridge https://skybridge.fly.dev/", 26 | ], 27 | ) 28 | 29 | reply_thread_captions = [ 30 | item.post.caption.text 31 | for reply_thread in thread.reply_threads 32 | for item in reply_thread.thread_items 33 | ] 34 | self.assertEqual(reply_thread_captions[0], "🤍💙🤍💙💙") 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /tests/get_timeline.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | from time import sleep 4 | 5 | 6 | class TestGetTimeline(unittest.TestCase): 7 | def setUp(self): 8 | self.threads_api = ThreadsAPI( 9 | verbose=True, 10 | username="username", 11 | password="password", 12 | # token="token" 13 | ) 14 | 15 | def test_get_timeline_wo_max_id(self): 16 | timeline = self.threads_api.get_timeline() 17 | self.assertIsInstance(timeline, dict) 18 | 19 | def test_get_timeline_w_max_id(self): 20 | timeline = self.threads_api.get_timeline(max_id=2) 21 | self.assertIsInstance(timeline, dict) 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /tests/get_token.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetToken(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, username="username", password="password" 9 | ) 10 | 11 | def test_get_token(self): 12 | token = self.threads_api.get_token() 13 | self.assertIsInstance(token, str) 14 | self.assertEqual(token, self.threads_api.token) 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/get_user_id_from_username.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetUserIdFromUsername(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.username = "_junhoyeo" 9 | 10 | def test_get_user_id_from_username(self): 11 | user_id = self.threads_api.get_user_id_from_username(self.username) 12 | self.assertIsInstance(user_id, str) 13 | self.assertEqual(user_id, self.threads_api.user_id) 14 | self.assertEqual(user_id, "5438123050") 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/get_user_profile.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetUserProfile(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.username = "_junhoyeo" 9 | self.user_id = "5438123050" 10 | 11 | def test_get_user_profile(self): 12 | user = self.threads_api.get_user_profile( 13 | username=self.username, user_id=self.user_id 14 | ) 15 | self.assertEqual(user.username, self.username) 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /tests/get_user_profile_replies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetUserProfileReplies(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.username = "_junhoyeo" 9 | self.user_id = "5438123050" 10 | 11 | def test_get_user_profile_replies(self): 12 | posts = self.threads_api.get_user_profile_replies( 13 | username=self.username, user_id=self.user_id 14 | ) 15 | 16 | self.assertIsInstance(posts, list) 17 | self.assertIn("thread_items", posts[0].to_dict()) 18 | self.assertGreaterEqual(len(posts[0].thread_items), 2) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /tests/get_user_profile_threads.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestGetUserProfileThreads(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI(verbose=True) 8 | self.username = "_junhoyeo" 9 | 10 | def test_get_user_profile_threads(self): 11 | user_id = self.threads_api.get_user_id_from_username(username=self.username) 12 | posts = self.threads_api.get_user_profile_threads( 13 | username=self.username, user_id=user_id 14 | ) 15 | 16 | self.assertIsInstance(posts, list) 17 | if len(posts) > 0: 18 | self.assertIn("thread_items", posts[0].to_dict().keys()) 19 | self.assertIsInstance(posts[0].thread_items, list) 20 | self.assertIn("post", posts[0].thread_items[0].to_dict().keys()) 21 | 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /tests/like_unlike.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestLikeAndUnLike(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, 9 | username="username", 10 | password="password", 11 | ) 12 | 13 | def test_like(self): 14 | post_url = "https://www.threads.net/t/CugF-EjhQ3r" 15 | post_id = self.threads_api.get_post_id_from_url( 16 | post_url 17 | ) # or use `get_post_id_from_thread_id` 18 | check_sum = self.threads_api.like(post_id=post_id) 19 | self.assertEqual(check_sum, True) 20 | 21 | def test_unlike(self): 22 | post_url = "https://www.threads.net/t/CugF-EjhQ3r" 23 | post_id = self.threads_api.get_post_id_from_url( 24 | post_url 25 | ) # or use `get_post_id_from_thread_id` 26 | check_sum = self.threads_api.unlike(post_id=post_id) 27 | self.assertEqual(check_sum, True) 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /tests/publish.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestPublish(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, 9 | username="username", 10 | password="password", 11 | ) 12 | 13 | def test_publish(self): 14 | check_sum = self.threads_api.publish("🤖 Hello World!") 15 | self.assertEqual(check_sum, True) 16 | 17 | def test_publish_with_attach_link(self): 18 | check_sum = self.threads_api.publish( 19 | "🤖 Hello World!", url="https://github.com/junhoyeo/threads-py)" 20 | ) 21 | self.assertEqual(check_sum, True) 22 | 23 | def test_publish_with_reply(self): 24 | parent_post_url = "https://www.threads.net/t/CugF-EjhQ3r" 25 | parent_post_id = self.threads_api.get_post_id_from_url(parent_post_url) 26 | check_sum = self.threads_api.publish( 27 | "🤖 Hello World!", parent_post_id=parent_post_id 28 | ) 29 | self.assertEqual(check_sum, True) 30 | 31 | def test_publish_with_image_url(self): 32 | image_path = "https://github.com/junhoyeo/threads-py/blob/main/.github/logo.jpg" 33 | check_sum = self.threads_api.publish("🤖 Hello World!", image_path=image_path) 34 | self.assertEqual(check_sum, True) 35 | 36 | def test_publish_with_local_image(self): 37 | check_sum = self.threads_api.publish( 38 | "🤖 Hello World!", image_path=".github/logo.jpg" 39 | ) 40 | self.assertEqual(check_sum, True) 41 | 42 | 43 | if __name__ == "__main__": 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /tests/publish_with_image.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | 4 | 5 | class TestPublishWithImage(unittest.TestCase): 6 | def setUp(self): 7 | self.threads_api = ThreadsAPI( 8 | verbose=True, username="username", password="password" 9 | ) 10 | 11 | def test_publish_with_local_image(self): 12 | check_sum = self.threads_api.publish_with_image( 13 | "🤖 Hello World!", image_path=".github/logo.jpg" 14 | ) 15 | self.assertEqual(check_sum, True) 16 | 17 | def test_publish_with_image_url(self): 18 | image_path = "https://github.com/junhoyeo/threads-py/raw/main/.github/logo.jpg" 19 | check_sum = self.threads_api.publish_with_image( 20 | "🤖 Hello World!", 21 | image_path=image_path, 22 | ) 23 | self.assertEqual(check_sum, True) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /tests/repost_unrepost.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from threadspy import ThreadsAPI 3 | from time import sleep 4 | 5 | 6 | class TestRepostAndUnrepost(unittest.TestCase): 7 | def setUp(self): 8 | self.threads_api = ThreadsAPI( 9 | verbose=True, 10 | username="username", 11 | password="password", 12 | # token="token" 13 | ) 14 | 15 | def test_repost(self): 16 | post_id = self.threads_api.get_post_id_from_thread_id("CugF-EjhQ3r") # or use `get_post_id_from_url` 17 | check_sum = self.threads_api.repost_thread(post_id=post_id) 18 | self.assertEqual(check_sum, True) 19 | 20 | post_id = self.threads_api.get_post_id_from_thread_id("Cuo1nCjNis1") # or use `get_post_id_from_url` 21 | check_sum = self.threads_api.repost_thread(post_id=post_id) 22 | self.assertEqual(check_sum, True) 23 | 24 | post_id = self.threads_api.get_post_id_from_thread_id("CunBJvlBv2l") # or use `get_post_id_from_url` 25 | check_sum = self.threads_api.repost_thread(post_id=post_id) 26 | self.assertEqual(check_sum, True) 27 | 28 | def test_unrepost(self): 29 | post_id = self.threads_api.get_post_id_from_thread_id("CugF-EjhQ3r") # or use `get_post_id_from_url` 30 | check_sum = self.threads_api.unrepost_thread(post_id=post_id) 31 | self.assertEqual(check_sum, True) 32 | 33 | post_id = self.threads_api.get_post_id_from_thread_id("Cuo1nCjNis1") # or use `get_post_id_from_url` 34 | check_sum = self.threads_api.unrepost_thread(post_id=post_id) 35 | self.assertEqual(check_sum, True) 36 | 37 | post_id = self.threads_api.get_post_id_from_thread_id("CunBJvlBv2l") # or use `get_post_id_from_url` 38 | check_sum = self.threads_api.unrepost_thread(post_id=post_id) 39 | self.assertEqual(check_sum, True) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /threadspy/__init__.py: -------------------------------------------------------------------------------- 1 | from threadspy.threads_api import ThreadsAPI 2 | 3 | __all__ = ["ThreadsAPI"] 4 | __version__ = "0.0.9" 5 | __author__ = "junhoyeo" 6 | -------------------------------------------------------------------------------- /threadspy/ai/__init__.py: -------------------------------------------------------------------------------- 1 | from .agent import ThreadsAgent 2 | 3 | __all__ = [ThreadsAgent] 4 | -------------------------------------------------------------------------------- /threadspy/ai/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import itertools 5 | from datetime import datetime 6 | from langchain import PromptTemplate, LLMChain 7 | from langchain.llms import LlamaCpp, OpenAIChat 8 | from langchain.callbacks.manager import CallbackManager 9 | from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler 10 | 11 | from ..threads_api import ThreadsAPI 12 | from .templates import QA_template 13 | 14 | # Set up logging 15 | logging.basicConfig(filename='threads_agent.log', level=logging.INFO) 16 | 17 | class ThreadsAgent: 18 | mode = None 19 | model = None 20 | mode_cpu = False 21 | mode_ppu = False 22 | mode_mps = False 23 | OPENAI_API_KEY = None 24 | threads_api = None 25 | 26 | def __init__( 27 | self, 28 | mode="openai", 29 | model=None, 30 | mode_cpu=True, 31 | mode_gpu=False, 32 | mode_mps=False, 33 | OPENAI_API_KEY=None, 34 | username=None, 35 | verbose=False, 36 | ): 37 | self.threads_api = ThreadsAPI(verbose=verbose, username=username) 38 | if mode in ["llama", "openai"]: 39 | if mode == "openai": 40 | try: 41 | if OPENAI_API_KEY is not None: 42 | os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY 43 | self.OPENAI_API_KEY = OPENAI_API_KEY 44 | except: 45 | logging.error("Please, export your OpenAI API KEY over 'OPENAI_API_KEY' environment variable") 46 | logging.error("You may create the key here: https://platform.openai.com/account/api-keys") 47 | sys.exit(1) 48 | if model is not None: 49 | self.model = model 50 | if mode is not None and mode in ["llama"]: 51 | if mode_mps: 52 | self.mode_cpu = False 53 | self.mode_gpu = False 54 | self.mode_mps = True 55 | elif mode_gpu: 56 | self.mode_cpu = False 57 | self.mode_gpu = True 58 | self.mode_mps = False 59 | elif mode_cpu: 60 | self.mode_cpu = True 61 | self.mode_gpu = False 62 | self.mode_mps = False 63 | else: 64 | self.mode_cpu = False 65 | self.mode_gpu = False 66 | self.mode_mps = False 67 | else: 68 | raise "Choose a mode between 'llama' and 'openai'" 69 | 70 | def analysis_profile( 71 | self, username: str, boundary: str = "all", onlyText=False, sort="DESC" 72 | ): 73 | self.threads_api.username = username 74 | user_id = self.threads_api.get_user_id_from_username(username=username) 75 | if user_id is None: 76 | raise "[Auth] Private profiles cannot be analyzed." 77 | if boundary in ["all", "replies", "threads"]: 78 | threads = [] 79 | if boundary in ["all", "replies"]: 80 | replies_tab = self.threads_api.get_user_profile_replies( 81 | username=username, user_id=self.threads_api.user_id 82 | ) 83 | threads.extend(replies_tab) 84 | if boundary in ["all", "threads"]: 85 | threads_tab = self.threads_api.get_user_profile_threads( 86 | username=username, user_id=self.threads_api.user_id 87 | ) 88 | threads.extend(threads_tab) 89 | threads = [ 90 | [item["post"] for item in x.to_dict()["thread_items"]] for x in threads 91 | ] 92 | threads = list(itertools.chain(*threads)) 93 | if sort == "DESC": 94 | threads.sort(key=lambda x: (x["taken_at"],)) 95 | else: 96 | threads.sort(key=lambda x: (x["taken_at"] * -1,)) 97 | threads = [ 98 | { 99 | "text": item["caption"]["text"], 100 | "taken_at": str(datetime.fromtimestamp(item["taken_at"])), 101 | "media": item["image_versions2"]["candidates"][0] 102 | if len(item["image_versions2"]["candidates"]) > 0 103 | else None, 104 | } 105 | if not onlyText 106 | else { 107 | "text": item["caption"]["text"], 108 | "taken_at": str(datetime.fromtimestamp(item["taken_at"])), 109 | } 110 | for item in threads 111 | ] 112 | # FIXME: connect llm 113 | return threads 114 | else: 115 | raise "[Error] Choose a boundary between 'all', 'replies' and 'threads'" 116 | 117 | def ask(self, question: str) -> str: 118 | """ 119 | Returns answer 120 | 121 | Args: 122 | question (str): "What NFL team won the Super Bowl in the year Justin Bieber was born?" 123 | 124 | Returns: 125 | str: answer 126 | """ 127 | template = QA_template 128 | prompt = PromptTemplate(template=template, input_variables=["question"]) 129 | callback_manager = CallbackManager([StreamingStdOutCallbackHandler()]) 130 | 131 | if self.mode == "llama": 132 | n_gpu_layers = 40 133 | if self.mode_mps: 134 | n_gpu_layers = 1 135 | n_batch = 128 136 | llm = LlamaCpp( 137 | model_path="./models/ggml-model-f16.bin", 138 | n_gpu_layers=n_gpu_layers, 139 | n_batch=n_batch, 140 | f16_kv=self.mode_mps, 141 | callback_manager=callback_manager, 142 | verbose=True, 143 | ) 144 | elif self.mode == "openai": 145 | if self.model == None: 146 | model = "gpt3.5-turbo" 147 | llm = OpenAIChat( 148 | model=model, 149 | callback_manager=callback_manager, 150 | ) 151 | llm_chain = LLMChain(prompt=prompt, llm=llm) 152 | 153 | answer = llm_chain.run(question) 154 | 155 | # Log the question and answer 156 | logging.info(f"Question: {question}") 157 | logging.info(f"Answer: {answer}") 158 | 159 | return answer 160 | -------------------------------------------------------------------------------- /threadspy/ai/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from .qna import QA_template 2 | -------------------------------------------------------------------------------- /threadspy/ai/templates/qna.py: -------------------------------------------------------------------------------- 1 | QA_template = """Question: {question} 2 | Answer: Let's do things step by step so we make sure we have the right answer before moving on to the next one.""" 3 | -------------------------------------------------------------------------------- /threadspy/constants.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | 4 | DEFAULT_LSD_TOKEN = "NjppQDEgONsU_1LCzrmp6q" 5 | DEFAULT_DEVICE_ID = ( 6 | f"android-${hashlib.sha256(str(time.time()).encode()).hexdigest()[:16]}" 7 | ) 8 | LATEST_ANDROID_APP_VERSION = "289.0.0.77.109" 9 | BASE_API_URL = "https://i.instagram.com" 10 | -------------------------------------------------------------------------------- /threadspy/threads_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | import json 5 | import uuid 6 | import base64 7 | import urllib 8 | from urllib.parse import quote 9 | import random 10 | import requests 11 | import mimetypes 12 | from typing import List 13 | from datetime import datetime 14 | from Crypto.Random import get_random_bytes 15 | from Crypto.PublicKey import RSA 16 | from Crypto.Cipher import ( 17 | AES, 18 | PKCS1_v1_5, 19 | ) 20 | 21 | from threadspy.types import ( 22 | Thread, 23 | UsersData, 24 | ThreadsUser, 25 | ThreadData, 26 | SuggestedUser, 27 | GetUserProfileResponse, 28 | GetThreadLikersResponse, 29 | GetUserProfileThreadResponse, 30 | GetUserProfileRepliesResponse, 31 | GetUserProfileThreadsResponse, 32 | GetSuggestedUsersResponse, 33 | ) 34 | 35 | from threadspy.constants import ( 36 | LATEST_ANDROID_APP_VERSION, 37 | DEFAULT_LSD_TOKEN, 38 | DEFAULT_DEVICE_ID, 39 | BASE_API_URL, 40 | ) 41 | 42 | 43 | class ThreadsAPI: 44 | fbLSDToken = DEFAULT_LSD_TOKEN 45 | verbose = False 46 | noUpdateLSD = False 47 | username = None 48 | password = None 49 | user_id = None 50 | token = None 51 | device_id = DEFAULT_DEVICE_ID 52 | http_client = requests.Session() 53 | timestamp_string = None 54 | encrypted_password = None 55 | 56 | def __init__( 57 | self, 58 | verbose: True | False = None, 59 | noUpdateLSD: str | None = None, 60 | fbLSDToken: str | None = None, 61 | username: str | None = None, 62 | password: str | None = None, 63 | device_id: str | None = None, 64 | token: str | None = None, 65 | ) -> None: 66 | if fbLSDToken is not None and isinstance(fbLSDToken, str): 67 | self.fbLSDToken = fbLSDToken 68 | if device_id is not None and isinstance(device_id, str): 69 | self.device_id = device_id 70 | if noUpdateLSD is not None and isinstance(noUpdateLSD, str): 71 | self.noUpdateLSD = noUpdateLSD 72 | if verbose is not None and isinstance(verbose, bool): 73 | self.verbose = verbose 74 | 75 | if ( 76 | username is not None 77 | and isinstance(username, str) 78 | and password is not None 79 | and isinstance(password, str) 80 | ): 81 | self.username = username 82 | self.password = password 83 | self.public_key, self.public_key_id = self._get_ig_public_key() 84 | self.encrypted_password, self.timestamp_string = self._password_encryption( 85 | password 86 | ) 87 | self.user_id = self.get_user_id_from_username(username) 88 | 89 | if token is not None and isinstance(token, str): 90 | self.token = token 91 | else: 92 | self.token = self.get_token() 93 | 94 | def __is_valid_url(self, url: str) -> bool: 95 | url_pattern = re.compile(r"https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+") 96 | if re.match(url_pattern, url) is not None: 97 | try: 98 | response = requests.head(url) 99 | return response.status_code == 200 or response.status_code == 302 100 | except requests.exceptions.RequestException as e: 101 | return False 102 | return False 103 | 104 | def __download(self, url: str) -> bytes: 105 | try: 106 | response = requests.get(url, stream=True) 107 | response.raise_for_status() 108 | return response.content 109 | except requests.exceptions.RequestException as e: 110 | print("[ERROR] fail to file load: ", e) 111 | return None 112 | 113 | def __get_app_headers(self) -> dict: 114 | """ 115 | Generates App Headers. 116 | Returns: 117 | Random generated header (dict) 118 | """ 119 | headers = { 120 | "User-Agent": f"Barcelona {LATEST_ANDROID_APP_VERSION} Android", 121 | "Sec-Fetch-Site": "same-origin", 122 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 123 | } 124 | if self.token is not None: 125 | headers["Authorization"] = f"Bearer IGT:2:{self.token}" 126 | return headers 127 | 128 | def __get_default_headers(self, username: str = None) -> dict: 129 | headers = { 130 | "authority": "www.threads.net", 131 | "accept": "*/*", 132 | "accept-language": "ko,en;q=0.9,ko-KR;q=0.8,ja;q=0.7", 133 | "cache-control": "no-cache", 134 | "origin": "https://www.threads.net", 135 | "pragma": "no-cache", 136 | "x-asbd-id": "129477", 137 | "x-fb-lsd": self.fbLSDToken, 138 | "x-ig-app-id": "238260118697367", 139 | } 140 | 141 | if username is not None: 142 | self.username = username 143 | headers["referer"] = f"https://www.threads.net/@{username}" 144 | 145 | return headers 146 | 147 | def _get_ig_public_key(self) -> tuple[str, int]: 148 | """ 149 | Get Instagram public key to encrypt the password. 150 | 151 | Returns: 152 | The public key and the key identifier as tuple(str, int). 153 | """ 154 | str_parameters = json.dumps( 155 | { 156 | "id": str(uuid.uuid4()), 157 | }, 158 | ) 159 | encoded_parameters = quote(string=str_parameters, safe="!~*'()") 160 | 161 | response = requests.post( 162 | url=f"{BASE_API_URL}/api/v1/qe/sync/", 163 | headers={ 164 | "User-Agent": f"Barcelona {LATEST_ANDROID_APP_VERSION} Android", 165 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 166 | }, 167 | data=f"params={encoded_parameters}", 168 | ) 169 | public_key = response.headers.get("ig-set-password-encryption-pub-key") 170 | public_key_id = response.headers.get("ig-set-password-encryption-key-id") 171 | 172 | return public_key, int(public_key_id) 173 | 174 | def _password_encryption(self, password: str) -> tuple[str, str]: 175 | password_bytes = password.encode("utf-8") 176 | 177 | timestamp = int(time.time()) 178 | timestamp_string = str(timestamp).encode("utf-8") 179 | 180 | secret_key = get_random_bytes(32) 181 | key_id_mixed_bytes = int(1).to_bytes(1, "big") + self.public_key_id.to_bytes( 182 | 1, "big" 183 | ) 184 | initialization_vector = get_random_bytes(12) 185 | encrypted_rsa_key_mixed_bytes = int(0).to_bytes(1, "big") + int(1).to_bytes( 186 | 1, "big" 187 | ) 188 | public_key_bytes = base64.b64decode(self.public_key) 189 | public_key = RSA.import_key(extern_key=public_key_bytes) 190 | cipher = PKCS1_v1_5.new(public_key) 191 | encrypted_secret_key = cipher.encrypt(secret_key) 192 | cipher = AES.new(secret_key, AES.MODE_GCM, nonce=initialization_vector) 193 | cipher.update(timestamp_string) 194 | encrypted_password, auth_tag = cipher.encrypt_and_digest(password_bytes) 195 | 196 | password_as_encryption_sequence = ( 197 | key_id_mixed_bytes 198 | + initialization_vector 199 | + encrypted_rsa_key_mixed_bytes 200 | + encrypted_secret_key 201 | + auth_tag 202 | + encrypted_password 203 | ) 204 | password_encryption_base64 = base64.b64encode( 205 | s=password_as_encryption_sequence, 206 | ).decode("ascii") 207 | 208 | return password_encryption_base64, str(timestamp) 209 | 210 | def get_user_id_from_username(self, username) -> str: 211 | """ 212 | set user id by username. 213 | 214 | Args: 215 | username (str): username on threads.net 216 | 217 | Returns: 218 | string: user_id if not valid return None 219 | """ 220 | headers = self.__get_default_headers(username) 221 | headers.update( 222 | { 223 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 224 | "accept-language": "ko,en;q=0.9,ko-KR;q=0.8,ja;q=0.7", 225 | "pragma": "no-cache", 226 | "referer": "https://www.instagram.com/", 227 | "sec-fetch-dest": "document", 228 | "sec-fetch-mode": "navigate", 229 | "sec-fetch-site": "cross-site", 230 | "sec-fetch-user": "?1", 231 | "upgrade-insecure-requests": "1", 232 | "x-asbd-id": None, 233 | "x-fb-lsd": None, 234 | "x-ig-app-id": None, 235 | } 236 | ) 237 | response = self.http_client.get( 238 | f"https://www.instagram.com/{username}", headers=headers 239 | ) 240 | 241 | text = response.text.replace("\n", "") 242 | 243 | user_id_match = re.search('"user_id":"(\d+)",', text) 244 | user_id = user_id_match.group(1) if user_id_match else None 245 | 246 | lsd_token_match = re.search('"LSD",\[\],{"token":"(\w+)"},\d+\]', text) 247 | lsd_token = lsd_token_match.group(1) if lsd_token_match else None 248 | 249 | if not self.noUpdateLSD and self.fbLSDToken is not None: 250 | self.fbLSDToken = lsd_token 251 | if self.verbose: 252 | print("[fbLSDToken] UPDATED", self.fbLSDToken) 253 | if user_id is not None: 254 | self.user_id = user_id 255 | return self.user_id 256 | else: 257 | return None 258 | 259 | def get_current_user_id(self) -> str: 260 | if self.user_id: 261 | if self.verbose: 262 | print("[userID] USING", self.user_id) 263 | return self.user_id 264 | if self.username is None: 265 | raise "username is not defined" 266 | self.user_id = self.get_user_id_from_username(self.username) 267 | if self.verbose: 268 | print("[userID] UPDATED", self.user_id) 269 | return self.user_id 270 | 271 | def get_user_profile(self, username, user_id=None) -> ThreadsUser: 272 | """ 273 | Returns profile info by username. 274 | 275 | Args: 276 | username (str): username on threads.net 277 | user_id (str, optional):: user_id which is unique to each user. 278 | 279 | Returns: 280 | ThreadsUser: a profile info. 281 | """ 282 | if self.verbose: 283 | print("[fbLSDToken] USING", self.fbLSDToken) 284 | 285 | if not user_id: 286 | user_id = self.get_user_id_from_username(username) 287 | headers = self.__get_default_headers(username) 288 | headers["x-fb-friendly-name"] = "BarcelonaProfileRootQuery" 289 | 290 | params = { 291 | "lsd": self.fbLSDToken, 292 | "variables": f'{{"userID":"{user_id}"}}', 293 | "doc_id": "23996318473300828", 294 | } 295 | 296 | response = self.http_client.post( 297 | "https://www.threads.net/api/graphql", params=params, headers=headers 298 | ) 299 | 300 | try: 301 | user = GetUserProfileResponse.from_dict(response.json()) 302 | return user.data.userData.user 303 | except Exception as e: 304 | if self.verbose: 305 | print("[ERROR] ", e) 306 | return ThreadsUser( 307 | pk="", 308 | full_name="", 309 | profile_pic_url="", 310 | follower_count=0, 311 | is_verified=False, 312 | username="", 313 | profile_context_facepile_users=None, 314 | id=None, 315 | ) 316 | 317 | def get_user_profile_threads( 318 | self, username: str, user_id: str | None = None 319 | ) -> List[Thread]: 320 | """ 321 | Returns a list of threads posted in the profile. 322 | 323 | Args: 324 | username (str): username on threads.net 325 | user_id (str): user_id which is unique to each user. 326 | 327 | Returns: 328 | List[Thread]: list of threads posted in the profile. 329 | """ 330 | if self.verbose: 331 | print("[fbLSDToken] USING", self.fbLSDToken) 332 | if not user_id: 333 | user_id = self.get_user_id_from_username(username) 334 | 335 | headers = self.__get_default_headers(username) 336 | headers["x-fb-friendly-name"] = "BarcelonaProfileThreadsTabQuery" 337 | 338 | params = { 339 | "lsd": f"{self.fbLSDToken}", 340 | "variables": f'{{"userID":"{user_id}"}}', 341 | "doc_id": "6232751443445612", 342 | } 343 | 344 | response = self.http_client.post( 345 | "https://www.threads.net/api/graphql", params=params, headers=headers 346 | ) 347 | 348 | try: 349 | threads = GetUserProfileThreadsResponse.from_dict(response.json()) 350 | return threads.data.mediaData.threads 351 | except Exception as e: 352 | if self.verbose: 353 | print("[ERROR] ", e) 354 | return [] 355 | 356 | def get_user_profile_replies( 357 | self, username: str, user_id: str | None = None 358 | ) -> List[Thread]: 359 | """ 360 | Returns a list of replies in the thread. 361 | 362 | Args: 363 | username (str): username on threads.net 364 | user_id (str): user_id which is unique to each user. 365 | 366 | Returns: 367 | List[Thread]: list of replies in the thread. 368 | """ 369 | if self.verbose: 370 | print("[fbLSDToken] USING", self.fbLSDToken) 371 | if not user_id: 372 | user_id = self.get_user_id_from_username(username) 373 | 374 | headers = self.__get_default_headers(username) 375 | headers["x-fb-friendly-name"] = "BarcelonaProfileRepliesTabQuery" 376 | 377 | params = { 378 | "lsd": f"{self.fbLSDToken}", 379 | "variables": f'{{"userID":"{user_id}"}}', 380 | "doc_id": "6684830921547925", 381 | } 382 | 383 | response = self.http_client.post( 384 | "https://www.threads.net/api/graphql", params=params, headers=headers 385 | ) 386 | 387 | try: 388 | replies = GetUserProfileRepliesResponse.from_dict(response.json()) 389 | return replies.data.mediaData.threads 390 | except Exception as e: 391 | if self.verbose: 392 | print("[ERROR] ", e) 393 | return [] 394 | 395 | def get_post_id_from_thread_id(self, thread_id: str) -> str: 396 | """ 397 | Returns a thread info from thread id. 398 | 399 | Args: 400 | thread_id (str): thread_id which is unique to each thread. 401 | 402 | Returns: 403 | str: a post id 404 | """ 405 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 406 | post_id = 0 407 | for letter in thread_id: 408 | post_id = (post_id * 64) + alphabet.index(letter) 409 | 410 | return str(post_id) 411 | 412 | def get_post_id_from_url(self, post_url) -> str: 413 | """ 414 | Returns the post_id of a specific one thread. 415 | 416 | Args: 417 | post_url (str): a threads app direct link 418 | 419 | Returns: 420 | str: a post id 421 | """ 422 | response = requests.get(post_url) 423 | text = response.text 424 | text = text.replace("\n", "") 425 | post_id_match = re.search(r'{"post_id":"(.*?)"', text) 426 | post_id = post_id_match.group(1) if post_id_match else None 427 | 428 | lsd_token_match = re.search(r'"LSD",\[\],{"token":"(\w+)"},\d+\]', text) 429 | lsd_token = lsd_token_match.group(1) if lsd_token_match else None 430 | 431 | if not self.noUpdateLSD and self.fbLSDToken is not None: 432 | self.fbLSDToken = lsd_token 433 | if self.verbose: 434 | print("[fbLSDToken] UPDATED", self.fbLSDToken) 435 | return post_id 436 | 437 | def get_threads(self, post_id: str) -> ThreadData: 438 | """ 439 | Returns a thread info from post id. 440 | 441 | Args: 442 | post_id (str): post_id which is unique to each post. 443 | 444 | Returns: 445 | ThreadData: a thread info 446 | """ 447 | if self.verbose: 448 | print("[fbLSDToken] USING", self.fbLSDToken) 449 | headers = self.__get_default_headers() 450 | headers["x-fb-friendly-name"] = "BarcelonaPostPageQuery" 451 | 452 | params = { 453 | "lsd": f"{self.fbLSDToken}", 454 | "variables": f'{{"postID":"{post_id}"}}', 455 | "doc_id": "5587632691339264", 456 | } 457 | 458 | response = self.http_client.post( 459 | "https://www.threads.net/api/graphql", params=params, headers=headers 460 | ) 461 | 462 | try: 463 | thread = GetUserProfileThreadResponse.from_dict(response.json()) 464 | return thread.data.data 465 | except Exception as e: 466 | if self.verbose: 467 | print("[ERROR] ", e) 468 | return ThreadData(containing_thread=None, reply_threads=[]) 469 | 470 | def get_thread_likers(self, post_id: str) -> UsersData: 471 | """ 472 | Returns a thread likers 473 | 474 | Args: 475 | post_id (str): post_id which is unique to each post. 476 | 477 | Returns: 478 | UsersData: a thread likers 479 | """ 480 | if self.verbose: 481 | print("[fbLSDToken] USING", self.fbLSDToken) 482 | headers = self.__get_default_headers() 483 | 484 | params = { 485 | "lsd": f"{self.fbLSDToken}", 486 | "variables": f'{{"mediaID":"{post_id}"}}', 487 | "doc_id": "9360915773983802", 488 | } 489 | 490 | response = self.http_client.post( 491 | "https://www.threads.net/api/graphql", params=params, headers=headers 492 | ) 493 | 494 | try: 495 | thread = GetThreadLikersResponse.from_dict(response.json()) 496 | return thread.data.likers 497 | except Exception as e: 498 | if self.verbose: 499 | print("[ERROR] ", e) 500 | return UsersData(users=[]) 501 | 502 | def __toggle_auth__post_request(self, url: str): 503 | if self.token is None: 504 | token = self.get_token() 505 | else: 506 | token = self.token 507 | if token is None: 508 | raise "Token not found" 509 | headers = self.__get_app_headers() 510 | response = self.http_client.post(url, headers=headers) 511 | return response 512 | 513 | def delete_thread(self, post_id: str) -> bool: 514 | """ 515 | Delete thread. 516 | 517 | Arguments: 518 | post_id (str): post identifier 519 | 520 | Returns: 521 | boolean and if verbose mode is enabled, prints response dict. 522 | """ 523 | token = self.get_token() if self.token is None else self.token 524 | if token is None: 525 | raise "Token not found" 526 | user_id = self.user_id or self.get_current_user_id() 527 | response = self.__toggle_auth__post_request( 528 | url=f"{BASE_API_URL}/api/v1/media/{post_id}_{user_id}/delete/?media_type=TEXT_POST", 529 | ) 530 | 531 | data = response.json() 532 | if self.verbose: 533 | print("[DELETE]", data) 534 | return data["status"] == "ok" 535 | 536 | def like(self, post_id: str) -> bool: 537 | """ 538 | like a post. 539 | 540 | Args: 541 | post_id (str): post identifier 542 | 543 | Returns: 544 | boolean and if verbose mode is enabled, prints response dict. 545 | """ 546 | user_id = self.user_id or self.get_current_user_id() 547 | response = self.__toggle_auth__post_request( 548 | url=f"{BASE_API_URL}/api/v1/media/{post_id}_{user_id}/like/", 549 | ) 550 | data = response.json() 551 | if self.verbose: 552 | print("[LIKE]", data) 553 | return data["status"] == "ok" 554 | 555 | def unlike(self, post_id: str) -> bool: 556 | """ 557 | takes your like back from a post. 558 | 559 | Args: 560 | post_id (str): post identifier 561 | 562 | Returns: 563 | boolean and if verbose mode is enabled, prints response dict 564 | """ 565 | user_id = self.user_id or self.get_current_user_id() 566 | response = self.__toggle_auth__post_request( 567 | f"{BASE_API_URL}/api/v1/media/{post_id}_{user_id}/unlike/", 568 | ) 569 | data = response.json() 570 | if self.verbose: 571 | print("[UNLIKE]", data) 572 | return response.json()["status"] == "ok" 573 | 574 | def repost_thread(self, post_id: str) -> bool: 575 | """ 576 | Repost a thread. 577 | 578 | Arguments: 579 | post_id (str): post identifier 580 | 581 | Returns: 582 | boolean and if verbose mode is enabled, prints response dict 583 | """ 584 | token = self.get_token() if self.token is None else self.token 585 | if token is None: 586 | raise "Token not found" 587 | response = self.http_client.post( 588 | url=f"{BASE_API_URL}/api/v1/repost/create_repost/", 589 | headers=self.__get_app_headers(), 590 | data=f"media_id={post_id}", 591 | ) 592 | data = response.json() 593 | if self.verbose: 594 | print("[Repost]", data) 595 | return data["status"] == "ok" 596 | 597 | def unrepost_thread(self, post_id: str) -> bool: 598 | """ 599 | Delete reposted thread. 600 | 601 | Arguments: 602 | post_id (str): post identifier 603 | 604 | Returns: 605 | boolean and if verbose mode is enabled, prints response dict 606 | """ 607 | token = self.get_token() if self.token is None else self.token 608 | if token is None: 609 | raise "Token not found" 610 | response = self.http_client.post( 611 | url=f"{BASE_API_URL}/api/v1/repost/delete_text_app_repost/", 612 | headers=self.__get_app_headers(), 613 | data=f"original_media_id={post_id}", 614 | ) 615 | data = response.json() 616 | if self.verbose: 617 | print("[Unrepost]", data) 618 | return data["status"] == "ok" 619 | 620 | def search(self, search_parameter: str) -> dict: 621 | """ 622 | Search for user. 623 | 624 | Args: 625 | search_parameter (str): parameter to search 626 | 627 | Returns: 628 | dict:{A list of users}. 629 | """ 630 | response = self.http_client.get( 631 | url=f"{BASE_API_URL}/api/v1/users/search/?q={search_parameter}", 632 | headers=self.__get_app_headers(), 633 | ) 634 | print("URL:", f"{BASE_API_URL}/api/v1/users/search/?q={search_parameter}") 635 | return response if response.status_code != 200 else response.json() 636 | 637 | def follow(self, user_id: str) -> bool: 638 | response = self.__toggle_auth__post_request( 639 | f"{BASE_API_URL}/api/v1/friendships/create/{user_id}/" 640 | ) 641 | data = response.json() 642 | if self.verbose: 643 | print("[FOLLOW]", data) 644 | return response.json()["status"] == "ok" 645 | 646 | def unfollow(self, user_id: str) -> bool: 647 | response = self.__toggle_auth__post_request( 648 | f"{BASE_API_URL}/api/v1/friendships/destroy/{user_id}/" 649 | ) 650 | data = response.json() 651 | if self.verbose: 652 | print("[UNFOLLOW]", data) 653 | return response.json()["status"] == "ok" 654 | 655 | def block(self, user_id: str) -> bool: 656 | """ 657 | Blocks a user. 658 | 659 | Args: 660 | user_id (str): user identifier 661 | 662 | Returns: 663 | boolean and if verbose mode is enabled, prints response dict 664 | """ 665 | params = quote( 666 | string=json.dumps( 667 | obj={ 668 | "user_id": user_id, 669 | "surface": "ig_text_feed_timeline", 670 | "is_auto_block_enabled": "true", 671 | }, 672 | ), 673 | safe="!~*'()", 674 | ) 675 | 676 | response = self.http_client.post( 677 | url=f"{BASE_API_URL}/api/v1/friendships/block/{user_id}/", 678 | headers=self.__get_app_headers(), 679 | data=f"signed_body=SIGNATURE.{params}", 680 | ) 681 | data = response.json() 682 | if self.verbose: 683 | print("[BLOCK]", data) 684 | return response.json()["status"] == "ok" 685 | 686 | def unblock(self, user_id: str) -> bool: 687 | """ 688 | Unblocks a user. 689 | 690 | Args: 691 | user_id (str): user identifier 692 | 693 | Returns: 694 | boolean and if verbose mode is enabled, prints response dict 695 | """ 696 | params = quote( 697 | string=json.dumps( 698 | obj={ 699 | "user_id": user_id, 700 | "surface": "ig_text_feed_timeline", 701 | }, 702 | ), 703 | safe="!~*'()", 704 | ) 705 | 706 | response = self.http_client.post( 707 | url=f"{BASE_API_URL}/api/v1/friendships/unblock/{user_id}/", 708 | headers=self.__get_app_headers(), 709 | data=f"signed_body=SIGNATURE.{params}", 710 | ) 711 | data = response.json() 712 | if self.verbose: 713 | print("[UNBLOCK]", data) 714 | return response.json()["status"] == "ok" 715 | 716 | def restrict(self, user_id: str) -> bool: 717 | """ 718 | Restrict a user. 719 | 720 | Args: 721 | user_id (str): user identifier 722 | 723 | Returns: 724 | boolean and if verbose mode is enabled, prints response dict 725 | """ 726 | params = quote( 727 | string=json.dumps( 728 | obj={ 729 | "user_ids": user_id, 730 | "container_module": "ig_text_feed_timeline", 731 | }, 732 | ), 733 | safe="!~*'()", 734 | ) 735 | 736 | response = self.http_client.post( 737 | url=f"{BASE_API_URL}/api/v1/restrict_action/restrict_many/", 738 | headers=self.__get_app_headers(), 739 | data=f"signed_body=SIGNATURE.{params}", 740 | ) 741 | data = response.json() 742 | if self.verbose: 743 | print("[RESTRICT]", data) 744 | return response.json()["status"] == "ok" 745 | 746 | def unrestrict(self, user_id: str) -> bool: 747 | """ 748 | Unrestrict a user. 749 | 750 | Args: 751 | user_id (str): user identifier 752 | 753 | Returns: 754 | boolean and if verbose mode is enabled, prints response dict 755 | """ 756 | params = quote( 757 | string=json.dumps( 758 | obj={ 759 | "target_user_id": user_id, 760 | "container_module": "ig_text_feed_timeline", 761 | }, 762 | ), 763 | safe="!~*'()", 764 | ) 765 | 766 | response = self.http_client.post( 767 | url=f"{BASE_API_URL}/api/v1/restrict_action/unrestrict/", 768 | headers=self.__get_app_headers(), 769 | data=f"signed_body=SIGNATURE.{params}", 770 | ) 771 | data = response.json() 772 | if self.verbose: 773 | print("[UNRESTRICT]", data) 774 | return response.json()["status"] == "ok" 775 | 776 | def mute(self, user_id: str) -> bool: 777 | """ 778 | Mutes a user. 779 | 780 | Args: 781 | user_id (str): user identifier 782 | 783 | Returns: 784 | boolean and if verbose mode is enabled, prints response dict 785 | """ 786 | params = quote( 787 | string=json.dumps( 788 | obj={ 789 | "target_posts_author_id": user_id, 790 | "container_module": "ig_text_feed_timeline", 791 | }, 792 | ), 793 | safe="!~*'()", 794 | ) 795 | 796 | response = self.http_client.post( 797 | url=f"{BASE_API_URL}/api/v1/friendships/mute_posts_or_story_from_follow/", 798 | headers=self.__get_app_headers(), 799 | data=f"signed_body=SIGNATURE.{params}", 800 | ) 801 | data = response.json() 802 | if self.verbose: 803 | print("[MUTE]", data) 804 | return response.json()["status"] == "ok" 805 | 806 | def unmute(self, user_id: str) -> bool: 807 | """ 808 | Unmutes a user. 809 | 810 | Args: 811 | user_id (str): user identifier 812 | 813 | Returns: 814 | boolean and if verbose mode is enabled, prints response dict 815 | """ 816 | params = quote( 817 | string=json.dumps( 818 | obj={ 819 | "target_posts_author_id": user_id, 820 | "container_module": "ig_text_feed_timeline", 821 | }, 822 | ), 823 | safe="!~*'()", 824 | ) 825 | 826 | response = self.http_client.post( 827 | url=f"{BASE_API_URL}/api/v1/friendships/unmute_posts_or_story_from_follow/", 828 | headers=self.__get_app_headers(), 829 | data=f"signed_body=SIGNATURE.{params}", 830 | ) 831 | data = response.json() 832 | if self.verbose: 833 | print("[UNMUTE]", data) 834 | return response.json()["status"] == "ok" 835 | 836 | def friendship_status(self, user_id: str) -> dict | int: 837 | """ 838 | Checks Friendship_status with other users. 839 | 840 | Arguments: 841 | user_id (str): target user identifier 842 | 843 | Returns: 844 | dict(friendship_status) or int(response.status_code) 845 | """ 846 | response = self.http_client.get( 847 | url=f"{BASE_API_URL}/api/v1/friendships/show/{user_id}/", 848 | headers=self.__get_app_headers(), 849 | ) 850 | if response.status_code == 200: 851 | return response.json() 852 | else: 853 | return response.status_code 854 | 855 | def get_followings(self, user_id: str) -> dict | int: 856 | """ 857 | Get user followings 858 | 859 | Arguments: 860 | user_id (str): user identifier. 861 | 862 | Returns: 863 | dict(user_followings_data) or int(response.status_code) 864 | """ 865 | response = self.http_client.get( 866 | url=f"{BASE_API_URL}/api/v1/friendships/{user_id}/following/", 867 | headers=self.__get_app_headers(), 868 | ) 869 | if response.status_code == 200: 870 | return response.json() 871 | else: 872 | return response.status_code 873 | 874 | def get_followers(self, user_id: str) -> dict | int: 875 | """ 876 | Get user followers 877 | 878 | Arguments: 879 | user_id (str): user identifier. 880 | 881 | Returns: 882 | dict(user_followers_data) or int(response.status_code) 883 | """ 884 | response = self.http_client.get( 885 | url=f"{BASE_API_URL}/api/v1/friendships/{user_id}/followers/", 886 | headers=self.__get_app_headers(), 887 | ) 888 | if response.status_code == 200: 889 | return response.json() 890 | else: 891 | return response.status_code 892 | 893 | def get_suggested_users( 894 | self, count: int = 15, paging: int | None = None 895 | ) -> List[SuggestedUser]: 896 | """ 897 | Get suggested users. 898 | Arguments: 899 | count (int): number of user suggestions. 900 | paging (int): set the page number. 901 | Returns: 902 | response (dict) | error (status_code) 903 | """ 904 | parameters = { 905 | "paging_token": paging, 906 | "count": count, 907 | } 908 | response = self.http_client.get( 909 | url=f"{BASE_API_URL}/api/v1/text_feed/recommended_users/", 910 | headers=self.__get_app_headers(), 911 | params=parameters, 912 | ) 913 | 914 | try: 915 | suggested = GetSuggestedUsersResponse.from_dict(response.json()) 916 | return suggested.users 917 | except Exception as e: 918 | if self.verbose: 919 | print("[ERROR] ", e) 920 | return [] 921 | 922 | def get_timeline(self, max_id: str | None = None) -> dict: 923 | """ 924 | Get User Timeline. 925 | Arguments: 926 | max_id (str): ID for the next batch of posts 927 | Returns: 928 | response (dict) | error (status_code) 929 | """ 930 | if max_id is None: 931 | parameters = { 932 | 'pagination_source': 'text_post_feed_threads', 933 | } 934 | else: 935 | parameters = { 936 | 'pagination_source': 'text_post_feed_threads', 937 | 'max_id': max_id 938 | } 939 | response = self.http_client.post( 940 | url=f"{BASE_API_URL}/api/v1/feed/text_post_app_timeline/", 941 | headers=self.__get_app_headers(), 942 | params=parameters, 943 | ) 944 | if response.status_code == 200: 945 | return response.json() 946 | else: 947 | return response.status_code 948 | 949 | def get_notifications( 950 | self, notification_filter : str = "replies", 951 | max_id : str | None = None, 952 | pagination: str | None = None 953 | ) -> dict: 954 | """ 955 | Get User Notifications. 956 | Arguments: 957 | notification_filter (str): "mentions", "replies", "verified" 958 | max_id (str): ID for the next batch of notifications. 959 | pagination (str): Timestamp of first record of pagination 960 | Returns: 961 | response (dict) | error (status_code) 962 | """ 963 | filters = { 964 | "mentions": "text_post_app_mentions", 965 | "replies": "text_post_app_replies", 966 | "verified": "verified" 967 | } 968 | timezone_offset = (datetime.now() - datetime.utcnow()).seconds 969 | parameters = { 970 | 'feed_type' : 'all', 971 | 'mark_as_seen' : False, 972 | 'timezone_offset':str(timezone_offset) 973 | } 974 | if filter: 975 | parameters.update({'selected_filter': filters[notification_filter]}) 976 | 977 | if max_id: 978 | parameters.update({ 979 | 'max_id': max_id, 980 | 'pagination_first_record_timestamp': pagination 981 | }) 982 | response = self.http_client.get( 983 | url=f"{BASE_API_URL}/api/v1/text_feed/text_app_notifications/", 984 | headers=self.__get_app_headers(), 985 | params=parameters, 986 | ) 987 | if response.status_code == 200: 988 | return response.json() 989 | else: 990 | return response.status_code 991 | 992 | def get_token(self) -> str: 993 | """ 994 | set fb login token 995 | 996 | Returns: 997 | str: validate token string None if not valid 998 | """ 999 | try: 1000 | blockVersion = ( 1001 | "5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73" 1002 | ) 1003 | params = json.dumps( 1004 | { 1005 | "client_input_params": { 1006 | "password": f"#PWD_INSTAGRAM:4:{self.timestamp_string}:{self.encrypted_password}", 1007 | "contact_point": self.username, 1008 | "device_id": self.device_id, 1009 | }, 1010 | "server_params": { 1011 | "credential_type": "password", 1012 | "device_id": self.device_id, 1013 | }, 1014 | }, 1015 | ) 1016 | bk_client_context = json.dumps( 1017 | {"bloks_version": blockVersion, "styles_id": "instagram"} 1018 | ) 1019 | params_quote = quote(string=params, safe="!~*'()") 1020 | bk_client_context_quote = quote(string=bk_client_context, safe="!~*'()") 1021 | 1022 | response = requests.post( 1023 | url=f"{BASE_API_URL}/api/v1/bloks/apps/com.bloks.www.bloks.caa.login.async.send_login_request/", 1024 | headers={ 1025 | "User-Agent": "Barcelona 289.0.0.77.109 Android", 1026 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 1027 | }, 1028 | data=f"params={params_quote}&bk_client_context={bk_client_context_quote}&bloks_versioning_id={blockVersion}", 1029 | ) 1030 | 1031 | data = response.text 1032 | if data == "Oops, an error occurred.": 1033 | return None 1034 | pos = data.find("Bearer IGT:2:") 1035 | data_txt = data[pos:] 1036 | backslash_pos = data_txt.find("\\\\") 1037 | token = data_txt[13:backslash_pos] 1038 | 1039 | return token 1040 | 1041 | except Exception as e: 1042 | print("[ERROR] ", e) 1043 | 1044 | return None 1045 | 1046 | def publish( 1047 | self, 1048 | caption: str, 1049 | image_path: str = None, 1050 | url: str = None, 1051 | parent_post_id: str = None, 1052 | ) -> bool: 1053 | """ 1054 | Returns publish post 1055 | 1056 | Args: 1057 | caption (str): post_id which is unique to each post. 1058 | image_path (str, optional): image_path which is unique to each user. 1059 | url (str, optional): url which is unique to each user. 1060 | parent_post_id (str, optional): parent_post_id which is unique to each user. 1061 | 1062 | Returns: 1063 | bool: verify that the post went publish 1064 | """ 1065 | if self.username is None or self.password is None: 1066 | return False 1067 | 1068 | user_id = self.get_user_id_from_username(self.username) 1069 | if user_id is None: 1070 | return False 1071 | if self.token is None: 1072 | token = self.get_token() 1073 | if token is not None: 1074 | return False 1075 | now = datetime.now() 1076 | timezone_offset = (datetime.now() - datetime.utcnow()).seconds 1077 | 1078 | params = { 1079 | "text_post_app_info": {"reply_control": 0}, 1080 | "timezone_offset": "-" + str(timezone_offset), 1081 | "source_type": "4", 1082 | "_uid": self.user_id, 1083 | "device_id": str(self.device_id), 1084 | "caption": caption, 1085 | "upload_id": str(int(now.timestamp() * 1000)), 1086 | "device": { 1087 | "manufacturer": "OnePlus", 1088 | "model": "ONEPLUS+A3010", 1089 | "android_version": 25, 1090 | "android_release": "7.1.1", 1091 | }, 1092 | } 1093 | post_url = f"{BASE_API_URL}/api/v1/media/configure_text_only_post/" 1094 | if image_path is not None: 1095 | post_url = f"{BASE_API_URL}/api/v1/media/configure_text_post_app_feed/" 1096 | 1097 | image_content = None 1098 | if not (os.path.isfile(image_path) and os.path.exists(image_path)): 1099 | if not self.__is_valid_url(image_path): 1100 | return False 1101 | else: 1102 | image_content = self.__download(image_path) 1103 | upload_id = self.upload_image( 1104 | image_url=image_path, image_content=image_content 1105 | ) 1106 | if upload_id == None: 1107 | return False 1108 | params["upload_id"] = upload_id["upload_id"] 1109 | params["scene_capture_type"] = "" 1110 | elif url is not None: 1111 | params["text_post_app_info"]["link_attachment_url"] = url 1112 | if image_path is None: 1113 | params["publish_mode"] = "text_post" 1114 | 1115 | if parent_post_id is not None: 1116 | params["text_post_app_info"]["reply_id"] = parent_post_id 1117 | params = json.dumps(params) 1118 | payload = f"signed_body=SIGNATURE.{urllib.parse.quote(params)}" 1119 | headers = self.__get_app_headers().copy() 1120 | try: 1121 | response = requests.post(post_url, headers=headers, data=payload) 1122 | if response.status_code == 200: 1123 | return True 1124 | else: 1125 | return False 1126 | except Exception as e: 1127 | if self.verbose: 1128 | print("[ERROR] ", e) 1129 | return False 1130 | 1131 | def publish_with_image(self, caption: str, image_path: str) -> bool: 1132 | """ 1133 | @@deprecated 1134 | Returns publish post with image 1135 | 1136 | Args: 1137 | caption (str): post_id which is unique to each post. 1138 | image_path (str): image path 1139 | 1140 | Returns: 1141 | bool: verify that the post with image went publish 1142 | """ 1143 | return self.publish(caption=caption, image_path=image_path) 1144 | 1145 | def upload_image(self, image_url: str, image_content: bytes) -> str: 1146 | headers = self.__get_app_headers().copy() 1147 | 1148 | upload_id = int(time.time() * 1000) 1149 | name = f"{upload_id}_0_{random.randint(1000000000, 9999999999)}" 1150 | url = "https://www.instagram.com/rupload_igphoto/" + name 1151 | mime_type = None 1152 | if image_content is None: 1153 | f = open(image_url, mode="rb") 1154 | content = f.read() 1155 | f.close() 1156 | mime_type, _ = mimetypes.guess_type(image_url) 1157 | else: 1158 | content = image_content 1159 | response = requests.head(image_url) 1160 | content_type = response.headers.get("Content-Type") 1161 | if not content_type: 1162 | file_name = url.split("/")[-1] 1163 | mime_type, _ = mimetypes.guess_type(file_name) 1164 | if mime_type == None: 1165 | mime_type = "jpeg" 1166 | 1167 | x_instagram_rupload_params = { 1168 | "upload_id": f"{upload_id}", 1169 | "media_type": "1", 1170 | "sticker_burnin_params": "[]", 1171 | "image_compression": json.dumps( 1172 | {"lib_name": "moz", "lib_version": "3.1.m", "quality": "80"} 1173 | ), 1174 | "xsharing_user_ids": "[]", 1175 | "retry_context": { 1176 | "num_step_auto_retry": "0", 1177 | "num_reupload": "0", 1178 | "num_step_manual_retry": "0", 1179 | }, 1180 | "IG-FB-Xpost-entry-point-v2": "feed", 1181 | } 1182 | contentLength = len(content) 1183 | if mime_type.startswith("image/"): 1184 | mime_type = mime_type.replace("image/", "") 1185 | image_headers = { 1186 | "X_FB_PHOTO_WATERFALL_ID": str(uuid.uuid4()), 1187 | "X-Entity-Type": "image/" + mime_type, 1188 | "Offset": "0", 1189 | "X-Instagram-Rupload-Params": json.dumps(x_instagram_rupload_params), 1190 | "X-Entity-Name": f"{name}", 1191 | "X-Entity-Length": f"{contentLength}", 1192 | "Content-Type": "application/octet-stream", 1193 | "Content-Length": f"{contentLength}", 1194 | "Accept-Encoding": "gzip", 1195 | } 1196 | 1197 | headers.update(image_headers) 1198 | response = self.http_client.post(url, headers=headers, data=content) 1199 | if response.status_code == 200: 1200 | return response.json() 1201 | else: 1202 | return None 1203 | -------------------------------------------------------------------------------- /threadspy/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .thread import Thread 2 | from .caption import Caption 3 | from .user_data import UserData 4 | from .candidate import Candidate 5 | from .media_data import MediaData 6 | from .share_info import ShareInfo 7 | from .extensions import Extensions 8 | from .quoted_post import QuotedPost 9 | from .thread_item import ThreadItem 10 | from .thread_data import ThreadData 11 | from .reposted_post import RepostedPost 12 | from .users import UsersData 13 | from .suggested_user import SuggestedUser 14 | from .threads_user import ThreadsUser 15 | from .video_version import VideoVersion 16 | from .image_versions2 import ImageVersions2 17 | from .user_profile_data import UserProfileData 18 | from .text_post_app_info import TextPostAppInfo 19 | from .reply_facepile_user import ReplyFacepileUser 20 | from .threads_user_summary import ThreadsUserSummary 21 | from .threads_hd_profile_pic_version import ThreadsHdProfilePicVersion 22 | from .common import ( 23 | CommonMediaDataResponse, 24 | CommonThreadDataResponse, 25 | CommonUserProfileDataResponse, 26 | ) 27 | 28 | 29 | from .get_user_profile_response import GetUserProfileResponse 30 | from .get_thread_likers_response import GetThreadLikersResponse 31 | from .get_suggested_users_response import GetSuggestedUsersResponse 32 | from .get_user_profile_thread_response import GetUserProfileThreadResponse 33 | from .get_user_profile_threads_response import GetUserProfileThreadsResponse 34 | from .get_user_profile_replies_response import GetUserProfileRepliesResponse 35 | 36 | __all__ = [ 37 | "Thread", 38 | "Caption", 39 | "UserData", 40 | "Candidate", 41 | "MediaData", 42 | "ShareInfo", 43 | "Extensions", 44 | "QuotedPost", 45 | "ThreadItem", 46 | "ThreadData", 47 | "RepostedPost", 48 | "UsersData", 49 | "SuggestedUser", 50 | "ThreadsUser", 51 | "VideoVersion", 52 | "ImageVersions2", 53 | "UserProfileData", 54 | "TextPostAppInfo", 55 | "ReplyFacepileUser", 56 | "ThreadsUserSummary", 57 | "ThreadsHdProfilePicVersion", 58 | "CommonMediaDataResponse", 59 | "CommonThreadDataResponse", 60 | "CommonUserProfileDataResponse", 61 | "GetUserProfileResponse", 62 | "GetThreadLikersResponse", 63 | "GetSuggestedUsersResponse", 64 | "GetUserProfileThreadResponse", 65 | "GetUserProfileThreadsResponse", 66 | "GetUserProfileRepliesResponse", 67 | ] 68 | -------------------------------------------------------------------------------- /threadspy/types/candidate.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class Candidate: 8 | height: int 9 | url: str 10 | width: int 11 | # __typename: str 12 | -------------------------------------------------------------------------------- /threadspy/types/caption.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class Caption: 8 | text: str 9 | -------------------------------------------------------------------------------- /threadspy/types/common.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.users import UsersData 4 | from threadspy.types.media_data import MediaData 5 | from threadspy.types.thread_data import ThreadData 6 | from threadspy.types.user_profile_data import UserProfileData 7 | 8 | 9 | @dataclass_json 10 | @dataclass 11 | class CommonMediaDataResponse: 12 | mediaData: MediaData 13 | 14 | 15 | @dataclass_json 16 | @dataclass 17 | class CommonThreadDataResponse: 18 | data: ThreadData 19 | 20 | 21 | @dataclass_json 22 | @dataclass 23 | class CommonUserProfileDataResponse: 24 | data: UserProfileData 25 | 26 | 27 | @dataclass_json 28 | @dataclass 29 | class CommonLikersResponse: 30 | likers: UsersData 31 | -------------------------------------------------------------------------------- /threadspy/types/extensions.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class Extensions: 8 | is_final: bool 9 | -------------------------------------------------------------------------------- /threadspy/types/get_suggested_users_response.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.suggested_user import SuggestedUser 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetSuggestedUsersResponse: 10 | users: List[SuggestedUser] 11 | paging_token: str 12 | has_more: bool 13 | status: str 14 | -------------------------------------------------------------------------------- /threadspy/types/get_thread_likers_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.extensions import Extensions 4 | from threadspy.types.common import CommonLikersResponse 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetThreadLikersResponse: 10 | data: CommonLikersResponse 11 | extensions: Extensions 12 | -------------------------------------------------------------------------------- /threadspy/types/get_user_profile_replies_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.extensions import Extensions 4 | from threadspy.types.common import CommonMediaDataResponse 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetUserProfileRepliesResponse: 10 | data: CommonMediaDataResponse 11 | extensions: Extensions 12 | -------------------------------------------------------------------------------- /threadspy/types/get_user_profile_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.extensions import Extensions 4 | from threadspy.types.user_profile_data import UserProfileData 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetUserProfileResponse: 10 | data: UserProfileData 11 | extensions: Extensions 12 | -------------------------------------------------------------------------------- /threadspy/types/get_user_profile_thread_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.extensions import Extensions 4 | from threadspy.types.common import CommonThreadDataResponse 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetUserProfileThreadResponse: 10 | data: CommonThreadDataResponse 11 | extensions: Extensions 12 | -------------------------------------------------------------------------------- /threadspy/types/get_user_profile_threads_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.extensions import Extensions 4 | from threadspy.types.common import CommonMediaDataResponse 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class GetUserProfileThreadsResponse: 10 | data: CommonMediaDataResponse 11 | extensions: Extensions 12 | -------------------------------------------------------------------------------- /threadspy/types/image_versions2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.candidate import Candidate 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class ImageVersions2: 10 | candidates: List[Candidate] 11 | -------------------------------------------------------------------------------- /threadspy/types/media_data.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.thread import Thread 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class MediaData: 10 | threads: List[Thread] 11 | -------------------------------------------------------------------------------- /threadspy/types/post.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | 5 | from threadspy.types.caption import Caption 6 | from threadspy.types.image_versions2 import ImageVersions2 7 | from threadspy.types.text_post_app_info import TextPostAppInfo 8 | from threadspy.types.threads_user_summary import ThreadsUserSummary 9 | 10 | 11 | @dataclass_json 12 | @dataclass 13 | class Post: 14 | user: ThreadsUserSummary 15 | image_versions2: ImageVersions2 16 | original_width: int 17 | original_height: int 18 | video_versions: List[Any] 19 | carousel_media: Any 20 | carousel_media_count: Any 21 | pk: str 22 | has_audio: Any 23 | text_post_app_info: TextPostAppInfo 24 | taken_at: int 25 | like_count: int 26 | code: str 27 | media_overlay_info: Any 28 | id: str 29 | caption: Caption = None 30 | -------------------------------------------------------------------------------- /threadspy/types/quoted_post.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | 5 | from threadspy.types.caption import Caption 6 | from threadspy.types.image_versions2 import ImageVersions2 7 | from threadspy.types.text_post_app_info import TextPostAppInfo 8 | from threadspy.types.threads_user_summary import ThreadsUserSummary 9 | 10 | 11 | @dataclass_json 12 | @dataclass 13 | class QuotedPost: 14 | text_post_app_info: TextPostAppInfo 15 | user: ThreadsUserSummary 16 | pk: str 17 | media_overlay_info: any 18 | code: str 19 | caption: Caption 20 | image_versions2: ImageVersions2 21 | original_width: int 22 | original_height: int 23 | video_versions: List[any] 24 | carousel_media: any 25 | carousel_media_count: any 26 | has_audio: any 27 | like_count: int 28 | taken_at: int 29 | id: str 30 | -------------------------------------------------------------------------------- /threadspy/types/reply_facepile_user.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class ReplyFacepileUser: 8 | # __typename: str 9 | id: any 10 | profile_pic_url: str 11 | -------------------------------------------------------------------------------- /threadspy/types/reposted_post.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | 5 | from threadspy.types.caption import Caption 6 | from threadspy.types.video_version import VideoVersion 7 | from threadspy.types.image_versions2 import ImageVersions2 8 | from threadspy.types.text_post_app_info import TextPostAppInfo 9 | from threadspy.types.threads_user_summary import ThreadsUserSummary 10 | 11 | 12 | @dataclass_json 13 | @dataclass 14 | class RepostedPost: 15 | pk: str 16 | user: ThreadsUserSummary 17 | image_versions2: ImageVersions2 18 | original_width: int 19 | original_height: int 20 | video_versions: List[VideoVersion] 21 | carousel_media: any 22 | carousel_media_count: any 23 | text_post_app_info: TextPostAppInfo 24 | caption: Caption 25 | like_count: int 26 | taken_at: int 27 | code: str 28 | id: str 29 | has_audio: bool = None 30 | -------------------------------------------------------------------------------- /threadspy/types/share_info.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.quoted_post import QuotedPost 4 | from threadspy.types.reposted_post import RepostedPost 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class ShareInfo: 10 | quoted_post: QuotedPost = None 11 | reposted_post: RepostedPost = None 12 | -------------------------------------------------------------------------------- /threadspy/types/suggested_user.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | 5 | 6 | @dataclass_json 7 | @dataclass 8 | class FriendshipStatus: 9 | following: bool 10 | followed_by: bool 11 | blocking: bool 12 | muting: bool 13 | is_private: bool 14 | incoming_request: bool 15 | outgoing_request: bool 16 | text_post_app_pre_following: bool 17 | is_bestie: bool 18 | is_restricted: bool 19 | is_feed_favorite: bool 20 | 21 | 22 | @dataclass_json 23 | @dataclass 24 | class ProfileContextFacepileUser: 25 | pk: int 26 | pk_id: str 27 | username: str 28 | full_name: str 29 | is_private: bool 30 | is_verified: bool 31 | profile_pic_id: str 32 | profile_pic_url: str 33 | has_onboarded_to_text_post_app: bool 34 | 35 | 36 | @dataclass_json 37 | @dataclass 38 | class SuggestedUser: 39 | pk: int 40 | pk_id: str 41 | username: str 42 | full_name: str 43 | account_badges: List[Any] 44 | profile_pic_url: str 45 | has_anonymous_profile_picture: bool 46 | has_onboarded_to_text_post_app: bool 47 | is_verified: bool 48 | friendship_status: FriendshipStatus 49 | profile_context_facepile_users: List[ProfileContextFacepileUser] 50 | follower_count: int 51 | -------------------------------------------------------------------------------- /threadspy/types/text_post_app_info.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | 5 | 6 | @dataclass_json 7 | @dataclass 8 | class TextPostAppInfo: 9 | link_preview_attachment: Any 10 | share_info: Any 11 | reply_to_author: Any 12 | is_post_unavailable: bool 13 | direct_reply_count: Optional[int] = 0 14 | -------------------------------------------------------------------------------- /threadspy/types/thread.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.thread_item import ThreadItem 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class Thread: 10 | id: str 11 | thread_items: List[ThreadItem] 12 | thread_type: str = None 13 | header: Any = None 14 | -------------------------------------------------------------------------------- /threadspy/types/thread_data.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.thread import Thread 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class ThreadData: 10 | reply_threads: List[Thread] 11 | containing_thread: Optional[Thread] = None 12 | -------------------------------------------------------------------------------- /threadspy/types/thread_item.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.post import Post 5 | from threadspy.types.reply_facepile_user import ReplyFacepileUser 6 | 7 | 8 | @dataclass_json 9 | @dataclass 10 | class ThreadItem: 11 | post: Post 12 | line_type: str 13 | reply_facepile_users: List[ReplyFacepileUser] 14 | should_show_replies_cta: bool 15 | # __typename: str 16 | view_replies_cta_string: str = None 17 | -------------------------------------------------------------------------------- /threadspy/types/threads_bio_link.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class ThreadsBioLink: 8 | url: str 9 | -------------------------------------------------------------------------------- /threadspy/types/threads_hd_profile_pic_version.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class ThreadsHdProfilePicVersion: 8 | height: int 9 | url: str 10 | width: int 11 | -------------------------------------------------------------------------------- /threadspy/types/threads_user.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any, Optional 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.threads_bio_link import ThreadsBioLink 5 | from threadspy.types.threads_hd_profile_pic_version import ThreadsHdProfilePicVersion 6 | 7 | 8 | @dataclass_json 9 | @dataclass 10 | class ThreadsUser: 11 | pk: str 12 | full_name: str 13 | profile_pic_url: str 14 | follower_count: int 15 | is_verified: bool 16 | username: str 17 | profile_context_facepile_users: Any 18 | id: Any 19 | hd_profile_pic_versions: Optional[List[ThreadsHdProfilePicVersion]] = None 20 | biography: Optional[str] = None 21 | biography_with_entities: Any = None 22 | bio_links: Optional[List[ThreadsBioLink]] = None 23 | is_private: Optional[bool] = None 24 | -------------------------------------------------------------------------------- /threadspy/types/threads_user_summary.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class ThreadsUserSummary: 8 | profile_pic_url: str 9 | username: str 10 | id: any 11 | is_verified: bool 12 | pk: str 13 | -------------------------------------------------------------------------------- /threadspy/types/user_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.threads_user import ThreadsUser 4 | 5 | 6 | @dataclass_json 7 | @dataclass 8 | class UserData: 9 | user: ThreadsUser 10 | -------------------------------------------------------------------------------- /threadspy/types/user_profile_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | from threadspy.types.user_data import UserData 4 | 5 | 6 | @dataclass_json 7 | @dataclass 8 | class UserProfileData: 9 | userData: UserData 10 | -------------------------------------------------------------------------------- /threadspy/types/users.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import dataclass 3 | from dataclasses_json import dataclass_json 4 | from threadspy.types.threads_user import ThreadsUser 5 | 6 | 7 | @dataclass_json 8 | @dataclass 9 | class UsersData: 10 | users: List[ThreadsUser] 11 | -------------------------------------------------------------------------------- /threadspy/types/video_version.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dataclasses_json import dataclass_json 3 | 4 | 5 | @dataclass_json 6 | @dataclass 7 | class VideoVersion: 8 | type: int 9 | url: str 10 | # __typename: str 11 | --------------------------------------------------------------------------------