import sys
import os
import random
import traceback
import time
import json
import re
import math
import argparse
import requests
import openpyxl
import aiohttp
import asyncio

from stringcolor import *
from tqdm import tqdm
from datetime import datetime, date

class SterraException(Exception): pass
'''ExportException, ArgumentException, LoginException, TargetException, InputTypeException''' #vérif qu'elles soient toutes bien là
def myexcepthook(type, value, tb):
    l = ''.join(traceback.format_exception(type, value, tb))
    print(l, end='')
sys.excepthook = myexcepthook

'''START OF THE GLOBAL INFOS BLOCK'''
L = {
    'Python': cs('|', 'DeepSkyBlue5')+bold('§').cs('LightGoldenrod2', 'DeepSkyBlue5')+cs('|', 'LightGoldenrod2'),
    'Excel' :cs('|', 'Green4')+cs('X', 'White', 'Green4')+cs('|', 'Green4'),
    'Instagram': cs('|', 'LightSalmon2')+cs('o', 'White', 'DeepPink4')+cs('|', 'MediumOrchid'),
    'Requests': cs('|', 'SeaGreen2')+bold('$').cs('White', 'DarkGrey')+cs('|', 'LightSalmon'),
    'CSV': cs('|', 'Orange')+bold('c').cs('White', 'Orange')+cs('|', 'Orange'),
    'i': cs('|', 'DodgerBlue2')+bold('i').cs('White', 'DodgerBlue2')+cs('|', 'DodgerBlue2'),
    '!': cs('|', 'Red2')+bold('!').cs('White', 'Red2')+cs('|', 'Red2'),
    '?': cs('|', 'Violet')+bold('?').cs('White', 'Violet')+cs('|', 'Violet'),
    '+': cs('|', 'DarkSeaGreen9')+bold('+').cs('DarkSeaGreen9', 'White')+cs('|', 'DarkSeaGreen9'),
    'x': cs('|', 'Red2')+bold('x').cs('Red2')+cs('|', 'Red2')}
followersList = []
followingList = []
probabilities = {}
'''END OF THE GLOBAL INFOS BLOCK'''

def outExcept(e):
    print(L['!']+' '+e)
    exit()

class accounts_data_analysis:
    '''Only works with list generated by retrieve_list_username_infos().strt()'''
    def __init__(self, lofdic, ignore_over=3500) -> dict:
        '''- lofdic: List of dictionnary made by retrieve_list_username_infos().strt().
        - ignore_over: Number of followers over wich you consider it is impossible that it is a personnal account.'''
        if type(lofdic) != list:
            outExcept(e='InputTypeException: "lofdic" must be a list.')
        if lofdic == []:
            outExcept(e='InputTypeException: "lofdic" must be a not empty list.')
        if type(lofdic[0]) != dict:
            outExcept(e='InputTypeException: elements of "lofdic" must be dictionnaries.')
        if type(ignore_over) != int:
            outExcept(e='InputTypeException: "ignore_over" must be an integer.')
        
        self.lofdic = lofdic
        self.ig_ovr = ignore_over

    def stats(self):
        def coef(v, p, c=1):
            for _ in range(c):
                p.append(v)
            return p

        o = {}
        for d in self.lofdic:
            p = []
            if d['biography'] != '':
                if d['is_private'] and d['followers'] < 200:
                    if re.findall(r'([pP]{1,4}[rR]{1,4}([iI]{0,4}[vV]{1,4}[aAéÉeE]{1,10}[tT]{0,4}[eE]{0,4}|[vV]{1,4}[tT]{1,4}[eE]{0,9})|[pP][vV]{1,4})', d['biography']) != []:
                        #If written "private" or this type of word, great chances to be this type of "private account" that teenager does, like their second account only for their best friends.
                        p = coef(p=p, c=2, v=100)
                        #Coefficient 2

            #If account is private, more chances to be personnal.
            p = coef(p=p, v=(100 if d['is_private'] else 0))
            #Coefficient 1

            #By follower count (++ followers == -- probability, -- followers == ++ probability).
            f = 100 - (int(d['followers'] if d['followers'] < self.ig_ovr else self.ig_ovr) * 100) / self.ig_ovr
            p = coef(p=p, c=5, v=(math.floor(f) if f < 50 else math.ceil(f) if f <= 96 else 100))
            #Coefficient 5

            #By following count (if too much following, it's weird)
            p = coef(p=p, c=2, v=(100 if d['following'] < 1200 else 50 if d['following'] < 2100 else 0))
            #Coefficient 2

            #External url because, in a lot of case, an external link means a link to a business, channel, or this type of things.
            p = coef(p=p, c=1, v=(0 if d['external_url'] != None else 100))
            #Coefficient 1
            
            #If is_professional_account, appends 0
            p = p if not d['is_professional_account'] else coef(p=p, c=1, v=(0 if p[1] < 90 else 60))
            #Coefficient 1 but not appened if not is_professional_account

            #for is_professional_account and is_business_account, if chances for follower are > 90, put 60 instead
            #of 0 to avoid having professionnal business personnal account ruining all the work made before

            #If is_business_account, appends 0
            p = p if not d['is_business_account'] else coef(p=p, c=2, v=(0 if p[1] < 90 else 60))
            #Coefficient 2 but not appened if not is_business_account

            #If is_verified, appends 0
            p = p if not d['is_verified'] else coef(p=p, c=3, v=0)
            #Coefficient 3 but not appened if not is_verified

            t = 0
            for i in p: t += i 
            T = int(t / len(p))

            o[d['username']] = T if T != 100 else 99
        return o

