1import base64 2import datetime 3import hashlib 4import hmac 5import json 6import logging 7import os 8import random 9import sys 10import time 11import uuid 12 13import pytz 14import requests 15import requests.utils 16import six.moves.urllib as urllib 17from requests_toolbelt import MultipartEncoder 18from tqdm import tqdm 19 20 21from . import config, devices 22from .api_login import ( 23 change_device_simulation, 24 generate_all_uuids, 25 load_uuid_and_cookie, 26 login_flow, 27 pre_login_flow, 28 reinstall_app_simulation, 29 save_uuid_and_cookie, 30 set_device, 31 sync_launcher, 32 sync_user_features, 33 get_prefill_candidates, 34 get_account_family, 35 get_zr_token_result, 36 banyan, 37 igtv_browse_feed, 38 creatives_ar_class, 39) 40from .api_photo import configure_photo, download_photo, upload_photo 41from .api_story import configure_story, download_story, upload_story_photo 42from .api_video import configure_video, download_video, upload_video 43from .prepare import delete_credentials, get_credentials 44 45 46try: 47 from json.decoder import JSONDecodeError 48except ImportError: 49 JSONDecodeError = ValueError 50 51 52version_info = sys.version_info[0:3] 53is_py2 = version_info[0] == 2 54is_py3 = version_info[0] == 3 55is_py37 = version_info[:2] == (3, 7) 56 57 58version = "0.117.0" 59current_path = os.path.abspath(os.getcwd()) 60 61 62class API(object): 63 def __init__( 64 self, 65 device=None, 66 base_path=current_path + "/config/", 67 save_logfile=True, 68 log_filename=None, 69 loglevel_file=logging.DEBUG, 70 loglevel_stream=logging.INFO, 71 ): 72 # Setup device and user_agent 73 self.device = device or devices.DEFAULT_DEVICE 74 75 self.cookie_fname = None 76 self.base_path = base_path 77 78 self.is_logged_in = False 79 self.last_login = None 80 81 self.last_response = None 82 self.total_requests = 0 83 84 # Setup logging 85 # instabot_version = Bot.version() 86 # self.logger = logging.getLogger("[instabot_{}]".format(instabot_version)) 87 self.logger = logging.getLogger("instabot version: " + version) 88 89 if not os.path.exists(base_path): 90 os.makedirs(base_path) # create base_path if not exists 91 92 if not os.path.exists(base_path + "/log/"): 93 os.makedirs(base_path + "/log/") # create log folder if not exists 94 95 if save_logfile is True: 96 if log_filename is None: 97 log_filename = os.path.join( 98 base_path, "log/instabot_{}.log".format(id(self)) 99 ) 100 101 fh = logging.FileHandler(filename=log_filename) 102 fh.setLevel(loglevel_file) 103 fh.setFormatter( 104 logging.Formatter( 105 "%(asctime)s - %(name)s (%(module)s) - %(levelname)s - %(message)s" 106 ) 107 ) 108 109 self.logger.addHandler(fh) 110 111 ch = logging.StreamHandler() 112 ch.setLevel(loglevel_stream) 113 ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) 114 115 self.logger.addHandler(ch) 116 self.logger.setLevel(logging.DEBUG) 117 118 self.last_json = None 119 120 def set_user(self, username, password, generate_all_uuids=True, set_device=True): 121 self.username = username 122 self.password = password 123 124 if set_device is True: 125 self.set_device() 126 127 if generate_all_uuids is True: 128 self.generate_all_uuids() 129 130 def set_contact_point_prefill(self, usage="prefill"): 131 data = json.dumps({"phone_id": self.phone_id, "usage": usage}) 132 return self.send_request("accounts/contact_point_prefill/", data, login=True) 133 134 def get_suggested_searches(self, _type="users"): 135 return self.send_request( 136 "fbsearch/suggested_searches/", self.json_data({"type": _type}) 137 ) 138 139 def read_msisdn_header(self, usage="default"): 140 data = json.dumps({"device_id": self.uuid, "mobile_subno_usage": usage}) 141 return self.send_request( 142 "accounts/read_msisdn_header/", 143 data, 144 login=True, 145 headers={"X-DEVICE-ID": self.uuid}, 146 ) 147 148 def log_attribution(self, usage="default"): 149 data = json.dumps({"adid": self.advertising_id}) 150 return self.send_request("attribution/log_attribution/", data, login=True) 151 152 # ====== ALL METHODS IMPORT FROM api_login ====== # 153 # def sync_device_features(self, login=False): 154 # return sync_device_features(self, login) 155 156 def sync_launcher(self, login=False): 157 return sync_launcher(self, login) 158 159 def igtv_browse_feed(self): 160 return igtv_browse_feed(self) 161 162 def creatives_ar_class(self): 163 return creatives_ar_class(self) 164 165 def sync_user_features(self): 166 return sync_user_features(self) 167 168 def get_prefill_candidates(self, login=False): 169 return get_prefill_candidates(self, login) 170 171 def get_account_family(self): 172 return get_account_family(self) 173 174 def get_zr_token_result(self): 175 return get_zr_token_result(self) 176 177 def banyan(self): 178 return banyan(self) 179 180 def pre_login_flow(self): 181 return pre_login_flow(self) 182 183 def login_flow(self, just_logged_in=False, app_refresh_interval=1800): 184 return login_flow(self, just_logged_in, app_refresh_interval) 185 186 def set_device(self): 187 return set_device(self) 188 189 def generate_all_uuids(self): 190 return generate_all_uuids(self) 191 192 def reinstall_app_simulation(self): 193 return reinstall_app_simulation(self) 194 195 def change_device_simulation(self): 196 return change_device_simulation(self) 197 198 def load_uuid_and_cookie(self, load_uuid=True, load_cookie=True): 199 return load_uuid_and_cookie(self, load_uuid=load_uuid, load_cookie=load_cookie) 200 201 def save_uuid_and_cookie(self): 202 return save_uuid_and_cookie(self) 203 204 def login( 205 self, 206 username=None, 207 password=None, 208 force=False, 209 proxy=None, 210 use_cookie=True, 211 use_uuid=True, 212 cookie_fname=None, 213 ask_for_code=False, 214 set_device=True, 215 generate_all_uuids=True, 216 is_threaded=False, 217 ): 218 if password is None: 219 username, password = get_credentials(username=username) 220 221 set_device = generate_all_uuids = True 222 self.set_user(username, password) 223 self.session = requests.Session() 224 225 self.proxy = proxy 226 self.set_proxy() # Only happens if `self.proxy` 227 228 self.cookie_fname = cookie_fname 229 if self.cookie_fname is None: 230 fmt = "{username}_uuid_and_cookie.json" 231 cookie_fname = fmt.format(username=username) 232 self.cookie_fname = os.path.join(self.base_path, cookie_fname) 233 234 cookie_is_loaded = False 235 msg = "Login flow failed, the cookie is broken. Relogin again." 236 237 if use_cookie is True: 238 # try: 239 if ( 240 self.load_uuid_and_cookie(load_cookie=use_cookie, load_uuid=use_uuid) 241 is True 242 ): 243 # Check if the token loaded is valid. 244 if self.login_flow(False) is True: 245 cookie_is_loaded = True 246 self.save_successful_login() 247 else: 248 self.logger.info(msg) 249 set_device = generate_all_uuids = False 250 force = True 251 252 if not cookie_is_loaded and (not self.is_logged_in or force): 253 self.session = requests.Session() 254 if use_uuid is True: 255 if ( 256 self.load_uuid_and_cookie( 257 load_cookie=use_cookie, load_uuid=use_uuid 258 ) 259 is False 260 ): 261 if set_device is True: 262 self.set_device() 263 if generate_all_uuids is True: 264 self.generate_all_uuids() 265 self.pre_login_flow() 266 data = json.dumps( 267 { 268 "jazoest": "22264", 269 "country_codes": '[{"country_code":"1","source":["default"]}]', 270 "phone_id": self.phone_id, 271 "_csrftoken": self.token, 272 "username": self.username, 273 "adid": "", 274 "guid": self.uuid, 275 "device_id": self.device_id, 276 "google_tokens": "[]", 277 "password": self.password, 278 # "enc_password:" "#PWD_INSTAGRAM:4:TIME:ENCRYPTED_PASSWORD" 279 "login_attempt_count": "0", 280 } 281 ) 282 283 if self.send_request("accounts/login/", data, True): 284 self.save_successful_login() 285 self.login_flow(True) 286 return True 287 288 elif ( 289 self.last_json.get("error_type", "") == "checkpoint_challenge_required" 290 ): 291 # self.logger.info("Checkpoint challenge required...") 292 if ask_for_code is True: 293 solved = self.solve_challenge() 294 if solved: 295 self.save_successful_login() 296 self.login_flow(True) 297 return True 298 else: 299 self.logger.error( 300 "Failed to login, unable to solve the challenge" 301 ) 302 self.save_failed_login() 303 return False 304 else: 305 return False 306 elif self.last_json.get("two_factor_required"): 307 if self.two_factor_auth(): 308 self.save_successful_login() 309 self.login_flow(True) 310 return True 311 else: 312 self.logger.error("Failed to login with 2FA!") 313 self.save_failed_login() 314 return False 315 else: 316 self.logger.error( 317 "Failed to login go to instagram and change your password" 318 ) 319 self.save_failed_login() 320 delete_credentials() 321 return False 322 323 def two_factor_auth(self): 324 self.logger.info("Two-factor authentication required") 325 two_factor_code = input("Enter 2FA verification code: ") 326 two_factor_id = self.last_json["two_factor_info"]["two_factor_identifier"] 327 328 login = self.session.post( 329 config.API_URL + "accounts/two_factor_login/", 330 data={ 331 "username": self.username, 332 "verification_code": two_factor_code, 333 "two_factor_identifier": two_factor_id, 334 "password": self.password, 335 "device_id": self.device_id, 336 "ig_sig_key_version": config.SIG_KEY_VERSION, 337 }, 338 allow_redirects=True, 339 ) 340 341 if login.status_code == 200: 342 resp_json = json.loads(login.text) 343 if resp_json["status"] != "ok": 344 if "message" in resp_json: 345 self.logger.error("Login error: {}".format(resp_json["message"])) 346 else: 347 self.logger.error( 348 ('Login error: "{}" status and' " message {}.").format( 349 resp_json["status"], login.text 350 ) 351 ) 352 return False 353 return True 354 else: 355 self.logger.error( 356 ( 357 "Two-factor authentication request returns " 358 "{} error with message {} !" 359 ).format(login.status_code, login.text) 360 ) 361 return False 362 363 def save_successful_login(self): 364 self.is_logged_in = True 365 self.last_login = time.time() 366 self.logger.info("Logged-in successfully as '{}'!".format(self.username)) 367 368 def save_failed_login(self): 369 self.logger.info("Username or password is incorrect.") 370 delete_credentials() 371 sys.exit() 372 373 def sync_device_features(self, login=False): 374 data = { 375 "id": self.uuid, 376 "server_config_retrieval": "1", 377 "experiments": config.LOGIN_EXPERIMENTS, 378 } 379 if login is False: 380 data["_uuid"] = self.uuid 381 data["_uid"] = self.user_id 382 data["_csrftoken"] = self.token 383 data = json.dumps(data) 384 return self.send_request( 385 "qe/sync/", data, login=login, headers={"X-DEVICE-ID": self.uuid} 386 ) 387 388 def solve_challenge(self): 389 challenge_url = self.last_json["challenge"]["api_path"][1:] 390 try: 391 self.send_request(challenge_url, None, login=True, with_signature=False) 392 except Exception as e: 393 self.logger.error("solve_challenge; {}".format(e)) 394 return False 395 396 choices = self.get_challenge_choices() 397 for choice in choices: 398 print(choice) 399 code = input("Insert choice: ") 400 401 data = json.dumps({"choice": code}) 402 try: 403 self.send_request(challenge_url, data, login=True) 404 except Exception as e: 405 self.logger.error(e) 406 return False 407 408 print("A code has been sent to the method selected, please check.") 409 code = input("Insert code: ").replace(" ", "") 410 411 data = json.dumps({"security_code": code}) 412 try: 413 self.send_request(challenge_url, data, login=True) 414 except Exception as e: 415 self.logger.error(e) 416 return False 417 418 worked = ( 419 ("logged_in_user" in self.last_json) 420 and (self.last_json.get("action", "") == "close") 421 and (self.last_json.get("status", "") == "ok") 422 ) 423 424 if worked: 425 return True 426 427 self.logger.error("Not possible to log in. Reset and try again") 428 return False 429 430 def get_challenge_choices(self): 431 last_json = self.last_json 432 choices = [] 433 434 if last_json.get("step_name", "") == "select_verify_method": 435 choices.append("Checkpoint challenge received") 436 if "phone_number" in last_json["step_data"]: 437 choices.append("0 - Phone") 438 if "email" in last_json["step_data"]: 439 choices.append("1 - Email") 440 441 if last_json.get("step_name", "") == "delta_login_review": 442 choices.append("Login attempt challenge received") 443 choices.append("0 - It was me") 444 choices.append("0 - It wasn't me") 445 446 if not choices: 447 choices.append( 448 '"{}" challenge received'.format(last_json.get("step_name", "Unknown")) 449 ) 450 choices.append("0 - Default") 451 452 return choices 453 454 def logout(self, *args, **kwargs): 455 if not self.is_logged_in: 456 return True 457 data = json.dumps({}) 458 self.is_logged_in = not self.send_request( 459 "accounts/logout/", data, with_signature=False 460 ) 461 return not self.is_logged_in 462 463 def set_proxy(self): 464 if getattr(self, "proxy", None): 465 parsed = urllib.parse.urlparse(self.proxy) 466 scheme = "http://" if not parsed.scheme else "" 467 self.session.proxies["http"] = scheme + self.proxy 468 self.session.proxies["https"] = scheme + self.proxy 469 470 def send_request( 471 self, 472 endpoint, 473 post=None, 474 login=False, 475 with_signature=True, 476 headers=None, 477 extra_sig=None, 478 timeout_minutes=None, 479 ): 480 self.set_proxy() # Only happens if `self.proxy` 481 self.session.headers.update(config.REQUEST_HEADERS) 482 self.session.headers.update({"User-Agent": self.user_agent}) 483 if not self.is_logged_in and not login: 484 msg = "Not logged in!" 485 self.logger.critical(msg) 486 raise Exception(msg) 487 if headers: 488 self.session.headers.update(headers) 489 try: 490 self.total_requests += 1 491 if post is not None: # POST 492 if with_signature: 493 post = self.generate_signature( 494 post 495 ) # Only `send_direct_item` doesn't need a signature 496 if extra_sig is not None and extra_sig != []: 497 post += "&".join(extra_sig) 498 # time.sleep(random.randint(1, 2)) 499 response = self.session.post(config.API_URL + endpoint, data=post) 500 else: # GET 501 # time.sleep(random.randint(1, 2)) 502 response = self.session.get(config.API_URL + endpoint) 503 except Exception as e: 504 self.logger.warning(str(e)) 505 return False 506 507 self.last_response = response 508 if post is not None: 509 self.logger.debug( 510 "POST to endpoint: {} returned response: {}".format(endpoint, response) 511 ) 512 else: 513 self.logger.debug( 514 "GET to endpoint: {} returned response: {}".format(endpoint, response) 515 ) 516 if response.status_code == 200: 517 try: 518 self.last_json = json.loads(response.text) 519 return True 520 except JSONDecodeError: 521 return False 522 else: 523 self.logger.debug( 524 "Responsecode indicates error; response content: {}".format( 525 response.content 526 ) 527 ) 528 if response.status_code != 404 and response.status_code != "404": 529 self.logger.error( 530 "Request returns {} error!".format(response.status_code) 531 ) 532 try: 533 response_data = json.loads(response.text) 534 if response_data.get( 535 "message" 536 ) is not None and "feedback_required" in str( 537 response_data.get("message").encode("utf-8") 538 ): 539 self.logger.error( 540 "ATTENTION!: `feedback_required`" 541 + str(response_data.get("feedback_message").encode("utf-8")) 542 ) 543 try: 544 self.last_response = response 545 self.last_json = json.loads(response.text) 546 except Exception: 547 pass 548 return "feedback_required" 549 except ValueError: 550 self.logger.error( 551 "Error checking for `feedback_required`, " 552 "response text is not JSON" 553 ) 554 self.logger.info("Full Response: {}".format(str(response))) 555 try: 556 self.logger.info("Response Text: {}".format(str(response.text))) 557 except Exception: 558 pass 559 if response.status_code == 429: 560 # if we come to this error, add 5 minutes of sleep everytime we hit the 429 error (aka soft bann) keep increasing untill we are unbanned 561 if timeout_minutes is None: 562 timeout_minutes = 0 563 if timeout_minutes == 15: 564 # If we have been waiting for more than 15 minutes, lets restart. 565 time.sleep(1) 566 self.logger.error( 567 "Since we hit 15 minutes of time outs, we have to restart. Removing session and cookies. Please relogin." 568 ) 569 delete_credentials() 570 sys.exit() 571 timeout_minutes += 5 572 self.logger.warning( 573 "That means 'too many requests'. I'll go to sleep " 574 "for {} minutes.".format(timeout_minutes) 575 ) 576 time.sleep(timeout_minutes * 60) 577 return self.send_request( 578 endpoint, 579 post, 580 login, 581 with_signature, 582 headers, 583 extra_sig, 584 timeout_minutes, 585 ) 586 if response.status_code == 400: 587 response_data = json.loads(response.text) 588 if response_data.get("challenge_required"): 589 # Try and fix the challenge required error by totally restarting 590 self.logger.error( 591 "Failed to login go to instagram and change your password" 592 ) 593 delete_credentials() 594 # PERFORM Interactive Two-Factor Authentication 595 if response_data.get("two_factor_required"): 596 try: 597 self.last_response = response 598 self.last_json = json.loads(response.text) 599 except Exception: 600 self.logger.error("Error unknown send request 400 2FA") 601 pass 602 return self.two_factor_auth() 603 # End of Interactive Two-Factor Authentication 604 else: 605 msg = "Instagram's error message: {}" 606 self.logger.info(msg.format(response_data.get("message"))) 607 if "error_type" in response_data: 608 msg = "Error type: {}".format(response_data["error_type"]) 609 self.logger.info(msg) 610 611 # For debugging 612 try: 613 self.last_response = response 614 self.last_json = json.loads(response.text) 615 except Exception: 616 self.logger.error("Error unknown send request") 617 pass 618 return False 619 620 @property 621 def cookie_dict(self): 622 return self.session.cookies.get_dict() 623 624 @property 625 def token(self): 626 return self.cookie_dict["csrftoken"] 627 628 @property 629 def user_id(self): 630 return self.cookie_dict["ds_user_id"] 631 632 @property 633 def rank_token(self): 634 return "{}_{}".format(self.user_id, self.uuid) 635 636 @property 637 def default_data(self): 638 return {"_uuid": self.uuid, "_uid": self.user_id, "_csrftoken": self.token} 639 640 def json_data(self, data=None): 641 """Adds the default_data to data and dumps it to a json.""" 642 if data is None: 643 data = {} 644 data.update(self.default_data) 645 return json.dumps(data) 646 647 def action_data(self, data): 648 _data = {"radio_type": "wifi-none", "device_id": self.device_id} 649 data.update(_data) 650 return data 651 652 def auto_complete_user_list(self): 653 return self.send_request("friendships/autocomplete_user_list/") 654 655 def batch_fetch(self): 656 data = { 657 "scale": 3, 658 "version": 1, 659 "vc_policy": "default", 660 "surfaces_to_triggers": '{"5734":["instagram_feed_prompt"],' 661 + '"4715":["instagram_feed_header"],' 662 + '"5858":["instagram_feed_tool_tip"]}', # noqa 663 "surfaces_to_queries": ( 664 '{"5734":"viewer() {eligible_promotions.trigger_context_v2(<tr' 665 "igger_context_v2>).ig_parameters(<ig_parameters>).trigger_nam" 666 "e(<trigger_name>).surface_nux_id(<surface>).external_gating_p" 667 "ermitted_qps(<external_gating_permitted_qps>).supports_client" 668 "_filters(true).include_holdouts(true) {edges {client_ttl_seco" 669 "nds,log_eligibility_waterfall,is_holdout,priority,time_range " 670 "{start,end},node {id,promotion_id,logging_data,max_impression" 671 "s,triggers,contextual_filters {clause_type,filters {filter_ty" 672 "pe,unknown_action,value {name,required,bool_value,int_value,s" 673 "tring_value},extra_datas {name,required,bool_value,int_value," 674 "string_value}},clauses {clause_type,filters {filter_type,unkn" 675 "own_action,value {name,required,bool_value,int_value,string_v" 676 "alue},extra_datas {name,required,bool_value,int_value,string_" 677 "value}},clauses {clause_type,filters {filter_type,unknown_act" 678 "ion,value {name,required,bool_value,int_value,string_value},e" 679 "xtra_datas {name,required,bool_value,int_value,string_value}}" 680 ",clauses {clause_type,filters {filter_type,unknown_action,val" 681 "ue {name,required,bool_value,int_value,string_value},extra_da" 682 "tas {name,required,bool_value,int_value,string_value}}}}}},is" 683 "_uncancelable,template {name,parameters {name,required,bool_v" 684 "alue,string_value,color_value,}},creatives {title {text},cont" 685 "ent {text},footer {text},social_context {text},social_context" 686 "_images,primary_action{title {text},url,limit,dismiss_promoti" 687 "on},secondary_action{title {text},url,limit,dismiss_promotion" 688 "},dismiss_action{title {text},url,limit,dismiss_promotion},im" 689 'age.scale(<scale>) {uri,width,height}}}}}}","4715":"viewer() ' 690 "{eligible_promotions.trigger_context_v2(<trigger_context_v2>)" 691 ".ig_parameters(<ig_parameters>).trigger_name(<trigger_name>)." 692 "surface_nux_id(<surface>).external_gating_permitted_qps(<exte" 693 "rnal_gating_permitted_qps>).supports_client_filters(true).inc" 694 "lude_holdouts(true) {edges {client_ttl_seconds,log_eligibilit" 695 "y_waterfall,is_holdout,priority,time_range {start,end},node {" 696 "id,promotion_id,logging_data,max_impressions,triggers,context" 697 "ual_filters {clause_type,filters {filter_type,unknown_action," 698 "value {name,required,bool_value,int_value,string_value},extra" 699 "_datas {name,required,bool_value,int_value,string_value}},cla" 700 "uses {clause_type,filters {filter_type,unknown_action,value {" 701 "name,required,bool_value,int_value,string_value},extra_datas " 702 "{name,required,bool_value,int_value,string_value}},clauses {c" 703 "lause_type,filters {filter_type,unknown_action,value {name,re" 704 "quired,bool_value,int_value,string_value},extra_datas {name,r" 705 "equired,bool_value,int_value,string_value}},clauses {clause_t" 706 "ype,filters {filter_type,unknown_action,value {name,required," 707 "bool_value,int_value,string_value},extra_datas {name,required" 708 ",bool_value,int_value,string_value}}}}}},is_uncancelable,temp" 709 "late {name,parameters {name,required,bool_value,string_value," 710 "color_value,}},creatives {title {text},content {text},footer " 711 "{text},social_context {text},social_context_images,primary_ac" 712 "tion{title {text},url,limit,dismiss_promotion},secondary_acti" 713 "on{title {text},url,limit,dismiss_promotion},dismiss_action{t" 714 "itle {text},url,limit,dismiss_promotion},image.scale(<scale>)" 715 ' {uri,width,height}}}}}}","5858":"viewer() {eligible_promotio' 716 "ns.trigger_context_v2(<trigger_context_v2>).ig_parameters(<ig" 717 "_parameters>).trigger_name(<trigger_name>).surface_nux_id(<su" 718 "rface>).external_gating_permitted_qps(<external_gating_permit" 719 "ted_qps>).supports_client_filters(true).include_holdouts(true" 720 ") {edges {client_ttl_seconds,log_eligibility_waterfall,is_hol" 721 "dout,priority,time_range {start,end},node {id,promotion_id,lo" 722 "gging_data,max_impressions,triggers,contextual_filters {claus" 723 "e_type,filters {filter_type,unknown_action,value {name,requir" 724 "ed,bool_value,int_value,string_value},extra_datas {name,requi" 725 "red,bool_value,int_value,string_value}},clauses {clause_type," 726 "filters {filter_type,unknown_action,value {name,required,bool" 727 "_value,int_value,string_value},extra_datas {name,required,boo" 728 "l_value,int_value,string_value}},clauses {clause_type,filters" 729 " {filter_type,unknown_action,value {name,required,bool_value," 730 "int_value,string_value},extra_datas {name,required,bool_value" 731 ",int_value,string_value}},clauses {clause_type,filters {filte" 732 "r_type,unknown_action,value {name,required,bool_value,int_val" 733 "ue,string_value},extra_datas {name,required,bool_value,int_va" 734 "lue,string_value}}}}}},is_uncancelable,template {name,paramet" 735 "ers {name,required,bool_value,string_value,color_value,}},cre" 736 "atives {title {text},content {text},footer {text},social_cont" 737 "ext {text},social_context_images,primary_action{title {text}," 738 "url,limit,dismiss_promotion},secondary_action{title {text},ur" 739 "l,limit,dismiss_promotion},dismiss_action{title {text},url,li" 740 "mit,dismiss_promotion},image.scale(<scale>) {uri,width,height" 741 '}}}}}}"}' 742 ), # noqa (Just copied from request) 743 } 744 data = self.json_data(data) 745 return self.send_request("qp/batch_fetch/", data) 746 747 def get_timeline_feed(self, options=[]): 748 headers = { 749 "X-Ads-Opt-Out": "0", 750 "X-DEVICE-ID": self.uuid, 751 "X-CM-Bandwidth-KBPS": str(random.randint(2000, 5000)), 752 "X-CM-Latency": str(random.randint(1, 5)), 753 } 754 data = { 755 "feed_view_info": "", 756 "phone_id": self.phone_id, 757 "battery_level": random.randint(25, 100), 758 "timezone_offset": datetime.datetime.now(pytz.timezone("CET")).strftime( 759 "%z" 760 ), 761 "_csrftoken": self.token, 762 "device_id": self.uuid, 763 "request_id": self.device_id, 764 "_uuid": self.uuid, 765 "is_charging": random.randint(0, 1), 766 "will_sound_on": random.randint(0, 1), 767 "session_id": self.client_session_id, 768 "bloks_versioning_id": "e538d4591f238824118bfcb9528c8d005f2ea3becd947a3973c030ac971bb88e", 769 } 770 771 if "is_pull_to_refresh" in options: 772 data["reason"] = "pull_to_refresh" 773 data["is_pull_to_refresh"] = "1" 774 elif "is_pull_to_refresh" not in options: 775 data["reason"] = "cold_start_fetch" 776 data["is_pull_to_refresh"] = "0" 777 778 if "push_disabled" in options: 779 data["push_disabled"] = "true" 780 781 if "recovered_from_crash" in options: 782 data["recovered_from_crash"] = "1" 783 784 data = json.dumps(data) 785 return self.send_request( 786 "feed/timeline/", data, with_signature=False, headers=headers 787 ) 788 789 def get_megaphone_log(self): 790 return self.send_request("megaphone/log/") 791 792 def expose(self): 793 data = self.json_data( 794 {"id": self.uuid, "experiment": "ig_android_profile_contextual_feed"} 795 ) 796 return self.send_request("qe/expose/", data) 797 798 # ====== PHOTO METHODS ====== # 799 def upload_photo( 800 self, 801 photo, 802 caption=None, 803 upload_id=None, 804 from_video=False, 805 force_resize=False, 806 options={}, 807 ): 808 """Upload photo to Instagram 809 810 @param photo Path to photo file (String) 811 @param caption Media description (String) 812 @param upload_id Unique upload_id (String). When None, then 813 generate automatically 814 @param from_video A flag that signals whether the photo is loaded 815 from the video or by itself 816 (Boolean, DEPRECATED: not used) 817 @param force_resize Force photo resize (Boolean) 818 @param options Object with difference options, e.g. 819 configure_timeout, rename (Dict) 820 Designed to reduce the number of function 821 arguments! This is the simplest request object. 822 823 @return Boolean 824 """ 825 return upload_photo( 826 self, photo, caption, upload_id, from_video, force_resize, options 827 ) 828 829 def download_photo(self, media_id, filename, media=False, folder="photos"): 830 return download_photo(self, media_id, filename, media, folder) 831 832 def configure_photo(self, upload_id, photo, caption=""): 833 return configure_photo(self, upload_id, photo, caption) 834 835 # ====== STORY METHODS ====== # 836 def download_story(self, filename, story_url, username): 837 return download_story(self, filename, story_url, username) 838 839 def upload_story_photo(self, photo, upload_id=None): 840 return upload_story_photo(self, photo, upload_id) 841 842 def configure_story(self, upload_id, photo): 843 return configure_story(self, upload_id, photo) 844 845 # ====== VIDEO METHODS ====== # 846 def upload_video( 847 self, video, caption=None, upload_id=None, thumbnail=None, options={} 848 ): 849 """Upload video to Instagram 850 851 @param video Path to video file (String) 852 @param caption Media description (String) 853 @param upload_id Unique upload_id (String). When None, then 854 generate automatically 855 @param thumbnail Path to thumbnail for video (String). When None, 856 then thumbnail is generate automatically 857 @param options Object with difference options, e.g. 858 configure_timeout, rename_thumbnail, rename (Dict) 859 Designed to reduce the number of function arguments! 860 This is the simplest request object. 861 862 @return Object with state of uploading to 863 Instagram (or False) 864 """ 865 return upload_video(self, video, caption, upload_id, thumbnail, options) 866 867 def download_video(self, media_id, filename, media=False, folder="video"): 868 return download_video(self, media_id, filename, media, folder) 869 870 def configure_video( 871 self, 872 upload_id, 873 video, 874 thumbnail, 875 width, 876 height, 877 duration, 878 caption="", 879 options={}, 880 ): 881 """Post Configure Video 882 (send caption, thumbnail andmore else to Instagram) 883 884 @param upload_id Unique upload_id (String). Received 885 from "upload_video" 886 @param video Path to video file (String) 887 @param thumbnail Path to thumbnail for video (String). When None, 888 then thumbnail is generate automatically 889 @param width Width in px (Integer) 890 @param height Height in px (Integer) 891 @param duration Duration in seconds (Integer) 892 @param caption Media description (String) 893 @param options Object with difference options, e.g. 894 configure_timeout, rename_thumbnail, rename (Dict) 895 Designed to reduce the number of function arguments! 896 This is the simplest request object. 897 """ 898 return configure_video( 899 self, upload_id, video, thumbnail, width, height, duration, caption, options 900 ) 901 902 # ====== MEDIA METHODS ====== # 903 def edit_media(self, media_id, captionText=""): 904 data = self.json_data({"caption_text": captionText}) 905 url = "media/{media_id}/edit_media/".format(media_id=media_id) 906 return self.send_request(url, data) 907 908 def remove_self_tag(self, media_id): 909 data = self.json_data() 910 url = "media/{media_id}/remove/".format(media_id=media_id) 911 return self.send_request(url, data) 912 913 def media_info(self, media_id): 914 # data = self.json_data({'media_id': media_id}) 915 url = "media/{media_id}/info/".format(media_id=media_id) 916 return self.send_request(url) 917 918 def archive_media(self, media, undo=False): 919 action = "only_me" if not undo else "undo_only_me" 920 data = self.json_data({"media_id": media["id"]}) 921 url = "media/{media_id}/{action}/?media_type={media_type}".format( 922 media_id=media["id"], action=action, media_type=media["media_type"] 923 ) 924 return self.send_request(url, data) 925 926 def delete_media(self, media): 927 data = self.json_data({"media_id": media.get("id")}) 928 url = "media/{media_id}/delete/".format(media_id=media.get("id")) 929 return self.send_request(url, data) 930 931 def gen_user_breadcrumb(self, size): 932 key = "iN4$aGr0m" 933 dt = int(time.time() * 1000) 934 935 time_elapsed = random.randint(500, 1500) + size * random.randint(500, 1500) 936 text_change_event_count = max(1, size / random.randint(3, 5)) 937 938 data = "{size!s} {elapsed!s} {count!s} {dt!s}".format( 939 **{ 940 "size": size, 941 "elapsed": time_elapsed, 942 "count": text_change_event_count, 943 "dt": dt, 944 } 945 ) 946 return "{!s}\n{!s}\n".format( 947 base64.b64encode( 948 hmac.new( 949 key.encode("ascii"), data.encode("ascii"), digestmod=hashlib.sha256 950 ).digest() 951 ), 952 base64.b64encode(data.encode("ascii")), 953 ) 954 955 def comment(self, media_id, comment_text): 956 return self.send_request( 957 endpoint="media/{media_id}/comment/".format(media_id=media_id), 958 post=self.json_data( 959 self.action_data( 960 { 961 "container_module": "comments_v2", 962 "user_breadcrumb": self.gen_user_breadcrumb(len(comment_text)), 963 "idempotence_token": self.generate_UUID(True), 964 "comment_text": comment_text, 965 } 966 ) 967 ), 968 ) 969 970 def reply_to_comment(self, media_id, comment_text, parent_comment_id): 971 data = self.json_data( 972 {"comment_text": comment_text, "replied_to_comment_id": parent_comment_id} 973 ) 974 url = "media/{media_id}/comment/".format(media_id=media_id) 975 return self.send_request(url, data) 976 977 def delete_comment(self, media_id, comment_id): 978 data = self.json_data() 979 url = "media/{media_id}/comment/{comment_id}/delete/" 980 url = url.format(media_id=media_id, comment_id=comment_id) 981 return self.send_request(url, data) 982 983 def get_comment_likers(self, comment_id): 984 url = "media/{comment_id}/comment_likers/?".format(comment_id=comment_id) 985 return self.send_request(url) 986 987 def get_media_likers(self, media_id): 988 url = "media/{media_id}/likers/?".format(media_id=media_id) 989 return self.send_request(url) 990 991 def like_comment(self, comment_id): 992 data = self.json_data( 993 { 994 "is_carousel_bumped_post": "false", 995 "container_module": "comments_v2", 996 "feed_position": "0", 997 } 998 ) 999 url = "media/{comment_id}/comment_like/".format(comment_id=comment_id) 1000 return self.send_request(url, data) 1001 1002 def unlike_comment(self, comment_id): 1003 data = self.json_data( 1004 { 1005 "is_carousel_bumped_post": "false", 1006 "container_module": "comments_v2", 1007 "feed_position": "0", 1008 } 1009 ) 1010 url = "media/{comment_id}/comment_unlike/".format(comment_id=comment_id) 1011 return self.send_request(url, data) 1012 1013 # From profile => "is_carousel_bumped_post":"false", 1014 # "container_module":"feed_contextual_profile", "feed_position":"0" # noqa 1015 # From home/feed => "inventory_source":"media_or_ad", 1016 # "is_carousel_bumped_post":"false", "container_module":"feed_timeline", 1017 # "feed_position":"0" # noqa 1018 def like( 1019 self, 1020 media_id, 1021 double_tap=None, 1022 container_module="feed_short_url", 1023 feed_position=0, 1024 username=None, 1025 user_id=None, 1026 hashtag_name=None, 1027 hashtag_id=None, 1028 entity_page_name=None, 1029 entity_page_id=None, 1030 ): 1031 1032 data = self.action_data( 1033 { 1034 "inventory_source": "media_or_ad", 1035 "media_id": media_id, 1036 "radio_type": "wifi-none", 1037 "container_module": container_module, 1038 "feed_position": str(feed_position), 1039 "is_carousel_bumped_post": "false", 1040 } 1041 ) 1042 if container_module == "feed_timeline": 1043 data.update({"inventory_source": "media_or_ad"}) 1044 if username: 1045 data.update({"username": username, "user_id": user_id}) 1046 if hashtag_name: 1047 data.update({"hashtag_name": hashtag_name, "hashtag_id": hashtag_id}) 1048 if entity_page_name: 1049 data.update( 1050 {"entity_page_name": entity_page_name, "entity_page_id": entity_page_id} 1051 ) 1052 # if double_tap is None: 1053 double_tap = random.randint(0, 1) 1054 json_data = self.json_data(data) 1055 # TODO: comment out debug log out when done 1056 self.logger.debug("post data: {}".format(json_data)) 1057 return self.send_request( 1058 endpoint="media/{media_id}/like/".format(media_id=media_id), 1059 post=json_data, 1060 extra_sig=["d={}".format(double_tap)], 1061 headers={ 1062 "X-IG-WWW-Claim": "hmac.AR1ETv6FsubYON5DwNj_0CLNmbW7hSNR1yIMeXuhHJORN4n7" 1063 }, 1064 ) 1065 1066 def unlike(self, media_id): 1067 data = self.json_data( 1068 { 1069 "media_id": media_id, 1070 "radio_type": "wifi-none", 1071 "is_carousel_bumped_post": "false", 1072 "container_module": "photo_view_other", 1073 "feed_position": "0", 1074 } 1075 ) 1076 url = "media/{media_id}/unlike/".format(media_id=media_id) 1077 return self.send_request(url, data) 1078 1079 def get_media_comments(self, media_id, max_id=""): 1080 url = "media/{media_id}/comments/".format(media_id=media_id) 1081 if max_id: 1082 url += "?max_id={max_id}".format(max_id=max_id) 1083 return self.send_request(url) 1084 1085 def explore(self, is_prefetch=False): 1086 data = { 1087 "is_prefetch": is_prefetch, 1088 "is_from_promote": False, 1089 "timezone_offset": datetime.datetime.now(pytz.timezone("CET")).strftime( 1090 "%z" 1091 ), 1092 "session_id": self.client_session_id, 1093 "supported_capabilities_new": config.SUPPORTED_CAPABILITIES, 1094 } 1095 if is_prefetch: 1096 data["max_id"] = 0 1097 data["module"] = "explore_popular" 1098 data = json.dumps(data) 1099 return self.send_request("discover/explore/", data) 1100 1101 def get_username_info(self, user_id): 1102 url = "users/{user_id}/info/".format(user_id=user_id) 1103 return self.send_request(url) 1104 1105 def get_self_username_info(self): 1106 return self.get_username_info(self.user_id) 1107 1108 def get_news_inbox(self): 1109 return self.send_request("news/inbox/") 1110 1111 def get_recent_activity(self): 1112 return self.send_request("news/inbox/?limited_activity=true&show_su=true") 1113 1114 def get_following_recent_activity(self): 1115 return self.send_request("news") 1116 1117 def get_user_tags(self, user_id): 1118 url = ( 1119 "usertags/{user_id}/feed/?rank_token=" "{rank_token}&ranked_content=true&" 1120 ).format(user_id=user_id, rank_token=self.rank_token) 1121 return self.send_request(url) 1122 1123 def get_self_user_tags(self): 1124 return self.get_user_tags(self.user_id) 1125 1126 def get_geo_media(self, user_id): 1127 url = "maps/user/{user_id}/".format(user_id=user_id) 1128 return self.send_request(url) 1129 1130 def get_self_geo_media(self): 1131 return self.get_geo_media(self.user_id) 1132 1133 def sync_from_adress_book(self, contacts): 1134 url = "address_book/link/?include=extra_display_name,thumbnails" 1135 return self.send_request(url, "contacts=" + json.dumps(contacts)) 1136 1137 # ====== FEED METHODS ====== # 1138 def tag_feed(self, tag): 1139 url = "feed/tag/{tag}/?rank_token={rank_token}&ranked_content=true&" 1140 return self.send_request(url.format(tag=tag, rank_token=self.rank_token)) 1141 1142 def get_timeline(self): 1143 url = "feed/timeline/?rank_token={rank_token}&ranked_content=true&" 1144 return self.send_request(url.format(rank_token=self.rank_token)) 1145 1146 def get_archive_feed(self): 1147 url = "feed/only_me_feed/?rank_token={rank_token}&ranked_content=true&" 1148 return self.send_request(url.format(rank_token=self.rank_token)) 1149 1150 def get_user_feed(self, user_id, max_id="", min_timestamp=None): 1151 url = ( 1152 "feed/user/{user_id}/?max_id={max_id}&min_timestamp=" 1153 "{min_timestamp}&rank_token={rank_token}&ranked_content=true" 1154 # noqa 1155 ).format( 1156 user_id=user_id, 1157 max_id=max_id, 1158 min_timestamp=min_timestamp, 1159 rank_token=self.rank_token, 1160 ) 1161 return self.send_request(url) 1162 1163 def get_self_user_feed(self, max_id="", min_timestamp=None): 1164 return self.get_user_feed(self.user_id, max_id, min_timestamp) 1165 1166 def get_hashtag_feed(self, hashtag, max_id=""): 1167 url = ( 1168 "feed/tag/{hashtag}/?max_id={max_id}" 1169 "&rank_token={rank_token}&ranked_content=true&" 1170 ).format(hashtag=hashtag, max_id=max_id, rank_token=self.rank_token) 1171 return self.send_request(url) 1172 1173 def get_location_feed(self, location_id, max_id=""): 1174 url = ( 1175 "feed/location/{location_id}/?max_id={max_id}" 1176 "&rank_token={rank_token}&ranked_content=true&" 1177 ).format(location_id=location_id, max_id=max_id, rank_token=self.rank_token) 1178 return self.send_request(url) 1179 1180 def get_popular_feed(self): 1181 url = ( 1182 "feed/popular/?people_teaser_supported=1" 1183 "&rank_token={rank_token}&ranked_content=true&" 1184 ) 1185 return self.send_request(url.format(rank_token=self.rank_token)) 1186 1187 def get_liked_media(self, max_id=""): 1188 url = "feed/liked/?max_id={max_id}".format(max_id=max_id) 1189 return self.send_request(url) 1190 1191 # ====== FRIENDSHIPS METHODS ====== # 1192 def get_user_followings(self, user_id, max_id=""): 1193 url = ( 1194 "friendships/{user_id}/following/?max_id={max_id}" 1195 "&ig_sig_key_version={sig_key}&rank_token={rank_token}" 1196 ).format( 1197 user_id=user_id, 1198 max_id=max_id, 1199 sig_key=config.SIG_KEY_VERSION, 1200 rank_token=self.rank_token, 1201 ) 1202 return self.send_request(url) 1203 1204 def get_self_users_following(self): 1205 return self.get_user_followings(self.user_id) 1206 1207 def get_user_followers(self, user_id, max_id=""): 1208 url = "friendships/{user_id}/followers/?rank_token={rank_token}" 1209 url = url.format(user_id=user_id, rank_token=self.rank_token) 1210 if max_id: 1211 url += "&max_id={max_id}".format(max_id=max_id) 1212 return self.send_request(url) 1213 1214 def get_self_user_followers(self): 1215 return self.followers 1216 1217 def follow(self, user_id): 1218 data = self.json_data(self.action_data({"user_id": user_id})) 1219 self.logger.debug("post data: {}".format(data)) 1220 url = "friendships/create/{user_id}/".format(user_id=user_id) 1221 return self.send_request(url, data) 1222 1223 def unfollow(self, user_id): 1224 data = self.json_data({"user_id": user_id, "radio_type": "wifi-none"}) 1225 url = "friendships/destroy/{user_id}/".format(user_id=user_id) 1226 return self.send_request(url, data) 1227 1228 def remove_follower(self, user_id): 1229 data = self.json_data({"user_id": user_id}) 1230 url = "friendships/remove_follower/{user_id}/".format(user_id=user_id) 1231 return self.send_request(url, data) 1232 1233 def block(self, user_id): 1234 data = self.json_data({"user_id": user_id}) 1235 url = "friendships/block/{user_id}/".format(user_id=user_id) 1236 return self.send_request(url, data) 1237 1238 def unblock(self, user_id): 1239 data = self.json_data({"user_id": user_id}) 1240 url = "friendships/unblock/{user_id}/".format(user_id=user_id) 1241 return self.send_request(url, data) 1242 1243 def user_friendship(self, user_id): 1244 data = self.json_data({"user_id": user_id}) 1245 url = "friendships/show/{user_id}/".format(user_id=user_id) 1246 return self.send_request(url, data) 1247 1248 def all_friendship(self, user_id): 1249 url = "friendships/show_many" 1250 return self.send_request(url) 1251 1252 def mute_user(self, user, mute_story=False, mute_posts=False): 1253 data_dict = {} 1254 if mute_posts: 1255 data_dict["target_posts_author_id"] = user 1256 if mute_story: 1257 data_dict["target_reel_author_id"] = user 1258 data = self.json_data(data_dict) 1259 url = "friendships/mute_posts_or_story_from_follow/" 1260 return self.send_request(url, data) 1261 1262 def get_muted_friends(self, muted_content): 1263 # ToDo update endpoints for posts 1264 if muted_content == "stories": 1265 url = "friendships/muted_reels" 1266 elif muted_content == "posts": 1267 raise NotImplementedError( 1268 "API does not support getting friends " 1269 "with muted {}".format(muted_content) 1270 ) 1271 else: 1272 raise NotImplementedError( 1273 "API does not support getting friends" 1274 " with muted {}".format(muted_content) 1275 ) 1276 1277 return self.send_request(url) 1278 1279 def unmute_user(self, user, unmute_posts=False, unmute_stories=False): 1280 data_dict = {} 1281 if unmute_posts: 1282 data_dict["target_posts_author_id"] = user 1283 if unmute_stories: 1284 data_dict["target_reel_author_id"] = user 1285 data = self.json_data(data_dict) 1286 url = "friendships/unmute_posts_or_story_from_follow/" 1287 return self.send_request(url, data) 1288 1289 def get_pending_friendships(self): 1290 """Get pending follow requests""" 1291 url = "friendships/pending/" 1292 return self.send_request(url) 1293 1294 def approve_pending_friendship(self, user_id): 1295 data = self.json_data( 1296 { 1297 "_uuid": self.uuid, 1298 "_uid": self.user_id, 1299 "user_id": user_id, 1300 "_csrftoken": self.token, 1301 } 1302 ) 1303 url = "friendships/approve/{}/".format(user_id) 1304 return self.send_request(url, post=data) 1305 1306 def reject_pending_friendship(self, user_id): 1307 data = self.json_data( 1308 { 1309 "_uuid": self.uuid, 1310 "_uid": self.user_id, 1311 "user_id": user_id, 1312 "_csrftoken": self.token, 1313 } 1314 ) 1315 url = "friendships/ignore/{}/".format(user_id) 1316 return self.send_request(url, post=data) 1317 1318 def get_direct_share(self): 1319 return self.send_request("direct_share/inbox/?") 1320 1321 @staticmethod 1322 def _prepare_recipients(users, thread_id=None, use_quotes=False): 1323 if not isinstance(users, list): 1324 print("Users must be an list") 1325 return False 1326 result = {"users": "[[{}]]".format(",".join(users))} 1327 if thread_id: 1328 template = '["{}"]' if use_quotes else "[{}]" 1329 result["thread"] = template.format(thread_id) 1330 return result 1331 1332 @staticmethod 1333 def generate_signature(data): 1334 body = ( 1335 hmac.new( 1336 config.IG_SIG_KEY.encode("utf-8"), data.encode("utf-8"), hashlib.sha256 1337 ).hexdigest() 1338 + "." 1339 + urllib.parse.quote(data) 1340 ) 1341 signature = "signed_body={body}&ig_sig_key_version={sig_key}" 1342 return signature.format(sig_key=config.SIG_KEY_VERSION, body=body) 1343 1344 @staticmethod 1345 def generate_device_id(seed): 1346 volatile_seed = "12345" 1347 m = hashlib.md5() 1348 m.update(seed.encode("utf-8") + volatile_seed.encode("utf-8")) 1349 return "android-" + m.hexdigest()[:16] 1350 1351 @staticmethod 1352 def get_seed(*args): 1353 m = hashlib.md5() 1354 m.update(b"".join([arg.encode("utf-8") for arg in args])) 1355 return m.hexdigest() 1356 1357 @staticmethod 1358 def generate_UUID(uuid_type): 1359 generated_uuid = str(uuid.uuid4()) 1360 if uuid_type: 1361 return generated_uuid 1362 else: 1363 return generated_uuid.replace("-", "") 1364 1365 def get_total_followers_or_followings( # noqa: C901 1366 self, 1367 user_id, 1368 amount=None, 1369 which="followers", 1370 filter_private=False, 1371 filter_business=False, 1372 filter_verified=False, 1373 usernames=False, 1374 to_file=None, 1375 overwrite=False, 1376 ): 1377 from io import StringIO 1378 1379 if which == "followers": 1380 key = "follower_count" 1381 get = self.get_user_followers 1382 elif which == "followings": 1383 key = "following_count" 1384 get = self.get_user_followings 1385 1386 sleep_track = 0 1387 result = [] 1388 next_max_id = "" 1389 self.get_username_info(user_id) 1390 username_info = self.last_json 1391 if "user" in username_info: 1392 total = amount or username_info["user"][key] 1393 1394 if total > 200000: 1395 print( 1396 "Consider temporarily saving the result of this big " 1397 "operation. This will take a while.\n" 1398 ) 1399 else: 1400 return False 1401 if filter_business: 1402 print( 1403 "--> You are going to filter business accounts. " 1404 "This will take time! <--" 1405 ) 1406 if to_file is not None: 1407 if os.path.isfile(to_file): 1408 if not overwrite: 1409 print("File `{}` already exists. Not overwriting.".format(to_file)) 1410 return False 1411 else: 1412 print("Overwriting file `{}`".format(to_file)) 1413 with open(to_file, "w"): 1414 pass 1415 desc = "Getting {} of {}".format(which, user_id) 1416 with tqdm(total=total, desc=desc, leave=True) as pbar: 1417 while True: 1418 get(user_id, next_max_id) 1419 last_json = self.last_json 1420 try: 1421 with open(to_file, "a") if to_file is not None else StringIO() as f: 1422 for item in last_json["users"]: 1423 if filter_private and item["is_private"]: 1424 continue 1425 if filter_business: 1426 time.sleep(2 * random.random()) 1427 self.get_username_info(item["pk"]) 1428 item_info = self.last_json 1429 if item_info["user"]["is_business"]: 1430 continue 1431 if filter_verified and item["is_verified"]: 1432 continue 1433 if to_file is not None: 1434 if usernames: 1435 f.write("{}\n".format(item["username"])) 1436 else: 1437 f.write("{}\n".format(item["pk"])) 1438 result.append(item) 1439 pbar.update(1) 1440 sleep_track += 1 1441 if sleep_track >= 20000: 1442 sleep_time = random.uniform(120, 180) 1443 msg = ( 1444 "\nWaiting {:.2f} min. " "due to too many requests." 1445 ).format(sleep_time / 60) 1446 print(msg) 1447 time.sleep(sleep_time) 1448 sleep_track = 0 1449 if not last_json["users"] or len(result) >= total: 1450 return result[:total] 1451 except Exception as e: 1452 print("ERROR: {}".format(e)) 1453 return result[:total] 1454 1455 if last_json["big_list"] is False: 1456 return result[:total] 1457 1458 next_max_id = last_json.get("next_max_id", "") 1459 1460 def get_total_followers(self, user_id, amount=None): 1461 return self.get_total_followers_or_followings(user_id, amount, "followers") 1462 1463 def get_total_followings(self, user_id, amount=None): 1464 return self.get_total_followers_or_followings(user_id, amount, "followings") 1465 1466 def get_total_user_feed(self, user_id, min_timestamp=None): 1467 return self.get_last_user_feed( 1468 user_id, amount=float("inf"), min_timestamp=min_timestamp 1469 ) 1470 1471 def get_last_user_feed(self, user_id, amount, min_timestamp=None): 1472 user_feed = [] 1473 next_max_id = "" 1474 while True: 1475 if len(user_feed) >= float(amount): 1476 # one request returns max 13 items 1477 return user_feed[:amount] 1478 self.get_user_feed(user_id, next_max_id, min_timestamp) 1479 last_json = self.last_json 1480 if "items" not in last_json: 1481 return user_feed 1482 user_feed += last_json["items"] 1483 if not last_json.get("more_available"): 1484 return user_feed 1485 next_max_id = last_json.get("next_max_id", "") 1486 1487 def get_total_hashtag_feed(self, hashtag_str, amount=100): 1488 hashtag_feed = [] 1489 next_max_id = "" 1490 1491 with tqdm(total=amount, desc="Getting hashtag media.", leave=False) as pbar: 1492 while True: 1493 self.get_hashtag_feed(hashtag_str, next_max_id) 1494 last_json = self.last_json 1495 if "items" not in last_json: 1496 return hashtag_feed[:amount] 1497 items = last_json["items"] 1498 try: 1499 pbar.update(len(items)) 1500 hashtag_feed += items 1501 if not items or len(hashtag_feed) >= amount: 1502 return hashtag_feed[:amount] 1503 except Exception: 1504 return hashtag_feed[:amount] 1505 next_max_id = last_json.get("next_max_id", "") 1506 1507 def get_total_self_user_feed(self, min_timestamp=None): 1508 return self.get_total_user_feed(self.user_id, min_timestamp) 1509 1510 def get_total_self_followers(self): 1511 return self.get_total_followers(self.user_id) 1512 1513 def get_total_self_followings(self): 1514 return self.get_total_followings(self.user_id) 1515 1516 def get_total_liked_media(self, scan_rate=1): 1517 next_id = "" 1518 liked_items = [] 1519 for _ in range(scan_rate): 1520 self.get_liked_media(next_id) 1521 last_json = self.last_json 1522 next_id = last_json.get("next_max_id", "") 1523 liked_items += last_json["items"] 1524 return liked_items 1525 1526 # ====== ACCOUNT / PERSONAL INFO METHODS ====== # 1527 def change_password(self, new_password): 1528 data = self.json_data( 1529 { 1530 "old_password": self.password, 1531 "new_password1": new_password, 1532 "new_password2": new_password, 1533 } 1534 ) 1535 return self.send_request("accounts/change_password/", data) 1536 1537 def remove_profile_picture(self): 1538 data = self.json_data() 1539 return self.send_request("accounts/remove_profile_picture/", data) 1540 1541 def set_private_account(self): 1542 data = self.json_data() 1543 return self.send_request("accounts/set_private/", data) 1544 1545 def set_public_account(self): 1546 data = self.json_data() 1547 return self.send_request("accounts/set_public/", data) 1548 1549 def set_name_and_phone(self, name="", phone=""): 1550 return self.send_request( 1551 "accounts/set_phone_and_name/", 1552 self.json_data({"first_name": name, "phone_number": phone}), 1553 ) 1554 1555 def get_profile_data(self): 1556 data = self.json_data() 1557 return self.send_request("accounts/current_user/?edit=true", data) 1558 1559 def edit_profile(self, url, phone, first_name, biography, email, gender): 1560 data = self.json_data( 1561 { 1562 "external_url": url, 1563 "phone_number": phone, 1564 "username": self.username, 1565 "full_name": first_name, 1566 "biography": biography, 1567 "email": email, 1568 "gender": gender, 1569 } 1570 ) 1571 return self.send_request("accounts/edit_profile/", data) 1572 1573 def fb_user_search(self, query): 1574 url = ( 1575 "fbsearch/topsearch/?context=blended&query={query}" 1576 "&rank_token={rank_token}" 1577 ) 1578 return self.send_request(url.format(query=query, rank_token=self.rank_token)) 1579 1580 def search_users(self, query): 1581 url = ( 1582 "users/search/?ig_sig_key_version={sig_key}" 1583 "&is_typeahead=true&query={query}&rank_token={rank_token}" 1584 ) 1585 return self.send_request( 1586 url.format( 1587 sig_key=config.SIG_KEY_VERSION, query=query, rank_token=self.rank_token 1588 ) 1589 ) 1590 1591 def search_username(self, username): 1592 url = "users/{username}/usernameinfo/".format(username=username) 1593 return self.send_request(url) 1594 1595 def search_tags(self, query): 1596 url = "tags/search/?is_typeahead=true&q={query}" "&rank_token={rank_token}" 1597 return self.send_request(url.format(query=query, rank_token=self.rank_token)) 1598 1599 def search_location(self, query="", lat=None, lng=None): 1600 url = ( 1601 "fbsearch/places/?rank_token={rank_token}" 1602 "&query={query}&lat={lat}&lng={lng}" 1603 ) 1604 url = url.format(rank_token=self.rank_token, query=query, lat=lat, lng=lng) 1605 return self.send_request(url) 1606 1607 def get_user_reel(self, user_id): 1608 url = "feed/user/{}/reel_media/".format(user_id) 1609 return self.send_request(url) 1610 1611 def get_reels_tray_feed( 1612 self, reason="pull_to_refresh" 1613 ): # reason can be = cold_start, pull_to_refresh 1614 data = { 1615 "supported_capabilities_new": config.SUPPORTED_CAPABILITIES, 1616 "reason": reason, 1617 "_csrftoken": self.token, 1618 "_uuid": self.uuid, 1619 } 1620 data = json.dumps(data) 1621 return self.send_request("feed/reels_tray/", data) 1622 1623 def get_reels_media(self): 1624 data = { 1625 "supported_capabilities_new": config.SUPPORTED_CAPABILITIES, 1626 "source": "feed_timeline", 1627 "_csrftoken": self.token, 1628 "_uuid": self.uuid, 1629 "_uid": self.user_id, 1630 "user_ids": self.user_id, 1631 } 1632 data = json.dumps(data) 1633 return self.send_request("feed/reels_media/", data) 1634 1635 def push_register(self): 1636 data = { 1637 "device_type": "android_mqtt", 1638 "is_main_push_channel": "true", 1639 "device_sub_type": "2", 1640 # TODO find out what &device_token={"k":"eyJwbiI6ImNvbS5pbnN0YWdyYW0uYW5kcm9pZCIsImRpIjoiNzhlNGMxNmQtN2YzNC00NDlkLTg4OWMtMTAwZDg5OTU0NDJhIiwiYWkiOjU2NzMxMDIwMzQxNTA1MiwiY2siOiIxNjgzNTY3Mzg0NjQyOTQifQ==","v":0,"t":"fbns-b64"} is 1641 "device_token": "", 1642 "_csrftoken": self.token, 1643 "guid": self.uuid, 1644 "_uuid": self.uuid, 1645 "users": self.user_id, 1646 "familiy_device_id": "9d9aa0f0-40fe-4524-a920-9910f45ba18d", 1647 } 1648 data = json.dumps(data) 1649 return self.send_request("push/register/", data) 1650 1651 def media_blocked(self): 1652 url = "media/blocked/" 1653 return self.send_request(url) 1654 1655 def get_users_reel(self, user_ids): 1656 """ 1657 Input: user_ids - a list of user_id 1658 Output: dictionary: user_id - stories data. 1659 Basically, for each user output the same as after 1660 self.get_user_reel 1661 """ 1662 url = "feed/reels_media/" 1663 res = self.send_request( 1664 url, post=self.json_data({"user_ids": [str(x) for x in user_ids]}) 1665 ) 1666 if res: 1667 return self.last_json["reels"] if "reels" in self.last_json else [] 1668 return [] 1669 1670 def see_reels(self, reels): 1671 """ 1672 Input - the list of reels jsons 1673 They can be aquired by using get_users_reel() 1674 or get_user_reel() methods 1675 """ 1676 if not isinstance(reels, list): 1677 # In case of only one reel as input 1678 reels = [reels] 1679 1680 story_seen = {} 1681 now = int(time.time()) 1682 for i, story in enumerate( 1683 sorted(reels, key=lambda m: m["taken_at"], reverse=True) 1684 ): 1685 story_seen_at = now - min( 1686 i + 1 + random.randint(0, 2), max(0, now - story["taken_at"]) 1687 ) 1688 story_seen["{!s}_{!s}".format(story["id"], story["user"]["pk"])] = [ 1689 "{!s}_{!s}".format(story["taken_at"], story_seen_at) 1690 ] 1691 1692 data = self.json_data( 1693 { 1694 "reels": story_seen, 1695 "_csrftoken": self.token, 1696 "_uuid": self.uuid, 1697 "_uid": self.user_id, 1698 } 1699 ) 1700 data = self.generate_signature(data) 1701 return self.session.post( 1702 "https://i.instagram.com/api/v2/" + "media/seen/", data=data 1703 ).ok 1704 1705 def get_user_stories(self, user_id): 1706 url = "feed/user/{}/story/".format(user_id) 1707 return self.send_request(url) 1708 1709 def get_self_story_viewers(self, story_id): 1710 url = ("media/{}/list_reel_media_viewer/?supported_capabilities_new={}").format( 1711 story_id, config.SUPPORTED_CAPABILITIES 1712 ) 1713 return self.send_request(url) 1714 1715 def get_tv_suggestions(self): 1716 url = "igtv/tv_guide/" 1717 return self.send_request(url) 1718 1719 def get_hashtag_stories(self, hashtag): 1720 url = "tags/{}/story/".format(hashtag) 1721 return self.send_request(url) 1722 1723 def follow_hashtag(self, hashtag): 1724 data = self.json_data({}) 1725 url = "tags/follow/{}/".format(hashtag) 1726 return self.send_request(url, data) 1727 1728 def unfollow_hashtag(self, hashtag): 1729 data = self.json_data({}) 1730 url = "tags/unfollow/{}/".format(hashtag) 1731 return self.send_request(url, data) 1732 1733 def get_tags_followed_by_user(self, user_id): 1734 url = "users/{}/following_tags_info/".format(user_id) 1735 return self.send_request(url) 1736 1737 def get_hashtag_sections(self, hashtag): 1738 data = self.json_data( 1739 { 1740 "supported_tabs": "['top','recent','places']", 1741 "include_persistent": "true", 1742 } 1743 ) 1744 url = "tags/{}/sections/".format(hashtag) 1745 return self.send_request(url, data) 1746 1747 def get_media_insight(self, media_id): 1748 url = ("insights/media_organic_insights/{}/?ig_sig_key_version={}").format( 1749 media_id, config.IG_SIG_KEY 1750 ) 1751 return self.send_request(url) 1752 1753 def get_self_insight(self): 1754 # TODO: 1755 url = ( 1756 "insights/account_organic_insights/?" 1757 "show_promotions_in_landing_page=true&first={}" 1758 ).format() 1759 return self.send_request(url) 1760 1761 # From profile => "module_name":"feed_contextual_profile" 1762 # From home/feed => "module_name":"feed_timeline" 1763 def save_media(self, media_id, module_name="feed_timeline"): 1764 return self.send_request( 1765 endpoint="media/{media_id}/save/".format(media_id=media_id), 1766 post=self.json_data(self.action_data({"module_name": module_name})), 1767 ) 1768 1769 def unsave_media(self, media_id): 1770 data = self.json_data() 1771 url = "media/{}/unsave/".format(media_id) 1772 return self.send_request(url, data) 1773 1774 def get_saved_medias(self): 1775 url = "feed/saved/" 1776 return self.send_request(url) 1777 1778 def get_loom_fetch_config(self): 1779 return self.send_request("loom/fetch_config/") 1780 1781 def get_request_country(self): 1782 return self.send_request("locations/request_country/") 1783 1784 def get_linked_accounts(self): 1785 return self.send_request("linked_accounts/get_linkage_status/") 1786 1787 def get_profile_notice(self): 1788 return self.send_request("users/profile_notice/") 1789 1790 def get_business_branded_content(self): 1791 return self.send_request( 1792 "business/branded_content/should_require_professional_account/" 1793 ) 1794 1795 def get_monetization_products_eligibility_data(self): 1796 return self.send_request( 1797 "business/eligibility/get_monetization_products_eligibility_data/?product_types=branded_content" 1798 ) 1799 1800 def get_cooldowns(self): 1801 body = self.generate_signature(config.SIG_KEY_VERSION) 1802 url = ("qp/get_cooldowns/?signed_body={}&ig_sig_key_version={}").format( 1803 body, config.SIG_KEY_VERSION 1804 ) 1805 return self.send_request(url) 1806 1807 def log_resurrect_attribution(self): 1808 data = { 1809 "_csrftoken": self.token, 1810 "_uuid": self.uuid, 1811 "_uid": self.user_id, 1812 } 1813 data = json.dumps(data) 1814 return self.send_request("attribution/log_resurrect_attribution/", data) 1815 1816 def store_client_push_permissions(self): 1817 data = { 1818 "enabled": "true", 1819 "_csrftoken": self.token, 1820 "device_id": self.device_id, 1821 "_uuid": self.uuid, 1822 } 1823 data = json.dumps(data) 1824 return self.send_request("attribution/log_resurrect_attribution/", data) 1825 1826 def process_contact_point_signals(self): 1827 data = { 1828 "phone_id": self.phone_id, 1829 "_csrftoken": self.token, 1830 "_uid": self.user_id, 1831 "device_id": self.device_id, 1832 "_uuid": self.uuid, 1833 "google_tokens": "", 1834 } 1835 data = json.dumps(data) 1836 return self.send_request("accounts/process_contact_point_signals/", data) 1837 1838 def write_supported_capabilities(self): 1839 data = { 1840 "supported_capabilities_new": config.SUPPORTED_CAPABILITIES, 1841 "_csrftoken": self.token, 1842 "_uid": self.user_id, 1843 "_uuid": self.uuid, 1844 } 1845 data = json.dumps(data) 1846 return self.send_request("creatives/write_supported_capabilities/", data) 1847 1848 def arlink_download_info(self): 1849 return self.send_request("users/arlink_download_info/?version_override=2.2.1") 1850 1851 def get_direct_v2_inbox(self): 1852 return self.send_request( 1853 "direct_v2/inbox/?visual_message_return_type=unseen&thread_message_limit=10&persistentBadging=true&limit=20" 1854 ) 1855 1856 def get_direct_v2_inbox2(self): 1857 return self.send_request( 1858 "direct_v2/inbox/?visual_message_return_type=unseen&persistentBadging=true&limit=0" 1859 ) 1860 1861 def topical_explore(self): 1862 url = ( 1863 "discover/topical_explore/?is_prefetch=true&omit_cover_media=true&use_sectional_payload=true&timezone_offset=0&session_id={}&include_fixed_destinations=true" 1864 ).format(self.client_session_id) 1865 return self.send_request(url) 1866 1867 def notification_badge(self): 1868 data = { 1869 "phone_id": self.phone_id, 1870 "_csrftoken": self.token, 1871 "user_ids": self.user_id, 1872 "device_id": self.device_id, 1873 "_uuid": self.uuid, 1874 } 1875 data = json.dumps(data) 1876 return self.send_request("notifications/badge/", data) 1877 1878 # ====== DIRECT METHODS ====== # 1879 def get_inbox_v2(self): 1880 data = json.dumps( 1881 { 1882 "visual_message_return_type": "unseen", 1883 "persistentBadging": "True", 1884 "limit": "0", 1885 } 1886 ) 1887 return self.send_request("direct_v2/inbox/", data) 1888 1889 def get_presence(self): 1890 return self.send_request("direct_v2/get_presence/") 1891 1892 def get_thread(self, thread_id, cursor_id=None): 1893 data = json.dumps( 1894 {"visual_message_return_type": "unseen", "seq_id": "40065", "limit": "10"} 1895 ) 1896 if cursor_id is not None: 1897 data["cursor"] = cursor_id 1898 return self.send_request( 1899 "direct_v2/threads/{}/".format(thread_id), json.dumps(data) 1900 ) 1901 1902 def get_ranked_recipients(self, mode, show_threads, query=None): 1903 data = { 1904 "mode": mode, 1905 "show_threads": "false" if show_threads is False else "true", 1906 "use_unified_inbox": "true", 1907 } 1908 if query is not None: 1909 data["query"] = query 1910 return self.send_request("direct_v2/ranked_recipients/", json.dumps(data)) 1911 1912 def get_scores_bootstrap(self): 1913 url = "scores/bootstrap/users/?surfaces={surfaces}" 1914 url = url.format( 1915 surfaces='["autocomplete_user_list","coefficient_besties_list_ranking","coefficient_rank_recipient_user_suggestion","coefficient_ios_section_test_bootstrap_ranking","coefficient_direct_recipients_ranking_variant_2"]' 1916 ) 1917 return self.send_request(url) 1918 1919 def send_direct_item(self, item_type, users, **options): 1920 data = {"client_context": self.generate_UUID(True), "action": "send_item"} 1921 headers = {} 1922 recipients = self._prepare_recipients( 1923 users, options.get("thread"), use_quotes=False 1924 ) 1925 if not recipients: 1926 return False 1927 data["recipient_users"] = recipients.get("users") 1928 if recipients.get("thread"): 1929 data["thread_ids"] = recipients.get("thread") 1930 data.update(self.default_data) 1931 1932 url = "direct_v2/threads/broadcast/{}/".format(item_type) 1933 text = options.get("text", "") 1934 if item_type == "link": 1935 data["link_text"] = text 1936 data["link_urls"] = json.dumps(options.get("urls")) 1937 elif item_type == "text": 1938 data["text"] = text 1939 elif item_type == "media_share": 1940 data["text"] = text 1941 data["media_type"] = options.get("media_type", "photo") 1942 data["media_id"] = options.get("media_id", "") 1943 elif item_type == "hashtag": 1944 data["text"] = text 1945 data["hashtag"] = options.get("hashtag", "") 1946 elif item_type == "profile": 1947 data["text"] = text 1948 data["profile_user_id"] = options.get("profile_user_id") 1949 elif item_type == "photo": 1950 url = "direct_v2/threads/broadcast/upload_photo/" 1951 filepath = options["filepath"] 1952 upload_id = str(int(time.time() * 1000)) 1953 with open(filepath, "rb") as f: 1954 photo = f.read() 1955 1956 data["photo"] = ( 1957 "direct_temp_photo_%s.jpg" % upload_id, 1958 photo, 1959 "application/octet-stream", 1960 {"Content-Transfer-Encoding": "binary"}, 1961 ) 1962 1963 m = MultipartEncoder(data, boundary=self.uuid) 1964 data = m.to_string() 1965 headers.update({"Content-type": m.content_type}) 1966 1967 return self.send_request(url, data, with_signature=False, headers=headers) 1968 1969 def get_pending_inbox(self): 1970 url = ( 1971 "direct_v2/pending_inbox/?persistentBadging=true" "&use_unified_inbox=true" 1972 ) 1973 return self.send_request(url) 1974 1975 # ACCEPT button in pending request 1976 def approve_pending_thread(self, thread_id): 1977 data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token}) 1978 url = "direct_v2/threads/{}/approve/".format(thread_id) 1979 return self.send_request(url, post=data) 1980 1981 # DELETE button in pending request 1982 def hide_pending_thread(self, thread_id): 1983 data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token}) 1984 url = "direct_v2/threads/{}/hide/".format(thread_id) 1985 return self.send_request(url, post=data) 1986 1987 # BLOCK button in pending request 1988 def decline_pending_thread(self, thread_id): 1989 data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token}) 1990 url = "direct_v2/threads/{}/decline/".format(thread_id) 1991 return self.send_request(url, post=data) 1992 1993 def open_instagram_link(self, link): 1994 return self.send_request( 1995 "oembed/?url={}".format(urllib.parse.quote(link, safe="")) 1996 ) 1997