Skip to content

Commit

Permalink
Merge pull request #110 from stanvanrooy/various_things
Browse files Browse the repository at this point in the history
Various things
  • Loading branch information
stanvanrooy authored Nov 21, 2020
2 parents 8f73726 + dc10e96 commit 7c2823a
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 27 deletions.
33 changes: 33 additions & 0 deletions examples/ready/interact_with_followers_of_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os

from instauto.api.client import ApiClient
from instauto.ready.interact_with_followers_of_account import interact_with_followers_of_account


if __name__ == '__main__':
if os.path.isfile('./.instauto.save'):
client = ApiClient.initiate_from_file('./.instauto.save')
else:
client = ApiClient(user_name=os.environ.get("INSTAUTO_USER") or "your_username", password=os.environ.get("INSTAUTO_PASS") or "your_password")
client.login()
client.save_to_disk('./.instauto.save')

# This snippet, does the following:
# from the followers of the Instagram account:
# like between 1 and 5 posts of the follower
# comment on 0 to 1 posts of the user
# with the comment: "Looks good, {full_name}!", where {full_name} is replaced
# by the name set on the ig account
# follow 1 out of 10 followers retrieved
# wait between 20 and 120 seconds before moving on to the next follower

interact_with_followers_of_account(
client=client,
target="instagram",
delay=[20.0, 120.0],
duration=60.0 * 60.0,
likes_per_follower=[1, 5],
comments_per_follower=[0, 1],
follow_chance=10,
comments=["Looks good, {full_name}!"],
)
19 changes: 10 additions & 9 deletions instauto/api/actions/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from instauto.api.structs import DeviceProfile, IGProfile, State, Method
from instauto.api.constants import API_BASE_URL
from instauto.api.exceptions import WrongMethodException, IncorrectLoginDetails, InvalidUserId, BadResponse
from instauto.api.exceptions import WrongMethodException, IncorrectLoginDetails, InvalidUserId, BadResponse, AuthorizationError

logger = logging.getLogger(__name__)
logging.captureWarnings(True)
Expand Down Expand Up @@ -235,18 +235,17 @@ def _request(self, endpoint: str, method: Method, query: dict = None, data: Unio
return resp

def _check_response_for_errors(self, resp: requests.Response) -> None:
if resp.ok:
return

try:
parsed = resp.json()
except json.JSONDecodeError:
if not resp.ok:
if resp.status_code == 404 and '/friendships/' in resp.url:
raise InvalidUserId(f"account id: {resp.url.split('/')[-2]} is not recognized by Instagram or you do not have a relation with this account.")
if resp.status_code == 404 and '/friendships/' in resp.url:
raise InvalidUserId(f"account id: {resp.url.split('/')[-2]} is not recognized by Instagram or you do not have a relation with this account.")

logger.exception(f"response received: \n{resp.text}\nurl: {resp.url}\nstatus code: {resp.status_code}")
raise BadResponse("Received a non-200 response from Instagram")
return
if resp.ok:
return
logger.exception(f"response received: \n{resp.text}\nurl: {resp.url}\nstatus code: {resp.status_code}")
raise BadResponse("Received a non-200 response from Instagram")

if parsed.get('error_type') == 'bad_password':
raise IncorrectLoginDetails("Instagram does not recognize the provided login details")
Expand All @@ -261,4 +260,6 @@ def _check_response_for_errors(self, resp: requests.Response) -> None:
raise BadResponse("Something unexpected happened. Please check the IG app.")
if parsed.get('message') == 'rate_limit_error':
raise TimeoutError("Calm down. Please try again in a few minutes.")
if parsed.get('message') == 'Not authorized to view user':
raise AuthorizationError("This is a private user, which you do not follow.")
raise BadResponse("Received a non-200 response from Instagram")
5 changes: 3 additions & 2 deletions instauto/api/actions/structs/friendships.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ class _Base(cmmn.Base):
_uuid: str = ''

def __init__(self, user_id: str, surface: Surface = None, *args, **kwargs) -> None:
self.user_id = user_id
# user_id is returned as int by instagram. That makes it error prone,
# since sending the user_id as int will not work.
self.user_id = str(user_id)
self.surface = surface

super().__init__(*args, **kwargs)
self._exempt.append('user_id')
self._exempt.append('endpoint')


Expand Down
5 changes: 5 additions & 0 deletions instauto/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ class BadResponse(Exception):
class MissingValue(Exception):
"""Raised when an action struct is initiated with a missing value"""
pass


class AuthorizationError(Exception):
"""Raised when you try to get an object you're not authorized to get"""
pass
2 changes: 1 addition & 1 deletion instauto/helpers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from requests import Response


def _is_resp_ok(resp: Response) -> bool:
def is_resp_ok(resp: Response) -> bool:
if not resp.ok:
return False
if not resp.content:
Expand Down
38 changes: 38 additions & 0 deletions instauto/helpers/friendships.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from instauto.api.client import ApiClient
from instauto.api.actions.structs.friendships import GetFollowers, Create
from instauto.helpers.search import get_user_id_from_username

import typing
import logging
logger = logging.getLogger(__name__)

from .common import is_resp_ok


def get_followers(client: ApiClient, user_id: str, limit: int) -> typing.List[dict]:
obj = GetFollowers(user_id)

obj, result = client.followers_get(obj)
followers = []
while result and len(followers) < limit:
followers.extend(
result.json()["users"]
)
logger.info("Retrieved {} followers, {} more to go.".format(len(followers), limit - len(followers)))
obj, result = client.followers_get(obj)
return followers[:min(len(followers), limit)]


def follow_user(client: ApiClient, user_id: str = None, username: str = None) -> bool:
if user_id is not None and username is not None:
raise ValueError("Both `user_id` and `username` are provided.")

if user_id is None and username is not None:
user_id = get_user_id_from_username(client, username)

if user_id is None:
raise ValueError("Both `user_id` and `username` are not provided.")

obj = Create(user_id)
resp = client.user_follow(obj)
return is_resp_ok(resp)
20 changes: 10 additions & 10 deletions instauto/helpers/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from instauto.api.client import ApiClient
from instauto.api.actions import post as ps

from .common import _is_resp_ok
from .common import is_resp_ok
from .search import get_user_id_from_username

import logging
Expand All @@ -17,15 +17,15 @@ def upload_image_to_feed(client: ApiClient, image_path: str, caption: str = None
location=location,
)
resp = client.post_post(post, 80).ok
return _is_resp_ok(resp)
return is_resp_ok(resp)


def upload_image_to_story(client: ApiClient, image_path: str) -> bool:
post = ps.PostStory(
path=image_path
)
resp = client.post_post(post)
return _is_resp_ok(resp)
return is_resp_ok(resp)


def update_caption(client: ApiClient, media_id: str, new_caption: str) -> bool:
Expand All @@ -34,23 +34,23 @@ def update_caption(client: ApiClient, media_id: str, new_caption: str) -> bool:
caption_text=new_caption
)
resp = client.post_update_caption(caption)
return _is_resp_ok(resp)
return is_resp_ok(resp)


