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.
Files changed (73) hide show
  1. ensta/Authentication.py +252 -0
  2. ensta/Credentials.py +251 -0
  3. ensta/Direct.py +145 -0
  4. ensta/Guest.py +497 -0
  5. ensta/MediaResolver.py +20 -0
  6. ensta/Mobile.py +1125 -0
  7. ensta/PasswordEncryption.py +50 -0
  8. ensta/SessionManager.py +82 -0
  9. ensta/Utils.py +10 -0
  10. ensta/Web.py +121 -0
  11. ensta/WebSession.py +1408 -0
  12. ensta/__init__.py +11 -0
  13. ensta/containers/BaseResponseData.py +60 -0
  14. ensta/containers/DirectThread.py +74 -0
  15. ensta/containers/DirectThreadInviter.py +22 -0
  16. ensta/containers/DirectThreadLastPermanentItem.py +7 -0
  17. ensta/containers/FollowPerson.py +15 -0
  18. ensta/containers/FollowedStatus.py +10 -0
  19. ensta/containers/Inbox.py +10 -0
  20. ensta/containers/Liker.py +15 -0
  21. ensta/containers/Likers.py +9 -0
  22. ensta/containers/PhotoUpload.py +82 -0
  23. ensta/containers/Post.py +44 -0
  24. ensta/containers/PostDetail.py +102 -0
  25. ensta/containers/PostUser.py +22 -0
  26. ensta/containers/PrivateInfo.py +25 -0
  27. ensta/containers/Profile.py +36 -0
  28. ensta/containers/ProfileHost.py +16 -0
  29. ensta/containers/ReelUpload.py +109 -0
  30. ensta/containers/Shared.py +18 -0
  31. ensta/containers/UnfollowedStatus.py +8 -0
  32. ensta/containers/__init__.py +18 -0
  33. ensta/lib/Exceptions.py +61 -0
  34. ensta/lib/Searcher.py +83 -0
  35. ensta/lib/__init__.py +11 -0
  36. ensta/parser/AddedCommentParser.py +30 -0
  37. ensta/parser/AddedCommentUserParser.py +19 -0
  38. ensta/parser/BiographyLinkParser.py +17 -0
  39. ensta/parser/CaptionParser.py +22 -0
  40. ensta/parser/CarouselMediaParser.py +22 -0
  41. ensta/parser/FollowersListParser.py +26 -0
  42. ensta/parser/FollowersParser.py +19 -0
  43. ensta/parser/FollowingsListParser.py +27 -0
  44. ensta/parser/FollowingsParser.py +19 -0
  45. ensta/parser/ImageVersionsParser.py +18 -0
  46. ensta/parser/PrivateInfoParser.py +26 -0
  47. ensta/parser/ProfileParser.py +29 -0
  48. ensta/parser/UploadedPhotoParser.py +31 -0
  49. ensta/parser/UploadedSidecarParser.py +42 -0
  50. ensta/parser/__init__.py +14 -0
  51. ensta/structures/AddedComment.py +29 -0
  52. ensta/structures/AddedCommentUser.py +22 -0
  53. ensta/structures/BioLink.py +12 -0
  54. ensta/structures/Caption.py +25 -0
  55. ensta/structures/CarouselMedia.py +19 -0
  56. ensta/structures/Follower.py +21 -0
  57. ensta/structures/Followers.py +19 -0
  58. ensta/structures/Following.py +22 -0
  59. ensta/structures/Followings.py +19 -0
  60. ensta/structures/ImageVersion.py +16 -0
  61. ensta/structures/PrivateInfo.py +29 -0
  62. ensta/structures/Profile.py +26 -0
  63. ensta/structures/ReturnedBioLink.py +18 -0
  64. ensta/structures/SidecarChild.py +12 -0
  65. ensta/structures/StoryLink.py +22 -0
  66. ensta/structures/UploadedPhoto.py +31 -0
  67. ensta/structures/UploadedSidecar.py +39 -0
  68. ensta/structures/__init__.py +17 -0
  69. ensta-5.2.11.dist-info/METADATA +663 -0
  70. ensta-5.2.11.dist-info/RECORD +73 -0
  71. ensta-5.2.11.dist-info/WHEEL +5 -0
  72. ensta-5.2.11.dist-info/licenses/LICENSE.txt +21 -0
  73. ensta-5.2.11.dist-info/top_level.txt +1 -0
@@ -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