From 1a5b42706aa4fc78d396117f0ba3935d4258293b Mon Sep 17 00:00:00 2001 From: Erik Lopez Date: Mon, 18 Nov 2019 12:13:08 +0900 Subject: [PATCH] Add activity, follow and unfollow methods to 'Profile' API --- README.md | 95 ++++++++++++++---------- examples/follow_profile.py | 31 ++++++++ instpector/apis/instagram/definitions.py | 6 ++ instpector/apis/instagram/parser.py | 30 +++++++- instpector/apis/instagram/profile.py | 24 ++++++ setup.py | 2 +- 6 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 examples/follow_profile.py diff --git a/README.md b/README.md index de3133e..a88a6fa 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,15 @@ More to come |Method|Details| |---|---| -|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`| +|login(user: `str`, password: `str`, two_factor_code: `str` = None) -> `bool`|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() -> `Session`|Returns the current session used by `instpector`.| `EndpointFactory` |Method|Details| |---|---| -|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"`| +|create(endpoint_name: `str`, 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 @@ -84,7 +84,10 @@ Gets the profile of any public or friend user account. |Method|Details| |---|---| -|of_user(username: `string`)|Returns a `TProfile` instance for the provided username.| +|of_user(username: `str`) -> `TProfile`|Returns a `TProfile` instance for the provided username.| +|follow(user: `TProfile` \| `str`) -> `bool`|Follows a user. You can provide a `TProfile` instance or an Instagram's user Id.| +|unfollow(user: `TProfile` \| `str`) -> `bool`|Unfollows a user. You can provide a `TProfile` instance or an Instagram's user Id.| +|activity() -> `TActivity`|Yields a list of `TActivity` items for the current logged in account.| ### Followers @@ -92,7 +95,7 @@ Endpoint for accessing the follower list of any public or friend user account. |Method|Details| |---|---| -|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.| +|of_user(user_id: `str`) -> `TUser`|Yields a list 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 @@ -100,7 +103,7 @@ Endpoint for accessing the followees list of any public or friend user account. |Method|Details| |---|---| -|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.| +|of_user(user_id: `str`) -> `TUser` |Yields a list 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 @@ -108,10 +111,10 @@ Endpoint for accessing the timeline of any public or friend account. |Method|Details| |---|---| -|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.| +|of_user(user_id: `str`) -> `TTimelinePost`|Yields a list 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.| +|like(timeline_post: `TTimelinePost` \| `TActivityPost`) -> `bool`|Likes a post.| +|unlike(timeline_post: `TTimelinePost` \| `TActivityPost`) -> `bool`|Unlikes a post.| ### Comments @@ -119,12 +122,12 @@ Endponint for accessing comments and threaded comments of any public or friends |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.| -|add(timeline_post: `TTimelinePost`, text: `string`, parent_comment: `TComment` = None) -> `TComment` \| `None`|Adds a new comment to a post. You can reply to a comment if `parent_comment` argument is provided. An instance of the created comment is return if succeeded otherwise `None`.| -|remove(timeline_post: `TTimelinePost`, comment: `TComment`)|Removes a comment from a post. Only comments authored by the current logged in account can be removed.| +|of_post(timeline_post: `TTimelinePost` \| `TActivityPost`) -> `TComment`|Yields a list of `TComment` instances with all post comments.| +|of_comment(comment: `TComment`) -> `TComment`|Yields a list of `TComment` instances with all threaded comments of a comment.| +|like(comment: `TComment`) -> `bool`|Likes a comment.| +|unlike(comment: `TComment`) -> `bool`|Unlikes a comment.| +|add(timeline_post: `TTimelinePost` \| `TActivityPost`, text: `str`, parent_comment: `TComment` = None) -> `TComment` \| `None`|Adds a new comment to a post. You can reply to a comment if `parent_comment` argument is provided. An instance of the created comment is return if succeeded otherwise `None`.| +|remove(timeline_post: `TTimelinePost` \| `TActivityPost`, comment: `TComment`) -> `bool`|Removes a comment from a post. Only comments authored by the current logged in account can be removed.| ### StoryReel @@ -132,7 +135,7 @@ Endpoint for accessing the story reel (stories) of any public or friend user acc |Method|Details| |---|---| -|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.| +|of_user(user_id: `str`) -> `TStoryReelItem`|Yields a list 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 @@ -141,7 +144,7 @@ Endpoint for accessing the story details of a story reel item. This endpoint is |Method|Details| |---|---| -|viewers_for(story_id: `string`)|Returns a generator of `TStoryViewer` instances with all viewers of the provided story id.| +|viewers_for(story_id: `str`) -> `TStoryViewer`|Yields a list of `TStoryViewer` instances with all viewers of the provided story id.| ## Types @@ -149,18 +152,18 @@ Endpoint for accessing the story details of a story reel item. This endpoint is |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the user| -|username|`string`|The user's name| -|full_name|`string`|The full name of the user| +|id|`str`|The Instagram Id of the user| +|username|`str`|The user's name| +|full_name|`str`|The full name of the user| |is_private|`bool`|A flag to show if the user account is private| ### TProfile |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the user| -|username|`string`|The user's name| -|biography|`string`|The biography of the user| +|id|`str`|The Instagram Id of the user| +|username|`str`|The user's name| +|biography|`str`|The biography of the user| |is_private|`bool`|A flag to show if the user account is private| |followers_count|`integer`|The follower count of the user| |following_count|`integer`|The following count of the user| @@ -168,23 +171,23 @@ Endpoint for accessing the story details of a story reel item. This endpoint is ### TTimelinePost |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the post| -|shortcode|`string`|The Instagram shortcode Id of the post| -|owner|`string`|The post author's Instagram Id| +|id|`str`|The Instagram Id of the post| +|shortcode|`str`|The Instagram shortcode Id of the post| +|owner|`str`|The post author's Instagram Id| |timestamp|`integer`|The created timestamp of the post| -|caption|`string`|The caption of the post| +|caption|`str`|The caption of the post| |is_video|`bool`|A flag to know if the post is a video| |like_count|`integer`|The like count of the post| |comment_count|`integer`|The comment count of the post| |display_resources|`list`|A list of image URL strings associated with the post| -|video_url|`string`|The video URL (if available) associated with the post| +|video_url|`str`|The video URL (if available) associated with the post| ### TComment |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the comment| -|text|`string`|The text of the comment| -|username|`string`|The author's username| +|id|`str`|The Instagram Id of the comment| +|text|`str`|The text of the comment| +|username|`str`|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| @@ -193,21 +196,37 @@ Endpoint for accessing the story details of a story reel item. This endpoint is ### TStoryReelItem |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the story| -|owner|`string`|The story author's Instagram Id| +|id|`str`|The Instagram Id of the story| +|owner|`str`|The story author's Instagram Id| |timestamp|`integer`|The created timestamp of the story| |expire_at|`integer`|The expiration timestamp of the story| -|audience|`string`|The type of audience of the story. If public the value is `MediaAudience.DEFAULT`, if private the value is `MediaAudience.BESTIES`| +|audience|`str`|The type of audience of the story. If public the value is `MediaAudience.DEFAULT`, if private the value is `MediaAudience.BESTIES`| |is_video|`bool`|A flag to know if the story is a video| -|view_count|`integer`|The view count of the story. The count is only available for stories posted by the currently logged in user. Other accounts will have a count equal to `0`.| +|view_count|`integer`|The view count of the story. The count is only available for stories posted by the currently logged in user. Other accounts will have a count equal to `0`| |display_resources|`list`|A list of image URL strings associated with the story| |video_resources|`list`|A list of video URL strings associated with the story| ### TStoryViewer |Field|Type|Details| |---|---|---| -|id|`string`|The Instagram Id of the story viewer| -|username|`string`|The user name of the viewer| +|id|`str`|The Instagram Id of the story viewer| +|username|`str`|The user name of the viewer| + +### TActivity +|Field|Type|Details| +|---|---|---| +|id|`str`|The Instagram Id of the activity| +|timestamp|`integer`|The timestamp of the activity| +|username|`str`|The user name linked to the activity| +|activity_type|`str`|The activity type. Either `NEW_LIKE` or `NEW_FOLLOW`| +|liked_post|`TActivityPost` \| `None`|If the activity type is `NEW_LIKE`, an `TActivityPost` instance is returned| + +### TActivityPost +|Field|Type|Details| +|---|---|---| +|id|`str`|The Instagram Id of the post| +|shortcode|`str`|The Instagram shortcode Id of the post| +|thumbnail_resources|`list`|A list of thumbnails URL strings associated with the post| # Development dependencies diff --git a/examples/follow_profile.py b/examples/follow_profile.py new file mode 100644 index 0000000..97e526a --- /dev/null +++ b/examples/follow_profile.py @@ -0,0 +1,31 @@ +from sys import argv +from context import Instpector, endpoints + +def get_profile(**options): + + instpector = Instpector() + if not instpector.login(user=options.get("user"), password=options.get("password")): + return + + profile = endpoints.factory.create("profile", instpector) + + print(profile.follow(options.get("target_username"))) + ### Unfollow + # profile.unfollow(options.get("target_username")) + + instpector.logout() + +if __name__ == '__main__': + if len(argv) < 6: + print(( + "Missing arguments: " + "--user {user} " + "--password {password} " + "--target_username {username}" + )) + exit(1) + get_profile( + user=argv[2], + password=argv[4], + target_username=argv[6] + ) diff --git a/instpector/apis/instagram/definitions.py b/instpector/apis/instagram/definitions.py index a58cf61..0a1d330 100644 --- a/instpector/apis/instagram/definitions.py +++ b/instpector/apis/instagram/definitions.py @@ -22,3 +22,9 @@ TComment = namedtuple("TComment", ( "id text timestamp username viewer_has_liked liked_count, thread_count" )) + +TActivity = namedtuple("TActivity", "id timestamp username activity_type liked_post") + +TActivityPost = namedtuple("TActivityPost", ( + "id shortcode thumbnail_resources" +)) diff --git a/instpector/apis/instagram/parser.py b/instpector/apis/instagram/parser.py index bb6ab23..9800827 100644 --- a/instpector/apis/instagram/parser.py +++ b/instpector/apis/instagram/parser.py @@ -1,5 +1,5 @@ from .definitions import TUser, TPageInfo, TProfile, TTimelinePost, \ - TStoryReelItem, TStoryViewer, TComment + TStoryReelItem, TStoryViewer, TComment, TActivity, TActivityPost from ..exceptions import NoDataException @@ -79,9 +79,9 @@ def timeline(data): yield post @staticmethod - def _get_edges(data, endpoint, d_path=None): + def _get_edges(data, endpoint, d_path=None, r_path='data'): data_path = d_path or "user" - data_root = data.get("data") or {} + data_root = data.get(r_path) or {} root = data_root.get(data_path) or {} endpoint_root = root.get(endpoint) or {} edges = endpoint_root.get("edges") @@ -157,3 +157,27 @@ def threaded_comments(data): thread_count=None ) yield comment + + @staticmethod + def activity(data): + graphql = data.get("graphql") or {} + for edge in Parser._get_edges(graphql, "edge_web_activity_feed", "activity_feed", "user"): + node = edge.get("node") or {} + liked_media = node.get("media") or {} + liked_post = None + user = node.get("user") or {} + if liked_media: + liked_post = TActivityPost( + id=liked_media.get("id"), + thumbnail_resources=list(map(lambda res: res.get("src"), + liked_media.get("thumbnail_resources", []))), + shortcode=liked_media.get("shortcode") + ) + activity = TActivity( + id=node.get("id", ""), + username=user.get("username"), + timestamp=node.get("timestamp"), + liked_post=liked_post, + activity_type="NEW_LIKE" if liked_post else "NEW_FOLLOW" + ) + yield activity diff --git a/instpector/apis/instagram/profile.py b/instpector/apis/instagram/profile.py index f85122b..6132d30 100644 --- a/instpector/apis/instagram/profile.py +++ b/instpector/apis/instagram/profile.py @@ -21,3 +21,27 @@ def of_user(self, username): except ParseDataException: print(f"Invalid data for username {username}") return None + + def activity(self): + data = super().get("/accounts/activity/?__a=1&include_reel=true") + if data: + try: + return Parser.activity(data) + except ParseDataException: + pass + return None + + def _toggle_follow(self, user, action): + tuser = user + if isinstance(user, str): + tuser = self.of_user(user) + user_id = getattr(tuser, "id", "") + if user_id: + return super().quick_post(f"/web/friendships/{user_id}/{action}/") + return False + + def follow(self, user): + return self._toggle_follow(user, "follow") + + def unfollow(self, user): + return self._toggle_follow(user, "unfollow") diff --git a/setup.py b/setup.py index d1a2a85..2a768cf 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="instpector", - version="0.2.5", + version="0.2.6", description="A simple Instagram's web API library", author="Erik Lopez", long_description=README,