def unlike_post(client: ApiClient, media_id: str) -> bool:
like = ps.Unlike(
media_id=media_id
)
resp = client.post_unlike(like)
return _is_resp_ok(resp)
return is_resp_ok(resp)


def save_post(client: ApiClient, media_id: str) -> bool:
save = ps.Save(
media_id=media_id
)
resp = client.post_save(save)
return _is_resp_ok(resp)
return is_resp_ok(resp)


def retrieve_posts_from_user(client: ApiClient, limit: int, username: str = None, user_id: str = None) -> List[dict]:
Expand All @@ -59,8 +59,8 @@ def retrieve_posts_from_user(client: ApiClient, limit: int, username: str = None

if username is not None and user_id is None:
user_id = get_user_id_from_username(client, username)
else:
logger.warning("user_id is always being used.")
elif username is not None and user is not None:
logger.warning("Both `username` and `user_id` are provided. `user_id` will be used.")

obj = ps.RetrieveByUser(
user_id=user_id
Expand Down Expand Up @@ -100,7 +100,7 @@ def like_post(client: ApiClient, media_id: str) -> bool:
media_id=media_id
)
resp = client.post_like(like)
return _is_resp_ok(resp)
return is_resp_ok(resp)


def comment_post(client: ApiClient, media_id: str, comment: str) -> bool:
Expand All @@ -109,4 +109,4 @@ def comment_post(client: ApiClient, media_id: str, comment: str) -> bool:
comment_text=comment
)
resp = client.post_comment(comment)
return _is_resp_ok(resp)
return is_resp_ok(resp)
5 changes: 3 additions & 2 deletions instauto/helpers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ def search_username(client: ApiClient, username, count: int) -> List[dict]:

def get_user_by_username(client: ApiClient, username: str) -> dict:
users = search_username(client, username, 1)
if users[0]['username'] == username:
return users[0]
correct_user = [x for x in users if x['username'] == username]
if correct_user:
return correct_user[0]


def get_user_id_from_username(client: ApiClient, username: str):
Expand Down
1 change: 1 addition & 0 deletions instauto/ready/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

24 changes: 24 additions & 0 deletions instauto/ready/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import random
import string
from typing import List


def _get_fill_ins_from_comment(comment: str) -> List[str]:
"""Get all format strings in the comment"""
return [tup[1] for tup in string.Formatter().parse(comment) if tup[1] is not None]


