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.
- async_instalib/__init__.py +225 -0
- async_instalib/__main__.py +6 -0
- async_instalib/auth/__init__.py +8 -0
- async_instalib/auth/bloks.py +146 -0
- async_instalib/auth/caa.py +254 -0
- async_instalib/auth/password.py +49 -0
- async_instalib/auth/service.py +189 -0
- async_instalib/cli.py +115 -0
- async_instalib/client.py +114 -0
- async_instalib/config/__init__.py +8 -0
- async_instalib/config/about.py +78 -0
- async_instalib/config/auth.py +62 -0
- async_instalib/config/comments.py +29 -0
- async_instalib/config/content.py +195 -0
- async_instalib/config/devices.py +58 -0
- async_instalib/config/direct.py +19 -0
- async_instalib/config/metadata.py +47 -0
- async_instalib/config/muted.py +23 -0
- async_instalib/config/options.py +108 -0
- async_instalib/config/people.py +54 -0
- async_instalib/config/policy.py +31 -0
- async_instalib/config/posts.py +184 -0
- async_instalib/config/profile.py +48 -0
- async_instalib/config/relationship_lists.py +52 -0
- async_instalib/config/relationships.py +188 -0
- async_instalib/config/validation.py +11 -0
- async_instalib/exceptions.py +89 -0
- async_instalib/models/__init__.py +9 -0
- async_instalib/models/about.py +107 -0
- async_instalib/models/activity.py +58 -0
- async_instalib/models/comments.py +49 -0
- async_instalib/models/content.py +50 -0
- async_instalib/models/direct.py +91 -0
- async_instalib/models/muted.py +23 -0
- async_instalib/models/pagination.py +90 -0
- async_instalib/models/people.py +172 -0
- async_instalib/models/posts.py +96 -0
- async_instalib/models/stories.py +245 -0
- async_instalib/models/uploads.py +196 -0
- async_instalib/models/user.py +31 -0
- async_instalib/py.typed +0 -0
- async_instalib/resources/__init__.py +5 -0
- async_instalib/resources/account.py +31 -0
- async_instalib/resources/activity.py +46 -0
- async_instalib/resources/comment_settings.py +148 -0
- async_instalib/resources/comments.py +285 -0
- async_instalib/resources/content.py +415 -0
- async_instalib/resources/direct.py +207 -0
- async_instalib/resources/direct_common.py +92 -0
- async_instalib/resources/direct_items.py +120 -0
- async_instalib/resources/direct_send.py +256 -0
- async_instalib/resources/highlights.py +97 -0
- async_instalib/resources/media_actions.py +199 -0
- async_instalib/resources/media_uploads.py +130 -0
- async_instalib/resources/muted_metadata.py +288 -0
- async_instalib/resources/people.py +179 -0
- async_instalib/resources/people_lists.py +194 -0
- async_instalib/resources/places.py +46 -0
- async_instalib/resources/posts.py +261 -0
- async_instalib/resources/profile_metadata.py +64 -0
- async_instalib/resources/profiles.py +229 -0
- async_instalib/resources/relationship_lists.py +242 -0
- async_instalib/resources/relationship_muting.py +129 -0
- async_instalib/resources/relationships.py +439 -0
- async_instalib/session/__init__.py +11 -0
- async_instalib/session/lease.py +32 -0
- async_instalib/session/state.py +201 -0
- async_instalib/transport/__init__.py +9 -0
- async_instalib/transport/graphql.py +284 -0
- async_instalib/transport/guard.py +58 -0
- async_instalib/transport/http.py +374 -0
- async_instalib/transport/response.py +146 -0
- async_instalib/transport/upload.py +108 -0
- async_instalib/triggers/__init__.py +55 -0
- async_instalib/triggers/events.py +72 -0
- async_instalib/triggers/runner.py +143 -0
- async_instalib/triggers/sources.py +188 -0
- async_instalib/triggers/state.py +57 -0
- async_instalib/triggers/webhooks.py +96 -0
- async_instalib-0.1.0.dist-info/METADATA +1205 -0
- async_instalib-0.1.0.dist-info/RECORD +84 -0
- async_instalib-0.1.0.dist-info/WHEEL +4 -0
- async_instalib-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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
|
+
}
|