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