class export_in_file:
    '''Exports the lists of details of follows in csv or excel.'''
    def __init__(self, diclist, typelist, _format, targetname, path, all_infos) -> None:
        '''- diclist: list of ?__a=1 disctionnaries
        - typelist: content name of the list
        - _format: format in wich the file will be saved
        - targetname: instagram username of the target
        - path: path where to create the file
        - all_infos: save with all infos method'''

        if not _format in ['csv', 'excel']:
            outExcept('ExportException: "_format" must be "csv" or "excel".')

        self.diclist = diclist
        self.typelist = typelist
        self._format = _format
        self.targetname = targetname
        self.filename = path+self.targetname.replace('.', '_')+'_'+self.typelist+('.csv' if self._format == 'csv' else '.xlsx')
        self.all_infos = all_infos

    def make_excel(self):
        '''Create the excel file'''
        def fileInfos(x):
            ExcelSheet.cell(row = 1, column = x, value = 'S T E R R A X C Y L')
            ExcelSheet.cell(row = 2, column = x, value = '=HYPERLINK("https://github.com/novitae", "** Made By novitae **")').style='Hyperlink'
            ExcelSheet.cell(row = 4, column = x, value = 'Targeted account :')
            ExcelSheet.cell(row = 5, column = x, value = f'=HYPERLINK("https://www.instagram.com/{self.targetname}/", "{self.targetname}")').style='Hyperlink'
            ExcelSheet.cell(row = 6, column = x, value = 'List :')
            ExcelSheet.cell(row = 7, column = x, value = self.typelist)
            ExcelSheet.cell(row = 9, column = x, value = 'Date :')
            ExcelSheet.cell(row = 10, column = x, value = date.today().strftime("%b-%d-%Y")+datetime.now().strftime(" %H:%M"))
        
        ExcelFile = openpyxl.Workbook()
        ExcelSheet = ExcelFile[ExcelFile.sheetnames[0]]
        ExcelSheet.cell(row = 1, column = 1, value = 'ID')
        ExcelSheet.cell(row = 1, column = 2, value = 'Username')
        ExcelSheet.cell(row = 1, column = 3, value = 'Name')
        ExcelSheet.cell(row = 1, column = 4, value = 'Page URL')
        ExcelSheet.cell(row = 1, column = 5, value = 'Biography')
        ExcelSheet.cell(row = 1, column = 6, value = 'IsPrivate')
        ExcelSheet.cell(row = 1, column = 7, value = 'Followers')
        ExcelSheet.cell(row = 1, column = 8, value = 'Following')
        ExcelSheet.cell(row = 1, column = 9, value = 'Post Count')
        ExcelSheet.cell(row = 1, column = 10, value = 'External Link')
        ExcelSheet.cell(row = 1, column = 11, value = 'IsBusiness')
        ExcelSheet.cell(row = 1, column = 12, value = 'IsProfessional')
        ExcelSheet.cell(row = 1, column = 13, value = 'IsVerified')
        if self.all_infos:
            ExcelSheet.cell(row = 1, column = 14, value = 'Business Adress')
            ExcelSheet.cell(row = 1, column = 15, value = 'Business Category')
            ExcelSheet.cell(row = 1, column = 16, value = 'Business Contact Method')
            ExcelSheet.cell(row = 1, column = 17, value = 'Business Email')
            ExcelSheet.cell(row = 1, column = 18, value = 'Business Phone Number')
            ExcelSheet.cell(row = 1, column = 19, value = 'Connected Facebook Page')
            ExcelSheet.cell(row = 1, column = 20, value = 'Mutual Followed By')
            ExcelSheet.cell(row = 1, column = 21, value = 'Has Effects')
            ExcelSheet.cell(row = 1, column = 22, value = 'Has Channel')
            ExcelSheet.cell(row = 1, column = 23, value = 'Has Clips')
            ExcelSheet.cell(row = 1, column = 24, value = 'Has Guide')
            ExcelSheet.cell(row = 1, column = 25, value = 'Hide Like and View Count')
            ExcelSheet.cell(row = 1, column = 26, value = 'Has joined Recently')
            fileInfos(27)
        else:
            fileInfos(14)
        ExcelFile.save(self.filename)

    def fillexcel(self, dic, rowN):
        '''Fills the excel file created before'''
        rowN += 2
        ExcelFile = openpyxl.load_workbook(filename = self.filename)
        ExcelSheet = ExcelFile[ExcelFile.sheetnames[0]]
        ExcelSheet.cell(row = rowN, column = 1, value = int(dic['id']))
        ExcelSheet.cell(row = rowN, column = 2, value = dic['username'])
        ExcelSheet.cell(row = rowN, column = 3, value = dic['name'])
        ExcelSheet.cell(row = rowN, column = 4, value = '=HYPERLINK("{}", "Account")'.format(dic['link'])).style='Hyperlink'
        ExcelSheet.cell(row = rowN, column = 5, value = dic['biography'])
        ExcelSheet.cell(row = rowN, column = 6, value = dic['is_private'])
        ExcelSheet.cell(row = rowN, column = 7, value = int(dic['followers']))
        ExcelSheet.cell(row = rowN, column = 8, value = int(dic['following']))
        ExcelSheet.cell(row = rowN, column = 9, value = int(dic['posts']))
        if dic['external_url'] == None:
            pass
        else:
            ExcelSheet.cell(row = rowN, column = 10, value = '=HYPERLINK("{}", "{}")'.format(dic['external_url'], dic['external_url'])).style='Hyperlink'
        ExcelSheet.cell(row = rowN, column = 11, value = dic['is_business_account'])
        ExcelSheet.cell(row = rowN, column = 12, value = dic['is_professional_account'])
        ExcelSheet.cell(row = rowN, column = 13, value = dic['is_verified'])
        if self.all_infos:
            ExcelSheet.cell(row = rowN, column = 14, value = dic['business_address_json'])
            ExcelSheet.cell(row = rowN, column = 15, value = dic['business_category_name'])
            ExcelSheet.cell(row = rowN, column = 16, value = dic['business_contact_method'])
            ExcelSheet.cell(row = rowN, column = 17, value = dic['business_email'])
            ExcelSheet.cell(row = rowN, column = 18, value = dic['business_phone_number'])
            if dic['connected_fb_page'] == None:
                pass
            else:
                ExcelSheet.cell(row = rowN, column = 19, value = '=HYPERLINK("{}", "{}")'.format(dic['connected_fb_page'], dic['connected_fb_page'])).style='Hyperlink'
            ExcelSheet.cell(row = rowN, column = 20, value = dic['mutual_followed_by_count'])
            ExcelSheet.cell(row = rowN, column = 21, value = dic['has_ar_effects'])
            ExcelSheet.cell(row = rowN, column = 22, value = dic['has_channel'])
            ExcelSheet.cell(row = rowN, column = 23, value = dic['has_clips'])
            ExcelSheet.cell(row = rowN, column = 24, value = dic['has_guides'])
            ExcelSheet.cell(row = rowN, column = 25, value = dic['hide_like_and_view_counts'])
            ExcelSheet.cell(row = rowN, column = 26, value = dic['is_joined_recently'])
        
        ExcelFile.save(self.filename)

    def export(self):
        '''This module is not made to be used as an external module.'''
        open(self.filename, 'w').close()
        if self._format == 'excel':
            export_in_file.make_excel(self=self)
            print(L['Excel']+bold(' Writing ')+self.filename+' ...')
        else:
            print(L['CSV']+bold(' Writing ')+self.filename+' ...')

        cycle = 0
        for dic in self.diclist:
            if self._format == 'excel':
                export_in_file.fillexcel(self=self, dic=dic, rowN=cycle)

            elif self._format == 'csv':
                if cycle == 0:
                    open(self.filename, 'a').write(','.join(dic.keys())+'\n')

                open(self.filename, 'a').write(','.join([str(dic[key]).replace('\n', '<breakline>').replace(',', '<coma>') for key in dic.keys()])+'\n')
            cycle += 1
        
        print((L['Excel'] if self._format == 'excel' else L['CSV'])+' '+cs.bold(self.typelist.capitalize())+' of '+cs.bold(self.targetname)+' exported under '+cs.bold(self.filename))

