text: import base64
  import datetime
  import hashlib
  import hmac
  import json
  import logging
  import os
  import random
  import sys
  import time
  import uuid
  import secrets
  import pytz
  import requests
  import requests.utils
  import six.moves.urllib as urllib
  
  from requests_toolbelt import MultipartEncoder
  from tqdm import tqdm
  from Crypto.PublicKey import RSA
  import rsa
  from Cryptodome.Cipher import AES
  
  from . import config, devices
  from .api_login import (
      change_device_simulation,
      generate_all_uuids,
      load_uuid_and_cookie,
      login_flow,
      pre_login_flow,
      reinstall_app_simulation,
      save_uuid_and_cookie,
      set_device,
      sync_launcher,
      get_prefill_candidates,
      get_account_family,
      get_zr_token_result,
      banyan,
      igtv_browse_feed,
      sync_device_features,
      creatives_ar_class,
      set_contact_point_prefill,
  )
  from .api_photo import configure_photo, download_photo, upload_photo, upload_album
  from .api_story import configure_story, download_story, upload_story_photo
  from .api_video import configure_video, download_video, upload_video
  from .prepare import delete_credentials, get_credentials
  
  
  try:
      from json.decoder import JSONDecodeError
  except ImportError:
      JSONDecodeError = ValueError
  
  
  version_info = sys.version_info[0:3]
  is_py2 = version_info[0] == 2
  is_py3 = version_info[0] == 3
  is_py37 = version_info[:2] == (3, 7)
  
  
  version = "0.117.0"
  current_path = os.path.abspath(os.getcwd())
  
  
  class API(object):
      def __init__(
          self,
          device=None,
          base_path=current_path + "/config/",
          save_logfile=True,
          log_filename=None,
          loglevel_file=logging.DEBUG,
          loglevel_stream=logging.INFO,
      ):
          # Setup device and user_agent
          self.device = device or devices.DEFAULT_DEVICE
  
          self.cookie_fname = None
          self.base_path = base_path
  
          self.is_logged_in = False
          self.last_login = None
  
          self.last_response = None
          self.total_requests = 0
  
          # Setup logging
          # instabot_version = Bot.version()
          # self.logger = logging.getLogger("[instabot_{}]".format(instabot_version))
          self.logger = logging.getLogger("instabot version: " + version)
  
          if not os.path.exists(base_path):
              os.makedirs(base_path)  # create base_path if not exists
  
          if not os.path.exists(base_path + "/log/"):
              os.makedirs(base_path + "/log/")  # create log folder if not exists
  
          if save_logfile is True:
              if log_filename is None:
                  log_filename = os.path.join(
                      base_path, "log/instabot_{}.log".format(id(self))
                  )
  
              fh = logging.FileHandler(filename=log_filename)
              fh.setLevel(loglevel_file)
              fh.setFormatter(
                  logging.Formatter(
                      "%(asctime)s - %(name)s (%(module)s %(pathname)s:%(lineno)s) - %(levelname)s - %(message)s"
                  )
              )
  
              handler_existed = False
              for handler in self.logger.handlers:
                  if isinstance(handler, logging.FileHandler):
                      handler_existed = True
                      break
              if not handler_existed:
                  self.logger.addHandler(fh)
  
          ch = logging.StreamHandler()
          ch.setLevel(loglevel_stream)
          ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
  
          handler_existed = False
          for handler in self.logger.handlers:
              if isinstance(handler, logging.StreamHandler):
                  handler_existed = True
                  break
          if not handler_existed:
              self.logger.addHandler(ch)
          self.logger.setLevel(logging.DEBUG)
  
          self.last_json = None
  
      def set_user(self, username, password, generate_all_uuids=True, set_device=True):
          self.username = username
          self.password = password
  
          if set_device is True:
              self.set_device()
  
          if generate_all_uuids is True:
              self.generate_all_uuids()
  
      def get_suggested_searches(self, _type="users"):
          return self.send_request(
              "fbsearch/suggested_searches/", self.json_data({"type": _type})
          )
  
      def read_msisdn_header(self, usage="default"):
          data = json.dumps({"device_id": self.uuid, "mobile_subno_usage": usage})
          return self.send_request(
              "accounts/read_msisdn_header/",
              data,
              login=True,
              headers={"X-DEVICE-ID": self.uuid},
          )
  
      def log_attribution(self, usage="default"):
          data = json.dumps({"adid": self.advertising_id})
          return self.send_request("attribution/log_attribution/", data, login=True)
  
      # ====== ALL METHODS IMPORT FROM api_login ====== #
      def sync_device_features(self, login=None):
          return sync_device_features(self, login)
  
      def sync_launcher(self, login=None):
          return sync_launcher(self, login)
  
      def set_contact_point_prefill(self, usage=None, login=False):
          return set_contact_point_prefill(self, usage, login)
  
      def igtv_browse_feed(self):
          return igtv_browse_feed(self)
  
      def creatives_ar_class(self):
          return creatives_ar_class(self)
  
      def get_prefill_candidates(self, login=False):
          return get_prefill_candidates(self, login)
  
      def get_account_family(self):
          return get_account_family(self)
  
      def get_zr_token_result(self):
          return get_zr_token_result(self)
  
      def banyan(self):
          return banyan(self)
  
      def pre_login_flow(self):
          return pre_login_flow(self)
  
      def login_flow(self, just_logged_in=False, app_refresh_interval=1800):
          return login_flow(self, just_logged_in, app_refresh_interval)
  
      def set_device(self):
          return set_device(self)
  
      def generate_all_uuids(self):
          return generate_all_uuids(self)
  
      def reinstall_app_simulation(self):
          return reinstall_app_simulation(self)
  
      def change_device_simulation(self):
          return change_device_simulation(self)
  
      def load_uuid_and_cookie(self, load_uuid=True, load_cookie=True):
          return load_uuid_and_cookie(self, load_uuid=load_uuid, load_cookie=load_cookie)
  
      def save_uuid_and_cookie(self):
          return save_uuid_and_cookie(self)
  
      def encrypt_password(self, password):
          IG_LOGIN_ANDROID_PUBLIC_KEY = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF1enRZOEZvUlRGRU9mK1RkTGlUdAplN3FIQXY1cmdBMmk5RkQ0YjgzZk1GK3hheW14b0xSdU5KTitRanJ3dnBuSm1LQ0QxNGd3K2w3TGQ0RHkvRHVFCkRiZlpKcmRRWkJIT3drS3RqdDdkNWlhZFdOSjdLczlBM0NNbzB5UktyZFBGU1dsS21lQVJsTlFrVXF0YkNmTzcKT2phY3ZYV2dJcGlqTkdJRVk4UkdzRWJWZmdxSmsrZzhuQWZiT0xjNmEwbTMxckJWZUJ6Z0hkYWExeFNKOGJHcQplbG4zbWh4WDU2cmpTOG5LZGk4MzRZSlNaV3VxUHZmWWUrbEV6Nk5laU1FMEo3dE80eWxmeWlPQ05ycnF3SnJnCjBXWTFEeDd4MHlZajdrN1NkUWVLVUVaZ3FjNUFuVitjNUQ2SjJTSTlGMnNoZWxGNWVvZjJOYkl2TmFNakpSRDgKb1FJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
          IG_LOGIN_ANDROID_PUBLIC_KEY_ID = 205
  
          key = secrets.token_bytes(32)
          iv = secrets.token_bytes(12)
          time = str(int(datetime.datetime.now().timestamp()))
  
          base64_decoded_device_public_key = base64.b64decode(
              IG_LOGIN_ANDROID_PUBLIC_KEY.encode()
          )
  
          public_key = RSA.importKey(base64_decoded_device_public_key)
  
          encrypted_aes_key = rsa.encrypt(key, public_key)
  
          cipher = AES.new(key, AES.MODE_GCM, iv)
          cipher.update(time.encode())
          encrypted_password, tag = cipher.encrypt_and_digest(password.encode())
  
          payload = (
              b"\x01"
              + str(IG_LOGIN_ANDROID_PUBLIC_KEY_ID).encode()
              + iv
              + b"0001"
              + encrypted_aes_key
              + tag
              + encrypted_password
          )
  
          base64_encoded_payload = base64.b64encode(payload)
  
          return f"#PWD_INSTAGRAM:4:{time}:{base64_encoded_payload.decode()}"
  
      def login(
          self,
          username=None,
          password=None,
          force=False,
          proxy=None,
          use_cookie=True,
          use_uuid=True,
          cookie_fname=None,
          ask_for_code=False,
          set_device=True,
          generate_all_uuids=True,
          is_threaded=False,
      ):
          if password is None:
              username, password = get_credentials(
                  base_path=self.base_path, username=username
              )
  
          set_device = generate_all_uuids = True
          self.set_user(username, password)
          self.session = requests.Session()
  
          self.proxy = proxy
          self.set_proxy()  # Only happens if `self.proxy`
  
          self.cookie_fname = cookie_fname
          if self.cookie_fname is None:
              fmt = "{username}_uuid_and_cookie.json"
              cookie_fname = fmt.format(username=username)
              self.cookie_fname = os.path.join(self.base_path, cookie_fname)
  
          cookie_is_loaded = False
          msg = "Login flow failed, the cookie is broken. Relogin again."
  
          if use_cookie is True:
              # try:
              if (
                  self.load_uuid_and_cookie(load_cookie=use_cookie, load_uuid=use_uuid)
                  is True
              ):
                  # Check if the token loaded is valid.
                  if self.login_flow(False) is True:
                      cookie_is_loaded = True
                      self.save_successful_login()
                  else:
                      self.logger.info(msg)
                      set_device = generate_all_uuids = False
                      force = True
  
          if not cookie_is_loaded and (not self.is_logged_in or force):
              self.session = requests.Session()
              if use_uuid is True:
                  if (
                      self.load_uuid_and_cookie(
                          load_cookie=use_cookie, load_uuid=use_uuid
                      )
                      is False
                  ):
                      if set_device is True:
                          self.set_device()
                      if generate_all_uuids is True:
                          self.generate_all_uuids()
              self.pre_login_flow()
              data = json.dumps(
                  {
                      "jazoest": str(random.randint(22000, 22999)),
                      "country_codes": '[{"country_code":"1","source":["default"]}]',
                      "phone_id": self.phone_id,
                      "_csrftoken": self.token,
                      "username": self.username,
                      "adid": "",
                      "guid": self.uuid,
                      "device_id": self.device_id,
                      "google_tokens": "[]",
                      "password": self.password,
                      # "enc_password": self.encrypt_password(self.password),
                      # "enc_password:" "#PWD_INSTAGRAM:4:TIME:ENCRYPTED_PASSWORD"
                      "login_attempt_count": "1",
                  }
              )
  
              if self.send_request("accounts/login/", data, True):
                  self.save_successful_login()
                  self.login_flow(True)
                  return True
  
              elif (
                  self.last_json.get("error_type", "") == "checkpoint_challenge_required"
              ):
                  # self.logger.info("Checkpoint challenge required...")
                  if ask_for_code is True:
                      solved = self.solve_challenge()
                      if solved:
                          self.save_successful_login()
                          self.login_flow(True)
                          return True
                      else:
                          self.logger.error(
                              "Failed to login, unable to solve the challenge"
                          )
                          self.save_failed_login()
                          return False
                  else:
                      return False
              elif self.last_json.get("two_factor_required"):
                  if self.two_factor_auth():
                      self.save_successful_login()
                      self.login_flow(True)
                      return True
                  else:
                      self.logger.error("Failed to login with 2FA!")
                      self.save_failed_login()
                      return False
              else:
                  self.logger.error(
                      "Failed to login go to instagram and change your password"
                  )
                  self.save_failed_login()
                  delete_credentials(self.base_path)
                  return False
  
      def two_factor_auth(self):
          self.logger.info("Two-factor authentication required")
          two_factor_code = input("Enter 2FA verification code: ")
          two_factor_id = self.last_json["two_factor_info"]["two_factor_identifier"]
  
          login = self.session.post(
              config.API_URL + "accounts/two_factor_login/",
              data={
                  "username": self.username,
                  "verification_code": two_factor_code,
                  "two_factor_identifier": two_factor_id,
                  "password": self.password,
                  "device_id": self.device_id,
                  "ig_sig_key_version": config.SIG_KEY_VERSION,
              },
              allow_redirects=True,
          )
  
          if login.status_code == 200:
              resp_json = json.loads(login.text)
              if resp_json["status"] != "ok":
                  if "message" in resp_json:
                      self.logger.error("Login error: {}".format(resp_json["message"]))
                  else:
                      self.logger.error(
                          ('Login error: "{}" status and' " message {}.").format(
                              resp_json["status"], login.text
                          )
                      )
                  return False
              return True
          else:
              self.logger.error(
                  (
                      "Two-factor authentication request returns "
                      "{} error with message {} !"
                  ).format(login.status_code, login.text)
              )
              return False
  
      def save_successful_login(self):
          self.is_logged_in = True
          self.last_login = time.time()
          self.logger.info("Logged-in successfully as '{}'!".format(self.username))
  
      def save_failed_login(self):
          self.logger.info("Username or password is incorrect.")
          delete_credentials(self.base_path)
          # sys.exit()
  
      def solve_challenge(self):
          challenge_url = self.last_json["challenge"]["api_path"][1:]
          try:
              self.send_request(challenge_url, None, login=True, with_signature=False)
          except Exception as e:
              self.logger.error("solve_challenge; {}".format(e))
              return False
  
          choices = self.get_challenge_choices()
          for choice in choices:
              print(choice)
          code = input("Insert choice: ")
  
          data = json.dumps({"choice": code})
          try:
              self.send_request(challenge_url, data, login=True)
          except Exception as e:
              self.logger.error(e)
              return False
  
          print("A code has been sent to the method selected, please check.")
          code = input("Insert code: ").replace(" ", "")
  
          data = json.dumps({"security_code": code})
          try:
              self.send_request(challenge_url, data, login=True)
          except Exception as e:
              self.logger.error(e)
              return False
  
          worked = (
              ("logged_in_user" in self.last_json)
              and (self.last_json.get("action", "") == "close")
              and (self.last_json.get("status", "") == "ok")
          )
  
          if worked:
              return True
  
          self.logger.error("Not possible to log in. Reset and try again")
          return False
  
      def get_challenge_choices(self):
          last_json = self.last_json
          choices = []
  
          if last_json.get("step_name", "") == "select_verify_method":
              choices.append("Checkpoint challenge received")
              if "phone_number" in last_json["step_data"]:
                  choices.append("0 - Phone")
              if "email" in last_json["step_data"]:
                  choices.append("1 - Email")
  
          if last_json.get("step_name", "") == "delta_login_review":
              choices.append("Login attempt challenge received")
              choices.append("0 - It was me")
              choices.append("0 - It wasn't me")
  
          if not choices:
              choices.append(
                  '"{}" challenge received'.format(last_json.get("step_name", "Unknown"))
              )
              choices.append("0 - Default")
  
          return choices
  
      def logout(self, *args, **kwargs):
          if not self.is_logged_in:
              return True
          data = json.dumps({})
          self.is_logged_in = not self.send_request(
              "accounts/logout/", data, with_signature=False
          )
          return not self.is_logged_in
  
      def set_proxy(self):
          if getattr(self, "proxy", None):
              parsed = urllib.parse.urlparse(self.proxy)
              scheme = "http://" if not parsed.scheme else ""
              self.session.proxies["http"] = scheme + self.proxy
              self.session.proxies["https"] = scheme + self.proxy
  
      def send_request(
          self,
          endpoint,
          post=None,
          login=False,
          with_signature=True,
          headers=None,
          extra_sig=None,
          timeout_minutes=None,
      ):
          self.set_proxy()  # Only happens if `self.proxy`
          # TODO: fix the request_headers
          self.session.max_redirects = 1
          self.session.headers.update(config.REQUEST_HEADERS)
          self.session.headers.update({"User-Agent": self.user_agent})
          # print("printing headers", self.session.headers)
          if not self.is_logged_in and not login:
              msg = "Not logged in!"
              self.logger.critical(msg)
              raise Exception(msg)
          if headers:
              self.session.headers.update(headers)
  
          try:
              self.total_requests += 1
              if post is not None:  # POST
                  if with_signature:
                      post = self.generate_signature(
                          post
                      )  # Only `send_direct_item` doesn't need a signature
                      if extra_sig is not None and extra_sig != []:
                          post += "&".join(extra_sig)
                  # time.sleep(random.randint(1, 2))
                  response = self.session.post(config.API_URL + endpoint, data=post, verify=False)
              else:  # GET
                  # time.sleep(random.randint(1, 2))
                  response = self.session.get(config.API_URL + endpoint, verify=False)
          except Exception as e:
              self.logger.warning(str(e))
              return False
  
          self.last_response = response
          if post is not None:
              self.logger.debug(
                  "POST to endpoint: {} returned response: {}".format(endpoint, response)
              )
          else:
              self.logger.debug(
                  "GET to endpoint: {} returned response: {}".format(endpoint, response)
              )
  
          if response.status_code == 200:
              try:
                  self.last_json = json.loads(response.text)
                  return True
              except JSONDecodeError:
                  return False
          else:
              self.logger.debug(
                  "Responsecode indicates error; response content: {}".format(
                      response.content
                  )
              )
              if response.status_code != 404 and response.status_code != "404":
                  self.logger.error(
                      "Request returns {} error!".format(response.status_code)
                  )
              try:
                  response_data = json.loads(response.text)
                  if response_data.get(
                      "message"
                  ) is not None and "feedback_required" in str(
                      response_data.get("message").encode("utf-8")
                  ):
                      self.logger.error(
                          "ATTENTION!: `feedback_required`"
                          + str(response_data.get("feedback_message").encode("utf-8"))
                      )
                      try:
                          self.last_response = response
                          self.last_json = json.loads(response.text)
                      except Exception:
                          pass
                      return "feedback_required"
              except ValueError:
                  self.logger.error(
                      "Error checking for `feedback_required`, "
                      "response text is not JSON"
                  )
                  self.logger.info("Full Response: {}".format(str(response)))
                  try:
                      self.logger.info("Response Text: {}".format(str(response.text)))
                  except Exception:
                      pass
              if response.status_code == 429:
                  raise Exception('429 error')
                  # 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
                  # if timeout_minutes is None:
                  #     timeout_minutes = 0
                  # if timeout_minutes == 15:
                  #     # If we have been waiting for more than 15 minutes, lets restart.
                  #     time.sleep(1)
                  #     self.logger.error(
                  #         "Since we hit 15 minutes of time outs, we have to restart. Removing session and cookies. Please relogin."
                  #     )
                  #     delete_credentials(self.base_path)
                  #     sys.exit()
                  # timeout_minutes += 5
                  # self.logger.warning(
                  #     "That means 'too many requests'. I'll go to sleep "
                  #     "for {} minutes.".format(timeout_minutes)
                  # )
                  # time.sleep(timeout_minutes * 60)
                  # return self.send_request(
                  #     endpoint,
                  #     post,
                  #     login,
                  #     with_signature,
                  #     headers,
                  #     extra_sig,
                  #     timeout_minutes,
                  # )
              if response.status_code == 400:
                  response_data = json.loads(response.text)
                  if response_data.get("challenge_required"):
                      # Try and fix the challenge required error by totally restarting
                      self.logger.error(
                          "Failed to login go to instagram and change your password"
                      )
                      delete_credentials(self.base_path)
                  # PERFORM Interactive Two-Factor Authentication
                  if response_data.get("two_factor_required"):
                      try:
                          self.last_response = response
                          self.last_json = json.loads(response.text)
                      except Exception:
                          self.logger.error("Error unknown send request 400 2FA")
                          pass
                      return self.two_factor_auth()
                  # End of Interactive Two-Factor Authentication
                  else:
                      msg = "Instagram's error message: {}"
                      self.logger.info(msg.format(response_data.get("message")))
                      if "error_type" in response_data:
                          msg = "Error type: {}".format(response_data["error_type"])
                      self.logger.info(msg)
  
              # For debugging
              try:
                  self.last_response = response
                  self.last_json = json.loads(response.text)
              except Exception:
                  self.logger.error("Error unknown send request")
                  pass
              return False
  
      @property
      def cookie_dict(self):
          return self.session.cookies.get_dict()
  
      @property
      def token(self):
          return self.cookie_dict["csrftoken"]
  
      @property
      def user_id(self):
          return self.cookie_dict["ds_user_id"]
  
      @property
      def mid(self):
          return self.cookie_dict["mid"]
  
      @property
      def sessionid(self):
          return self.cookie_dict["sessionid"]
  
      @property
      def views(self):
          return self.cookie_dict["views"]
  
      @property
      def rank_token(self):
          return "{}_{}".format(self.user_id, self.uuid)
  
      @property
      def default_data(self):
          return {"_uuid": self.uuid, "_uid": self.user_id, "_csrftoken": self.token}
  
      def json_data(self, data=None):
          """Adds the default_data to data and dumps it to a json."""
          if data is None:
              data = {}
          data.update(self.default_data)
          return json.dumps(data)
  
      def action_data(self, data):
          _data = {"radio_type": "wifi-none", "device_id": self.device_id}
          data.update(_data)
          return data
  
      def auto_complete_user_list(self):
          return self.send_request("friendships/autocomplete_user_list/")
  
      def batch_fetch(self):
          data = {
              "surfaces_to_triggers": '{"4715":["instagram_feed_header"],"5858":["instagram_feed_tool_tip"],"5734":["instagram_feed_prompt"]}',  # noqa
              "surfaces_to_queries": '{"4715":"Query+QuickPromotionSurfaceQuery:+Viewer+{viewer()+{eligible_promotions.trigger_context_v2(<trigger_context_v2>).ig_parameters(<ig_parameters>).trigger_name(<trigger_name>).surface_nux_id(<surface>).external_gating_permitted_qps(<external_gating_permitted_qps>).supports_client_filters(true).include_holdouts(true)+{edges+{client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range+{start,end},node+{id,promotion_id,logging_data,max_impressions,triggers,contextual_filters+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template+{name,parameters+{name,required,bool_value,string_value,color_value,}},creatives+{title+{text},content+{text},footer+{text},social_context+{text},social_context_images,primary_action{title+{text},url,limit,dismiss_promotion},secondary_action{title+{text},url,limit,dismiss_promotion},dismiss_action{title+{text},url,limit,dismiss_promotion},image.scale(<scale>)+{uri,width,height}}}}}}}","5858":"Query+QuickPromotionSurfaceQuery:+Viewer+{viewer()+{eligible_promotions.trigger_context_v2(<trigger_context_v2>).ig_parameters(<ig_parameters>).trigger_name(<trigger_name>).surface_nux_id(<surface>).external_gating_permitted_qps(<external_gating_permitted_qps>).supports_client_filters(true).include_holdouts(true)+{edges+{client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range+{start,end},node+{id,promotion_id,logging_data,max_impressions,triggers,contextual_filters+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template+{name,parameters+{name,required,bool_value,string_value,color_value,}},creatives+{title+{text},content+{text},footer+{text},social_context+{text},social_context_images,primary_action{title+{text},url,limit,dismiss_promotion},secondary_action{title+{text},url,limit,dismiss_promotion},dismiss_action{title+{text},url,limit,dismiss_promotion},image.scale(<scale>)+{uri,width,height}}}}}}}","5734":"Query+QuickPromotionSurfaceQuery:+Viewer+{viewer()+{eligible_promotions.trigger_context_v2(<trigger_context_v2>).ig_parameters(<ig_parameters>).trigger_name(<trigger_name>).surface_nux_id(<surface>).external_gating_permitted_qps(<external_gating_permitted_qps>).supports_client_filters(true).include_holdouts(true)+{edges+{client_ttl_seconds,log_eligibility_waterfall,is_holdout,priority,time_range+{start,end},node+{id,promotion_id,logging_data,max_impressions,triggers,contextual_filters+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}},clauses+{clause_type,filters+{filter_type,unknown_action,value+{name,required,bool_value,int_value,string_value},extra_datas+{name,required,bool_value,int_value,string_value}}}}}},is_uncancelable,template+{name,parameters+{name,required,bool_value,string_value,color_value,}},creatives+{title+{text},content+{text},footer+{text},social_context+{text},social_context_images,primary_action{title+{text},url,limit,dismiss_promotion},secondary_action{title+{text},url,limit,dismiss_promotion},dismiss_action{title+{text},url,limit,dismiss_promotion},image.scale(<scale>)+{uri,width,height}}}}}}}"}',
              "vc_policy": "default",
              "_csrftoken": self.token,
              "_uid": self.user_id,
              "_uuid": self.uuid,
              "scale": 2,
              "version": 1,
          }
          data = self.json_data(data)
          return self.send_request("qp/batch_fetch/", data)
  
      def get_timeline_feed(self, reason=None, options=[]):
          headers = {
              "X-Ads-Opt-Out": "0",
              # "X-DEVICE-ID": self.uuid,
              # "X-CM-Bandwidth-KBPS": str(random.randint(2000, 5000)),
              # "X-CM-Latency": str(random.randint(1, 5)),
          }
          data = {
              "feed_view_info": "[]",
              "phone_id": self.phone_id,
              "battery_level": random.randint(25, 100),
              # "timezone_offset": datetime.datetime.now(pytz.timezone("CET")).strftime(
              #    "%z"
              # ),
              "timezone_offset": "0",
              "_csrftoken": self.token,
              "device_id": self.uuid,
              "request_id": self.uuid,
              "_uuid": self.uuid,
              "is_charging": random.randint(0, 1),
              "will_sound_on": random.randint(0, 1),
              "session_id": self.client_session_id,
              "bloks_versioning_id": "e538d4591f238824118bfcb9528c8d005f2ea3becd947a3973c030ac971bb88e",
          }
  
          if "is_pull_to_refresh" in options:
              data["reason"] = "pull_to_refresh"
              data["is_pull_to_refresh"] = "1"
          elif "is_pull_to_refresh" not in options:
              data["reason"] = "cold_start_fetch"
              data["is_pull_to_refresh"] = "0"
  
          if "push_disabled" in options:
              data["push_disabled"] = "true"
  
          if "recovered_from_crash" in options:
              data["recovered_from_crash"] = "1"
  
          data = json.dumps(data)
          return self.send_request(
              "feed/timeline/", data, with_signature=False, headers=headers
          )
  
      def get_megaphone_log(self):
          return self.send_request("megaphone/log/")
  
      def expose(self):
          data = self.json_data(
              {"id": self.uuid, "experiment": "ig_android_profile_contextual_feed"}
          )
          return self.send_request("qe/expose/", data)
  
      # ====== PHOTO METHODS ====== #
      def upload_photo(
          self,
          photo,
          caption=None,
          upload_id=None,
          from_video=False,
          force_resize=False,
          options={},
          user_tags=None,
          is_sidecar=False
      ):
          """Upload photo to Instagram
  
          @param photo         Path to photo file (String)
          @param caption       Media description (String)
          @param upload_id     Unique upload_id (String). When None, then
                               generate automatically
          @param from_video    A flag that signals whether the photo is loaded
                               from the video or by itself
                               (Boolean, DEPRECATED: not used)
          @param force_resize  Force photo resize (Boolean)
          @param options       Object with difference options, e.g.
                               configure_timeout, rename (Dict)
                               Designed to reduce the number of function
                               arguments! This is the simplest request object.
          @param user_tags     Tag other users (List)
                               usertags = [
                                  {"user_id": user_id, "position": [x, y]}
                               ]
          @param is_sidecar    An album element (Boolean)
  
          @return Boolean
          """
          return upload_photo(
              self, photo, caption, upload_id, from_video, force_resize, options, user_tags, is_sidecar
          )
  
      def upload_album(
          self,
          photos,
          caption=None,
          upload_id=None,
          from_video=False,
          force_resize=False,
          options={},
          user_tags=None
      ):
          """Upload album to Instagram
  
          @param photos        List of paths to photo files (List of strings)
          @param caption       Media description (String)
          @param upload_id     Unique upload_id (String). When None, then
                               generate automatically
          @param from_video    A flag that signals whether the photo is loaded
                               from the video or by itself
                               (Boolean, DEPRECATED: not used)
          @param force_resize  Force photo resize (Boolean)
          @param options       Object with difference options, e.g.
                               configure_timeout, rename (Dict)
                               Designed to reduce the number of function
                               arguments! This is the simplest request object.
          @param user_tags
  
          @return Boolean
          """
          return upload_album(
              self, photos, caption, upload_id, from_video, force_resize, options, user_tags
          )
  
      def download_photo(self, media_id, filename, media=False, folder="photos"):
          return download_photo(self, media_id, filename, media, folder)
  
      def configure_photo(self, upload_id, photo, caption="", user_tags=None, is_sidecar=False):
          return configure_photo(self, upload_id, photo, caption, user_tags, is_sidecar)
  
      # ====== STORY METHODS ====== #
      def download_story(self, filename, story_url, username):
          return download_story(self, filename, story_url, username)
  
      def upload_story_photo(self, photo, upload_id=None):
          return upload_story_photo(self, photo, upload_id)
  
      def configure_story(self, upload_id, photo):
          return configure_story(self, upload_id, photo)
  
      # ====== VIDEO METHODS ====== #
      def upload_video(
          self, video, caption=None, upload_id=None, thumbnail=None, options={}
      ):
          """Upload video to Instagram
  
          @param video      Path to video file (String)
          @param caption    Media description (String)
          @param upload_id  Unique upload_id (String). When None, then
                            generate automatically
          @param thumbnail  Path to thumbnail for video (String). When None,
                            then thumbnail is generate automatically
          @param options    Object with difference options, e.g.
                            configure_timeout, rename_thumbnail, rename (Dict)
                            Designed to reduce the number of function arguments!
                            This is the simplest request object.
  
          @return           Object with state of uploading to
                            Instagram (or False)
          """
          return upload_video(self, video, caption, upload_id, thumbnail, options)
  
      def download_video(self, media_id, filename, media=False, folder="video"):
          return download_video(self, media_id, filename, media, folder)
  
      def configure_video(
          self,
          upload_id,
          video,
          thumbnail,
          width,
          height,
          duration,
          caption="",
          options={},
      ):
          """Post Configure Video
          (send caption, thumbnail andmore else to Instagram)
  
          @param upload_id  Unique upload_id (String). Received
                            from "upload_video"
          @param video      Path to video file (String)
          @param thumbnail  Path to thumbnail for video (String). When None,
                            then thumbnail is generate automatically
          @param width      Width in px (Integer)
          @param height     Height in px (Integer)
          @param duration   Duration in seconds (Integer)
          @param caption    Media description (String)
          @param options    Object with difference options, e.g.
                            configure_timeout, rename_thumbnail, rename (Dict)
                            Designed to reduce the number of function arguments!
                            This is the simplest request object.
          """
          return configure_video(
              self, upload_id, video, thumbnail, width, height, duration, caption, options
          )
  
      # ====== MEDIA METHODS ====== #
      def edit_media(self, media_id, captionText=""):
          data = self.json_data({"caption_text": captionText})
          url = "media/{media_id}/edit_media/".format(media_id=media_id)
          return self.send_request(url, data)
  
      def remove_self_tag(self, media_id):
          data = self.json_data()
          url = "media/{media_id}/remove/".format(media_id=media_id)
          return self.send_request(url, data)
  
      def media_info(self, media_id):
          # data = self.json_data({'media_id': media_id})
          url = "media/{media_id}/info/".format(media_id=media_id)
          return self.send_request(url)
  
      def archive_media(self, media, undo=False):
          action = "only_me" if not undo else "undo_only_me"
          data = self.json_data({"media_id": media["id"]})
          url = "media/{media_id}/{action}/?media_type={media_type}".format(
              media_id=media["id"], action=action, media_type=media["media_type"]
          )
          return self.send_request(url, data)
  
      def delete_media(self, media):
          data = self.json_data({"media_id": media.get("id")})
          url = "media/{media_id}/delete/".format(media_id=media.get("id"))
          return self.send_request(url, data)
  
      def gen_user_breadcrumb(self, size):
          key = "iN4$aGr0m"
          dt = int(time.time() * 1000)
  
          time_elapsed = random.randint(500, 1500) + size * random.randint(500, 1500)
          text_change_event_count = max(1, size / random.randint(3, 5))
  
          data = "{size!s} {elapsed!s} {count!s} {dt!s}".format(
              **{
                  "size": size,
                  "elapsed": time_elapsed,
                  "count": text_change_event_count,
                  "dt": dt,
              }
          )
          return "{!s}\n{!s}\n".format(
              base64.b64encode(
                  hmac.new(
                      key.encode("ascii"), data.encode("ascii"), digestmod=hashlib.sha256
                  ).digest()
              ),
              base64.b64encode(data.encode("ascii")),
          )
  
      def comment(self, media_id, comment_text):
          return self.send_request(
              endpoint="media/{media_id}/comment/".format(media_id=media_id),
              post=self.json_data(
                  self.action_data(
                      {
                          "container_module": "comments_v2",
                          "user_breadcrumb": self.gen_user_breadcrumb(len(comment_text)),
                          "idempotence_token": self.generate_UUID(True),
                          "comment_text": comment_text,
                      }
                  )
              ),
          )
  
      def reply_to_comment(self, media_id, comment_text, parent_comment_id):
          data = self.json_data(
              {"comment_text": comment_text, "replied_to_comment_id": parent_comment_id}
          )
          url = "media/{media_id}/comment/".format(media_id=media_id)
          return self.send_request(url, data)
  
      def delete_comment(self, media_id, comment_id):
          data = self.json_data()
          url = "media/{media_id}/comment/{comment_id}/delete/"
          url = url.format(media_id=media_id, comment_id=comment_id)
          return self.send_request(url, data)
  
      def get_comment_likers(self, comment_id):
          url = "media/{comment_id}/comment_likers/?".format(comment_id=comment_id)
          return self.send_request(url)
  
      def get_media_likers(self, media_id):
          url = "media/{media_id}/likers/?".format(media_id=media_id)
          return self.send_request(url)
  
      def like_comment(self, comment_id):
          data = self.json_data(
              {
                  "is_carousel_bumped_post": "false",
                  "container_module": "comments_v2",
                  "feed_position": "0",
              }
          )
          url = "media/{comment_id}/comment_like/".format(comment_id=comment_id)
          return self.send_request(url, data)
  
      def unlike_comment(self, comment_id):
          data = self.json_data(
              {
                  "is_carousel_bumped_post": "false",
                  "container_module": "comments_v2",
                  "feed_position": "0",
              }
          )
          url = "media/{comment_id}/comment_unlike/".format(comment_id=comment_id)
          return self.send_request(url, data)
  
      # From profile => "is_carousel_bumped_post":"false",
      # "container_module":"feed_contextual_profile", "feed_position":"0" # noqa
      # From home/feed => "inventory_source":"media_or_ad",
      # "is_carousel_bumped_post":"false", "container_module":"feed_timeline",
      # "feed_position":"0" # noqa
      def like(
          self,
          media_id,
          double_tap=None,
          container_module="feed_short_url",
          feed_position=0,
          username=None,
          user_id=None,
          hashtag_name=None,
          hashtag_id=None,
          entity_page_name=None,
          entity_page_id=None,
      ):
          data = self.action_data(
              {
                  "inventory_source": "media_or_ad",
                  "media_id": media_id,
                  "_csrftoken": self.token,
                  "radio_type": "wifi-none",
                  "_uid": self.user_id,
                  "_uuid": self.uuid,
                  "is_carousel_bumped_post": "false",
                  "container_module": container_module,
                  "feed_position": str(feed_position),
              }
          )
          if container_module == "feed_timeline":
              data.update({"inventory_source": "media_or_ad"})
          if username:
              data.update({"username": username, "user_id": user_id})
          if hashtag_name:
              data.update({"hashtag_name": hashtag_name, "hashtag_id": hashtag_id})
          if entity_page_name:
              data.update(
                  {"entity_page_name": entity_page_name, "entity_page_id": entity_page_id}
              )
          # if double_tap is None:
          double_tap = random.randint(0, 1)
          json_data = self.json_data(data)
          # TODO: comment out debug log out when done
          self.logger.debug("post data: {}".format(json_data))
          return self.send_request(
              endpoint="media/{media_id}/like/".format(media_id=media_id),
              post=json_data,
              extra_sig=["d={}".format(double_tap)],
          )
  
      def unlike(self, media_id):
          data = self.json_data(
              {
                  "media_id": media_id,
                  "radio_type": "wifi-none",
                  "is_carousel_bumped_post": "false",
                  "container_module": "photo_view_other",
                  "feed_position": "0",
              }
          )
          url = "media/{media_id}/unlike/".format(media_id=media_id)
          return self.send_request(url, data)
  
      def get_media_comments(self, media_id, max_id=""):
          url = "media/{media_id}/comments/".format(media_id=media_id)
          if max_id:
              url += "?max_id={max_id}".format(max_id=max_id)
          return self.send_request(url)
  
      def explore(self, is_prefetch=False):
          data = {
              "is_prefetch": is_prefetch,
              "is_from_promote": False,
              "timezone_offset": datetime.datetime.now(pytz.timezone("CET")).strftime(
                  "%z"
              ),
              "session_id": self.client_session_id,
              "supported_capabilities_new": config.SUPPORTED_CAPABILITIES,
          }
          if is_prefetch:
              data["max_id"] = 0
              data["module"] = "explore_popular"
          data = json.dumps(data)
          return self.send_request("discover/explore/", data)
  
      def get_username_info(self, user_id):
          url = "users/{user_id}/info/".format(user_id=user_id)
          return self.send_request(url)
  
      def get_self_username_info(self):
          return self.get_username_info(self.user_id)
  
      def get_news_inbox(self):
          return self.send_request("news/inbox/")
  
      def get_recent_activity(self):
          return self.send_request("news/inbox/?limited_activity=true&show_su=true")
  
      def get_following_recent_activity(self):
          return self.send_request("news")
  
      def get_user_tags(self, user_id):
          url = (
              "usertags/{user_id}/feed/?rank_token=" "{rank_token}&ranked_content=true&"
          ).format(user_id=user_id, rank_token=self.rank_token)
          return self.send_request(url)
  
      def get_self_user_tags(self):
          return self.get_user_tags(self.user_id)
  
      def get_geo_media(self, user_id):
          url = "maps/user/{user_id}/".format(user_id=user_id)
          return self.send_request(url)
  
      def get_self_geo_media(self):
          return self.get_geo_media(self.user_id)
  
      def sync_from_adress_book(self, contacts):
          url = "address_book/link/?include=extra_display_name,thumbnails"
          return self.send_request(url, "contacts=" + json.dumps(contacts))
  
      # ====== FEED METHODS ====== #
      def tag_feed(self, tag):
          url = "feed/tag/{tag}/?rank_token={rank_token}&ranked_content=true&"
          return self.send_request(url.format(tag=tag, rank_token=self.rank_token))
  
      def get_timeline(self):
          url = "feed/timeline/?rank_token={rank_token}&ranked_content=true&"
          return self.send_request(url.format(rank_token=self.rank_token))
  
      def get_archive_feed(self):
          url = "feed/only_me_feed/?rank_token={rank_token}&ranked_content=true&"
          return self.send_request(url.format(rank_token=self.rank_token))
  
      def get_user_feed(self, user_id, max_id="", min_timestamp=None):
          url = (
              "feed/user/{user_id}/?max_id={max_id}&min_timestamp="
              "{min_timestamp}&rank_token={rank_token}&ranked_content=true"
              # noqa
          ).format(
              user_id=user_id,
              max_id=max_id,
              min_timestamp=min_timestamp,
              rank_token=self.rank_token,
          )
          return self.send_request(url)
  
      def get_self_user_feed(self, max_id="", min_timestamp=None):
          return self.get_user_feed(self.user_id, max_id, min_timestamp)
  
      def get_hashtag_feed(self, hashtag, max_id=""):
          url = (
              "feed/tag/{hashtag}/?max_id={max_id}"
              "&rank_token={rank_token}&ranked_content=true&"
          ).format(hashtag=hashtag, max_id=max_id, rank_token=self.rank_token)
          return self.send_request(url)
  
      def get_location_feed(self, location_id, max_id=""):
          url = (
              "feed/location/{location_id}/?max_id={max_id}"
              "&rank_token={rank_token}&ranked_content=true&"
          ).format(location_id=location_id, max_id=max_id, rank_token=self.rank_token)
          return self.send_request(url)
  
      def get_popular_feed(self):
          url = (
              "feed/popular/?people_teaser_supported=1"
              "&rank_token={rank_token}&ranked_content=true&"
          )
          return self.send_request(url.format(rank_token=self.rank_token))
  
      def get_liked_media(self, max_id=""):
          url = "feed/liked/?max_id={max_id}".format(max_id=max_id)
          return self.send_request(url)
  
      # ====== FRIENDSHIPS METHODS ====== #
      def get_user_followings(self, user_id, max_id=""):
          url = (
              "friendships/{user_id}/following/?max_id={max_id}"
              "&ig_sig_key_version={sig_key}&rank_token={rank_token}"
          ).format(
              user_id=user_id,
              max_id=max_id,
              sig_key=config.SIG_KEY_VERSION,
              rank_token=self.rank_token,
          )
          return self.send_request(url)
  
      def get_self_users_following(self):
          return self.get_user_followings(self.user_id)
  
      def get_user_followers(self, user_id, max_id=""):
          url = "friendships/{user_id}/followers/?rank_token={rank_token}"
          url = url.format(user_id=user_id, rank_token=self.rank_token)
          if max_id:
              url += "&max_id={max_id}".format(max_id=max_id)
          return self.send_request(url)
  
      def get_self_user_followers(self):
          return self.followers
  
      def follow(self, user_id):
          data = self.json_data(
              {
                  "_csrftoken": self.token,
                  "user_id": user_id,
                  "radio_type": "wifi-none",
                  "_uid": user_id,
                  "device_id": self.device_id,
                  "_uuid": self.uuid,
              }
          )
          # self.logger.debug("post data: {}".format(data))
          url = "friendships/create/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def unfollow(self, user_id):
          data = self.json_data(
              {
                  "surface": "profile",
                  "_csrftoken": self.token,
                  "user_id": user_id,
                  "radio_type": "wifi-none",
                  "_uid": user_id,
                  "_uuid": self.uuid,
              }
          )
          url = "friendships/destroy/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def remove_follower(self, user_id):
          data = self.json_data({"user_id": user_id})
          url = "friendships/remove_follower/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def block(self, user_id):
          data = self.json_data({"user_id": user_id})
          url = "friendships/block/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def unblock(self, user_id):
          data = self.json_data({"user_id": user_id})
          url = "friendships/unblock/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def user_friendship(self, user_id):
          data = self.json_data({"user_id": user_id})
          url = "friendships/show/{user_id}/".format(user_id=user_id)
          return self.send_request(url, data)
  
      def all_friendship(self, user_id):
          url = "friendships/show_many"
          return self.send_request(url)
  
      def mute_user(self, user, mute_story=False, mute_posts=False):
          data_dict = {}
          if mute_posts:
              data_dict["target_posts_author_id"] = user
          if mute_story:
              data_dict["target_reel_author_id"] = user
          data = self.json_data(data_dict)
          url = "friendships/mute_posts_or_story_from_follow/"
          return self.send_request(url, data)
  
      def get_muted_friends(self, muted_content):
          # ToDo update endpoints for posts
          if muted_content == "stories":
              url = "friendships/muted_reels"
          elif muted_content == "posts":
              raise NotImplementedError(
                  "API does not support getting friends "
                  "with muted {}".format(muted_content)
              )
          else:
              raise NotImplementedError(
                  "API does not support getting friends"
                  " with muted {}".format(muted_content)
              )
  
          return self.send_request(url)
  
      def unmute_user(self, user, unmute_posts=False, unmute_stories=False):
          data_dict = {}
          if unmute_posts:
              data_dict["target_posts_author_id"] = user
          if unmute_stories:
              data_dict["target_reel_author_id"] = user
          data = self.json_data(data_dict)
          url = "friendships/unmute_posts_or_story_from_follow/"
          return self.send_request(url, data)
  
      def get_pending_friendships(self):
          """Get pending follow requests"""
          url = "friendships/pending/"
          return self.send_request(url)
  
      def approve_pending_friendship(self, user_id):
          data = self.json_data(
              {
                  "_uuid": self.uuid,
                  "_uid": self.user_id,
                  "user_id": user_id,
                  "_csrftoken": self.token,
              }
          )
          url = "friendships/approve/{}/".format(user_id)
          return self.send_request(url, post=data)
  
      def reject_pending_friendship(self, user_id):
          data = self.json_data(
              {
                  "_uuid": self.uuid,
                  "_uid": self.user_id,
                  "user_id": user_id,
                  "_csrftoken": self.token,
              }
          )
          url = "friendships/ignore/{}/".format(user_id)
          return self.send_request(url, post=data)
  
      def get_direct_share(self):
          return self.send_request("direct_share/inbox/?")
  
      @staticmethod
      def _prepare_recipients(users, thread_id=None, use_quotes=False):
          if not isinstance(users, list):
              print("Users must be an list")
              return False
          result = {"users": "[[{}]]".format(",".join(users))}
          if thread_id:
              template = '["{}"]' if use_quotes else "[{}]"
              result["thread"] = template.format(thread_id)
          return result
  
      @staticmethod
      def generate_signature(data):
          body = (
              hmac.new(
                  config.IG_SIG_KEY.encode("utf-8"), data.encode("utf-8"), hashlib.sha256
              ).hexdigest()
              + "."
              + urllib.parse.quote(data)
          )
          signature = "signed_body={body}&ig_sig_key_version={sig_key}"
          return signature.format(sig_key=config.SIG_KEY_VERSION, body=body)
  
      @staticmethod
      def generate_device_id(seed):
          volatile_seed = "12345"
          m = hashlib.md5()
          m.update(seed.encode("utf-8") + volatile_seed.encode("utf-8"))
          return "android-" + m.hexdigest()[:16]
  
      @staticmethod
      def get_seed(*args):
          m = hashlib.md5()
          m.update(b"".join([arg.encode("utf-8") for arg in args]))
          return m.hexdigest()
  
      @staticmethod
      def generate_UUID(uuid_type):
          generated_uuid = str(uuid.uuid4())
          if uuid_type:
              return generated_uuid
          else:
              return generated_uuid.replace("-", "")
  
      def get_total_followers_or_followings(  # noqa: C901
          self,
          user_id,
          amount=None,
          which="followers",
          filter_private=False,
          filter_business=False,
          filter_verified=False,
          usernames=False,
          to_file=None,
          overwrite=False,
      ):
          from io import StringIO
  
          if which == "followers":
              key = "follower_count"
              get = self.get_user_followers
          elif which == "followings":
              key = "following_count"
              get = self.get_user_followings
  
          sleep_track = 0
          result = []
          next_max_id = ""
          self.get_username_info(user_id)
          username_info = self.last_json
          if "user" in username_info:
              total = amount or username_info["user"][key]
  
              if total > 200000:
                  print(
                      "Consider temporarily saving the result of this big "
                      "operation. This will take a while.\n"
                  )
          else:
              return False
          if filter_business:
              print(
                  "--> You are going to filter business accounts. "
                  "This will take time! <--"
              )
          if to_file is not None:
              if os.path.isfile(to_file):
                  if not overwrite:
                      print("File `{}` already exists. Not overwriting.".format(to_file))
                      return False
                  else:
                      print("Overwriting file `{}`".format(to_file))
              with open(to_file, "w"):
                  pass
          desc = "Getting {} of {}".format(which, user_id)
          with tqdm(total=total, desc=desc, leave=True) as pbar:
              while True:
                  get(user_id, next_max_id)
                  last_json = self.last_json
                  try:
                      with open(to_file, "a") if to_file is not None else StringIO() as f:
                          for item in last_json["users"]:
                              if filter_private and item["is_private"]:
                                  continue
                              if filter_business:
                                  time.sleep(2 * random.random())
                                  self.get_username_info(item["pk"])
                                  item_info = self.last_json
                                  if item_info["user"]["is_business"]:
                                      continue
                              if filter_verified and item["is_verified"]:
                                  continue
                              if to_file is not None:
                                  if usernames:
                                      f.write("{}\n".format(item["username"]))
                                  else:
                                      f.write("{}\n".format(item["pk"]))
                              result.append(item)
                              pbar.update(1)
                              sleep_track += 1
                              if sleep_track >= 20000:
                                  sleep_time = random.uniform(120, 180)
                                  msg = (
                                      "\nWaiting {:.2f} min. " "due to too many requests."
                                  ).format(sleep_time / 60)
                                  print(msg)
                                  time.sleep(sleep_time)
                                  sleep_track = 0
                      if not last_json["users"] or len(result) >= total:
                          return result[:total]
                  except Exception as e:
                      print("ERROR: {}".format(e))
                      return result[:total]
  
                  if last_json["big_list"] is False:
                      return result[:total]
  
                  next_max_id = last_json.get("next_max_id", "")
  
      def get_total_followers(self, user_id, amount=None):
          return self.get_total_followers_or_followings(user_id, amount, "followers")
  
      def get_total_followings(self, user_id, amount=None):
          return self.get_total_followers_or_followings(user_id, amount, "followings")
  
      def get_total_user_feed(self, user_id, min_timestamp=None):
          return self.get_last_user_feed(
              user_id, amount=float("inf"), min_timestamp=min_timestamp
          )
  
      def get_last_user_feed(self, user_id, amount, min_timestamp=None):
          user_feed = []
          next_max_id = ""
          while True:
              if len(user_feed) >= float(amount):
                  # one request returns max 13 items
                  return user_feed[:amount]
              self.get_user_feed(user_id, next_max_id, min_timestamp)
              last_json = self.last_json
              if "items" not in last_json:
                  return user_feed
              user_feed += last_json["items"]
              if not last_json.get("more_available"):
                  return user_feed
              next_max_id = last_json.get("next_max_id", "")
  
      def get_total_hashtag_feed(self, hashtag_str, amount=100):
          hashtag_feed = []
          next_max_id = ""
  
          with tqdm(total=amount, desc="Getting hashtag media.", leave=False) as pbar:
              while True:
                  self.get_hashtag_feed(hashtag_str, next_max_id)
                  last_json = self.last_json
                  if "items" not in last_json:
                      return hashtag_feed[:amount]
                  items = last_json["items"]
                  try:
                      pbar.update(len(items))
                      hashtag_feed += items
                      if not items or len(hashtag_feed) >= amount:
                          return hashtag_feed[:amount]
                  except Exception:
                      return hashtag_feed[:amount]
                  next_max_id = last_json.get("next_max_id", "")
  
      def get_total_self_user_feed(self, min_timestamp=None):
          return self.get_total_user_feed(self.user_id, min_timestamp)
  
      def get_total_self_followers(self):
          return self.get_total_followers(self.user_id)
  
      def get_total_self_followings(self):
          return self.get_total_followings(self.user_id)
  
      def get_total_liked_media(self, scan_rate=1):
          next_id = ""
          liked_items = []
          for _ in range(scan_rate):
              self.get_liked_media(next_id)
              last_json = self.last_json
              next_id = last_json.get("next_max_id", "")
              liked_items += last_json["items"]
          return liked_items
  
      # ====== ACCOUNT / PERSONAL INFO METHODS ====== #
      def change_password(self, new_password):
          data = self.json_data(
              {
                  "old_password": self.password,
                  "new_password1": new_password,
                  "new_password2": new_password,
              }
          )
          return self.send_request("accounts/change_password/", data)
  
      def remove_profile_picture(self):
          data = self.json_data()
          return self.send_request("accounts/remove_profile_picture/", data)
  
      def set_private_account(self):
          data = self.json_data()
          return self.send_request("accounts/set_private/", data)
  
      def set_public_account(self):
          data = self.json_data()
          return self.send_request("accounts/set_public/", data)
  
      def set_name_and_phone(self, name="", phone=""):
          return self.send_request(
              "accounts/set_phone_and_name/",
              self.json_data({"first_name": name, "phone_number": phone}),
          )
  
      def get_profile_data(self):
          data = self.json_data()
          return self.send_request("accounts/current_user/?edit=true", data)
  
      def edit_profile(self, url, phone, first_name, biography, email, gender):
          data = self.json_data(
              {
                  "external_url": url,
                  "phone_number": phone,
                  "username": self.username,
                  "full_name": first_name,
                  "biography": biography,
                  "email": email,
                  "gender": gender,
              }
          )
          return self.send_request("accounts/edit_profile/", data)
  
      def fb_user_search(self, query):
          url = (
              "fbsearch/topsearch/?context=blended&query={query}"
              "&rank_token={rank_token}"
          )
          return self.send_request(url.format(query=query, rank_token=self.rank_token))
  
      def search_users(self, query):
          url = (
              "users/search/?ig_sig_key_version={sig_key}"
              "&is_typeahead=true&query={query}&rank_token={rank_token}"
          )
          return self.send_request(
              url.format(
                  sig_key=config.SIG_KEY_VERSION, query=query, rank_token=self.rank_token
              )
          )
  
      def search_username(self, username):
          url = "users/{username}/usernameinfo/".format(username=username)
          return self.send_request(url)
  
      def search_tags(self, query):
          url = "tags/search/?is_typeahead=true&q={query}" "&rank_token={rank_token}"
          return self.send_request(url.format(query=query, rank_token=self.rank_token))
  
      def search_location(self, query="", lat=None, lng=None):
          url = (
              "fbsearch/places/?rank_token={rank_token}"
              "&query={query}&lat={lat}&lng={lng}"
          )
          url = url.format(rank_token=self.rank_token, query=query, lat=lat, lng=lng)
          return self.send_request(url)
  
      def get_user_reel(self, user_id):
          url = "feed/user/{}/reel_media/".format(user_id)
          return self.send_request(url)
  
      def get_reels_tray_feed(
          self, reason=None
      ):  # reason can be = cold_start or pull_to_refresh
          data = {
              "supported_capabilities_new": config.SUPPORTED_CAPABILITIES,
              "reason": reason,
              "_csrftoken": self.token,
              "_uuid": self.uuid,
          }
          data = json.dumps(data)
          return self.send_request("feed/reels_tray/", data)
  
      def get_reels_media(self):
          data = {
              "supported_capabilities_new": config.SUPPORTED_CAPABILITIES,
              "source": "feed_timeline",
              "_csrftoken": self.token,
              "_uuid": self.uuid,
              "_uid": self.user_id,
              "user_ids": self.user_id,
          }
          data = json.dumps(data)
          return self.send_request("feed/reels_media/", data)
  
      def push_register(self):
          data = {
              "device_type": "android_mqtt",
              "is_main_push_channel": "true",
              "device_sub_type": "2",
              # TODO find out what &device_token={"k":"eyJwbiI6ImNvbS5pbnN0YWdyYW0uYW5kcm9pZCIsImRpIjoiNzhlNGMxNmQtN2YzNC00NDlkLTg4OWMtMTAwZDg5OTU0NDJhIiwiYWkiOjU2NzMxMDIwMzQxNTA1MiwiY2siOiIxNjgzNTY3Mzg0NjQyOTQifQ==","v":0,"t":"fbns-b64"} is
              "device_token": "{'k':'eyJwbiI6ImNvbS5pbnN0YWdyYW0uYW5kcm9pZCIsImRpIjoiYmY5ZjNhOTUtMzdjMi00NjEwLTk2MDctYjk2YjI4MDc5YWU4IiwiYWkiOjU2NzMxMDIwMzQxNTA1MiwiY2siOiI5NTk0MzgzMTAyMTUzMzAifQ==','v':'0','t':'fbns-b64'}",
              "_csrftoken": self.token,
              "guid": self.uuid,
              "_uuid": self.uuid,
              "users": self.user_id,
              "familiy_device_id": "7e5a10af-3890-4892-ad0a-4656cd301a2b",
          }
          data = json.dumps(data)
          return self.send_request("push/register/", data)
  
      def media_blocked(self):
          url = "media/blocked/"
          return self.send_request(url)
  
      def get_users_reel(self, user_ids):
          """
              Input: user_ids - a list of user_id
              Output: dictionary: user_id - stories data.
              Basically, for each user output the same as after
              self.get_user_reel
          """
          url = "feed/reels_media/"
          res = self.send_request(
              url, post=self.json_data({"user_ids": [str(x) for x in user_ids]})
          )
          if res:
              return self.last_json["reels"] if "reels" in self.last_json else []
          return []
  
      def see_reels(self, reels):
          """
              Input - the list of reels jsons
              They can be aquired by using get_users_reel()
              or get_user_reel() methods
          """
          if not isinstance(reels, list):
              # In case of only one reel as input
              reels = [reels]
  
          story_seen = {}
          now = int(time.time())
          for i, story in enumerate(
              sorted(reels, key=lambda m: m["taken_at"], reverse=True)
          ):
              story_seen_at = now - min(
                  i + 1 + random.randint(0, 2), max(0, now - story["taken_at"])
              )
              story_seen["{!s}_{!s}".format(story["id"], story["user"]["pk"])] = [
                  "{!s}_{!s}".format(story["taken_at"], story_seen_at)
              ]
  
          data = self.json_data(
              {
                  "reels": story_seen,
                  "_csrftoken": self.token,
                  "_uuid": self.uuid,
                  "_uid": self.user_id,
              }
          )
          data = self.generate_signature(data)
          return self.session.post(
              "https://i.instagram.com/api/v2/" + "media/seen/", data=data
          ).ok
  
      def get_user_stories(self, user_id):
          url = "feed/user/{}/story/".format(user_id)
          return self.send_request(url)
  
      def get_self_story_viewers(self, story_id):
          url = ("media/{}/list_reel_media_viewer/?supported_capabilities_new={}").format(
              story_id, config.SUPPORTED_CAPABILITIES
          )
          return self.send_request(url)
  
      def get_tv_suggestions(self):
          url = "igtv/tv_guide/"
          return self.send_request(url)
  
      def get_hashtag_stories(self, hashtag):
          url = "tags/{}/story/".format(hashtag)
          return self.send_request(url)
  
      def follow_hashtag(self, hashtag):
          data = self.json_data({})
          url = "tags/follow/{}/".format(hashtag)
          return self.send_request(url, data)
  
      def unfollow_hashtag(self, hashtag):
          data = self.json_data({})
          url = "tags/unfollow/{}/".format(hashtag)
          return self.send_request(url, data)
  
      def get_tags_followed_by_user(self, user_id):
          url = "users/{}/following_tags_info/".format(user_id)
          return self.send_request(url)
  
      def get_hashtag_sections(self, hashtag):
          data = self.json_data(
              {
                  "supported_tabs": "['top','recent','places']",
                  "include_persistent": "true",
              }
          )
          url = "tags/{}/sections/".format(hashtag)
          return self.send_request(url, data)
  
      def get_media_insight(self, media_id):
          url = ("insights/media_organic_insights/{}/?ig_sig_key_version={}").format(
              media_id, config.IG_SIG_KEY
          )
          return self.send_request(url)
  
      def get_self_insight(self):
          # TODO:
          url = (
              "insights/account_organic_insights/?"
              "show_promotions_in_landing_page=true&first={}"
          ).format()
          return self.send_request(url)
  
      # From profile => "module_name":"feed_contextual_profile"
      # From home/feed => "module_name":"feed_timeline"
      def save_media(self, media_id, module_name="feed_timeline"):
          return self.send_request(
              endpoint="media/{media_id}/save/".format(media_id=media_id),
              post=self.json_data(self.action_data({"module_name": module_name})),
          )
  
      def unsave_media(self, media_id):
          data = self.json_data()
          url = "media/{}/unsave/".format(media_id)
          return self.send_request(url, data)
  
      def get_saved_medias(self):
          url = "feed/saved/"
          return self.send_request(url)
  
      def get_loom_fetch_config(self):
          return self.send_request("loom/fetch_config/")
  
      def get_request_country(self):
          return self.send_request("locations/request_country/")
  
      def get_linked_accounts(self):
          return self.send_request("linked_accounts/get_linkage_status/")
  
      def get_profile_notice(self):
          return self.send_request("users/profile_notice/")
  
      def get_business_branded_content(self):
          return self.send_request(
              "business/branded_content/should_require_professional_account/"
          )
  
      def get_monetization_products_eligibility_data(self):
          return self.send_request(
              "business/eligibility/get_monetization_products_eligibility_data/?product_types=branded_content"
          )
  
      def get_cooldowns(self):
          body = self.generate_signature(config.SIG_KEY_VERSION)
          url = ("qp/get_cooldowns/?{}").format(body)
          return self.send_request(url)
  
      def log_resurrect_attribution(self):
          data = {
              "_csrftoken": self.token,
              "_uuid": self.uuid,
              "_uid": self.user_id,
          }
          data = json.dumps(data)
          return self.send_request("attribution/log_resurrect_attribution/", data)
  
      def store_client_push_permissions(self):
          data = {
              "enabled": "true",
              "_csrftoken": self.token,
              "device_id": self.device_id,
              "_uuid": self.uuid,
          }
          data = json.dumps(data)
          return self.send_request("attribution/store_client_push_permissions/", data)
  
      def process_contact_point_signals(self):
          data = {
              "phone_id": self.phone_id,
              "_csrftoken": self.token,
              "_uid": self.user_id,
              "device_id": self.device_id,
              "_uuid": self.uuid,
              "google_tokens": "[]",
          }
          data = json.dumps(data)
          return self.send_request("accounts/process_contact_point_signals/", data)
  
      def write_supported_capabilities(self):
          data = {
              "supported_capabilities_new": config.SUPPORTED_CAPABILITIES,
              "_csrftoken": self.token,
              "_uid": self.user_id,
              "_uuid": self.uuid,
          }
          data = json.dumps(data)
          return self.send_request("creatives/write_supported_capabilities/", data)
  
      def arlink_download_info(self):
          return self.send_request("users/arlink_download_info/?version_override=2.2.1")
  
      def get_direct_v2_inbox(self):
          return self.send_request(
              "direct_v2/inbox/?visual_message_return_type=unseen&thread_message_limit=10&persistentBadging=true&limit=20"
          )
  
      def get_direct_v2_inbox2(self):
          return self.send_request(
              "direct_v2/inbox/?visual_message_return_type=unseen&persistentBadging=true&limit=0"
          )
  
      def topical_explore(self):
          url = (
              "discover/topical_explore/?is_prefetch=true&omit_cover_media=true&use_sectional_payload=true&timezone_offset=0&session_id={}&include_fixed_destinations=true"
          ).format(self.client_session_id)
          return self.send_request(url)
  
      def notification_badge(self):
          data = {
              "phone_id": self.phone_id,
              "_csrftoken": self.token,
              "user_ids": self.user_id,
              "device_id": self.device_id,
              "_uuid": self.uuid,
          }
          data = json.dumps(data)
          return self.send_request("notifications/badge/", data)
  
      def facebook_ota(self):
          url = (
              "facebook_ota/?fields=update{download_uri,download_uri_delta_base,version_code_delta_base,download_uri_delta,fallback_to_full_update,file_size_delta,version_code,published_date,file_size,ota_bundle_type,resources_checksum,allowed_networks,release_id}&custom_user_id=3149016955&"
              + self.generate_signature(config.SIG_KEY_VERSION)
              + "&version_code=200396023&version_name="
              + config.APP_VERSION
              + "&custom_app_id=124024574287414&custom_device_id="
              + self.phone_id
              + ""
          )
          return self.send_request(url)
  
      # ====== DIRECT METHODS ====== #
      def get_inbox_v2(self):
          data = json.dumps(
              {
                  "visual_message_return_type": "unseen",
                  "persistentBadging": "True",
                  "limit": "0",
              }
          )
          return self.send_request("direct_v2/inbox/", data)
  
      def get_presence(self):
          return self.send_request("direct_v2/get_presence/")
  
      def get_thread(self, thread_id, cursor_id=None):
          data = json.dumps(
              {"visual_message_return_type": "unseen", "seq_id": "40065", "limit": "10"}
          )
          if cursor_id is not None:
              data["cursor"] = cursor_id
          return self.send_request(
              "direct_v2/threads/{}/".format(thread_id), json.dumps(data)
          )
  
      def get_ranked_recipients(self, mode, show_threads, query=None):
          data = {
              "mode": mode,
              "show_threads": "false" if show_threads is False else "true",
              "use_unified_inbox": "true",
          }
          if query is not None:
              data["query"] = query
          return self.send_request("direct_v2/ranked_recipients/", json.dumps(data))
  
      def get_scores_bootstrap(self):
          url = "scores/bootstrap/users/?surfaces={surfaces}"
          url = url.format(
              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"]'
          )
          return self.send_request(url)
  
      def send_direct_item(self, item_type, users, **options):
          data = {"client_context": self.generate_UUID(True), "action": "send_item"}
          headers = {}
          recipients = self._prepare_recipients(
              users, options.get("thread"), use_quotes=False
          )
          if not recipients:
              return False
          data["recipient_users"] = recipients.get("users")
          if recipients.get("thread"):
              data["thread_ids"] = recipients.get("thread")
          data.update(self.default_data)
  
          url = "direct_v2/threads/broadcast/{}/".format(item_type)
          text = options.get("text", "")
          if item_type == "link":
              data["link_text"] = text
              data["link_urls"] = json.dumps(options.get("urls"))
          elif item_type == "text":
              data["text"] = text
          elif item_type == "media_share":
              data["text"] = text
              data["media_type"] = options.get("media_type", "photo")
              data["media_id"] = options.get("media_id", "")
          elif item_type == "hashtag":
              data["text"] = text
              data["hashtag"] = options.get("hashtag", "")
          elif item_type == "profile":
              data["text"] = text
              data["profile_user_id"] = options.get("profile_user_id")
          elif item_type == "photo":
              url = "direct_v2/threads/broadcast/upload_photo/"
              filepath = options["filepath"]
              upload_id = str(int(time.time() * 1000))
              with open(filepath, "rb") as f:
                  photo = f.read()
  
              data["photo"] = (
                  "direct_temp_photo_%s.jpg" % upload_id,
                  photo,
                  "application/octet-stream",
                  {"Content-Transfer-Encoding": "binary"},
              )
  
              m = MultipartEncoder(data, boundary=self.uuid)
              data = m.to_string()
              headers.update({"Content-type": m.content_type})
  
          return self.send_request(url, data, with_signature=False, headers=headers)
  
      def get_pending_inbox(self):
          url = (
              "direct_v2/pending_inbox/?persistentBadging=true" "&use_unified_inbox=true"
          )
          return self.send_request(url)
  
      # ACCEPT button in pending request
      def approve_pending_thread(self, thread_id):
          data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token})
          url = "direct_v2/threads/{}/approve/".format(thread_id)
          return self.send_request(url, post=data)
  
      # DELETE button in pending request
      def hide_pending_thread(self, thread_id):
          data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token})
          url = "direct_v2/threads/{}/hide/".format(thread_id)
          return self.send_request(url, post=data)
  
      # BLOCK button in pending request
      def decline_pending_thread(self, thread_id):
          data = self.json_data({"_uuid": self.uuid, "_csrftoken": self.token})
          url = "direct_v2/threads/{}/decline/".format(thread_id)
          return self.send_request(url, post=data)
  
      def open_instagram_link(self, link):
          return self.send_request(
              "oembed/?url={}".format(urllib.parse.quote(link, safe=""))
          )
  