diff --git a/instauto/api/actions/authentication.py b/instauto/api/actions/authentication.py index ec0f4f2c..8b87ccbf 100644 --- a/instauto/api/actions/authentication.py +++ b/instauto/api/actions/authentication.py @@ -46,7 +46,7 @@ def log_in(self) -> None: raise e def change_password(self, new_password: str, current_password: Optional[str] = None) -> requests.Response: - cp = current_password or self._raw_password + cp = current_password or self._plain_password if cp is None: raise ValueError("No current password provided") @@ -88,7 +88,7 @@ def _encode_password(self, password: Optional[str] = None) -> Optional[str]: """Encrypts the raw password into a form that Instagram accepts.""" if not self.state.public_api_key: return - if not any([password, self._raw_password]): + if not any([password, self._plain_password]): return key = Random.get_random_bytes(32) @@ -104,7 +104,7 @@ def _encode_password(self, password: Optional[str] = None) -> Optional[str]: aes = AES.new(key, AES.MODE_GCM, nonce=iv) aes.update(str(time).encode('utf-8')) - encrypted_password, cipher_tag = aes.encrypt_and_digest(bytes(password or self._raw_password, 'utf-8')) + encrypted_password, cipher_tag = aes.encrypt_and_digest(bytes(password or self._plain_password, 'utf-8')) encrypted = bytes([1, int(self.state.public_api_key_id), diff --git a/instauto/api/actions/challenge.py b/instauto/api/actions/challenge.py index 3700accd..143b0497 100644 --- a/instauto/api/actions/challenge.py +++ b/instauto/api/actions/challenge.py @@ -14,10 +14,13 @@ class ChallengeMixin(StubMixin): def _handle_challenge(self, resp: requests.Response) -> bool: resp_data = self._json_loads(resp.text) - # pyre-ignore[6] + logger.debug('_handle_challenge -> resp_data: %s', resp_data) + if resp_data['message'] not in ('challenge_required', 'checkpoint_required'): raise BadResponse("Challenge required, but no URL provided.") - # pyre-ignore[6] + + assert 'challenge' in resp_data, f"'challenge' not found in resp_data" + assert 'api_path' in resp_data['challenge'], f"'api_path' not found in resp_data" api_path = resp_data['challenge']['api_path'][1:] resp = self._request( @@ -37,7 +40,6 @@ def _handle_challenge(self, resp: requests.Response) -> bool: "post": 1, } body = base_body.copy() - # pyre-ignore[16] body["choice"] = int(data.get("step_data", {}).get("choice", 0)) _ = self._request(endpoint=api_path, method=Method.POST, body=body) @@ -51,8 +53,8 @@ def _handle_challenge(self, resp: requests.Response) -> bool: def _handle_2fa(self, parsed: dict) -> None: endpoint = "accounts/two_factor_login/" username = parsed['two_factor_info']['username'] + code = self._get_2fa_code(username) - code = input(f"Enter 2fa code for {username}: ") if self._2fa_function is None else self._2fa_function(username) logger.debug("2fa code is: %s", code) # 1 = phone verification, 3 = authenticator app verification @@ -71,3 +73,9 @@ def _handle_2fa(self, parsed: dict) -> None: 'verification_method': verification_method } self._request(endpoint, Method.POST, body=body) + + def _get_2fa_code(self, username: str) -> str: + if self._2fa_function: + return self._2fa_function(username) + return input(f"Enter 2fa code for {username}: ") + diff --git a/instauto/api/actions/helpers.py b/instauto/api/actions/helpers.py index 0889d16b..a7ce2831 100644 --- a/instauto/api/actions/helpers.py +++ b/instauto/api/actions/helpers.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Union +from typing import Any, Union import orjson @@ -33,8 +33,9 @@ def _build_default_rupload_params(self, obj, quality: int, is_sidecar: bool) -> "is_sidecar": str(int(is_sidecar)) } - def _json_loads(self, text: Union[str, bytes, bytearray]) -> Union[dict, list]: + def _json_loads(self, text: Union[bytes, bytearray, memoryview, str]) -> Any: return orjson.loads(text) - def _json_dumps(self, obj: Union[dict, list]) -> str: + def _json_dumps(self, obj: Any) -> str: return orjson.dumps(obj).decode() + diff --git a/instauto/api/actions/post.py b/instauto/api/actions/post.py index e0dc4c1a..f4970d39 100644 --- a/instauto/api/actions/post.py +++ b/instauto/api/actions/post.py @@ -182,6 +182,7 @@ def post_carousel(self, posts: List[PostFeed], caption: str, quality: int) -> Di for i, post in enumerate(posts): responses[f'post{i}'] = self._upload_image(post, quality, True)[0] + breakpoint() responses['configure_sidecar'] = self._request('media/configure_sidecar/', Method.POST, body=data, headers=headers, sign_request=True) return responses diff --git a/instauto/api/actions/stub.py b/instauto/api/actions/stub.py index 1d5191b8..cd464e54 100644 --- a/instauto/api/actions/stub.py +++ b/instauto/api/actions/stub.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Callable, Union, Dict +from typing import Any, Callable, Optional, Union, Dict import requests @@ -27,11 +27,11 @@ def __call__(self, obj, quality: int, is_sidecar: bool) -> dict: ... class _json_loads: - def __call__(self, text: Union[str, bytes, bytearray]) -> Union[dict, list]: ... + def __call__(self, text: Union[bytes, bytearray, memoryview, str]) -> Any: ... class _json_dumps: - def __call__(self, obj: Union[dict, list]) -> str: ... + def __call__(self, obj: Any) -> str: ... class StubMixin: @@ -43,12 +43,12 @@ class StubMixin: _session: requests.Session _request_finished_callbacks: list _handle_challenge: Callable - _2fa_function: Callable[[str], str] - _handle_2fa: Callable[[dict], None] + _2fa_function: Optional[Callable[[str], str]] + _handle_2fa: Optional[Callable[[dict], None]] _request: _request - _username: str - _raw_password: str - _encoded_password: str + _username: Optional[str] + _plain_password: Optional[str] + _encoded_password: Optional[str] _gen_uuid: Callable[[], str] _get_image_type: _get_image_type _build_default_rupload_params: _build_default_rupload_params diff --git a/instauto/api/client.py b/instauto/api/client.py index 863cb63c..18532a92 100644 --- a/instauto/api/client.py +++ b/instauto/api/client.py @@ -15,7 +15,7 @@ from .actions.feed import FeedMixin from .actions.helpers import HelperMixin -from .structs import IGProfile, DeviceProfile, State, Inbox +from .structs import IGProfile, DeviceProfile, State from .constants import (DEFAULT_IG_PROFILE, DEFAULT_DEVICE_PROFILE, DEFAULT_STATE) from .exceptions import StateExpired, NoAuthDetailsProvided, CorruptedSaveData @@ -33,32 +33,39 @@ logging.captureWarnings(True) -class ApiClient(ProfileMixin, AuthenticationMixin, PostMixin, RequestMixin, FriendshipsMixin, - SearchMixin, ChallengeMixin, DirectMixin, HelperMixin, FeedMixin, ActivityMixin): +class ApiClient(ProfileMixin, AuthenticationMixin, PostMixin, + RequestMixin, FriendshipsMixin, SearchMixin, ChallengeMixin, + DirectMixin, HelperMixin, FeedMixin, ActivityMixin): breadcrumb_private_key = "iN4$aGr0m".encode() bc_hmac = hmac.HMAC(breadcrumb_private_key, digestmod='SHA256') def __init__( - self, ig_profile: Optional[IGProfile] = None, device_profile: Optional[DeviceProfile] = None, - state: Optional[State] = None, username: Optional[str] = None, password: Optional[str] = None, - session_cookies: Optional[dict] = None, testing=False, _2fa_function: Optional[Callable[[str], str]] = None + self, ig_profile: Optional[IGProfile] = None, device_profile: + Optional[DeviceProfile] = None, state: Optional[State] = None, + username: Optional[str] = None, password: Optional[str] = None, + session_cookies: Optional[dict] = None, testing=False, + _2fa_function: Optional[Callable[[str], str]] = None ) -> None: """Initializes all attributes. Can be instantiated with no params. - Needs to be provided with either: - 1) state and session_cookies, to resume an old session, in this case all other params are optional - 2) username and password, in this case all other params are optional + Needs to be provided with either: 1) state and session_cookies, + to resume an old session, in this case all other params are + optional 2) username and password, in this case all other params + are optional - In the case that the class is initialized without params, or with a few of the params not provided, - they will automatically be filled with default values from constants.py. + In the case that the class is initialized without params, or + with a few of the params not provided, they will automatically + be filled with default values from constants.py. - Using the default values should be fine for pretty much all use cases, but if for some reason you need to use - non-default values, that can be done by creating any of the profiles yourself and passing it in as an argument. + Using the default values should be fine for pretty much all use + cases, but if for some reason you need to use non-default + values, that can be done by creating any of the profiles + yourself and passing it in as an argument. """ super().__init__() self._2fa_function = _2fa_function self._username = username - self._raw_password = password + self._plain_password = password self._encoded_password = None self._init_ig_profile(ig_profile) @@ -67,7 +74,7 @@ def __init__( self._user_agent = self._build_user_agent() if (username is None or password is None) and (state is None or session_cookies is None) and not testing: - raise NoAuthDetailsProvided("Username, password and state are all not provided.") + raise NoAuthDetailsProvided("Neither a username and username or existing state is provided.") self._init_session(session_cookies, testing) self._request_finished_callbacks = [self._update_state_from_headers] diff --git a/requirements.txt b/requirements.txt index 7afe0f50..12fbae53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,14 @@ -APScheduler==3.6.3 -Babel==2.9.1 -certifi==2020.6.20 -cffi==1.14.0 -chardet==3.0.4 -commonmark==0.9.1 -idna==2.10 -imagesize==1.2.0 -Jinja2==2.11.3 -MarkupSafe==1.1.1 -numpydoc==1.1.0 -packaging==20.4 -pycparser==2.20 -pycryptodomex==3.9.8 -Pygments==2.7.4 -pyparsing==2.4.7 -pytz==2020.1 -recommonmark==0.6.0 -requests==2.25.1 -six==1.15.0 -snowballstemmer==2.0.0 -tzlocal==2.1 -urllib3==1.26.5 -orjson~=3.5.2 -setuptools~=51.0.0 \ No newline at end of file +APScheduler==3.9.1 +certifi==2021.10.8 +charset-normalizer==2.0.12 +idna==3.3 +imagesize==1.3.0 +orjson==3.6.8 +pycryptodomex==3.14.1 +pytz==2022.1 +pytz-deprecation-shim==0.1.0.post0 +requests==2.27.1 +six==1.16.0 +tzdata==2022.1 +tzlocal==4.2 +urllib3==1.26.9 diff --git a/setup.py b/setup.py index 3fce1f47..88f1e443 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ 'requests', 'apscheduler', 'pycryptodomex', - 'imagesize' + 'imagesize', + 'orjson' ], classifiers=[ 'Development Status :: 5 - Production/Stable',