class retrieve_list_username_infos:
    '''Retrieve the informations of each instagram username present in the list filled in.'''
    def __init__(self, usrnmlist, sessionid, type_of_list='', all_infos=False, express=False) -> list: #_format, path, 
        '''retrieve_list_username_infos(*args).strt() will convert the /?__a=1 data from each account of the input list and return a list containing all the json.

        - usrnmlist: List of instagram usernames.
        - sessionid: A valid sessionId.
        - type_of_list: Type of list; "followers" or "following" (can be something else, it is for the tqdm progression bar when the info retrieving is happening).
        - all_infos: True to export all infos (a lot might be meaningless), False to export only defaut infos.
        - express: True to enable extractions in express mode, False to disable it, None for automatic mode. Over 109 elements in all list, it will be disabled to avoid instagram blocking.'''

        self.useragent = 'Instagram 206.1.0.30.118 (iPhone; CPU iPhone OS 15 like Mac OS X, fr-fr) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1'
        self.usrnmlist = usrnmlist
        self.lenght = len(usrnmlist)
        self.type_of_list = type_of_list
        self.all_infos = all_infos
        self.express = express
        self.cookies = {'sessionid': sessionid}

    def keep_essential_data(j, self):
        '''Converts the json raw dictionnary responded by instagram /?__a=1 API into a dictionnary keeping only essential informations.'''
        j = j['graphql']['user']
        accinfo = {
            'id': j['id'],
            'username': j['username'],
            'name': j['full_name'],
            'link': 'https://www.instagram.com/'+j['username']+'/',
            'biography': j['biography'],
            'is_private': j['is_private'],
            'followers': j['edge_followed_by']['count'],
            'following': j['edge_follow']['count'],
            'posts': j['edge_owner_to_timeline_media']['count'],
            'external_url': j['external_url'],
            'is_business_account': j['is_business_account'],
            'is_professional_account': j['is_professional_account'],
            'is_verified': j['is_verified'],
        }
        if self.all_infos:
            accinfo['business_address_json'] = j['business_address_json']
            accinfo['business_category_name'] = j['business_category_name']
            accinfo['business_contact_method'] = j['business_contact_method']
            accinfo['business_email'] = j['business_email']
            accinfo['business_phone_number'] = j['business_phone_number']
            accinfo['connected_fb_page'] = j['connected_fb_page']
            accinfo['mutual_followed_by_count'] = j['edge_mutual_followed_by']['count']
            accinfo['has_ar_effects'] = j['has_ar_effects']
            accinfo['has_channel'] = j['has_channel']
            accinfo['has_clips'] = j['has_clips']
            accinfo['has_guides'] = j['has_guides']
            accinfo['hide_like_and_view_counts'] = j['hide_like_and_view_counts']
            accinfo['is_joined_recently'] = j['is_joined_recently']

        return accinfo

    def get_a__1_normal(self):
        '''Gets the /?__a=1 json of each instagram username in the given list, and returns under a list all their json data'''
        rtn = []
        progressbar = tqdm(self.usrnmlist, desc=L['Requests']+f' {self.type_of_list.capitalize()}'+cs.bold(' details extraction '), colour='Black')
        for usrnm in progressbar:
            __a1 = requests.get(f'https://www.instagram.com/{usrnm}/channel/?__a=1', headers = {'User-Agent': self.useragent, 'Referer': f'https://www.instagram.com/{usrnm}'}, cookies=self.cookies, allow_redirects=False)
            if __a1.status_code == 200 and __a1.text != {}:
                rtn.append(__a1.json())
            else:
                progressbar.close()
                while True:
                    progressbar.close()
                    keskonfé = input(L['?']+f' Do you prefer:\n    1 - Continue with what have been extracted,\n    2 - Stop the program.')
                    if keskonfé == '1':
                        return rtn
                    elif keskonfé == '2':
                        exit()
                    else:
                        print(L['!']+f' "{keskonfé}" is not a valid answer, please try again.')
        return rtn

    async def get_a__1_express(self):
        '''Gets in asynchronous mode the /?__a=1 json of each instagram username in the given list, and returns under a list all their json data'''
        rtn = []
        async def fetch(session, url):
            async with session.get(url, headers = {'User-Agent': self.useragent, 'Referer': f'https://www.instagram.com/'}, cookies=self.cookies, allow_redirects=False) as __a1:
                if __a1.status == 200 and await __a1.text() != {}:
                    progressbar.update(1)
                    rtn.append(await __a1.json())
                else:
                    progressbar.close()
                    while True:
                        progressbar.close()
                        keskonfé = input(L['?']+f' Do you prefer:\n    1 - Continue with what have been extracted,\n    2 - Stop the program.')
                        if keskonfé == '1':
                            return rtn
                        elif keskonfé == '2':
                            exit()
                        else:
                            print(L['!']+f' "{keskonfé}" is not a valid answer, please try again.')
                    
        progressbar = tqdm(self.usrnmlist, desc=L['Requests']+f' {self.type_of_list.capitalize()}'+cs.bold(' details extraction '), colour='Black')
        async with aiohttp.ClientSession() as session:
            requests_corroutines = []
            for usrnm in self.usrnmlist:
                requests_corroutines.append(fetch(session, f'https://www.instagram.com/{usrnm}/channel/?__a=1'))
            futures = asyncio.gather(*requests_corroutines)
            await futures
            await session.close()
        
        progressbar.reset()
        progressbar.update(len(self.usrnmlist))
        progressbar.close()
        return rtn
    
    def strt(self):
        '''Connects every retrieve_list_username_infos modules to make the command-line module work'''
        jsonlist = None
        if not self.express or self.lenght > 109: #express mode disabled or disabled because too much username soo we avoid blocking
            if self.lenght > 10:
                print(cs('(', 'Plum3')+bold('☇').cs('Plum3')+cs(')', 'Plum3')+' Details extraction '+cs('can take a while', 'White').bold().underline()+'.\n    You are free to '+cs.bold('do something else').underline()+' while the program does its job.')
            jsonlist = retrieve_list_username_infos.get_a__1_normal(self)
        elif self.express: #express mode activated
            loop = asyncio.get_event_loop()
            jsonlist = loop.run_until_complete(retrieve_list_username_infos.get_a__1_express(self))

        return [retrieve_list_username_infos.keep_essential_data(j, self) for j in jsonlist] #return the list of essential details

