Skip to content

Commit

Permalink
Merge pull request #5 from niuware/development
Browse files Browse the repository at this point in the history
Add 'Comments' endpoint
  • Loading branch information
niuware authored Nov 15, 2019
2 parents 45bdd63 + a92b5e8 commit 5c756a3
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 46 deletions.
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ instpector.logout()
```

## Using 2FA
For login in using two-factor authentication, generate your 2fa key once on Instagram's app and provide the code when logging in with `instpector`. The following example uses `pytop` to demonstrate the usage:
For login in using two-factor authentication, generate your 2fa key once on the Instagram's app and provide the code when logging in with `instpector`. The following example uses `pytop` library to demonstrate the usage:

```python
from pyotp import TOTP
Expand All @@ -51,6 +51,7 @@ Check out more examples [here](https://github.com/niuware/instpector/tree/master
- Followers
- Following
- Timeline
- Comments
- Profile
- Story Reel
- Story
Expand All @@ -65,15 +66,15 @@ More to come

|Method|Details|
|---|---|
|login(user, password, two_factor_code=None)|Login to an Instagram account. If your account is 2FA protected provide the 2FA code as in the [provided example](https://github.com/niuware/instpector/blob/master/examples/two_factor_auth.py).|
|login(user: `string`, password: `string`, two_factor_code: `string` = None)|Login to an Instagram account. If your account is 2FA protected provide the 2FA code as in the [provided example](https://github.com/niuware/instpector/blob/master/examples/two_factor_auth.py).|
|logout()|Logouts from an Instagram account|
|session()|Returns the current session used by `instpector`|

`EndpointFactory`

|Method|Details|
|---|---|
|create(endpoint_name, instpector_instance)|Creates and returns an endpoint instance based on the provided name. Available endpoint names are: `"followers"`, `"following"`, `"profile"`, `"timeline"`, `"story_reel"` and `"story"`|
|create(endpoint_name: `string`, instpector_instance: `Instpector`)|Creates and returns an endpoint instance based on the provided name. Available endpoint names are: `"followers"`, `"following"`, `"profile"`, `"timeline"`, `"comments"` `"story_reel"` and `"story"`|

## Endpoints

Expand All @@ -83,51 +84,62 @@ Gets the profile of any public or friend user account.

|Method|Details|
|---|---|
|of_user(username)|Returns a `TProfile` instance for the provided username.|
|of_user(username: `string`)|Returns a `TProfile` instance for the provided username.|

### Followers

Endpoint for accessing the follower list of any public or friend user account.

|Method|Details|
|---|---|
|of_user(user_id)|Returns a generator of `TUser` instances with all followers. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|
|of_user(user_id: `string`)|Returns a generator of `TUser` instances with all followers. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|

### Following

Endpoint for accessing the followees list of any public or friend user account.

|Method|Details|
|---|---|
|of_user(user_id)|Returns a generator of `TUser` instances with all followees. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|
|of_user(user_id: `string`)|Returns a generator of `TUser` instances with all followees. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|

### Timeline

Endpoint for accessing the timeline of any public or friend account.

|Method|Details|
|---|---|
|of_user(user_id)|Returns a generator of `TTimelinePost` instances with all timeline posts. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|
|download(timeline_post, only_image=False, low_quality=False)|Downloads and save the available resources (image and video) for the provided `TTimelinePost`. The file name convention is `ownerid_resourceid.extension` and saved in the execution directory. If `low_quality` is `True` the resource will be the downloaded with the lowest size available (only for image). If `only_image` is `True` a video file resource won't be downloaded.|
|like(timeline_post)|Likes a timeline post (`TTimelinePost`).|
|unlike(timeline_post)|Unlikes a timeline post (`TTimelinePost`).|
|of_user(user_id: `string`)|Returns a generator of `TTimelinePost` instances with all timeline posts. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|
|download(timeline_post: `TTimelinePost`, only_image: `bool` = False, low_quality: `bool` = False)|Downloads and save the available resources (image and video) for the provided `TTimelinePost`. The file name convention is `ownerid_resourceid.extension` and saved in the execution directory. If `low_quality` is `True` the resource will be the downloaded with the lowest size available (only for image). If `only_image` is `True` a video file resource won't be downloaded.|
|like(timeline_post: `TTimelinePost`)|Likes a timeline post.|
|unlike(timeline_post: `TTimelinePost`)|Unlikes a timeline post.|

### Comments

Endponint for accessing comments and threaded comments of any public or friends post or comment.

|Method|Details|
|---|---|
|of_post(timeline_post: `TTimelinePost`)|Returns a generator of `TComment` instances with all post comments.|
|of_comment(comment: `TComment`)|Returns a generator of `TComment` instances with all threaded comments of a comment.|
|like(comment: `TComment`)|Likes a comment.|
|unlike(comment: `TComment`)|Unlikes a comment.|

### StoryReel

Endpoint for accessing the story reel (stories) of any public or friend user account.

|Method|Details|
|---|---|
|of_user(user_id)|Returns a generator of `TStoryReelItem` instances with all stories. Note the method receives a user id and not a username. To get the user id use the `Profile` endpoint.|
|download(story_item, only_image=False, low_quality=False)|Downloads and save the available resources (image and video) for the provided `TStoryReelItem`. The file name convention is `ownerid_resourceid.extension` and saved in the execution directory. If `low_quality` is `True` the resource will be the downloaded with the lowest size available. If `only_image` is `True` a video file resource won't be downloaded.|
|of_user(user_id: `string`)|Returns a generator of `TStoryReelItem` instances with all stories. Note the method receives a user id and not a username. To get a user id use the `Profile` endpoint.|
|download(story_item: `TStoryReelItem`, only_image: `bool` = False, low_quality: `bool` = False)|Downloads and save the available resources (image and video) for the provided `TStoryReelItem`. The file name convention is `ownerid_resourceid.extension` and saved in the execution directory. If `low_quality` is `True` the resource will be the downloaded with the lowest size available. If `only_image` is `True` a video file resource won't be downloaded.|

### Story

Endpoint for accessing the story details of a story reel item. This endpoint is only available for stories posted by the current logged in user.

|Method|Details|
|---|---|
|viewers_for(story_id)|Returns a generator of `TStoryViewer` instances with all viewers of the provided story id.|
|viewers_for(story_id: `string`)|Returns a generator of `TStoryViewer` instances with all viewers of the provided story id.|

## Types

Expand Down Expand Up @@ -163,6 +175,17 @@ Endpoint for accessing the story details of a story reel item. This endpoint is
|display_resources|`list`|A list of image URLs associated with the post|
|video_url|`string`|The video URL (if available) associated with the post|

### TComment
|Field|Type|Details|
|---|---|---|
|id|`string`|The Instagram Id of the comment|
|text|`string`|The comment text|
|username|`string`|The author's username|
|timestamp|`integer`|The timestamp of the comment|
|viewer_has_liked|`bool`|A flag to know if the viewer liked the comment|
|liked_count|`integer`|The like count of the comment|
|thread_count|`integer` \| `None`|The comment's thread comments count. This value is `None` if the instance is a threaded comment.|

### TStoryReelItem
|Field|Type|Details|
|---|---|---|
Expand Down
3 changes: 3 additions & 0 deletions instpector/apis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ class ParseDataException(InstpectorException):

class NotImplementedException(InstpectorException):
pass

class NoDataException(InstpectorException):
pass
4 changes: 3 additions & 1 deletion instpector/apis/instagram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .timeline import Timeline
from .story_reel import StoryReel
from .story import Story
from .comments import Comments

__all__ = ["Authenticate",
"Followers",
Expand All @@ -14,5 +15,6 @@
"Profile",
"Timeline",
"StoryReel",
"Story"
"Story",
"Comments"
]
9 changes: 6 additions & 3 deletions instpector/apis/instagram/base_graph_ql.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from ..exceptions import ParseDataException
from ..exceptions import ParseDataException, NoDataException
from .base_api import BaseApi
from .parser import Parser
from .definitions import TPageInfo
Expand All @@ -22,8 +22,11 @@ def _loop(self, query_hash, variables, **parser_callbacks):
if data:
page_info = Parser.page_info(data, parser_callbacks.get("page_info_parser"),
parser_callbacks.get("page_info_parser_path"))
for result in parser_callbacks.get("data_parser")(data):
yield result
try:
for result in parser_callbacks.get("data_parser")(data):
yield result
except NoDataException:
return

def _get_partial_data(self, query_hash, variables, end_cursor, cursor_name):
cursor_name = cursor_name or "after"
Expand Down
36 changes: 36 additions & 0 deletions instpector/apis/instagram/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from .like_graph_ql import LikeGraphQL
from .parser import Parser


class Comments(LikeGraphQL):

def __init__(self, instance):
super().__init__(instance, "/web/comments/{action}/{id}/")

def of_post(self, timeline_post):
shortcode = getattr(timeline_post, "shortcode", None)
if shortcode is None:
return []
variables = {
"shortcode": shortcode,
"first": self.DEFAULT_EDGE_COUNT
}
return self._loop("97b41c52301f77ce508f55e66d17620e",
variables=variables,
page_info_parser="edge_media_to_parent_comment",
page_info_parser_path="shortcode_media",
data_parser=Parser.parent_comments)

def of_comment(self, comment):
comment_id = getattr(comment, "id", None)
if comment_id is None:
return []
variables = {
"comment_id": comment_id,
"first": 6
}
return self._loop("51fdd02b67508306ad4484ff574a0b62",
variables=variables,
page_info_parser="edge_threaded_comments",
page_info_parser_path="comment",
data_parser=Parser.threaded_comments)
6 changes: 5 additions & 1 deletion instpector/apis/instagram/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
))

TTimelinePost = namedtuple("TTimelinePost", (
"id owner timestamp is_video like_count comment_count display_resources video_url"
"id owner timestamp is_video like_count comment_count display_resources video_url shortcode"
))

TStoryReelItem = namedtuple("TStoryReelItem", (
"id owner timestamp expire_at audience is_video view_count display_resources video_resources"
))

TStoryViewer = namedtuple("TStoryViewer", "id username")

TComment = namedtuple("TComment", (
"id text timestamp username viewer_has_liked liked_count, thread_count"
))
28 changes: 28 additions & 0 deletions instpector/apis/instagram/like_graph_ql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from .base_graph_ql import BaseGraphQL


class LikeGraphQL(BaseGraphQL):

def __init__(self, instance, url):
self._url = url
super().__init__(instance)

def _toggle_like(self, obj, action):
endpoint = 'like' if action == 'like' else 'unlike'
obj_id = getattr(obj, "id", None)
if obj_id is None:
return False
response = self.post(self._url.format(action=endpoint, id=obj_id),
use_auth=True,
headers={
"Content-Type": "application/x-www-form-urlencoded"
})
if response and response.get("status") == "ok":
return True
return False

def unlike(self, obj):
return self._toggle_like(obj, 'unlike')

def like(self, obj):
return self._toggle_like(obj, 'like')
50 changes: 45 additions & 5 deletions instpector/apis/instagram/parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .definitions import TUser, TPageInfo, TProfile, TTimelinePost, TStoryReelItem, TStoryViewer
from .definitions import TUser, TPageInfo, TProfile, TTimelinePost, \
TStoryReelItem, TStoryViewer, TComment
from ..exceptions import NoDataException


class Parser:
Expand Down Expand Up @@ -66,7 +68,8 @@ def timeline(data):
like_count=likes.get("count", 0),
comment_count=comments.get("count", 0),
display_resources=list(map(lambda res: res.get("src"), display_resources)),
video_url=node.get("video_url")
video_url=node.get("video_url"),
shortcode=node.get("shortcode")
)
yield post

