Ensta 5.2.11__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.
- ensta/Authentication.py +252 -0
- ensta/Credentials.py +251 -0
- ensta/Direct.py +145 -0
- ensta/Guest.py +497 -0
- ensta/MediaResolver.py +20 -0
- ensta/Mobile.py +1125 -0
- ensta/PasswordEncryption.py +50 -0
- ensta/SessionManager.py +82 -0
- ensta/Utils.py +10 -0
- ensta/Web.py +121 -0
- ensta/WebSession.py +1408 -0
- ensta/__init__.py +11 -0
- ensta/containers/BaseResponseData.py +60 -0
- ensta/containers/DirectThread.py +74 -0
- ensta/containers/DirectThreadInviter.py +22 -0
- ensta/containers/DirectThreadLastPermanentItem.py +7 -0
- ensta/containers/FollowPerson.py +15 -0
- ensta/containers/FollowedStatus.py +10 -0
- ensta/containers/Inbox.py +10 -0
- ensta/containers/Liker.py +15 -0
- ensta/containers/Likers.py +9 -0
- ensta/containers/PhotoUpload.py +82 -0
- ensta/containers/Post.py +44 -0
- ensta/containers/PostDetail.py +102 -0
- ensta/containers/PostUser.py +22 -0
- ensta/containers/PrivateInfo.py +25 -0
- ensta/containers/Profile.py +36 -0
- ensta/containers/ProfileHost.py +16 -0
- ensta/containers/ReelUpload.py +109 -0
- ensta/containers/Shared.py +18 -0
- ensta/containers/UnfollowedStatus.py +8 -0
- ensta/containers/__init__.py +18 -0
- ensta/lib/Exceptions.py +61 -0
- ensta/lib/Searcher.py +83 -0
- ensta/lib/__init__.py +11 -0
- ensta/parser/AddedCommentParser.py +30 -0
- ensta/parser/AddedCommentUserParser.py +19 -0
- ensta/parser/BiographyLinkParser.py +17 -0
- ensta/parser/CaptionParser.py +22 -0
- ensta/parser/CarouselMediaParser.py +22 -0
- ensta/parser/FollowersListParser.py +26 -0
- ensta/parser/FollowersParser.py +19 -0
- ensta/parser/FollowingsListParser.py +27 -0
- ensta/parser/FollowingsParser.py +19 -0
- ensta/parser/ImageVersionsParser.py +18 -0
- ensta/parser/PrivateInfoParser.py +26 -0
- ensta/parser/ProfileParser.py +29 -0
- ensta/parser/UploadedPhotoParser.py +31 -0
- ensta/parser/UploadedSidecarParser.py +42 -0
- ensta/parser/__init__.py +14 -0
- ensta/structures/AddedComment.py +29 -0
- ensta/structures/AddedCommentUser.py +22 -0
- ensta/structures/BioLink.py +12 -0
- ensta/structures/Caption.py +25 -0
- ensta/structures/CarouselMedia.py +19 -0
- ensta/structures/Follower.py +21 -0
- ensta/structures/Followers.py +19 -0
- ensta/structures/Following.py +22 -0
- ensta/structures/Followings.py +19 -0
- ensta/structures/ImageVersion.py +16 -0
- ensta/structures/PrivateInfo.py +29 -0
- ensta/structures/Profile.py +26 -0
- ensta/structures/ReturnedBioLink.py +18 -0
- ensta/structures/SidecarChild.py +12 -0
- ensta/structures/StoryLink.py +22 -0
- ensta/structures/UploadedPhoto.py +31 -0
- ensta/structures/UploadedSidecar.py +39 -0
- ensta/structures/__init__.py +17 -0
- ensta-5.2.11.dist-info/METADATA +663 -0
- ensta-5.2.11.dist-info/RECORD +73 -0
- ensta-5.2.11.dist-info/WHEEL +5 -0
- ensta-5.2.11.dist-info/licenses/LICENSE.txt +21 -0
- ensta-5.2.11.dist-info/top_level.txt +1 -0
ensta/Authentication.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
import requests
|
|
5
|
+
import pyotp
|
|
6
|
+
import ntplib
|
|
7
|
+
from requests import Session
|
|
8
|
+
from requests import Response
|
|
9
|
+
from json import JSONDecodeError
|
|
10
|
+
from .PasswordEncryption import PasswordEncryption
|
|
11
|
+
from .lib.Exceptions import (AuthenticationError, NetworkError)
|
|
12
|
+
from .WebSession import WebSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def new_session_id(
|
|
16
|
+
user_identifier: str, # Username or Email
|
|
17
|
+
password: str,
|
|
18
|
+
proxy: dict[str, str],
|
|
19
|
+
totp_token: str = None
|
|
20
|
+
) -> str:
|
|
21
|
+
|
|
22
|
+
request_session: Session = requests.Session()
|
|
23
|
+
request_session.headers["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
24
|
+
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
|
|
25
|
+
|
|
26
|
+
if proxy is not None: request_session.proxies.update(proxy)
|
|
27
|
+
|
|
28
|
+
encryption = PasswordEncryption(request_session)
|
|
29
|
+
encrypted_password = encryption.encrypt(password)
|
|
30
|
+
|
|
31
|
+
data: dict = {
|
|
32
|
+
"enc_password": encrypted_password,
|
|
33
|
+
"optIntoOneTap": False,
|
|
34
|
+
"queryParams": "{}",
|
|
35
|
+
"trustedDeviceRecords": "{}",
|
|
36
|
+
"username": user_identifier # Accepts email as well
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
csrf_token: str = "".join(random.choices(string.ascii_letters + string.digits, k=32))
|
|
40
|
+
|
|
41
|
+
headers: dict = {
|
|
42
|
+
"accept": "*/*",
|
|
43
|
+
"accept-language": "en-US,en;q=0.9",
|
|
44
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
45
|
+
"dpr": "1.30208",
|
|
46
|
+
"sec-ch-prefers-color-scheme": "dark",
|
|
47
|
+
"sec-ch-ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
48
|
+
"Chrome/119.0.0.0 Safari/537.36",
|
|
49
|
+
"sec-ch-ua-full-version-list": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
50
|
+
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
51
|
+
"sec-ch-ua-mobile": "?0",
|
|
52
|
+
"sec-ch-ua-model": "\"\"",
|
|
53
|
+
"sec-ch-ua-platform": "\"Windows\"",
|
|
54
|
+
"sec-ch-ua-platform-version": "\"15.0.0\"",
|
|
55
|
+
"sec-fetch-dest": "empty",
|
|
56
|
+
"sec-fetch-mode": "cors",
|
|
57
|
+
"sec-fetch-site": "same-origin",
|
|
58
|
+
"viewport-width": "1475",
|
|
59
|
+
"x-asbd-id": "129477",
|
|
60
|
+
"x-csrftoken": csrf_token,
|
|
61
|
+
"x-ig-app-id": "936619743392459",
|
|
62
|
+
"x-ig-www-claim": "0",
|
|
63
|
+
"x-instagram-ajax": "1009977574",
|
|
64
|
+
"x-requested-with": "XMLHttpRequest",
|
|
65
|
+
"x-web-device-id": "25532C62-8BBC-4927-B6C5-02631D6E05BF",
|
|
66
|
+
"cookie": f"dpr=1.3020833730697632; csrftoken={csrf_token}",
|
|
67
|
+
"Referer": "https://www.instagram.com/",
|
|
68
|
+
"Referrer-Policy": "strict-origin-when-cross-origin"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
http_response: Response = request_session.post(
|
|
72
|
+
url="https://www.instagram.com/api/v1/web/accounts/login/ajax/",
|
|
73
|
+
data=data,
|
|
74
|
+
headers=headers
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
response_json: dict = http_response.json()
|
|
79
|
+
|
|
80
|
+
if response_json.get("status", "") != "ok":
|
|
81
|
+
|
|
82
|
+
verification_code: int | None = None
|
|
83
|
+
|
|
84
|
+
if response_json.get("two_factor_required", False) is True:
|
|
85
|
+
|
|
86
|
+
if response_json.get("two_factor_info", {}).get("totp_two_factor_on", False) is False:
|
|
87
|
+
|
|
88
|
+
if response_json.get("two_factor_info", {}).get("sms_two_factor_on", False) is False:
|
|
89
|
+
|
|
90
|
+
raise AuthenticationError(
|
|
91
|
+
"Some other 2FA method is enabled. Only TOTP-based"
|
|
92
|
+
" (Authenticator App) and SMS-based two factor is supported."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
verification_code: int = int(input("SMS 2FA enabled. Enter OTP: "))
|
|
96
|
+
|
|
97
|
+
if totp_token is None and verification_code is None:
|
|
98
|
+
raise AuthenticationError("Two-factor is enabled. Please provide the totp_token while logging in.")
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
tf_data: dict = {
|
|
102
|
+
"queryParams": '{"next":"/"}',
|
|
103
|
+
"trust_signal": True,
|
|
104
|
+
"identifier": response_json.get("two_factor_info", {}).get("two_factor_identifier"),
|
|
105
|
+
"verification_method": "1",
|
|
106
|
+
"username": user_identifier,
|
|
107
|
+
"verificationCode": verification_code if verification_code is not None else
|
|
108
|
+
|
|
109
|
+
pyotp.TOTP(totp_token).at(
|
|
110
|
+
int(
|
|
111
|
+
ntplib.NTPClient().request(
|
|
112
|
+
"time.google.com",
|
|
113
|
+
version=3
|
|
114
|
+
).tx_time
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tf_response: Response = request_session.post(
|
|
120
|
+
url="https://www.instagram.com/api/v1/web/accounts/login/ajax/two_factor/",
|
|
121
|
+
data=tf_data,
|
|
122
|
+
headers={
|
|
123
|
+
"sec-ch-prefers-color-scheme": "dark",
|
|
124
|
+
"sec-ch-ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
125
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
126
|
+
"Chrome/119.0.0.0 Safari/537.36",
|
|
127
|
+
"sec-ch-ua-full-version-list": "Mozilla/5.0 (Windows NT 10.0; Win64;"
|
|
128
|
+
" x64) AppleWebKit/537.36 "
|
|
129
|
+
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
130
|
+
"sec-ch-ua-mobile": "?0",
|
|
131
|
+
"sec-ch-ua-model": "\"\"",
|
|
132
|
+
"sec-ch-ua-platform": "\"Windows\"",
|
|
133
|
+
"sec-ch-ua-platform-version": "\"15.0.0\"",
|
|
134
|
+
"sec-fetch-dest": "empty",
|
|
135
|
+
"sec-fetch-mode": "cors",
|
|
136
|
+
"sec-fetch-site": "same-origin",
|
|
137
|
+
'Accept': '*/*',
|
|
138
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
139
|
+
'X-Mid': response_json.get("two_factor_info", {}).get("device_id"),
|
|
140
|
+
'X-CSRFToken': csrf_token,
|
|
141
|
+
"x-instagram-ajax": "1009977574",
|
|
142
|
+
"x-ig-app-id": "936619743392459",
|
|
143
|
+
'X-ASBD-ID': '129477',
|
|
144
|
+
'X-IG-WWW-Claim': '0',
|
|
145
|
+
"x-web-device-id": "25532C62-8BBC-4927-B6C5-02631D6E05BF",
|
|
146
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
147
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
148
|
+
'Origin': 'https://www.instagram.com',
|
|
149
|
+
'DNT': '1',
|
|
150
|
+
'Sec-GPC': '1',
|
|
151
|
+
'Connection': 'keep-alive',
|
|
152
|
+
'Referer': 'https://www.instagram.com/accounts/login/two_factor?next=%2F',
|
|
153
|
+
'Sec-Fetch-Dest': 'empty',
|
|
154
|
+
'Sec-Fetch-Mode': 'cors',
|
|
155
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
156
|
+
},
|
|
157
|
+
cookies={
|
|
158
|
+
"ig_did": response_json.get("ig_did", ""),
|
|
159
|
+
"mid": response_json.get("two_factor_info", {}).get("device_id"),
|
|
160
|
+
"csrftoken": csrf_token
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if "Oops, an error occurred." in tf_response.text:
|
|
165
|
+
|
|
166
|
+
raise AuthenticationError(
|
|
167
|
+
"IP temporarily banned most probably due to too many login requests."
|
|
168
|
+
" Please try again later or use proxies."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
tf_response_json: dict = tf_response.json()
|
|
173
|
+
|
|
174
|
+
if tf_response_json.get("status", "") != "ok" \
|
|
175
|
+
or tf_response_json.get("authenticated", False) is False:
|
|
176
|
+
|
|
177
|
+
raise AuthenticationError(
|
|
178
|
+
"Couldn't log in through 2FA. Most probably your totp_token is incorrect."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
session_id: str = tf_response.cookies.get("sessionid", "")
|
|
182
|
+
rur: str = tf_response.cookies.get("rur", "")
|
|
183
|
+
mid: str = response_json.get("two_factor_info", {}).get("device_id")
|
|
184
|
+
user_id: str = tf_response_json.get("userId", "")
|
|
185
|
+
ig_did: str = tf_response.cookies.get("ig_did", "")
|
|
186
|
+
|
|
187
|
+
if session_id == "" or user_id == "":
|
|
188
|
+
|
|
189
|
+
raise AuthenticationError(
|
|
190
|
+
"2FA authentication response didn't return a valid session_id or user_id."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return json.dumps({
|
|
194
|
+
"session_id": session_id,
|
|
195
|
+
"rur": rur,
|
|
196
|
+
"mid": mid,
|
|
197
|
+
"user_id": user_id,
|
|
198
|
+
"ig_did": ig_did,
|
|
199
|
+
"identifier": user_identifier,
|
|
200
|
+
"username": WebSession(
|
|
201
|
+
json.dumps({
|
|
202
|
+
"session_id": session_id,
|
|
203
|
+
"rur": rur,
|
|
204
|
+
"mid": mid,
|
|
205
|
+
"user_id": user_id,
|
|
206
|
+
"ig_did": ig_did,
|
|
207
|
+
})
|
|
208
|
+
).private_info().username
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
except JSONDecodeError:
|
|
212
|
+
raise NetworkError(
|
|
213
|
+
"Response got while logging in was not a valid "
|
|
214
|
+
"json. Are you able to visit Instagram on the web?"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
raise AuthenticationError(
|
|
218
|
+
"Either user doesn't exist or your password is too weak (change it to a stronger one)."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if response_json.get("user", False) is False or response_json.get("authenticated", False) is False:
|
|
222
|
+
raise AuthenticationError("Invalid password.")
|
|
223
|
+
|
|
224
|
+
session_id: str = http_response.cookies.get("sessionid", "")
|
|
225
|
+
rur: str = http_response.cookies.get("rur", "")
|
|
226
|
+
mid: str = http_response.cookies.get("mid", "")
|
|
227
|
+
user_id: str = response_json.get("userId", "")
|
|
228
|
+
ig_did: str = response_json.get("ig_did", "")
|
|
229
|
+
|
|
230
|
+
if session_id == "" or user_id == "": raise AuthenticationError("Unable to login.")
|
|
231
|
+
|
|
232
|
+
return json.dumps({
|
|
233
|
+
"session_id": session_id,
|
|
234
|
+
"rur": rur,
|
|
235
|
+
"mid": mid,
|
|
236
|
+
"user_id": user_id,
|
|
237
|
+
"ig_did": ig_did,
|
|
238
|
+
"identifier": user_identifier,
|
|
239
|
+
"username": WebSession(
|
|
240
|
+
json.dumps({
|
|
241
|
+
"session_id": session_id,
|
|
242
|
+
"rur": rur,
|
|
243
|
+
"mid": mid,
|
|
244
|
+
"user_id": user_id,
|
|
245
|
+
"ig_did": ig_did,
|
|
246
|
+
})
|
|
247
|
+
).private_info().username
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
except JSONDecodeError:
|
|
251
|
+
raise NetworkError("Response got while logging in was not a valid "
|
|
252
|
+
"json. Are you able to visit Instagram on the web?")
|
ensta/Credentials.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
from requests import Session, Response
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
from .PasswordEncryption import PasswordEncryption
|
|
6
|
+
from .lib.Exceptions import AuthenticationError
|
|
7
|
+
from requests.models import CaseInsensitiveDict
|
|
8
|
+
from .SessionManager import SessionManager
|
|
9
|
+
import pyotp
|
|
10
|
+
import ntplib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Credentials:
|
|
14
|
+
"""
|
|
15
|
+
Takes identifier-password and returns Credentials.
|
|
16
|
+
|
|
17
|
+
- Should be called with identifier and password.
|
|
18
|
+
- If stored session exists, validates it (not authenticates), and returns Credentials.
|
|
19
|
+
- Otherwise logs in using given identifier and password, stores it in file, and returns Credentials.
|
|
20
|
+
- Raises appropriate exceptions where needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
session: Session
|
|
24
|
+
user_agent: str
|
|
25
|
+
totp_token: str
|
|
26
|
+
|
|
27
|
+
bearer: str
|
|
28
|
+
user_id: str
|
|
29
|
+
username: str
|
|
30
|
+
phone_id: str
|
|
31
|
+
stored_identifier: str
|
|
32
|
+
|
|
33
|
+
device_id: str = "android-d105ec84a512642c"
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
identifier: str,
|
|
38
|
+
password: str,
|
|
39
|
+
user_agent: str,
|
|
40
|
+
session: Session,
|
|
41
|
+
save_folder: str,
|
|
42
|
+
totp_token: str,
|
|
43
|
+
session_data: str,
|
|
44
|
+
logging: bool = False
|
|
45
|
+
) -> None:
|
|
46
|
+
|
|
47
|
+
self.session = session
|
|
48
|
+
self.user_agent = user_agent
|
|
49
|
+
self.totp_token = totp_token
|
|
50
|
+
|
|
51
|
+
# Existing Stored Credentials?
|
|
52
|
+
if session_data is not None: stored_dict: dict = json.loads(session_data)
|
|
53
|
+
else: stored_dict: dict = SessionManager.load_from_file(identifier, password, save_folder)
|
|
54
|
+
|
|
55
|
+
stored_dict_valid: bool = True
|
|
56
|
+
|
|
57
|
+
# Validate StoredDict
|
|
58
|
+
for key in ("bearer", "user_id", "username", "phone_id", "identifier", "device_id"):
|
|
59
|
+
if key not in stored_dict.keys(): stored_dict_valid: bool = False
|
|
60
|
+
|
|
61
|
+
# Load From StoredDict
|
|
62
|
+
if stored_dict_valid and stored_dict.get("identifier") == identifier:
|
|
63
|
+
if logging: print("Loading from stored file...")
|
|
64
|
+
self.bearer = stored_dict.get("bearer")
|
|
65
|
+
self.user_id = stored_dict.get("user_id")
|
|
66
|
+
self.username= stored_dict.get("username")
|
|
67
|
+
self.phone_id = stored_dict.get("phone_id")
|
|
68
|
+
self.stored_identifier = stored_dict.get("identifier")
|
|
69
|
+
|
|
70
|
+
# Create New Session
|
|
71
|
+
else:
|
|
72
|
+
if logging: print("Creating new session...")
|
|
73
|
+
self.login(identifier, password, save_folder)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def login(self, identifier: str, password: str, save_folder: str) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Takes identifier and password, authenticates, stores new session in file, and returns a bunch of session data.
|
|
79
|
+
:param identifier: Username or Email
|
|
80
|
+
:param password: Password
|
|
81
|
+
:param save_folder: Folder path to store new session in. Empty to skip storing session.
|
|
82
|
+
:return: Tuple of session data
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
phone_id: str = str(uuid4())
|
|
86
|
+
guid: str = str(uuid4())
|
|
87
|
+
encrypted_password = PasswordEncryption(self.session).encrypt(password)
|
|
88
|
+
|
|
89
|
+
response: Response = self.session.post(
|
|
90
|
+
url="https://i.instagram.com/api/v1/accounts/login/",
|
|
91
|
+
headers={
|
|
92
|
+
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
93
|
+
"user-agent": self.user_agent,
|
|
94
|
+
"host": "i.instagram.com",
|
|
95
|
+
},
|
|
96
|
+
data={
|
|
97
|
+
"jazoest": "22506",
|
|
98
|
+
"country_codes": "[{\"country_code\":\"1\",\"source\":[\"default\"]}]",
|
|
99
|
+
"phone_id": phone_id,
|
|
100
|
+
"enc_password": encrypted_password,
|
|
101
|
+
"username": identifier,
|
|
102
|
+
"adid": "00000000-0000-0000-0000-000000000000",
|
|
103
|
+
"guid": guid,
|
|
104
|
+
"device_id": self.device_id,
|
|
105
|
+
"google_tokens": "[]",
|
|
106
|
+
"login_attempt_count": "0"
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
user_info: dict | None = None
|
|
112
|
+
response_headers: CaseInsensitiveDict[str] | None = None
|
|
113
|
+
|
|
114
|
+
# Parse Response Body Into A JSON
|
|
115
|
+
response_dict: dict = response.json()
|
|
116
|
+
|
|
117
|
+
# Request Failed
|
|
118
|
+
if response_dict.get("status", "fail") != "ok":
|
|
119
|
+
|
|
120
|
+
# Other Than 2FA?
|
|
121
|
+
if response_dict.get("error_type", "") != "two_factor_required":
|
|
122
|
+
raise AuthenticationError(
|
|
123
|
+
f"Login failed with given credentials.\n"
|
|
124
|
+
f"Response: {response_dict}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 2FA Required
|
|
128
|
+
user_info, response_headers = self.handle_2fa(
|
|
129
|
+
information=response_dict.get("two_factor_info", {}),
|
|
130
|
+
phone_id=phone_id,
|
|
131
|
+
guid=guid
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Request Succeeded: Collect Data
|
|
135
|
+
if user_info is None: user_info: dict = response_dict.get("logged_in_user", dict())
|
|
136
|
+
|
|
137
|
+
if response_headers is None: self.bearer: str = response.headers.get("ig-set-authorization")
|
|
138
|
+
else: self.bearer: str = response_headers.get("ig-set-authorization")
|
|
139
|
+
|
|
140
|
+
self.user_id: str = str(user_info.get("pk"))
|
|
141
|
+
self.phone_id = phone_id
|
|
142
|
+
self.username: str = user_info.get("username")
|
|
143
|
+
self.stored_identifier = identifier
|
|
144
|
+
|
|
145
|
+
# Save Session in File
|
|
146
|
+
if save_folder != "":
|
|
147
|
+
SessionManager.save_to_file(
|
|
148
|
+
folder_name=save_folder,
|
|
149
|
+
identifier=identifier,
|
|
150
|
+
password=password,
|
|
151
|
+
phone_id=phone_id,
|
|
152
|
+
instance=self
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Response Body Not A Valid JSON
|
|
156
|
+
except JSONDecodeError:
|
|
157
|
+
raise AuthenticationError(
|
|
158
|
+
"Response I got didn't parse into a JSON. Maybe you're being rate "
|
|
159
|
+
"limited. Change WiFi or use reputed proxies."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def handle_2fa(
|
|
163
|
+
self,
|
|
164
|
+
information: dict,
|
|
165
|
+
phone_id: str,
|
|
166
|
+
guid: str
|
|
167
|
+
) -> tuple[dict, CaseInsensitiveDict[str]]:
|
|
168
|
+
|
|
169
|
+
# Only TOTP Supported: No SMS, No WhatsApp
|
|
170
|
+
if information.get("totp_two_factor_on", False) is False:
|
|
171
|
+
raise AuthenticationError(
|
|
172
|
+
"Only TOTP-based 2FA is supported till now."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# TODO: Remove when implemented
|
|
176
|
+
raise NotImplementedError("TOTP 2FA not implemented yet!")
|
|
177
|
+
|
|
178
|
+
# TOTP Token Supplied? Take Input | Use That
|
|
179
|
+
if self.totp_token is None: code: str = input("Enter TOTP Code: ")
|
|
180
|
+
else: code: str = self.new_totp_code(self.totp_token)
|
|
181
|
+
|
|
182
|
+
while True:
|
|
183
|
+
user_info, response_headers = self.bypass_totp(
|
|
184
|
+
code=code,
|
|
185
|
+
two_factor_identifier=information.get("two_factor_identifier"),
|
|
186
|
+
phone_id=phone_id,
|
|
187
|
+
guid=guid
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if user_info is not None and response_headers is not None: break
|
|
191
|
+
|
|
192
|
+
if self.totp_token is None:
|
|
193
|
+
code: str = input(
|
|
194
|
+
"Unable to verify code.\n\n"
|
|
195
|
+
"Enter TOTP Code: "
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
raise AuthenticationError(
|
|
200
|
+
"Unable to verify TOTP 2FA Code. Is the totp_token correct?"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return user_info, response_headers
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def new_totp_code(token: str) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Generates a new TOTP Code for the current time using secret token.
|
|
209
|
+
|
|
210
|
+
:param token: TOTP Token generated by Instagram stored secretly in the Authenticator App.
|
|
211
|
+
:return: Generated Code
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
current_time: int = int(ntplib.NTPClient().request("time.google.com", version=3).tx_time)
|
|
215
|
+
|
|
216
|
+
return str(
|
|
217
|
+
pyotp.TOTP(token).at(current_time)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def bypass_totp(self, code: str, two_factor_identifier: str, phone_id: str, guid: str) -> tuple[dict, CaseInsensitiveDict[str]]:
|
|
221
|
+
|
|
222
|
+
response: Response = self.session.post(
|
|
223
|
+
url="https://i.instagram.com/api/v1/accounts/two_factor_login/",
|
|
224
|
+
headers={
|
|
225
|
+
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
226
|
+
"accept-encoding": "gzip",
|
|
227
|
+
"connection": "Keep-Alive"
|
|
228
|
+
},
|
|
229
|
+
data={
|
|
230
|
+
"signed_body": "SIGNATURE." + json.dumps(
|
|
231
|
+
{
|
|
232
|
+
"verification_code": code,
|
|
233
|
+
"phone_id": phone_id,
|
|
234
|
+
"two_factor_identifier": two_factor_identifier,
|
|
235
|
+
"trust_this_device": "1",
|
|
236
|
+
"guid": guid,
|
|
237
|
+
"device_id": self.device_id,
|
|
238
|
+
"waterfall_id": str(uuid4()),
|
|
239
|
+
"verification_method": "3" # '3' For TOTP-Based 2FA
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# TODO
|
|
246
|
+
"""
|
|
247
|
+
Return logged_in_user_info and response headers.
|
|
248
|
+
Error: CSRF Token missing or incorrect.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
raise Exception(response.json())
|
ensta/Direct.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from requests import Session, Response
|
|
2
|
+
from .containers.Inbox import Inbox
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
import string
|
|
5
|
+
import random
|
|
6
|
+
from .lib.Exceptions import FileTypeError, NetworkError
|
|
7
|
+
from json import JSONDecodeError
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from ensta.Utils import fb_uploader
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Direct:
|
|
13
|
+
|
|
14
|
+
session: Session
|
|
15
|
+
device_id: str
|
|
16
|
+
|
|
17
|
+
def __init__(self, session: Session, device_id: str) -> None:
|
|
18
|
+
self.session = session
|
|
19
|
+
self.device_id = device_id
|
|
20
|
+
|
|
21
|
+
def inbox(self) -> Inbox:
|
|
22
|
+
"""
|
|
23
|
+
Fetches all the chats from inbox.
|
|
24
|
+
:return: Inbox object with chat items and some other data
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
raise NotImplementedError()
|
|
28
|
+
|
|
29
|
+
def send_text(self, text: str, thread_id: int, silently: bool = False) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Sends a text message in a chat
|
|
32
|
+
:param text: Text message
|
|
33
|
+
:param thread_id: Thread ID of chat to send message to
|
|
34
|
+
:param silently: Whether to send message without any notification
|
|
35
|
+
:return: Boolean (Message sent or not)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
mutation_token: str = str().join(random.choices(string.digits, k=19))
|
|
39
|
+
|
|
40
|
+
response: Response = self.session.post(
|
|
41
|
+
url="https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/",
|
|
42
|
+
data={
|
|
43
|
+
"action": "send_item",
|
|
44
|
+
"is_x_transport_forward": False,
|
|
45
|
+
"is_shh_mode": 0,
|
|
46
|
+
"send_silently": silently,
|
|
47
|
+
"thread_ids": f"[{thread_id}]",
|
|
48
|
+
"send_attribution": "inbox",
|
|
49
|
+
"client_context": mutation_token,
|
|
50
|
+
"text": text,
|
|
51
|
+
"device_id": self.device_id,
|
|
52
|
+
"mutation_token": mutation_token,
|
|
53
|
+
"_uuid": str(uuid4()),
|
|
54
|
+
"btt_dual_send": False,
|
|
55
|
+
"is_ae_dual_send": False,
|
|
56
|
+
"offline_threading_id": mutation_token
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return response.json().get("status", "") == "ok"
|
|
62
|
+
|
|
63
|
+
except JSONDecodeError:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def fb_upload_image(self, media_path: str) -> int:
|
|
67
|
+
"""
|
|
68
|
+
Uploads given image to Instagram and returns MediaID.
|
|
69
|
+
:param media_path: Local path of image
|
|
70
|
+
:return: MediaID (Integer)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
media_path: Path = Path(media_path)
|
|
74
|
+
|
|
75
|
+
if media_path.suffix not in (".jpg", ".jpeg"):
|
|
76
|
+
raise FileTypeError(
|
|
77
|
+
"Only jpg and jpeg image types can be uploaded."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with open(media_path, "rb") as file:
|
|
81
|
+
media_data: bytes = file.read()
|
|
82
|
+
|
|
83
|
+
upload_name: str = fb_uploader()
|
|
84
|
+
media_length: str = str(len(media_data))
|
|
85
|
+
|
|
86
|
+
response: Response = self.session.post(
|
|
87
|
+
url=f"https://rupload.facebook.com/messenger_image/{upload_name}",
|
|
88
|
+
headers={
|
|
89
|
+
"content-length": media_length,
|
|
90
|
+
"content-type": "application/octet-stream",
|
|
91
|
+
"host": "rupload.facebook.com",
|
|
92
|
+
"x-entity-name": upload_name,
|
|
93
|
+
"x-entity-type": "image/jpeg",
|
|
94
|
+
"x-entity-length": media_length,
|
|
95
|
+
"offset": "0",
|
|
96
|
+
"image_type": "FILE_ATTACHMENT"
|
|
97
|
+
},
|
|
98
|
+
data=media_data
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return response.json().get("media_id")
|
|
103
|
+
|
|
104
|
+
except JSONDecodeError:
|
|
105
|
+
raise NetworkError(
|
|
106
|
+
"Unable to upload image. Make sure there's nothing wrong with the image. "
|
|
107
|
+
"If this problem persists, try using a different account, switch to a different "
|
|
108
|
+
"network, or use reputed proxies."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def send_photo(self, media_id: int, thread_id: int) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Sends a photo in a chat
|
|
114
|
+
:param media_id: Media ID of photo received by uploading it to 'FB Uploader'
|
|
115
|
+
:param thread_id: Thread ID of chat to send photo to
|
|
116
|
+
:return: Boolean (Photo sent or not)
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
mutation_token: str = str().join(random.choices(string.digits, k=19))
|
|
120
|
+
|
|
121
|
+
response: Response = self.session.post(
|
|
122
|
+
url="https://i.instagram.com/api/v1/direct_v2/threads/broadcast/photo_attachment/",
|
|
123
|
+
data={
|
|
124
|
+
"action": "send_item",
|
|
125
|
+
"is_x_transport_forward": False,
|
|
126
|
+
"is_shh_mode": 0,
|
|
127
|
+
"thread_ids": f"[{thread_id}]",
|
|
128
|
+
"send_attribution": "inbox",
|
|
129
|
+
"client_context": mutation_token,
|
|
130
|
+
"attachment_fbid": media_id,
|
|
131
|
+
"device_id": self.device_id,
|
|
132
|
+
"mutation_token": mutation_token,
|
|
133
|
+
"_uuid": str(uuid4()),
|
|
134
|
+
"allow_full_aspect_ratio": True,
|
|
135
|
+
"btt_dual_send": False,
|
|
136
|
+
"is_ae_dual_send": False,
|
|
137
|
+
"offline_threading_id": mutation_token
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
return response.json().get("status", "") == "ok"
|
|
143
|
+
|
|
144
|
+
except JSONDecodeError:
|
|
145
|
+
return False
|