class get_follow_list:
    '''Retrieve the followers or following list of an instagram account.'''
    def __init__(self, targid, sessionid=None, username=None, password=None, target='following', limit=True) -> list:
        '''/!\\ To login, or you use sessionId, or you use username and password, but you can't use both.

        - targid: The instagram username or id of the account to aim for.
        - sessionid: SessionID of the instagram account you want to use.
        - username: The instagram username of the account you want to use.
        - password: The instagram password of the account you want to use.
        - target: The list you want to extract, "following", "followers" or "both". It will be "following" by default.
        - limit: False to disable the stop of the program if the follow(ers|ing) list aimed is longer than 999.'''

        if not target in ['following', 'followers', 'both', 'mutuals']:
            outExcept(e='ArgumentException: Target argument must be; following OR followers OR both')
        self.trget = target     #targeted list
        self.usrgt = 'Instagram 206.1.0.30.118 (iPhone; CPU iPhone OS 15 like Mac OS X, fr-fr) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1'

        global sssid
        if sessionid == None and username == None and password == None:
            outExcept(e='LoginException: Please provide these login informations: Username and Password, or SessionID')
        elif sessionid == None and username != None and password != None:
            sssid = get_follow_list.login(username, password)
        elif sessionid != None and username == None and password == None:
            if re.match(r'[0-9]{10,12}%[0-9a-zA-Z]{13,20}%[0-9A-Z]{2,4}', sessionid):
                sssid = sessionid
            else:
                outExcept(e='LoginException: The provided sessionId doesn\'t match a normal sessionId format. Verify that you wrote it correctly.')
        else:
            outExcept(e='ArgumentException: You must provide; a sessionId OR an username and a password.')
        self.sssid = sssid

        reco, tpe = targid
        if tpe == 'username':
            self.usrnm = reco
        elif tpe == 'id':
            self.usrnm = get_follow_list.username_to_id(reco, self.usrgt, self.sssid)
            
        infos = get_follow_list.usernameCheck(self.usrnm, self.usrgt, self.sssid)
        if (int(infos['followers']) > 999 and limit) and self.trget in ['followers', 'both']:
            f = infos['followers']
            outExcept(e=f'TargetException: Sterraxcyl doesn\'t support followers list longer than 999 followers; {targid}\'s followers list is {f} long.')
        if (int(infos['following']) > 999 and limit) and self.trget in ['following', 'both']:
            f = infos['following']
            outExcept(e=f'TargetException: Sterraxcyl doesn\'t support following list longer than 999 follow; {targid}\'s following list is {f} long.')
        
        self.trgid = infos['id']
        self.infos = {'followersCount': infos['followers'], 'followingCount': infos['following']}
        print(L['x']+' '+cs('Target account', 'Red2').underline()+cs.bold(' >>> ')+cs(self.usrnm, 'White', 'Red2').bold()+', '+cs.bold(str(self.infos['followersCount']))+' followers, '+cs.bold(str(self.infos['followingCount']))+' following.')
        if self.trget in ['following', 'both', 'mutuals'] and self.infos['followersCount'] == 0:
            outExcept(e=f'TargetException: Argument "{self.trget}" needs a list longer than 0 following.')
        if self.trget in ['followers', 'both', 'mutuals'] and self.infos['followingCount'] == 0:
            outExcept(e=f'TargetException: Argument "{self.trget}" needs a list longer than 0 following.')

    def username_to_id(i, u, s):
        '''Returns the username from the ID of an account.'''
        j = requests.get(f'https://i.instagram.com/api/v1/users/{i}/info/', headers={'User-Agent': u}, cookies = {'sessionid': s}, allow_redirects=False)
        if j.status_code == 200:
            return j.json()['user']['username']
        elif j.status_code == 404:
            outExcept(e=f'TargetException: The target account associated to "{i}" ID does not exist.')
        else:
            outExcept(e=f'ResponseException: The API responded {j.status_code}, you might have been blocked.')

    def login(u, p):
        '''Returns the sessionid of the account you filled the password and username of.

        - u: username
        - p: password'''
        session = requests.Session()

        session.headers.update({
            'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'en-US,en;q=0.8',
            'Connection': 'keep-alive',
            'Content-Length': '0',
            'Host': 'www.instagram.com',
            'Origin': 'https://www.instagram.com',
            'Referer': 'https://www.instagram.com/',
            'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
                (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'),
            'X-Instagram-AJAX': '7a3a3e64fa87',
            'X-Requested-With': 'XMLHttpRequest'
        })

        reponse = session.get('https://www.instagram.com')
        csrf = re.findall(r"csrf_token\":\"(.*?)\"", reponse.text)[0]
        if csrf:
            session.headers.update({
                'x-csrftoken': csrf
                })
        else:
            outExcept(e='LoginException: No csrf token found in cookies, maybe you are temp ban? Wait 1 hour and retry')

        time.sleep(random.randint(2, 6))

        post_data = {'username': u,
            'enc_password': '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), p)}

        response = session.post('https://www.instagram.com/accounts/login/ajax/', data=post_data, allow_redirects=True)
        response_data = json.loads(response.text)

        if 'two_factor_required' in response_data:
            outExcept(e='LoginException: Please disable 2-factor authentication to login.')
        if 'message' in response_data and response_data['message'] == 'checkpoint_required':
            outExcept(e='LoginException: Please check Instagram app for a security confirmation that it is you trying to login.')
        if not response_data['authenticated']:
            outExcept(e='LoginException: Login failed. Be sure your of your credentials.')

        return response.cookies['sessionid']

    def usernameCheck(u, h, s):
        '''Checks the account existence, and returns its id, followers and following count if it exists, else raises a SterraException'''
        __a1 = requests.get(f'https://www.instagram.com/{u}/channel/?__a=1', cookies = {'sessionid': s}, headers={'User-Agent': h}, allow_redirects=False)
        try:
            if __a1.status_code == 302:
                outExcept(e=f'ResponseException:\n    Check if your filled well your SessionID. If you did, or logged with an Username and Password.')
            elif __a1.status_code == 404 or __a1.json() == {}:
                outExcept(e=f'TargetException: The target account "{u}" does not exist.')
            elif __a1.status_code == 200 and __a1.json() != {}:
                j = __a1.json()['graphql']['user']
                return {'id': j['id'], 'followers': j['edge_followed_by']['count'], 'following': j['edge_follow']['count']}
            else:
                outExcept(e=f'ResponseException: Response {__a1.status_code}, you might have been blocked by instagram.\n    You have no option than waiting or changing of account.')
        except json.decoder.JSONDecodeError:
            outExcept(e=f'ResponseException: API rate limit reached, wait an hour or try to use another account.')

    def ig_request(hash_id, variables, resolver, cookies, count, sleep_error = 10, tentatives = 3): #sleep_requests = 0
        '''This part of code is pasted from https://github.com/tuxity/insta-unfollower/blob/master/insta-unfollower.py, made to retrieve list of following or follower by 25 unity.'''
        has_next_page = True
        tentatives_actuelles = 0
        
        params = {
            "query_hash": hash_id,
            "variables": json.dumps(variables)
            }
        
        bfadd = 0
        fllwExtrct = tqdm(total=int(count), desc=(L['Instagram']+' '+(cs.bold('Followers') if hash_id == 'c76146de99bb02f6415203be841dd25a' else cs.bold('Following'))+' extraction '), colour='Black')
        
        while has_next_page and tentatives_actuelles < tentatives:
            resp = requests.get("https://www.instagram.com/graphql/query/", params = params, cookies = cookies)
            respcookies = resp.cookies.get_dict()
            if respcookies == {}:
                fllwExtrct.close()
                outExcept(e='TargetException: Be sure that you are following the account you are aiming for if it is in private.')
            
            if resp.status_code == 200:
                tentatives_actuelles = 0

                bfadd = len(followersList if hash_id == 'c76146de99bb02f6415203be841dd25a' else followingList)
                has_next_page = resolver(variables, resp.json())
                fllwExtrct.update(len(followersList if hash_id == 'c76146de99bb02f6415203be841dd25a' else followingList) - bfadd)
                if has_next_page:
                    params["variables"] = json.dumps(variables)
                    #time.sleep(sleep_requests)

            else:
                tentatives_actuelles += 1
                print(L['!']+f" An error happened, retry {tentatives_actuelles}/{tentatives} ({sleep_error} seconds between each retry).")
                time.sleep(sleep_error)

        fllwExtrct.reset()
        fllwExtrct.update(int(count))
        fllwExtrct.close()
        return tentatives_actuelles < tentatives

    def resolver_followers(variables, data_resp):
        '''This part of code is pasted from https://github.com/tuxity/insta-unfollower/blob/master/insta-unfollower.py, made to ask ig_request for 25 followers at time'''
        data_resp = data_resp["data"]["user"]["edge_followed_by"]
        
        for node in data_resp["edges"]:
            followersList.append(node["node"]["username"])
                
        if data_resp["page_info"]["has_next_page"]:
            variables["after"] = data_resp["page_info"]["end_cursor"]
            return True
        else:
            return False

    def resolver_following(variables, data_resp):
        '''This part of code is pasted from https://github.com/tuxity/insta-unfollower/blob/master/insta-unfollower.py, made to ask ig_request for 25 following at time'''
        data_resp = data_resp["data"]["user"]["edge_follow"]
        
        for item in data_resp["edges"]:
            followingList.append(item["node"]["username"])
                
        if data_resp["page_info"]["has_next_page"]:
            variables["after"] = data_resp["page_info"]["end_cursor"]
            return True
        else:
            return False

    def generate(self):
        '''Returns the list of every follower or/and following (depends on the arg "target"=???) under a dictionnary containing both'''
        cookies = {'sessionid': self.sssid}
        if self.trget == 'followers':
            get_follow_list.ig_request(hash_id="c76146de99bb02f6415203be841dd25a", variables={"id": self.trgid, "first": 50}, resolver=get_follow_list.resolver_followers, cookies = cookies, count=int(self.infos['followersCount']))
        elif self.trget == 'following':
            get_follow_list.ig_request(hash_id="d04b0a864b4b54837c0d870b0e77e076", variables={"id": self.trgid, "first": 50}, resolver=get_follow_list.resolver_following, cookies = cookies, count=int(self.infos['followingCount']))
        elif self.trget == 'both' or self.trget == 'mutuals':
            get_follow_list.ig_request(hash_id="c76146de99bb02f6415203be841dd25a", variables={"id": self.trgid, "first": 50}, resolver=get_follow_list.resolver_followers, cookies = cookies, count=int(self.infos['followersCount']))
            get_follow_list.ig_request(hash_id="d04b0a864b4b54837c0d870b0e77e076", variables={"id": self.trgid, "first": 50}, resolver=get_follow_list.resolver_following, cookies = cookies, count=int(self.infos['followingCount']))

        if followersList == [] and self.trget == 'followers' and int(self.infos['followersCount']) > 0 or followersList == [] and self.trget == 'both' and int(self.infos['followersCount']) > 0:
            outExcept(e=f'TargetException: Sterra didn\'t retrieve any username from {self.usrnm}\'s followers. Verify that you have access to this account if it is private.')
        if followingList == [] and self.trget == 'following' and int(self.infos['followingCount']) > 0 or followingList == [] and self.trget == 'both' and int(self.infos['followingCount']) > 0:
            outExcept(e=f'TargetException: Sterra didn\'t retrieve any username from {self.usrnm}\'s following. Verify that you have access to this account if it is private.')

        return {'followersList': followersList, 'followingList': followingList}

def main():
    igLogo = bold('i').cs('DodgerBlue2', 'Black')+bold('n').cs('SlateBlue2', 'Black')+bold('s').cs('SlateBlue', 'Black')+bold('t').cs('Pink4', 'Black')+bold('a').cs('LightPink3', 'Black')+bold('g').cs('SandyBrown', 'Black')+bold('r').cs('Gold', 'Black')+bold('a').cs('Yellow2', 'Black')+bold('m').cs('Yellow', 'Black')
    exLogo = bold('e').cs('White', 'DarkGreen')+bold('x').cs('White', 'Green4')+bold('c').cs('White', 'Green4')+bold('e').cs('White', 'DarkGreen')+bold('l').cs('White', 'Green4')+bold('.').cs('White', 'DarkGreen')+bold('x').cs('White', 'Green4')+bold('l').cs('White', 'Green4')+bold('s').cs('White', 'Green4')+bold('x').cs('White', 'DarkGreen')
    csLogo = bold('f').cs('White', 'DarkOrange3')+bold('o').cs('White', 'DarkOrange')+bold('l').cs('White', 'Orange2')+bold('l').cs('White', 'DarkOrange3')+bold('o').cs('White', 'OrangeRed')+bold('w').cs('White', 'DarkOrange')+bold('.').cs('White', 'Orange2')+bold('c').cs('White', 'OrangeRed')+bold('s').cs('White', 'DarkOrange3')+bold('v').cs('White', 'DarkOrange')
    print('~ v.2.0 ~ ~ '+exLogo+'  < < <  '+igLogo+'  > > >  '+csLogo+' ~ ~ GPL-3 ~')
    print('['+bold('*')+'] ~ ~ ~ '+cs('made by ', 'MediumPurple6')+bold('aet').cs('MediumPurple6')+cs(' € https://github.com/novitae/sterraxcyl', 'MediumPurple6')+' ~ ~ ~ ~ ~')


    parser = argparse.ArgumentParser(add_help=False)
    required = parser.add_argument_group(title='required arguments', description='')
    login = parser.add_argument_group(title='login arguments', description='')
    data_analysis = parser.add_argument_group(title='probabilities arguments', description='')
    optional = parser.add_argument_group(title='optional arguments', description='')
    
    rexclued = required.add_mutually_exclusive_group(required=True)
    rexclued.add_argument('-u', '--username', metavar='U', help='the instagram username of the aimed account', type=str)
    rexclued.add_argument('-id', metavar='ID', help='the instagram id of the aimed account', type=int)
    required.add_argument('-t', '--target', choices=["followers", "following", "both", "mutuals"], help='what do you want to export ("followers", "following", "both" or "mutuals")', required=True)

    lognn = login.add_mutually_exclusive_group(required=True)
    lognn.add_argument('-lcrd', '--login-credentials', metavar=('U', 'P'), nargs=2, help='login by credentials: USERNAME PASSWORD (be sure to keep a space between them)')
    lognn.add_argument('-ssid', '--login-session-id', metavar='S', help='login by SessionID')

    optional.add_argument('--all-infos', action='store_true', help='writes down the account extra informations that the program originaly ignores')
    optional.add_argument('--do-not-export', action='store_true', help='do not export to file')
    optional.add_argument('-e', '--express', action='store_true', help='sends ultra fast requests to get the table faster (deactivated if more than 109 total usernames to avoid blocking)')
    optional.add_argument('-f', '--format', choices=['excel', 'csv'], help='format of the export, by default "excel"')
    optional.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='show this help message and exit')
    optional.add_argument('--no-limit', action='store_true', help='disable the limitation for lists over 1000 follow(ers|ing); all errors you could get by doing this will not recieve help if you submit it at the issue page of sterraxcyl')
    optional.add_argument('--no-btc', action='store_true', help='hides the invitation to donations printed at the end')
    optional.add_argument('-p', '--path', metavar='P', help='directory path where export the files (by default in your module path)')

    data_analysis.add_argument('-a', '--activate-data-analysis', action='store_true', help='activates a data analysis that prints, for each accounts in the target lists, the probabilities of being an account from the close circle of the target')
    data_analysis.add_argument('--pctg', help='percentage under wich we won\'t print results (between 0 and 98)', type=int)
    data_analysis.add_argument('-s', '--size', help='size of the most probable username list (will be by default the size of the followers/mutuals/following list filled in)', type=int)
    data_analysis.add_argument('--url', action='store_true', help='instead of printing username, printing the url to the account')
    data_analysis.add_argument('--descending', action='store_true', help='instead of printing by highest probability, printing by lowest probability')
    args = parser.parse_args()


    args.format = "excel" if args.format == None else args.format
    args.express = False if args.express == None else args.express
    args.path = os.path.dirname(__file__)+'/export/' if args.path == None else args.path
    if not args.activate_data_analysis and (args.size != None or args.pctg != None):
        outExcept(e='ArgumentException: activate_data_analysis argument must be filled in to use data analysis tools.')
    pct =  args.pctg if args.pctg != None else 0
    if pct < 0 or pct > 98: #si pourcentage trop élevé ou trop bas : erreur
        outExcept(e='ArgumentException: pctg argument must be over or equal to 0, and under or equal to 98.')
    if not os.path.exists(args.path): os.mkdir(args.path)
    targgg = (args.id if args.id != None else args.username, 'id' if args.id != None else 'username')


    a = None #EXTRACTION DES LISTES
    if args.login_session_id != None: #login via sessionid
        a = get_follow_list(targid=targgg, sessionid=args.login_session_id, target=args.target, limit=args.no_limit).generate()
    elif args.login_session_id == None: #login via username and password
        a = get_follow_list(targid=targgg, username=args.login_credentials[0], password=args.login_credentials[1], target=args.target, limit=args.no_limit).generate()
    else:
        outExcept(e='LoginException: Please provide these login informations: Username and Password, or SessionID')


    def data_analysis(ll, ty, c): #ANALYSE DES DONNÉES
        d = accounts_data_analysis(ll).stats()
        p = dict(sorted(d.items(), key=lambda item: item[1], reverse=(False if args.descending else True)))
    
        def setcolor(i):
            if i > 85: return 'Green3'
            elif i > 80: return 'Green2'
            elif i > 75: return 'Chartreuse2'
            elif i > 70: return 'GreenYellow'
            elif i > 65: return 'Yellow4'
            elif i > 60: return 'Yellow'
            elif i > 55: return 'LightGoldenrod2'
            elif i > 50: return 'Orange'
            elif i > 45: return 'DarkOrange'
            elif i > 40: return 'OrangeRed'
            elif i > 35: return 'Red2'
            elif i > 30: return 'Red3'
            elif i > 25: return 'Red4'
            elif i > 20: return 'DarkRed2'
            else: return 'DarkRed'

        enum = list(p.keys())[:c] if c < len(p.keys()) else p.keys()
        enum = [x for x in enum if d[x] > pct] #garde/supprime en fonction du pourcentage
        
        print(L['i']+cs.bold(' Probabilities')+f' for {ty} accounts of '+cs.bold('being a personnal account')+f' (showing {len(enum)}/{len(d)}):')
        print(cs.bold(' >>> '+cs('|', 'DarkGreen')+bold('%%%').cs('White', 'DarkGreen')+cs('|', 'DarkGreen')+' = '+cs.bold('Username')))
        if enum == []:
            nn = cs('->', setcolor(30))
            print(f'      !!! {nn} '+bold('empty list'))
        else:
            for e in enum:
                lineco = setcolor(p[e])
                prob = str(p[e]) if len(str(p[e])) == 2 else '0'+str(p[e])
                print('      '+bold(prob)+'%'+cs(' -> ', lineco)+cs((e if not args.url else cs('https://www.instagram.com/', 'LightGrey9')+bold(e)+cs('/', 'LightGrey9')), 'White').underline())
    
    followers = None
    following = None
    mutualList = []

    if args.target == 'followers' or args.target == 'following' or args.target == 'both':
        if args.target == 'followers' or args.target == 'both':
            followers = retrieve_list_username_infos(usrnmlist=a['followersList'], type_of_list="followers", sessionid=sssid, all_infos=args.all_infos, express=args.express).strt()
            if not args.do_not_export:
                export_in_file(diclist=followers, typelist='followers', _format=args.format, targetname=args.username, path=args.path, all_infos=args.all_infos).export()

        if args.target == 'following' or args.target == 'both':
            following = retrieve_list_username_infos(usrnmlist=a['followingList'], type_of_list="following", sessionid=sssid, all_infos=args.all_infos, express=args.express).strt()
            if not args.do_not_export:
                export_in_file(diclist=following, typelist='following', _format=args.format, targetname=args.username, path=args.path, all_infos=args.all_infos).export()

    elif args.target == 'mutuals':
        mlngliste = a['followingList'] if len(a['followingList']) <= len(a['followersList']) else a['followersList']
        plngliste = a['followersList'] if len(a['followersList']) >= len(a['followingList']) else a['followingList']
        for flw in mlngliste:
            if flw in plngliste: mutualList.append(flw)

        if mutualList == []: outExcept(e=f'DataException: Sterra did\'nt find any mutually followed accounts on {args.username} account.')
        else:
            mutuals = retrieve_list_username_infos(usrnmlist=mutualList, type_of_list="mutuals", sessionid=sssid, all_infos=args.all_infos, express=args.express).strt()
            if not args.do_not_export:
                export_in_file(diclist=mutuals, typelist='mutuals', _format=args.format, targetname=args.username, path=args.path, all_infos=args.all_infos).export()


    if args.activate_data_analysis:
        if args.target == 'mutuals': data_analysis(ll=mutuals, ty='mutuals', c=args.size if args.size != None else len(mutuals))
        else:
            if followers != None: data_analysis(ll=followers, ty='followers', c=args.size if args.size != None else len(followers))
            if following != None: data_analysis(ll=following, ty='following', c=args.size if args.size != None else len(following))
    
    if not args.no_btc: print('[ € ] ~['+cs(' BTC ', 'MediumPurple6').bold()+cs('bc1qjdw2hsspdlw7j9j9qn24gnujnk5thdmt6h2kjh', 'DarkSeaGreen9').underline()+cs(' thx <3 ', 'MediumPurple6').bold()+']~ ~ ~ ~')

if __name__ == '__main__': main()