async_instalib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. async_instalib/__init__.py +225 -0
  2. async_instalib/__main__.py +6 -0
  3. async_instalib/auth/__init__.py +8 -0
  4. async_instalib/auth/bloks.py +146 -0
  5. async_instalib/auth/caa.py +254 -0
  6. async_instalib/auth/password.py +49 -0
  7. async_instalib/auth/service.py +189 -0
  8. async_instalib/cli.py +115 -0
  9. async_instalib/client.py +114 -0
  10. async_instalib/config/__init__.py +8 -0
  11. async_instalib/config/about.py +78 -0
  12. async_instalib/config/auth.py +62 -0
  13. async_instalib/config/comments.py +29 -0
  14. async_instalib/config/content.py +195 -0
  15. async_instalib/config/devices.py +58 -0
  16. async_instalib/config/direct.py +19 -0
  17. async_instalib/config/metadata.py +47 -0
  18. async_instalib/config/muted.py +23 -0
  19. async_instalib/config/options.py +108 -0
  20. async_instalib/config/people.py +54 -0
  21. async_instalib/config/policy.py +31 -0
  22. async_instalib/config/posts.py +184 -0
  23. async_instalib/config/profile.py +48 -0
  24. async_instalib/config/relationship_lists.py +52 -0
  25. async_instalib/config/relationships.py +188 -0
  26. async_instalib/config/validation.py +11 -0
  27. async_instalib/exceptions.py +89 -0
  28. async_instalib/models/__init__.py +9 -0
  29. async_instalib/models/about.py +107 -0
  30. async_instalib/models/activity.py +58 -0
  31. async_instalib/models/comments.py +49 -0
  32. async_instalib/models/content.py +50 -0
  33. async_instalib/models/direct.py +91 -0
  34. async_instalib/models/muted.py +23 -0
  35. async_instalib/models/pagination.py +90 -0
  36. async_instalib/models/people.py +172 -0
  37. async_instalib/models/posts.py +96 -0
  38. async_instalib/models/stories.py +245 -0
  39. async_instalib/models/uploads.py +196 -0
  40. async_instalib/models/user.py +31 -0
  41. async_instalib/py.typed +0 -0
  42. async_instalib/resources/__init__.py +5 -0
  43. async_instalib/resources/account.py +31 -0
  44. async_instalib/resources/activity.py +46 -0
  45. async_instalib/resources/comment_settings.py +148 -0
  46. async_instalib/resources/comments.py +285 -0
  47. async_instalib/resources/content.py +415 -0
  48. async_instalib/resources/direct.py +207 -0
  49. async_instalib/resources/direct_common.py +92 -0
  50. async_instalib/resources/direct_items.py +120 -0
  51. async_instalib/resources/direct_send.py +256 -0
  52. async_instalib/resources/highlights.py +97 -0
  53. async_instalib/resources/media_actions.py +199 -0
  54. async_instalib/resources/media_uploads.py +130 -0
  55. async_instalib/resources/muted_metadata.py +288 -0
  56. async_instalib/resources/people.py +179 -0
  57. async_instalib/resources/people_lists.py +194 -0
  58. async_instalib/resources/places.py +46 -0
  59. async_instalib/resources/posts.py +261 -0
  60. async_instalib/resources/profile_metadata.py +64 -0
  61. async_instalib/resources/profiles.py +229 -0
  62. async_instalib/resources/relationship_lists.py +242 -0
  63. async_instalib/resources/relationship_muting.py +129 -0
  64. async_instalib/resources/relationships.py +439 -0
  65. async_instalib/session/__init__.py +11 -0
  66. async_instalib/session/lease.py +32 -0
  67. async_instalib/session/state.py +201 -0
  68. async_instalib/transport/__init__.py +9 -0
  69. async_instalib/transport/graphql.py +284 -0
  70. async_instalib/transport/guard.py +58 -0
  71. async_instalib/transport/http.py +374 -0
  72. async_instalib/transport/response.py +146 -0
  73. async_instalib/transport/upload.py +108 -0
  74. async_instalib/triggers/__init__.py +55 -0
  75. async_instalib/triggers/events.py +72 -0
  76. async_instalib/triggers/runner.py +143 -0
  77. async_instalib/triggers/sources.py +188 -0
  78. async_instalib/triggers/state.py +57 -0
  79. async_instalib/triggers/webhooks.py +96 -0
  80. async_instalib-0.1.0.dist-info/METADATA +1205 -0
  81. async_instalib-0.1.0.dist-info/RECORD +84 -0
  82. async_instalib-0.1.0.dist-info/WHEEL +4 -0
  83. async_instalib-0.1.0.dist-info/entry_points.txt +2 -0
  84. async_instalib-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,225 @@
