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)

        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()
                if __name__ == '__main__':
                    print(L['!']+f' ExportException: "https://www.instagram.com/{usrnm}/channel/?__a=1" responded {__a1.status_code};\n    Instagram detected a suspicious activity. Verify the existence of the {usrnm} account.')
                    while True:
                        progressbar.close()
                        keskonfé = input(L['?']+f' Do you prefer:\n    1 - Export 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.')
                else:
                    outExcept(e=f'ExportException: "https://www.instagram.com/{usrnm}/channel/?__a=1" responded {__a1.status_code};\n    Instagram might have detected a suspicious activity.\n    Verify the existence of the {usrnm} account.\n    Verify the validity of your SessionID.')
        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()
                    usrnm = url.replace('https://www.instagram.com/', '').replace('/channel/?__a=1', '')
                    outExcept(e=f'ExportException: "{url}" responded {__a1.status};\n    Verify the existence of the {usrnm} account, and also the validity of your SessionID.')
            
        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='')
    
    required.add_argument('-t', '--target', choices=["followers", "following", "both", "mutuals"], help='what do you want to export ("followers", "following", "both" or "mutuals")', required=True)
    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)

    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('--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()

'''À faire:
- exportation liste de probas
- probas d'intérêts;
    pas en mettant la liste en décroissant,
    mais en créant un second filtre de données,
    coéfficienté différement et prenant en compte les éléments des comptes pro,
    en évitant de rendre exclusif au --all-infos
- liste d'useragent pour aléatoires comme pour holehe
- informer que l'ecriture du fichier est en cours (longue liste avait bloqué sans donner de nouvelles)'''