1import json
2import os
3import random
4import time
5import traceback
6
7import requests
8import requests.utils
9
10from . import config, devices
11
12# ====== SYNC METHODS ====== #
13
14
15def sync_device_features(self, login=False):
16    data = {
17        "id": self.uuid,
18        "server_config_retrieval": "1",
19        "experiments": config.LOGIN_EXPERIMENTS,
20    }
21    if login is False:
22        data["_uuid"] = self.uuid
23        data["_uid"] = self.user_id
24        data["_csrftoken"] = self.token
25    data = json.dumps(data)
26    self.last_experiments = time.time()
27    return self.send_request(
28        "qe/sync/", data, login=login, headers={"X-DEVICE-ID": self.uuid}
29    )
30
31
32def sync_launcher(self, login=False):
33    data = {
34        "id": self.uuid,
35        "server_config_retrieval": "1",
36    }
37    if login is False:
38        data["_uid"] = self.user_id
39        data["_uuid"] = self.uuid
40        data["_csrftoken"] = self.token
41    data = json.dumps(data)
42    return self.send_request("launcher/sync/", data, login=login)
43
44
45def sync_user_features(self):
46    data = self.default_data
47    data["id"] = self.uuid
48    data["experiments"] = config.EXPERIMENTS
49    data = json.dumps(data)
50    self.last_experiments = time.time()
51    return self.send_request("qe/sync/", data, headers={"X-DEVICE-ID": self.uuid})
52
53
54# "android_device_id":"android-f14b9731e4869eb",
55# "phone_id":"b4bd7978-ca2b-4ea0-a728-deb4180bd6ca",
56# "usages":"[\"account_recovery_omnibox\"]",
57# "_csrftoken":"9LZXBXXOztxNmg3h1r4gNzX5ohoOeBkI",
58# "device_id":"70db6a72-2663-48da-96f5-123edff1d458"
59def get_prefill_candidates(self, login=False):
60    data = {
61        "android_device_id": self.device_id,
62        "phone_id": self.phone_id,
63        "usages": '["account_recovery_omnibox"]',
64        "device_id": self.device_id,
65    }
66    if login is False:
67        data["_csrftoken"] = self.token
68    data = json.dumps(data)
69    return self.send_request("accounts/get_prefill_candidates/", data, login=login)
70
71
72def get_account_family(self):
73    return self.send_request("multiple_accounts/get_account_family/")
74
75
76def get_zr_token_result(self):
77    url = (
78        "zr/token/result/?device_id={rank_token}"
79        "&token_hash=&custom_device_id={custom_device_id}&fetch_reason=token_expired"
80    )
81    url = url.format(rank_token=self.device_id, custom_device_id=self.device_id)
82    return self.send_request(url)
83
84
85def banyan(self):
86    url = "banyan/banyan/?views=['story_share_sheet','threads_people_picker','group_stories_share_sheet','reshare_share_sheet']"
87    return self.send_request(url)
88
89
90def igtv_browse_feed(self):
91    url = "igtv/browse_feed/?prefetch=1"
92    return self.send_request(url)
93
94
95def creatives_ar_class(self):
96    data = {
97        "_csrftoken": self.token,
98        "_uuid": self.uuid,
99    }
100    data = json.dumps(data)
101    return self.send_request("creatives/ar_class/", data)
102
103
104# ====== LOGIN/PRE FLOWS METHODS ====== #
105
106
107def pre_login_flow(self):
108    self.logger.info("Not yet logged in starting: PRE-LOGIN FLOW!")
109
110    # /api/v1/accounts/get_prefill_candidates
111    self.get_prefill_candidates(True)
112
113    # /api/v1/qe/sync (server_config_retrieval)
114    self.sync_device_features(True)
115
116    # /api/v1/launcher/sync/ (server_config_retrieval)
117    self.sync_launcher(True)
118
119    # /api/v1/accounts/contact_point_prefill/
120    self.set_contact_point_prefill("prefill")
121
122    # /api/v1/accounts/get_prefill_candidates/
123    # self.get_prefill_candidates(True)
124
125    # /api/v1/accounts/contact_point_prefill/
126    # self.set_contact_point_prefill("prefill")
127
128    # /api/v1/qe/sync/ (server_config_retrieval)
129    # self.sync_device_features(True)
130
131    # /api/v1/launcher/sync/ (server_config_retrieval)
132    # self.sync_launcher(True)
133
134
135# DO NOT MOVE ANY OF THE ENDPOINTS THEYRE IN THE CORRECT ORDER
136def login_flow(self, just_logged_in=False, app_refresh_interval=1800):
137    self.last_experiments = time.time()
138    self.logger.info("LOGIN FLOW! Just logged-in: {}".format(just_logged_in))
139    check_flow = []
140    if just_logged_in:
141        try:
142            # SYNC
143            # /api/v1/qe/sync/ (server_config_retrieval)
144            check_flow.append(self.sync_device_features())
145
146            # /api/v1/launcher/sync/ (server_config_retrieval)
147            check_flow.append(self.sync_launcher(False))
148
149            # /api/v1/zr/token/result/?device_id=android-f14b9731e4869eb&token_hash=&custom_device_id=f3119c98-5663-4c47-95b5-a63b140a2b62&fetch_reason=token_expired
150            check_flow.append(self.get_zr_token_result())
151
152            # /api/v1/multiple_accounts/get_account_family/
153            check_flow.append(self.get_account_family())
154
155            # /api/v1/qe/sync/ (server_config_retrieval)
156            check_flow.append(self.sync_device_features())
157
158            # /api/v1/banyan/banyan/?views=%5B%22story_share_sheet%22%2C%22threads_people_picker%22%2C%22group_stories_share_sheet%22%2C%22reshare_share_sheet%22%5D
159            # error 400: {"message": "Bad request", "status": "fail"}
160            # check_flow.append(self.banyan())
161
162            # /api/v1/igtv/browse_feed/?prefetch=1
163            check_flow.append(self.igtv_browse_feed())
164
165            # /api/v1/creatives/ar_class/ (_csrftoken also add _uuid)
166            check_flow.append(self.creatives_ar_class())
167
168            # /api/v1/feed/reels_tray/ (supported_capabilities_new + reason=cold_start + _csrftoken + _uuid)
169            check_flow.append(self.get_reels_tray_feed(reason="cold_start"))
170
171            # /api/v1/feed/timeline/ (feed_view_info + phone_id + battery_level + timezone_offset + _csrftoken + device_id + request_id + is_pull_to_refresh=0 + _uuid + is_charging=1 + will_sound_on=0 + seesion_id + bloks_versioning_id)
172            check_flow.append(self.get_timeline_feed())
173
174            # /api/v1/feed/reels_media/
175            # error 400: {"message": "Invalid reel id list", "status": "fail"}
176            # check_flow.append(self.get_reels_media())
177
178            # /api/v1/push/register/ (device_type=android_mqtt + is_main_push_channel=true + device_sub_type=2 + device_toke + _csrftoken + guid + _uuid + users + family_device_id)
179            # device_type=android_mqtt&is_main_push_channel=true&device_sub_type=2&device_token={"k":"eyJwbiI6ImNvbS5pbnN0YWdyYW0uYW5kcm9pZCIsImRpIjoiNzhlNGMxNmQtN2YzNC00NDlkLTg4OWMtMTAwZDg5OTU0NDJhIiwiYWkiOjU2NzMxMDIwMzQxNTA1MiwiY2siOiIxNjgzNTY3Mzg0NjQyOTQifQ==","v":0,"t":"fbns-b64"}&_csrftoken=mmdoMLXFQEzt2w5xLbfm0FTs7gIgqAlc&guid=f87b5e9f-0663-42f8-9213-ec72cb49c961&_uuid=f87b5e9f-0663-42f8-9213-ec72cb49c961&users=3149016955&family_device_id=9d9aa0f0-40fe-4524-a920-9910f45ba18d
180            # error 400: {"message": "no token provided", "status": "fail"}
181            # check_flow.append(self.push_register())
182
183            # /api/v1/feed/reels_media/
184            # error 400: {"message": "Invalid reel id list", "status": "fail"}
185            # check_flow.append(self.get_reels_media())
186
187            # /api/v1/media/blocked/
188            check_flow.append(self.media_blocked())
189
190            # /api/v1/news/inbox/
191            check_flow.append(self.get_news_inbox())
192
193            # /api/v1/loom/fetch_config/
194            check_flow.append(self.get_loom_fetch_config())
195
196            # /api/v1/scores/bootstrap/users/?surfaces=%5B%22autocomplete_user_list%22%2C%22coefficient_besties_list_ranking%22%2C%22coefficient_rank_recipient_user_suggestion%22%2C%22coefficient_ios_section_test_bootstrap_ranking%22%2C%22coefficient_direct_recipients_ranking_variant_2%22%5D
197            check_flow.append(self.get_scores_bootstrap())
198
199            # /api/v1/business/eligibility/get_monetization_products_eligibility_data/?product_types=branded_content
200            check_flow.append(self.get_monetization_products_eligibility_data())
201
202            # /api/v1/business/branded_content/should_require_professional_account/
203            check_flow.append(self.get_business_branded_content())
204
205            # /api/v1/linked_accounts/get_linkage_status/
206            check_flow.append(self.get_linked_accounts())
207
208            # /api/v1/locations/request_country/
209            check_flow.append(self.get_request_country())
210
211            # /api/v1/qp/get_cooldowns/?signed_body=a7c6081ee2ae5b41a1475f83b6dbc8f1130c67d472f69748221468e1621823b5.%7B%7D&ig_sig_key_version=4
212            check_flow.append(self.get_cooldowns())
213
214            # /api/v1/users/arlink_download_info/?version_override=2.2.1
215            check_flow.append(self.arlink_download_info())
216
217            # push register
218            # error 400: {"message": "no token provided", "status": "fail"}
219            # check_flow.append(self.push_register())
220
221            # /api/v1/users/self.user_id/info/
222            check_flow.append(self.get_username_info(self.user_id))
223
224            # /api/v1/notifications/store_client_push_permissions/
225            # error 400: {"message": "missing param", "status": "fail"}
226            # check_flow.append(self.log_resurrect_attribution())
227
228            # /api/v1/accounts/process_contact_point_signals
229            # error 400: {"message": "", "status": "fail"}
230            # check_flow.append(self.process_contact_point_signals())
231
232            # creatives/write_supported_capabilities
233            # error 500: unknown
234            # check_flow.append(self.write_supported_capabilities())
235
236            check_flow.append(self.get_presence())
237            check_flow.append(self.get_direct_v2_inbox())
238            check_flow.append(self.get_direct_v2_inbox2())
239            check_flow.append(self.topical_explore())
240            check_flow.append(self.notification_badge())
241            check_flow.append(self.batch_fetch())
242
243            # TODO add facebook_ota
244            # /api/v1/facebook_ota/?fields=update%7Bdownload_uri%2Cdownload_uri_delta_base%2Cversion_code_delta_base%2Cdownload_uri_delta%2Cfallback_to_full_update%2Cfile_size_delta%2Cversion_code%2Cpublished_date%2Cfile_size%2Cota_bundle_type%2Cresources_checksum%2Callowed_networks%2Crelease_id%7D&custom_user_id=3149016955&signed_body=656adcfd879d775324e9c1668534f80a999801c7780f16cc720d7970941195de.&ig_sig_key_version=4&version_code=195435566&version_name=126.0.0.25.121&custom_app_id=124024574287414&custom_device_id=f87b5e9f-0663-42f8-9213-ec72cb49c961 HTTP/1.1
245            # check_flow.append(self.facebook_ota())
246        except Exception as e:
247            self.logger.error(
248                "Exception raised: {}\n{}".format(e, traceback.format_exc())
249            )
250            return False
251    else:
252        try:
253            pull_to_refresh = random.randint(1, 100) % 2 == 0
254            check_flow.append(
255                self.get_timeline_feed(
256                    options=["is_pull_to_refresh"] if pull_to_refresh is True else []
257                )
258            )  # Random pull_to_refresh :)
259            check_flow.append(
260                self.get_reels_tray_feed(
261                    reason="pull_to_refresh"
262                    if pull_to_refresh is True
263                    else "cold_start"
264                )
265            )
266
267            is_session_expired = (time.time() - self.last_login) > app_refresh_interval
268            if is_session_expired:
269                self.last_login = time.time()
270                self.client_session_id = self.generate_UUID(uuid_type=True)
271
272                # getBootstrapUsers() ...
273                check_flow.append(self.get_ranked_recipients("reshare", True))
274                check_flow.append(self.get_ranked_recipients("save", True))
275                check_flow.append(self.get_inbox_v2())
276                check_flow.append(self.get_presence())
277                check_flow.append(self.get_recent_activity())
278                check_flow.append(self.get_profile_notice())
279                check_flow.append(self.explore(False))
280
281            if (time.time() - self.last_experiments) > 7200:
282                check_flow.append(self.sync_user_features())
283                check_flow.append(self.sync_device_features())
284        except Exception as e:
285            self.logger.error(
286                "Error loginin, exception raised: {}\n{}".format(
287                    e, traceback.format_exc()
288                )
289            )
290            return False
291
292    self.save_uuid_and_cookie()
293    return False if False in check_flow else True
294
295
296# ====== DEVICE / CLIENT_ID / PHONE_ID AND OTHER VALUES (uuids) ====== #
297
298
299def set_device(self):
300    self.device_settings = devices.DEVICES[self.device]
301    self.user_agent = config.USER_AGENT_BASE.format(**self.device_settings)
302
303
304def generate_all_uuids(self):
305    self.phone_id = self.generate_UUID(uuid_type=True)
306    self.uuid = self.generate_UUID(uuid_type=True)
307    self.client_session_id = self.generate_UUID(uuid_type=True)
308    self.advertising_id = self.generate_UUID(uuid_type=True)
309    self.device_id = self.generate_device_id(
310        self.get_seed(self.username, self.password)
311    )
312
313
314def reinstall_app_simulation(self):
315    self.logger.info("Reinstall app simulation, generating new `phone_id`...")
316    self.phone_id = self.generate_UUID(uuid_type=True)
317    self.save_uuid_and_cookie()
318    self.logger.info("New phone_id: {}".format(self.phone_id))
319
320
321def change_device_simulation(self):
322    self.logger.info("Change device simulation")
323    self.reinstall_app_simulation()
324    self.logger.info("Generating new `android_device_id`...")
325    self.device_id = self.generate_device_id(
326        self.get_seed(self.generate_UUID(uuid_type=True))
327    )
328    self.save_uuid_and_cookie()
329    self.logger.info("New android_device_id: {}".format(self.device_id))
330
331
332def load_uuid_and_cookie(self, load_uuid=True, load_cookie=True):
333    if self.cookie_fname is None:
334        fname = "{}_uuid_and_cookie.json".format(self.username)
335        self.cookie_fname = os.path.join(self.base_path, fname)
336        print(os.path.join(self.base_path, fname))
337
338    if os.path.isfile(self.cookie_fname) is False:
339        return False
340
341    with open(self.cookie_fname, "r") as f:
342        data = json.load(f)
343        if "cookie" in data:
344            self.last_login = data["timing_value"]["last_login"]
345            self.last_experiments = data["timing_value"]["last_experiments"]
346
347            if load_cookie:
348                self.logger.debug("Loading cookies")
349                self.session.cookies = requests.utils.cookiejar_from_dict(
350                    data["cookie"]
351                )
352                cookie_username = self.cookie_dict["ds_user"]
353                assert cookie_username == self.username.lower()
354                self.cookie_dict["urlgen"]
355
356            if load_uuid:
357                self.logger.debug("Loading uuids")
358                self.phone_id = data["uuids"]["phone_id"]
359                self.uuid = data["uuids"]["uuid"]
360                self.client_session_id = data["uuids"]["client_session_id"]
361                self.advertising_id = data["uuids"]["advertising_id"]
362                self.device_id = data["uuids"]["device_id"]
363
364                self.device_settings = data["device_settings"]
365                self.user_agent = data["user_agent"]
366
367            msg = (
368                "Recovery from {}: COOKIE {} - UUIDs {} - TIMING, DEVICE "
369                "and ...\n- user-agent={}\n- phone_id={}\n- uuid={}\n- "
370                "client_session_id={}\n- device_id={}"
371            )
372
373            self.logger.info(
374                msg.format(
375                    self.cookie_fname,
376                    load_cookie,
377                    load_uuid,
378                    self.user_agent,
379                    self.phone_id,
380                    self.uuid,
381                    self.client_session_id,
382                    self.device_id,
383                )
384            )
385        else:
386            self.logger.info(
387                "The cookie seems to be the with the older structure. "
388                "Load and init again all uuids"
389            )
390            self.session.cookies = requests.utils.cookiejar_from_dict(data)
391            self.last_login = time.time()
392            self.last_experiments = time.time()
393            cookie_username = self.cookie_dict["ds_user"]
394            assert cookie_username == self.username
395            self.set_device()
396            self.generate_all_uuids()
397
398    self.is_logged_in = True
399    return True
400
401
402def save_uuid_and_cookie(self):
403    if self.cookie_fname is None:
404        fname = "{}_uuid_and_cookie.json".format(self.username)
405        self.cookie_fname = os.path.join(self.base_path, fname)
406
407    data = {
408        "uuids": {
409            "phone_id": self.phone_id,
410            "uuid": self.uuid,
411            "client_session_id": self.client_session_id,
412            "advertising_id": self.advertising_id,
413            "device_id": self.device_id,
414        },
415        "cookie": requests.utils.dict_from_cookiejar(self.session.cookies),
416        "timing_value": {
417            "last_login": self.last_login,
418            "last_experiments": self.last_experiments,
419        },
420        "device_settings": self.device_settings,
421        "user_agent": self.user_agent,
422    }
423    with open(self.cookie_fname, "w") as f:
424        json.dump(data, f)
425