Expand All @@ -76,7 +79,10 @@ def _get_edges(data, endpoint, d_path=None):
data_root = data.get("data") or {}
root = data_root.get(data_path) or {}
endpoint_root = root.get(endpoint) or {}
return endpoint_root.get("edges") or []
edges = endpoint_root.get("edges")
if not edges:
raise NoDataException
return edges

@staticmethod
def story_reel(data):
Expand Down Expand Up @@ -104,11 +110,45 @@ def story_reel(data):

@staticmethod
def story(data):
edges = Parser._get_edges(data, "edge_story_media_viewers", "media")
for edge in edges:
for edge in Parser._get_edges(data, "edge_story_media_viewers", "media"):
node = edge.get("node") or {}
viewer = TStoryViewer(
id=node.get("id"),
username=node.get("username")
)
yield viewer

@staticmethod
def parent_comments(data):
for edge in Parser._get_edges(data, "edge_media_to_parent_comment", "shortcode_media"):
node = edge.get("node") or {}
owner = node.get("owner") or {}
edge_liked = node.get("edge_liked_by") or {}
edge_threaded = node.get("edge_threaded_comments") or {}
comment = TComment(
id=node.get("id", ""),
text=node.get("text", ""),
timestamp=node.get("created_at"),
username=owner.get("username", ""),
viewer_has_liked=node.get("viewer_has_liked", False),
liked_count=edge_liked.get("count", 0),
thread_count=edge_threaded.get("count", 0)
)
yield comment