def fill_comment(comment: str, user: dict) -> str:
"""Fill in the format strings in a comment from the user object. Currently, only top-level keys are supported."""
fill_ins = _get_fill_ins_from_comment(comment)
for fill_in in fill_ins:
comment = comment.format(**{fill_in: user[fill_in]})
return comment


def get_random_num(sr: int, er: int, exclude: List[int]) -> int:
n = random.randint(sr, er)
while n in exclude:
n = random.randint(sr, er)
exclude.append(n)
return n
87 changes: 87 additions & 0 deletions instauto/ready/interact_with_followers_of_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import random
import time
import logging
import typing

from instauto.helpers.search import *
from instauto.helpers.post import retrieve_posts_from_user, like_post, comment_post
from instauto.helpers.friendships import get_followers, follow_user
from instauto.api.exceptions import AuthorizationError

from helpers import fill_comment, get_random_num

logger = logging.getLogger("Instauto")
logger.setLevel(logging.INFO)


def _get_posts(client: ApiClient, limit: int, user_id: str) -> typing.Tuple[List[dict], str]:
"""Helper function for retrieving posts from a user"""
try:
# if the List is empty and no `AuthorizationError` was thrown, we assume the user has no posts
return retrieve_posts_from_user(client, limit, user_id=user_id), "User has no posts"
except AuthorizationError as e:
return [], str(e)


def _do_likes(client: ApiClient, limit: int, posts: List[dict]) -> None:
already_liked = []
while len(already_liked) < limit:
pi = get_random_num(0, len(posts) - 1, already_liked)
post = posts[pi]
logger.info(f"Liking post {post['id']} of {post['user']['username']}")
like_post(client, post['id'])


def _do_comments(client: ApiClient, limit: int, comments: List[str], user: dict, posts: List[dict]) -> None:
already_commented_comments = []
already_commented_posts = []

while len(already_commented_posts) < limit:
# get random comment
comment = comments[get_random_num(0, len(comments) - 1, already_commented_comments)]
comment = fill_comment(comment, user)

# get random post
pi = get_random_num(0, len(posts) - 1, already_commented_posts)
post = posts[pi]
logger.info(f"Commenting {comment} on post {post['id']} of user {user['username']}")
# comment random comment on random post
comment_post(client, post['id'], comment)


def interact_with_followers_of_account(client: ApiClient, target: str, delay: List[float, float], duration: float,
likes_per_follower: List[int, int], comments_per_follower: List[int, int],
follow_chance: int, comments: List[str] = None):

if comments_per_follower[1] > 0 and comments is None:
raise ValueError("No comments provided.")
if comments_per_follower[1] > len(comments):
raise ValueError("The limit for comments per user is set higher then the length of the set of comments")
if follow_chance < 0 or follow_chance > 100:
raise ValueError("Follow chance needs to be a value between 0 and 100")

target_user_id = get_user_id_from_username(client, target)
followers = get_followers(client, target_user_id, int(duration // delay))
logger.info(f"Retrieved a total of {len(followers)} followers from {target}")

for follower in followers:
logger.info(f"Retrieving posts from {follower['username']}")

posts, message = _get_posts(client, likes_per_follower[1] + comments_per_follower[1], follower['pk'])
if not posts:
logger.info(f"Can't retrieve posts from {follower['username']}: {message}")
continue

logger.info(f"Retrieved {len(posts)} posts from {follower['username']}")
_do_likes(client, min(random.randint(*likes_per_follower), len(posts)), posts)
_do_comments(client, min(random.randint(*comments_per_follower), len(posts)), comments, follower, posts)

r = random.randint(0, 100)
if r < int(follow_chance * 100):
logger.info(f"Following user {follower['username']}")
follow_user(client, user_id=follower['pk'])
time.sleep(delay)


# TODO: add de-duplication functionality, so we do not like/comment on the same user multiple times.
# TODO: add multi-level comment fill-in functionality
3 changes: 2 additions & 1 deletion original_requests/friendships/create.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"radio_type": "-none",
"_uid": "",
"device_id": "",
"_uuid": ""
"_uuid": "",
"user_id": ""
},
"response": {
"friendship_status": {
Expand Down
3 changes: 2 additions & 1 deletion original_requests/friendships/destroy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"_csrftoken": "JWNQYfQRyhE5cHXW4Pqp7nw2z7jc3SoE",
"radio_type": "wifi-none",
"_uid":"4478472759",
"_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268"
"_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268",
"user_id": ""
},
"response": {
"friendship_status": {
Expand Down
3 changes: 2 additions & 1 deletion original_requests/friendships/remove.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"_csrftoken": "JWNQYfQRyhE5cHXW4Pqp7nw2z7jc3SoE",
"radio_type": "wifi-none",
"_uid": "4478472759",
"_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268"
"_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268",
"user_id": ""
},
"response": {
"friendship_status": {
Expand Down

0 comments on commit 7c2823a

Please sign in to comment.