Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Oct 1, 2023. It is now read-only.

Commit 2106b50

Browse filesBrowse files
authored
Implemented additional APIs (#10)
1 parent e00870d commit 2106b50
Copy full SHA for 2106b50

File tree

5 files changed

+201
-11
lines changed
Filter options

5 files changed

+201
-11
lines changed

‎README.md

Copy file name to clipboardExpand all lines: README.md
+18-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ loop.run_until_complete(main())
6666
```
6767

6868
## Usage Examples
69+
View [example.py](https://github.com/Danie1/threads-api/blob/main/example.py) for code examples.
70+
At the end of the file you will be able to uncomment and run the individual examples with ease.
71+
72+
Then simply run as:
73+
```
74+
# Pass the credentials as environment variables
75+
USERNAME=<Instagram Username> PASSWORD=<Instagram Password> python3 example.py
76+
```
77+
78+
### Samples
79+
6980
<details>
7081
<summary>"get_user_id_from_username" Function</summary>
7182

@@ -327,25 +338,29 @@ Post has been successfully posted
327338
## 📌 Roadmap
328339

329340
- [x] ✅ Login functionality 🔒
330-
- [x] ✅ Cache login token securely (reduce login requests)
341+
- [x] ✅ Cache login token securely (reduce login requests / due to restrictive limits)
331342
- [x] ✅ Write Posts (Requires Login 🔒)
332343
- [x] ✅ Posts with just text
333344
- [x] ✅ Posts with text and an image
334345
- [x] ✅ Posts with text that share a url
346+
- [x] ✅ Reply to Posts
335347
- [ ] 🚧 Post with text and share a video
336-
- [ ] 🚧 Reply to Posts
337348
- [x] ✅ Perform Actions (Requires Login 🔒)
338349
- [x] ✅ Like Posts
339350
- [x] ✅ Unlike Posts
351+
- [x] ✅ Delete post
340352
- [x] ✅ Follow User
341353
- [x] ✅ Unfollow User
342-
- [x] ✅ Read Data
354+
- [x] ✅ Read Public Data
343355
- [x] ✅ Read a user_id (eg. `314216`) via username(eg. `zuck`)
344356
- [x] ✅ Read a user's profile info
345357
- [x] ✅ Read list of a user's Threads
346358
- [x] ✅ Read list of a user's Replies
347359
- [x] ✅ Read Post and a list of its Replies
348360
- [x] ✅ View who liked a post
361+
- [x] ✅ Read Private Data (Requires Login 🔒)
362+
- [x] ✅ Read a user's followers list
363+
- [x] ✅ Read a user's following list
349364
- [x] ✅ CI/CD
350365
- [x] ✅ GitHub Actions Pipeline
351366
- [ ] 🚧 Pytest

‎example.py

Copy file name to clipboardExpand all lines: example.py
+99
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,100 @@ async def login_with_cache():
195195
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
196196
print(f"Login status: {'Success' if is_success else 'Failed'}")
197197

198+
async def get_user_followers():
199+
api = ThreadsAPI()
200+
201+
# Will login via REST to the Instagram API
202+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
203+
print(f"Login status: {'Success' if is_success else 'Failed'}")
204+
205+
if is_success:
206+
username_to_search = "zuck"
207+
number_of_likes_to_display = 10
208+
209+
user_id_to_search = await api.get_user_id_from_username(username_to_search)
210+
data = await api.get_user_followers(user_id_to_search)
211+
212+
for user in data['users'][0:number_of_likes_to_display]:
213+
print(f"Username: {user['username']}")
214+
215+
async def get_user_following():
216+
api = ThreadsAPI()
217+
218+
# Will login via REST to the Instagram API
219+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
220+
print(f"Login status: {'Success' if is_success else 'Failed'}")
221+
222+
if is_success:
223+
username_to_search = "zuck"
224+
number_of_likes_to_display = 10
225+
226+
user_id_to_search = await api.get_user_id_from_username(username_to_search)
227+
data = await api.get_user_following(user_id_to_search)
228+
229+
for user in data['users'][0:number_of_likes_to_display]:
230+
print(f"Username: {user['username']}")
231+
232+
# Asynchronously likes a post
233+
async def like_post():
234+
api = ThreadsAPI()
235+
236+
# Will login via REST to the Instagram API
237+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
238+
print(f"Login status: {'Success' if is_success else 'Failed'}")
239+
240+
if is_success:
241+
post_url = "https://www.threads.net/t/CuZsgfWLyiI"
242+
post_id = await api.get_post_id_from_url(post_url)
243+
244+
result = await api.like_post(post_id)
245+
246+
print(f"Like status: {'Success' if result else 'Failed'}")
247+
248+
# Asynchronously unlikes a post
249+
async def unlike_post():
250+
api = ThreadsAPI()
251+
252+
# Will login via REST to the Instagram API
253+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
254+
print(f"Login status: {'Success' if is_success else 'Failed'}")
255+
256+
if is_success:
257+
post_url = "https://www.threads.net/t/CuZsgfWLyiI"
258+
post_id = await api.get_post_id_from_url(post_url)
259+
260+
result = await api.unlike_post(post_id)
261+
262+
print(f"Unlike status: {'Success' if result else 'Failed'}")
263+
264+
# Asynchronously creates then deletes the same post
265+
async def create_and_delete_post():
266+
api = ThreadsAPI()
267+
268+
# Will login via REST to the Instagram API
269+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
270+
print(f"Login status: {'Success' if is_success else 'Failed'}")
271+
272+
if is_success:
273+
post_id = await api.post("Hello World!")
274+
await api.delete_post(post_id)
275+
276+
print(f"Created and deleted post {post_id} successfully")
277+
278+
# Asynchronously creates then deletes the same post
279+
async def post_and_reply_to_post():
280+
api = ThreadsAPI()
281+
282+
# Will login via REST to the Instagram API
283+
is_success = await api.login(username=os.environ.get('USERNAME'), password=os.environ.get('PASSWORD'), cached_token_path=".token")
284+
print(f"Login status: {'Success' if is_success else 'Failed'}")
285+
286+
if is_success:
287+
first_post_id = await api.post("Hello World!")
288+
second_post_id = await api.post("Hello World to you too!", parent_post_id=first_post_id)
289+
290+
print(f"Created parent post {first_post_id} and replied to it with post {second_post_id} successfully")
291+
198292
'''
199293
Remove the # to run an individual example function wrapper.
200294
@@ -218,3 +312,8 @@ async def login_with_cache():
218312
#asyncio.run(follow_user()) # Follows a user.
219313
#asyncio.run(unfollow_user()) # Unfollows a user.
220314
#asyncio.run(login_with_cache()) # Displays token cache capability
315+
#asyncio.run(get_user_followers()) # Displays users who follow a given user
316+
#asyncio.run(like_post()) # Likes a post
317+
#asyncio.run(unlike_post()) # Unlikes a post
318+
#asyncio.run(create_and_delete_post()) # Creates and deletes the same post
319+
#asyncio.run(post_and_reply_to_post()) # Post and then reply to the same post

‎pyproject.toml

Copy file name to clipboardExpand all lines: pyproject.toml
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "threads-api"
3-
version = "1.1.1"
3+
version = "1.1.2"
44
description = "Unofficial Python client for Meta Threads.net API"
55
authors = ["Danie1"]
66
readme = "README.md"

‎setup.py

Copy file name to clipboardExpand all lines: setup.py
+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='threads-api',
8-
version='1.1.1',
8+
version='1.1.2',
99
description='Unofficial Python client for Meta Threads.net API',
1010
long_description=long_description,
1111
long_description_content_type='text/markdown',

‎threads_api/src/threads_api.py

Copy file name to clipboardExpand all lines: threads_api/src/threads_api.py
+82-6
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ async def __auth_required_post_request(self, url: str):
229229
async with session.post(url, headers=self.auth_headers) as response:
230230
return await response.json()
231231

232+
async def __auth_required_get_request(self, url: str):
233+
async with aiohttp.ClientSession() as session:
234+
async with session.get(url, headers=self.auth_headers) as response:
235+
return await response.json()
232236

233237

234238
async def get_user_id_from_username(self, username: str) -> str:
@@ -401,7 +405,21 @@ async def get_user_replies(self, user_id: str):
401405

402406
threads = data['data']['mediaData']['threads']
403407
return threads
404-
408+
409+
async def get_user_followers(self, user_id: str) -> bool:
410+
if not self.is_logged_in:
411+
raise Exception("The action 'get_user_followers' can only be perfomed while logged-in")
412+
413+
res = await self.__auth_required_get_request(f"{BASE_URL}/friendships/{user_id}/followers")
414+
return res
415+
416+
async def get_user_following(self, user_id: str) -> bool:
417+
if not self.is_logged_in:
418+
raise Exception("The action 'get_user_following' can only be perfomed while logged-in")
419+
420+
res = await self.__auth_required_get_request(f"{BASE_URL}/friendships/{user_id}/following")
421+
return res
422+
405423
async def follow_user(self, user_id: str) -> bool:
406424
"""
407425
Follows a user with the given user ID.
@@ -416,7 +434,7 @@ async def follow_user(self, user_id: str) -> bool:
416434
Exception: If an error occurs during the follow process.
417435
"""
418436
if not self.is_logged_in:
419-
raise Exception("The action 'follow' can only be perfomed while logged-in")
437+
raise Exception("The action 'follow_user' can only be perfomed while logged-in")
420438

421439
res = await self.__auth_required_post_request(f"{BASE_URL}/friendships/create/{user_id}/")
422440
return res["status"] == "ok"
@@ -435,12 +453,68 @@ async def unfollow_user(self, user_id: str) -> bool:
435453
Exception: If an error occurs during the unfollow process.
436454
"""
437455
if not self.is_logged_in:
438-
raise Exception("The action 'unfollow' can only be perfomed while logged-in")
456+
raise Exception("The action 'unfollow_user' can only be perfomed while logged-in")
439457

440458
res = await self.__auth_required_post_request(f"{BASE_URL}/friendships/destroy/{user_id}/")
441459
return res["status"] == "ok"
442460

461+
async def like_post(self, post_id: str) -> bool:
462+
"""
463+
Likes a post with the given ID.
464+
465+
Args:
466+
user_id (str): The ID of the post to like.
467+
468+
Returns:
469+
bool: True if the post was liked successfully, False otherwise.
470+
471+
Raises:
472+
Exception: If an error occurs during the like process.
473+
"""
474+
if not self.is_logged_in:
475+
raise Exception("The action 'like_post' can only be perfomed while logged-in")
476+
477+
res = await self.__auth_required_post_request(f"{BASE_URL}/media/{post_id}_{self.user_id}/like/")
478+
return res["status"] == "ok"
479+
480+
async def unlike_post(self, post_id: str) -> bool:
481+
"""
482+
Unlikes a post with the given ID.
483+
484+
Args:
485+
user_id (str): The ID of the post to unlike.
486+
487+
Returns:
488+
bool: True if the post was unliked successfully, False otherwise.
489+
490+
Raises:
491+
Exception: If an error occurs during the like process.
492+
"""
493+
if not self.is_logged_in:
494+
raise Exception("The action 'unlike_post' can only be perfomed while logged-in")
495+
496+
res = await self.__auth_required_post_request(f"{BASE_URL}/media/{post_id}_{self.user_id}/unlike/")
497+
return res["status"] == "ok"
498+
499+
async def delete_post(self, post_id: str) -> bool:
500+
"""
501+
Deletes a post with the given ID.
502+
503+
Args:
504+
user_id (str): The ID of the post to delete.
505+
506+
Returns:
507+
bool: True if the post was deleted successfully, False otherwise.
443508
509+
Raises:
510+
Exception: If an error occurs during the deletion process.
511+
"""
512+
if not self.is_logged_in:
513+
raise Exception("The action 'delete_post' can only be perfomed while logged-in")
514+
515+
res = await self.__auth_required_post_request(f"{BASE_URL}/media/{post_id}_{self.user_id}/delete/?media_type=TEXT_POST")
516+
return res["status"] == "ok"
517+
444518
async def get_post_id_from_url(self, post_url):
445519
"""
446520
Retrieves the post ID from a given URL.
@@ -714,10 +788,12 @@ async def __upload_image(image_url: str, image_content: bytes) -> str:
714788
try:
715789
async with aiohttp.ClientSession() as session:
716790
async with session.post(post_url, headers=headers, data=payload) as response:
717-
if response.status == 200:
718-
return True
791+
data = await response.json()
792+
793+
if 'media' in data and 'pk' in data['media']:
794+
# Return the newly created post_id
795+
return data['media']['pk']
719796
else:
720-
print(response)
721797
raise Exception("Failed to post. Got response:\n" + str(response))
722798
except Exception as e:
723799
print("[ERROR] ", e)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.