@staticmethod
def threaded_comments(data):
for edge in Parser._get_edges(data, "edge_threaded_comments", "comment"):
node = edge.get("node") or {}
owner = node.get("owner") or {}
edge_liked = node.get("edge_liked_by") or {}
comment = TComment(
id=node.get("id", ""),
text=node.get("text", ""),
timestamp=node.get("created_at"),
username=owner.get("username", ""),
viewer_has_liked=node.get("viewer_has_liked", False),
liked_count=edge_liked.get("count", 0),
thread_count=None
)
yield comment
27 changes: 5 additions & 22 deletions instpector/apis/instagram/timeline.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from .base_graph_ql import BaseGraphQL
from .like_graph_ql import LikeGraphQL
from .parser import Parser


class Timeline(BaseGraphQL):
class Timeline(LikeGraphQL):

def __init__(self, instance):
super().__init__(instance, "/web/likes/{id}/{action}/")

def of_user(self, user_id):
variables = {
Expand All @@ -20,23 +23,3 @@ def download(self, timeline_post, only_image=False, low_quality=False):
if timeline_post.video_url:
file_name = f"{timeline_post.owner}_{timeline_post.id}.mp4"
super().download_file(timeline_post.video_url, file_name)

def _toggle_like(self, timeline_post, action):
endpoint = 'like' if action == 'like' else 'unlike'
post_id = getattr(timeline_post, "id", None)
if post_id is None:
return False
response = self.post("/web/likes/" + post_id + "/" + endpoint + "/",
use_auth=True,
headers={
"Content-Type": "application/x-www-form-urlencoded"
})
if response and response.get("status") == "ok":
return True
return False

def unlike(self, timeline_post):
return self._toggle_like(timeline_post, 'unlike')

def like(self, timeline_post):
return self._toggle_like(timeline_post, 'like')
1 change: 1 addition & 0 deletions instpector/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
factory.register_endpoint('timeline', instagram.Timeline)
factory.register_endpoint('story_reel', instagram.StoryReel)
factory.register_endpoint('story', instagram.Story)
factory.register_endpoint('comments', instagram.Comments)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="instpector",
version="0.2.3",
version="0.2.4",
description="A simple Instagram's web API library",
author="Erik Lopez",
long_description=README,
Expand Down

0 comments on commit 5c756a3

Please sign in to comment.