1
+ """Async access to Instagram's mobile API."""
2
+
3
+ from .auth import TwoFactorMethod # Select the pending login's verification method.
4
+ from .client import Instagram # Own one account's async resources and connection pool.
5
+ from .config import (
6
+ AuthOptions, # Customize login metadata and supported payload parameters.
7
+ RequestPolicy, # Bound account work and control response-driven pauses.
8
+ )
9
+ from .config.about import AboutOptions # Configure the captured account-information screen request.
10
+ from .config.comments import CommentListOptions # Choose comment sort order and continue pages.
11
+ from .config.content import (
12
+ ContentOptions, # Configure one profile content page.
13
+ MetadataOptions, # Override story/highlight query parameters.
14
+ ProfileOptions, # Configure captured profile entry context.
15
+ ReelsOptions, # Configure one recorded reels-grid page.
16
+ RelatedOptions, # Configure related-account suggestions.
17
+ RepostsOptions, # Configure repost metadata pages without playback hydration.
18
+ TaggedOptions, # Configure tagged-media pages and explicit continuations.
19
+ )
20
+ from .config.devices import (
21
+ GALAXY_S24, # Samsung Galaxy S24 (SM-S921U), Android 14.
22
+ GALAXY_S25, # Samsung Galaxy S25 (SM-S931B), Android 16; the default.
23
+ GALAXY_S25_ULTRA, # Samsung Galaxy S25 Ultra (SM-S938B), Android 16.
24
+ INFINIX_X6880, # Infinix X6880, Android 15.
25
+ PIXEL_9_PRO, # Google Pixel 9 Pro, Android 17.
26
+ AndroidDevice, # A real phone's fields in the Instagram user agent.
27
+ )
28
+ from .config.direct import InboxOptions # Choose how many threads and messages the inbox returns.
29
+ from .config.metadata import (
30
+ AboutDecodeOptions, # Bound inert account-information bundle decoding.
31
+ HighlightItemsOptions, # Configure one selected highlight item read.
32
+ )
33
+ from .config.muted import MutedDecodeOptions # Bound inert muted-account screen decoding.
34
+ from .config.people import (
35
+ MutualFollowersOptions, # Configure the captured mutual-followers preview.
36
+ PeopleListOptions, # Configure bounded relationship lists.
37
+ SearchOptions, # Configure one account-search page.
38
+ )
39
+ from .config.posts import (
40
+ CarouselItem, # Attach tags and alt text to one carousel photo.
41
+ EditOptions, # Replace a post's caption and set alt text, location, or added tags.
42
+ HighlightChanges, # Rename a highlight or add and remove stories.
43
+ PostOptions, # Configure a feed photo or carousel.
44
+ ReelOptions, # Configure a reel, including whether it appears in the main grid.
45
+ StoryOptions, # Configure a story's caption and Close Friends audience.
46
+ UserTag, # Tag one person at a position on a photo.
47
+ )
48
+ from .config.relationship_lists import (
49
+ CloseFriendsListOptions, # Configure the captured member page.
50
+ FollowRequestsListOptions, # Configure the incoming-request list.
51
+ MutedListOptions, # Configure the muted-accounts screen envelope.
52
+ )
53
+ from .config.relationships import (
54
+ ActionOptions, # Customize explicit relationship request metadata.
55
+ BlockOptions, # Select block scope and explicit provider metadata.
56
+ CloseFriendsOptions, # Configure one-member Close Friends edits.
57
+ FavoritesOptions, # Configure one-member feed Favorites edits.
58
+ MuteOptions, # Choose one captured content category to mute.
59
+ NotificationOptions, # Choose content and notification preference.
60
+ )
61
+ from .exceptions import (
62
+ AccountPaused, # Require explicit review before further account requests.
63
+ AccountRestricted, # Surface restrictions without automatic retries.
64
+ APIError, # Report otherwise unclassified provider failures.
65
+ BadCredentials, # Report a rejected username or password.
66
+ ChallengeRequired, # Surface account-owner verification requirements.
67
+ InstagramError, # Catch all expected SDK failures.
68
+ LoginFlowUnsupported, # Identify a provider flow not yet established.
69
+ LoginRequired, # Identify an absent or rejected session.
70
+ NetworkError, # Report uncertain network outcomes without replaying writes.
71
+ PersonNotFound, # Identify an unavailable username from the direct profile lookup.
72
+ ProtocolError, # Identify invalid or unsupported provider responses.
73
+ RateLimited, # Expose the provider's wait hint.
74
+ RequestBudgetExceeded, # Report local request or login budget exhaustion.
75
+ SessionInUse, # Prevent two owners of the same session file.
76
+ TwoFactorRequired, # Continue a password login with a verification code.
77
+ )
78
+ from .models import (
79
+ ClientOptions, # Configure bounded connections, timeouts, and an explicit proxy.
80
+ ClientProfile, # Configure public mobile app metadata.
81
+ User, # Read stable account fields and explicit raw provider data.
82
+ )
83
+ from .models.about import (
84
+ AboutAccount, # Read named account-information state without executing UI code.
85
+ AboutState, # Preserve a named state and its initial provider value.
86
+ )
87
+ from .models.activity import (
88
+ Notification,
89
+ ) # Read one activity-feed row: comment, mention, like, follow.
90
+ from .models.comments import Comment # Read one comment or reply with its author and counts.
91
+ from .models.content import Media # Read media identity, caption, and provider metadata.
92
+ from .models.direct import (
93
+ Message, # Read one direct message; pass it to reply, react, edit, or unsend.
94
+ Thread, # Read one conversation with its newest messages.
95
+ )
96
+ from .models.muted import MutedPerson # Read verified account IDs and nullable mute categories.
97
+ from .models.pagination import (
98
+ Continuation, # Bind opaque cursors to their originating request.
99
+ Page, # Return one explicit result page.
100
+ PageContext, # Identify the account, operation, target, and query.
101
+ )
102
+ from .models.people import (
103
+ PersonProfile, # Read profile details without inventing missing values.
104
+ PersonSummary, # Read partial person identity and relationship fields.
105
+ Relationship, # Represent account-relative relationship flags.
106
+ )
107
+ from .models.posts import (
108
+ Place, # Tag a location found with posts.places.search().
109
+ Publication, # Read a publish acknowledgement and its processing status.
110
+ )
111
+ from .models.stories import (
112
+ Highlight, # Read one highlight identity and metadata.
113
+ HighlightCover, # Preserve cover media metadata.
114
+ HighlightImage, # Read a highlight cover image variant.
115
+ HighlightItems, # Read one selected highlight's typed media items.
116
+ HighlightTray, # Read typed highlight tray entries.
117
+ StoryAuthor, # Preserve partial story author identity without inventing a username.
118
+ StoryCollection, # Read a typed story response with raw metadata.
119
+ StoryItem, # Read typed story media and optional timestamps.
120
+ StoryReel, # Group accessible stories under their owner.
121
+ )
122
+ from .models.uploads import (
123
+ Audio, # Send an MP4/M4A recording as a voice message.
124
+ Photo, # Publish a JPEG unchanged, with its dimensions read from the file.
125
+ Video, # Publish an MP4 unchanged, with an optional first-frame cover.
126
+ )
127
+ from .resources.relationships import RelationshipActionResult # Read status-only acknowledgements.
128
+ from .session import (
129
+ Device, # Persist device identity across client restarts.
130
+ Session, # Import, save, and restore private session state.
131
+ )
132
+
133
+ __all__ = [
134
+ "Audio",
135
+ "AndroidDevice",
136
+ "GALAXY_S24",
137
+ "GALAXY_S25",
138
+ "GALAXY_S25_ULTRA",
139
+ "INFINIX_X6880",
140
+ "PIXEL_9_PRO",
141
+ "InboxOptions",
142
+ "Message",
143
+ "Thread",
144
+ "Notification",
145
+ "Comment",
146
+ "CommentListOptions",
147
+ "CarouselItem",
148
+ "EditOptions",
149
+ "HighlightChanges",
150
+ "Photo",
151
+ "Place",
152
+ "PostOptions",
153
+ "Publication",
154
+ "ReelOptions",
155
+ "StoryOptions",
156
+ "UserTag",
157
+ "Video",
158
+ "MutedDecodeOptions",
159
+ "MutedPerson",
160
+ "HighlightItemsOptions",
161
+ "HighlightItems",
162
+ "StoryAuthor",
163
+ "MutualFollowersOptions",
164
+ "FollowRequestsListOptions",
165
+ "FavoritesOptions",
166
+ "MutedListOptions",
167
+ "CloseFriendsOptions",
168
+ "CloseFriendsListOptions",
169
+ "AboutDecodeOptions",
170
+ "AboutAccount",
171
+ "AboutState",
172
+ "StoryItem",
173
+ "StoryReel",
174
+ "StoryCollection",
175
+ "HighlightImage",
176
+ "HighlightCover",
177
+ "Highlight",
178
+ "HighlightTray",
179
+ "AboutOptions",
180
+ "TaggedOptions",
181
+ "RepostsOptions",
182
+ "ReelsOptions",
183
+ "BlockOptions",
184
+ "RelatedOptions",
185
+ "ActionOptions",
186
+ "MuteOptions",
187
+ "NotificationOptions",
188
+ "RelationshipActionResult",
189
+ "PersonNotFound",
190
+ "ProfileOptions",
191
+ "ContentOptions",
192
+ "MetadataOptions",
193
+ "Media",
194
+ "SearchOptions",
195
+ "PeopleListOptions",
196
+ "PersonSummary",
197
+ "PersonProfile",
198
+ "Relationship",
199
+ "Page",
200
+ "Continuation",
201
+ "PageContext",
202
+ "AuthOptions",
203
+ "RequestPolicy",
204
+ "AccountPaused",
205
+ "RequestBudgetExceeded",
206
+ "SessionInUse",
207
+ "Instagram",
208
+ "Session",
209
+ "Device",
210
+ "ClientOptions",
211
+ "ClientProfile",
212
+ "User",
213
+ "TwoFactorMethod",
214
+ "InstagramError",
215
+ "APIError",
216
+ "AccountRestricted",
217
+ "BadCredentials",
218
+ "ChallengeRequired",
219
+ "LoginFlowUnsupported",
220
+ "LoginRequired",
221
+ "NetworkError",
222
+ "ProtocolError",
223
+ "RateLimited",
224
+ "TwoFactorRequired",
225
+ ]
@@ -0,0 +1,6 @@
1
+ """Run the interactive CLI with python -m async_instalib."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ """Auth package exports."""
2
+
3
+ from .service import (
4
+ Auth, # Expose the Auth contract.
5
+ TwoFactorMethod, # Expose the TwoFactorMethod contract.
6
+ )
7
+
8
+ __all__ = ["Auth", "TwoFactorMethod"]
@@ -0,0 +1,146 @@
1
+ """Read the small data subset needed from Bloks; never execute server expressions."""
2
+
3
+ import json
4
+ from collections.abc import Iterator
5
+ from typing import Any
6
+
7
+ from ..exceptions import LoginFlowUnsupported, ProtocolError
8
+
9
+
10
+ def strings(value: Any) -> Iterator[str]:
11
+ """Walk JSON values without treating object keys as executable instructions."""
12
+ pending = [value]
13
+ while pending:
14
+ item = pending.pop()
15
+ if isinstance(item, str):
16
+ yield item
17
+ elif isinstance(item, dict):
18
+ pending.extend(reversed(list(item.values())))
19
+ elif isinstance(item, list):
20
+ pending.extend(reversed(item))
21
+
22
+
23
+ def quoted_strings(expression: str) -> Iterator[str]:
24
+ decoder, position = json.JSONDecoder(), 0
25
+ while position < len(expression):
26
+ if expression[position] != '"':
27
+ position += 1
28
+ continue
29
+ try:
30
+ value, position = decoder.raw_decode(expression, position)
31
+ except ValueError:
32
+ position += 1
33
+ continue
34
+ if isinstance(value, str):
35
+ yield value
36
+
37
+
38
+ def expression_at(text: str, position: int) -> Any:
39
+ """Parse one parenthesized value with bounded depth; operators remain inert strings."""
40
+ stack: list[list[Any]] = []
41
+ decoder = json.JSONDecoder()
42
+ value: Any
43
+ while position < len(text):
44
+ character = text[position]
45
+ if character.isspace() or character == ",":
46
+ position += 1
47
+ elif character == "(":
48
+ stack.append([])
49
+ if len(stack) > 128:
50
+ raise ProtocolError("Bloks expression nesting exceeds the supported limit.")
51
+ position += 1
52
+ elif character == ")":
53
+ if not stack:
54
+ break
55
+ value = stack.pop()
56
+ if not stack:
57
+ return value
58
+ stack[-1].append(value)
59
+ position += 1
60
+ elif character == '"':
61
+ value, position = decoder.raw_decode(text, position)
62
+ if not stack:
63
+ break
64
+ stack[-1].append(value)
65
+ else:
66
+ end = position
67
+ while end < len(text) and not text[end].isspace() and text[end] not in "(),":
68
+ end += 1
69
+ atom = text[position:end]
70
+ try:
71
+ value = json.loads(atom)
72
+ except ValueError:
73
+ value = atom
74
+ if not stack:
75
+ break
76
+ stack[-1].append(value)
77
+ position = end
78
+ raise ProtocolError("Incomplete Bloks data expression.")
79
+
80
+
81
+ def server_parameters(payload: dict[str, Any], action: str) -> dict[str, Any]:
82
+ """Read the literal server-parameter map immediately following the named action."""
83
+ marker = json.dumps(action)
84
+ for text in strings(payload):
85
+ position = text.find(marker)
86
+ if position < 0:
87
+ continue
88
+ start = text.find("(", position + len(marker))
89
+ if start < 0:
90
+ continue
91
+ try:
92
+ node = expression_at(text, start)
93
+ except (ValueError, ProtocolError):
94
+ continue
95
+ if not isinstance(node, list) or len(node) != 3:
96
+ continue
97
+ keys, values = node[1:]
98
+ if (
99
+ not isinstance(keys, list)
100
+ or not isinstance(values, list)
101
+ or len(keys) != len(values)
102
+ or not keys
103
+ or keys[0] != values[0]
104
+ or not all(isinstance(k, str) for k in keys[1:])
105
+ or any(isinstance(v, (list, dict)) for v in values[1:])
106
+ ):
107
+ continue
108
+ return dict(zip(keys[1:], values[1:], strict=True))
109
+ raise LoginFlowUnsupported("Instagram's login screen did not expose supported action data.")
110
+
111
+
112
+ def account_access_context(payload: dict[str, Any]) -> str:
113
+ for item in payload.get("layout", {}).get("bloks_payload", {}).get("data", []):
114
+ data = item.get("data", {}) if isinstance(item, dict) else {}
115
+ if not isinstance(data, dict) or data.get("key") != "CAA_ACCOUNT_ACCESS_CONTEXT:aac":
116
+ continue
117
+ initial = data.get("initial")
118
+ if isinstance(initial, str) and initial:
119
+ return initial
120
+ expression = data.get("initial_lispy")
121
+ if isinstance(expression, str):
122
+ value = next(quoted_strings(expression), None)
123
+ if value:
124
+ return value
125
+ raise LoginFlowUnsupported("Instagram did not issue an account access context.")
126
+
127
+
128
+ def login_result(payload: dict[str, Any]) -> dict[str, Any]:
129
+ action = payload.get("layout", {}).get("bloks_payload", {}).get("action", "")
130
+ if not isinstance(action, str):
131
+ raise ProtocolError("Instagram's login response has no supported action data.")
132
+ for value in quoted_strings(action):
133
+ if "login_response" not in value:
134
+ continue
135
+ try:
136
+ parsed = json.loads(value)
137
+ result = json.loads(parsed["login_response"])
138
+ headers = json.loads(parsed.get("headers") or "{}")
139
+ except (ValueError, KeyError, TypeError):
140
+ continue
141
+ if isinstance(result, dict) and isinstance(headers, dict):
142
+ return {"result": result, "headers": headers}
143
+ raise LoginFlowUnsupported(
144
+ "Instagram returned a verification or login continuation this client does not support. "
145
+ "Complete verification in the app; no automatic password retry was made."
146
+ )
@@ -0,0 +1,254 @@
1
+ """CAA password flow derived from the Android 452.1 login capture."""
2
+
3
+ import json
4
+ import time
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+ import httpx
9
+
10
+ from ..exceptions import LoginFlowUnsupported, ProtocolError
11
+ from ..transport import APIResponse, RequestData, Transport
12
+ from . import bloks
13
+ from .password import PasswordKey
14
+
15
+ PREPARE = "com.bloks.www.bloks.caa.login.process_client_data_and_redirect"
16
+ OAUTH = "com.bloks.www.caa.login.oauth.token.fetch.async"
17
+ LOGIN = "com.bloks.www.bloks.caa.login.async.send_login_request"
18
+
19
+
20
+ class CAALogin:
21
+ """Obtain fresh server context, then submit one encrypted password exchange."""
22
+
23
+ def __init__(self, transport: Transport) -> None:
24
+ self._transport = transport
25
+ self._options = transport.auth_options
26
+
27
+ async def login(self, username: str, password: str) -> APIResponse:
28
+ session = self._transport.session
29
+ device = session.device
30
+ waterfall_id = str(uuid4())
31
+ nonce = await self._transport.request(
32
+ "POST",
33
+ "attestation/create_android_keystore/",
34
+ {"app_scoped_device_id": device.id, "key_hash": ""},
35
+ )
36
+ challenge_nonce = nonce.get("challenge_nonce")
37
+ if not isinstance(challenge_nonce, str) or not challenge_nonce:
38
+ raise ProtocolError("Instagram did not supply a device challenge nonce.")
39
+ screen = await self._action(
40
+ PREPARE,
41
+ {
42
+ "is_from_logged_out": False,
43
+ "logged_out_user": "",
44
+ "qpl_join_id": None,
45
+ "family_device_id": device.family_id,
46
+ "device_id": device.android_id,
47
+ "offline_experiment_group": self._options.offline_experiment_group,
48
+ "waterfall_id": waterfall_id,
49
+ "logout_source": "",
50
+ "show_internal_settings": False,
51
+ "last_auto_login_time": 0,
52
+ "disable_auto_login": False,
53
+ "qe_device_id": device.id,
54
+ "use_auto_login_interstitial": True,
55
+ "disable_recursive_auto_login_interstitial": True,
56
+ "auto_login_interstitial_experiment_group_name": "",
57
+ "is_from_logged_in_switcher": False,
58
+ "switcher_logged_in_uid": "",
59
+ "account_list": [],
60
+ "blocked_uid": [],
61
+ "INTERNAL_INFRA_THEME": "THREE_NEUTRAL_GRAY",
62
+ "layered_homepage_experiment_group": "Deploy: Not in Experiment",
63
+ "launched_url": "",
64
+ "sim_phone_numbers": [],
65
+ "is_from_registration_reminder": False,
66
+ },
67
+ )
68
+ context = bloks.account_access_context(screen)
69
+ server = bloks.server_parameters(screen, LOGIN)
70
+ if not all(server.get(k) for k in ("username_text_input_id", "password_text_input_id")):
71
+ raise LoginFlowUnsupported("Instagram did not supply login input identifiers.")
72
+ server.update(self._options.server_parameters)
73
+ oauth_server: dict[str, Any] = {
74
+ key: server[key]
75
+ for key in (
76
+ "is_from_logged_out",
77
+ "layered_homepage_experiment_group",
78
+ "device_id",
79
+ "login_surface",
80
+ "waterfall_id",
81
+ "is_platform_login",
82
+ "login_entry_point",
83
+ "family_device_id",
84
+ "offline_experiment_group",
85
+ "access_flow_version",
86
+ "is_from_logged_in_switcher",
87
+ "qe_device_id",
88
+ )
89
+ if key in server
90
+ }
91
+ self._latency(oauth_server)
92
+ await self._action(
93
+ OAUTH,
94
+ {
95
+ "client_input_params": {
96
+ "username_input": username,
97
+ "aac": context,
98
+ "lois_settings": {"lois_token": ""},
99
+ "cloud_trust_token": None,
100
+ "zero_balance_state": "",
101
+ "si_device_param_network_info": self._network_info(),
102
+ },
103
+ "server_params": oauth_server,
104
+ },
105
+ )
106
+ bootstrap = await self._transport.send("GET", "qe/sync/")
107
+ if bootstrap.status_code not in {200, 405}:
108
+ bootstrap.raise_for_error(bootstrap.json())
109
+ try:
110
+ key = PasswordKey(
111
+ int(bootstrap.headers["ig-set-password-encryption-key-id"]),
112
+ bootstrap.headers["ig-set-password-encryption-pub-key"],
113
+ )
114
+ except (KeyError, ValueError):
115
+ raise ProtocolError(
116
+ "Instagram did not supply valid password encryption keys."
117
+ ) from None
118
+ client: dict[str, Any] = {
119
+ "blocked_uids": [],
120
+ "aac": context,
121
+ "sim_phones": [],
122
+ "aymh_accounts": [],
123
+ "secure_family_device_id": "",
124
+ "has_granted_read_contacts_permissions": 0,
125
+ "auth_secure_device_id": "",
126
+ "has_whatsapp_installed": 0,
127
+ "si_device_param_network_info": self._network_info(),
128
+ "password": key.encrypt(password),
129
+ "sso_token_map_json_string": "",
130
+ "block_store_machine_id": "",
131
+ "ig_vetted_device_nonces": None,
132
+ "cloud_trust_token": None,
133
+ "event_flow": "login_manual",
134
+ "password_contains_non_ascii": str(not password.isascii()).lower(),
135
+ "client_known_key_hash": "",
136
+ "sso_accounts_auth_data": [],
137
+ "encrypted_msisdn": "",
138
+ "has_granted_read_phone_permissions": 0,
139
+ "app_manager_id": "",
140
+ "should_show_nested_nta_from_aymh": 0,
141
+ "device_id": device.android_id,
142
+ "zero_balance_state": "",
143
+ "login_attempt_count": 1,
144
+ "machine_id": session.mid or "",
145
+ "flash_call_permission_status": dict.fromkeys(
146
+ ("READ_PHONE_STATE", "READ_CALL_LOG", "ANSWER_PHONE_CALLS"), "DENIED"
147
+ ),
148
+ "accounts_list": [],
149
+ "gms_incoming_call_retriever_eligibility": "not_eligible",
150
+ "family_device_id": device.family_id,
151
+ "fb_ig_device_id": [],
152
+ "device_emails": [],
153
+ "try_num": 1,
154
+ "lois_settings": {"lois_token": ""},
155
+ "event_step": "home_page",
156
+ "headers_infra_flow_id": "",
157
+ "openid_tokens": {},
158
+ "contact_point": username,
159
+ }
160
+ client.update(self._options.client_parameters)
161
+ self._latency(server)
162
+ server["is_from_empty_password"] = 0
163
+ # The captured emulator reports unavailable Android keystore (-1013). Python also
164
+ # lacks that keystore; no certificate, signed nonce, or hardware claim is fabricated.
165
+ attest = json.dumps(
166
+ {
167
+ "attestation": [
168
+ {
169
+ "version": 2,
170
+ "type": "keystore",
171
+ "errors": [-1013],
172
+ "challenge_nonce": challenge_nonce,
173
+ "signed_nonce": "",
174
+ "key_hash": "",
175
+ }
176
+ ]
177
+ },
178
+ separators=(",", ":"),
179
+ )
180
+ payload = await self._action(
181
+ LOGIN,
182
+ {"client_input_params": client, "server_params": server},
183
+ {"X-IG-Attest-Params": attest},
184
+ )
185
+ parsed = bloks.login_result(payload)
186
+ # Embedded CAA headers are JSON values; the live key-id header can be an integer.
187
+ # Normalize scalar values before handing them to HTTPX's wire-header container.
188
+ if any(
189
+ not isinstance(k, str) or type(v) not in (str, int)
190
+ for k, v in parsed["headers"].items()
191
+ ):
192
+ raise ProtocolError("Instagram returned an unsupported embedded header value.")
193
+ try:
194
+ headers = httpx.Headers({k: str(v) for k, v in parsed["headers"].items()})
195
+ except (ValueError, UnicodeError):
196
+ raise ProtocolError("Instagram returned an invalid embedded header.") from None
197
+ self._transport.receive_state(headers)
198
+ return APIResponse(
199
+ 200, headers, self._endpoint(LOGIN), json.dumps(parsed["result"]).encode()
200
+ )
201
+
202
+ async def _action(
203
+ self, name: str, params: dict[str, Any], headers: dict[str, str] | None = None
204
+ ) -> dict[str, Any]:
205
+ version = self._transport.session.profile.bloks_version
206
+ if name == PREPARE:
207
+ params = {**params, **self._options.prepare_parameters}
208
+ form = {
209
+ "params": json.dumps(params, separators=(",", ":")),
210
+ "bloks_versioning_id": version,
211
+ "bk_client_context": json.dumps(
212
+ {
213
+ "bloks_version": version,
214
+ "styles_id": self._options.styles_id,
215
+ "theme_params": [
216
+ {
217
+ "value": [self._options.theme],
218
+ "design_system_name": self._options.design_system,
219
+ }
220
+ ],
221
+ },
222
+ separators=(",", ":"),
223
+ ),
224
+ }
225
+ response = await self._transport.send(
226
+ "POST", self._endpoint(name), RequestData(form, headers or {})
227
+ )
228
+ result = response.json()
229
+ response.raise_for_error(result)
230
+ return result
231
+
232
+ @staticmethod
233
+ def _endpoint(name: str) -> str:
234
+ return f"bloks/async_action/{name}/"
235
+
236
+ def _latency(self, params: dict[str, Any]) -> None:
237
+ params["INTERNAL__latency_qpl_marker_id"] = self._options.latency_marker_id
238
+ params["INTERNAL__latency_qpl_instance_id"] = int(time.time() * 1000)
239
+
240
+ def _network_info(self) -> dict[str, Any]:
241
+ if self._options.network_info is not None:
242
+ return self._options.network_info
243
+ # A Python process has no Android telephony subscriptions to report.
244
+ return {
245
+ "active_subscriptions_info": None,
246
+ "default_subscription_info": None,
247
+ "active_data_subscription_info": None,
248
+ "default_sms_subscription_info": None,
249
+ "is_airplane_mode": 0,
250
+ "is_active_network_cellular": 0,
251
+ "is_device_sms_capable": 0,
252
+ "sim_count": 0,
253
+ "is_wifi": 1,
254
+ }