├── .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 | [](https://pypi.org/project/threads-py) [](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://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 |
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 | [](https://pypi.org/project/threads-py) [](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 |
--------------------------------------------------------------------------------