├── yocial ├── __main__.py ├── __init__.py ├── features │ ├── SocialDetector.py │ ├── BaseSocialApp.py │ ├── PhoneNumber.py │ ├── Whatsapp │ │ ├── Whatsapp.py │ │ └── whatsapp_detect.go │ ├── Telegram │ │ └── Telegram.py │ └── Instagram │ │ └── Instagram.py ├── Main.py └── Utils.py ├── requirements.txt ├── .idea ├── dictionaries │ └── yazeed.xml └── vcs.xml ├── README.md └── .gitignore /yocial/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | ''' 4 | Entry point to run yocial module `python3 -m yocial`. 5 | ''' 6 | 7 | from yocial.Main import main 8 | 9 | main() 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.11.28 2 | chardet==3.0.4 3 | idna==2.8 4 | pyaes==1.6.1 5 | pyasn1==0.4.8 6 | requests==2.22.0 7 | rsa==4.0 8 | Telethon==1.10.9 9 | urllib3==1.25.7 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/yazeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | instagram 5 | whatsapp 6 | 7 | 8 | -------------------------------------------------------------------------------- /yocial/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile, join 2 | import glob 3 | modules = glob.glob(join(dirname(__file__), "*.py")) 4 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /yocial/features/SocialDetector.py: -------------------------------------------------------------------------------- 1 | class SocialDetector: 2 | apps_to_detect = [] 3 | 4 | # This method takes an implementation of BaseSocialApp and add it to the list to be detected later 5 | def add_social_app(self, app): 6 | self.apps_to_detect.append(app) 7 | 8 | def detect(self): 9 | for app in self.apps_to_detect: 10 | app.process() 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # social-media-detector-api 2 | 3 | 4 | API for reporting social media accounts linked to a phone number. For example, the API could take a phone number as an input and outputs ”This phone number has Telegram, Whatsapp, Twitter tied to it but not Instagram”. We can scale this to tens, hundreds, thousands of phone numbers which makes it a very useful tool for collecting information about a large sample of people. 5 | 6 | The API can also be used in command line and makes it possible to integrate every programming language that has access to operating system command line. Which is pretty much all of them. so that we’re not tied to only python. 7 | 8 | # Thanks 9 | - [Bassam Mutairi](https://github.com/mutairibassam) for his contribution to this repo 10 | -------------------------------------------------------------------------------- /yocial/Main.py: -------------------------------------------------------------------------------- 1 | # TODO Write unit tests 2 | # TODO Write documentation 3 | 4 | # Ultimately, I want Main.py to only have command arguments processing. Will work on that later 5 | # Right now Main.py is used to demonstrate how to use the API 6 | import sys 7 | import yocial.Utils as Utils 8 | import private_constants 9 | from yocial.features.Instagram.Instagram import Instagram 10 | from yocial.features.SocialDetector import SocialDetector 11 | from yocial.features.Telegram.Telegram import Telegram 12 | from yocial.features.Whatsapp.Whatsapp import Whatsapp 13 | 14 | 15 | def main(): 16 | phone_number_list = Utils.cmd_args_to_phone_number(sys.argv) 17 | detector = SocialDetector() 18 | detector.add_social_app(Telegram(phone_number_list, private_constants.TELEGRAM_API_ID, private_constants.TELEGRAM_API_HASH)) 19 | detector.add_social_app(Whatsapp(phone_number_list)) 20 | detector.add_social_app(Instagram(phone_number_list)) 21 | detector.detect() 22 | for number in phone_number_list: 23 | print(number) 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /yocial/features/BaseSocialApp.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseSocialApp(ABC): 5 | # These numbers will be looking to detect whether it is using a certain application or not 6 | phone_numbers_to_detect = None 7 | 8 | def __init__(self, phone_numbers_to_detect): 9 | self.phone_numbers_to_detect = phone_numbers_to_detect 10 | 11 | @abstractmethod 12 | def authenticate(self): 13 | pass 14 | 15 | # TODO Find a way to separate detection & authentication into a new class 16 | @abstractmethod 17 | def detect_single_number(self, phone_number): 18 | pass 19 | 20 | @abstractmethod 21 | def detect_numbers(self, phone_numbers): 22 | pass 23 | 24 | @abstractmethod 25 | def process(self): 26 | # This method is the highest of the order in here. It will be called by SocialDetector to start the whole 27 | # detecting process For example, say you wanted to you know if 100k phone number are using Whatsapp. You 28 | # would use this method to divide those phone numbers among the phone numbers you are using to detect 29 | pass 30 | 31 | @abstractmethod 32 | def get_name(self): 33 | # Each social media app would have a unique name that would be used to identify it 34 | pass 35 | -------------------------------------------------------------------------------- /yocial/Utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from yocial.features.PhoneNumber import PhoneNumber 4 | 5 | 6 | def file_to_phone_number(file_path): 7 | phone_number_list = [] 8 | with open(file_path) as file: 9 | for line in file: 10 | phone_number_list.append(PhoneNumber(line.strip('\n'))) 11 | return phone_number_list 12 | 13 | # Convert command line arguments to PhoneNumber objects. If the arguments is a file's name then that file will be 14 | # processed and not the arguments themselves 15 | def cmd_args_to_phone_number(arguments): 16 | 17 | # Currently the processing of numbers to objects is sensitive. Meaning that empty and invalid lines might get 18 | # processed into an objects, which obviously shouldn't. Bear that in mind 19 | 20 | # as a bypass for the time being until I develop verify function I will incrment the condition to 2 21 | # as arguments[0] is always the project entry point `__main__.py` 22 | assert len(arguments) >= 2, "No arguments are provided" 23 | 24 | phone_number_list = [] 25 | if os.path.isfile(arguments[1]): 26 | return file_to_phone_number(arguments[1]) 27 | else: 28 | for raw_phone_number in arguments[1:]: 29 | # ensure PhoneNumber object initialize only for valid numbers 30 | verified = PhoneNumber.verifier(number=raw_phone_number) 31 | if(verified): 32 | phone_number_list.append(PhoneNumber(raw_phone_number)) 33 | return phone_number_list 34 | -------------------------------------------------------------------------------- /yocial/features/PhoneNumber.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | from dataclasses import dataclass, field 4 | class AppUsageEnum(Enum): 5 | ERROR = -1 # There was an error while trying to detect whether this phone number uses a certain social app. 6 | NO_USAGE = 0 # This phone number does not use the app 7 | USAGE = 1 # This phone number has the app and it uses it 8 | 9 | @dataclass 10 | class PhoneNumber: 11 | _phone_number: str = "" 12 | # Used to track which apps are used by this phone number 13 | app_usage: dict = field(init=False,default_factory=dict) 14 | 15 | # get a current phone number 16 | @property 17 | def phone_number(self) -> str: 18 | return self._phone_number 19 | 20 | # set a new phone number 21 | @phone_number.setter 22 | def phone_number(self, value: str) -> None: 23 | self._phone_number = value 24 | 25 | # Return a constant of AppUsageEnum 26 | def has_app(self, app_name): 27 | return self.app_usage[app_name] 28 | 29 | # Sets whether a phone number is using an app 30 | # state will be a constant of AppUsageEnum 31 | def set_app_state(self, app_name, state): 32 | self.app_usage[app_name] = state 33 | 34 | def __str__(self): 35 | usage_as_string = self.phone_number + "\n" 36 | for key, val in self.app_usage.items(): 37 | # to handle errors 38 | try: 39 | usage_as_string += key + " => " + val.name + "\n" 40 | except: 41 | usage_as_string += key + " => " + val + "\n" 42 | return usage_as_string 43 | 44 | def verifier(number) -> bool: 45 | ''' 46 | Return boolean value based on the verification logic 47 | 48 | Parameters: 49 | number -> the number need to be verified 50 | Return: 51 | True -> if it's valid 52 | False -> if it's not valid 53 | ''' 54 | # TODO: substring condition 55 | if(len(number) == 10): 56 | return True 57 | else: 58 | return False -------------------------------------------------------------------------------- /yocial/features/Whatsapp/Whatsapp.py: -------------------------------------------------------------------------------- 1 | from BaseSocialApp import BaseSocialApp 2 | import subprocess 3 | import os.path 4 | 5 | from PhoneNumber import AppUsageEnum 6 | 7 | 8 | class Whatsapp(BaseSocialApp): 9 | 10 | def authenticate(self): 11 | # Currently, Whatsapp go will try to verify through qr code and will time out if not entered within a minute 12 | # command = 'go run whatsapp_detect.go auth' 13 | # output = subprocess.run(command, shell=True) 14 | # print(output) 15 | # TODO implement a way to have multiple whatsapp sessions 16 | if not os.path.isfile("whatsappSession.gob"): 17 | print("Whatsapp will need authorization to your account. Scan the shown qr code. You might need to " 18 | "restart the program after scanning the qr code") 19 | 20 | subprocess.run('go run whatsapp_detect.go auth', shell=True) 21 | 22 | def detect_single_number(self, phone_number): 23 | self.detect_numbers([phone_number]) 24 | 25 | def detect_numbers(self, phone_numbers): 26 | # The first time whatsapp_detect.go is used will require a qr code scan. 27 | # TODO fix a potential bug where the result of the commands will not match the length of phone numbers' length 28 | self.authenticate() 29 | command = 'go run whatsapp_detect.go' 30 | for phone in phone_numbers: 31 | command = command + " " + phone.get_phone_number() 32 | output = subprocess.run(command, shell=True, stdout=subprocess.PIPE) 33 | result_decoded = output.stdout.decode('utf-8').splitlines() 34 | for index, phone in enumerate(phone_numbers): 35 | # For each phone number, look for the corresponding line in the output 36 | # TODO find a way to differentiate a way between NO_USAGE and ERROR 37 | phone.set_app_state(self.get_name(), AppUsageEnum.USAGE if "\"status\":200" in result_decoded[ 38 | index] else AppUsageEnum.NO_USAGE) 39 | return phone_numbers 40 | 41 | def process(self): 42 | # TODO implement an optimal algorithm for whatsapp where various client phone numbers are used to detect 43 | self.detect_numbers(self.phone_numbers_to_detect) 44 | 45 | def get_name(self): 46 | return "Whatsapp" 47 | -------------------------------------------------------------------------------- /yocial/features/Telegram/Telegram.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient, sync 2 | from telethon.tl.functions.contacts import ImportContactsRequest 3 | from telethon.tl.types import InputPhoneContact 4 | 5 | from BaseSocialApp import BaseSocialApp 6 | 7 | import os.path 8 | 9 | from PhoneNumber import AppUsageEnum 10 | 11 | 12 | # TODO investigate limitations of Telegram API regarding importing contacts 13 | 14 | class Telegram(BaseSocialApp): 15 | # Two lists of authenticators, and will be checked to assure that they are of the same length 16 | def __init__(self, phone_numbers_to_detect, api_id, api_hash): 17 | self.api_ID = api_id 18 | self.api_hash = api_hash 19 | super().__init__(phone_numbers_to_detect) 20 | 21 | def authenticate(self): 22 | # detect.session file is a file used by Telegram API to keep current sessions going 23 | # TODO implement a method to have multiple sessions 24 | if not os.path.isfile("detect.session"): 25 | print("Telegram will need authorization into your account") 26 | 27 | def detect_single_number(self, phone_number): 28 | self.detect_numbers([phone_number]) 29 | 30 | def detect_numbers(self, phone_numbers): 31 | self.authenticate() 32 | with TelegramClient('detect', self.api_ID, self.api_hash) as client: 33 | input_contact_list = [] 34 | for phone_number in phone_numbers: 35 | input_contact_list.append( 36 | InputPhoneContact(client_id=0, phone=phone_number.get_phone_number(), 37 | first_name=phone_number.get_phone_number(), 38 | last_name=phone_number.get_phone_number())) 39 | client(ImportContactsRequest(input_contact_list)) 40 | for phone_number in phone_numbers: 41 | try: 42 | contact = client.get_input_entity(phone_number.get_phone_number()) 43 | # If the user's id is not 0 then the user has an account in Telegram 44 | phone_number.set_app_state(self.get_name(), 45 | AppUsageEnum.USAGE if contact.user_id > 0 else AppUsageEnum.NO_USAGE) 46 | 47 | except ValueError as e: 48 | 49 | # TODO Use an error logger 50 | if "Cannot find any entity corresponding to" in str(e): 51 | # If this error happens that means the API call was successfully, but the phone number does 52 | # not have Telegram 53 | phone_number.set_app_state(self.get_name(), AppUsageEnum.NO_USAGE) 54 | else: 55 | print(e) 56 | phone_number.set_app_state(self.get_name(), AppUsageEnum.ERROR) 57 | return phone_numbers 58 | 59 | def process(self): 60 | # TODO implement an optimal algorithm for Telegram where various client phone numbers are used to detect 61 | self.detect_numbers(self.phone_numbers_to_detect) 62 | 63 | def get_name(self): 64 | return "Telegram" 65 | -------------------------------------------------------------------------------- /yocial/features/Whatsapp/whatsapp_detect.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "encoding/gob" 6 | "fmt" 7 | _ "github.com/Baozisoftware/qrcode-terminal-go" 8 | qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" 9 | "github.com/Rhymen/go-whatsapp" 10 | "log" 11 | "os" 12 | "time" 13 | ) 14 | 15 | 16 | type waHandler struct { 17 | c *whatsapp.Conn 18 | } 19 | 20 | 21 | //HandleError needs to be implemented to be a valid WhatsApp handler 22 | func (h *waHandler) HandleError(err error) { 23 | 24 | if e, ok := err.(*whatsapp.ErrConnectionFailed); ok { 25 | log.Printf("Connection failed, underlying error: %v", e.Err) 26 | log.Println("Waiting 30sec...") 27 | <-time.After(30 * time.Second) 28 | log.Println("Reconnecting...") 29 | err := h.c.Restore() 30 | if err != nil { 31 | log.Fatalf("Restore failed: %v", err) 32 | } 33 | } else { 34 | log.Printf("error occoured: %v\n", err) 35 | } 36 | } 37 | 38 | 39 | func whatsapp_exists(phoneNumber *string, wac *whatsapp.Conn) { 40 | jid := *phoneNumber + "@s.whatsapp.net" 41 | ch, err := wac.Exist(jid) 42 | if err != nil { 43 | panic(err) 44 | } 45 | resp := <- ch 46 | fmt.Println(resp) 47 | } 48 | 49 | func main() { 50 | //create new WhatsApp connection 51 | wac, err := whatsapp.NewConn(5 * time.Second) 52 | if err != nil { 53 | log.Fatalf("error creating connection: %v\n", err) 54 | } 55 | 56 | //Add handler 57 | wac.AddHandler(&waHandler{wac}) 58 | 59 | //login or restore 60 | if err := login(wac); err != nil { 61 | log.Fatalf("error logging in: %v\n", err) 62 | } 63 | 64 | //This program only takes raw phone numbers. It does not accept files as inputs 65 | if os.Args[1] != "auth" { 66 | //If auth is included in the input, then only try to login 67 | for _, element := range os.Args[1:] { 68 | whatsapp_exists(&element, wac) 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | session, err := wac.Disconnect() 76 | if err != nil { 77 | log.Fatalf("error disconnecting: %v\n", err) 78 | } 79 | if err := writeSession(session); err != nil { 80 | log.Fatalf("error saving session: %v", err) 81 | } 82 | } 83 | 84 | func login(wac *whatsapp.Conn) error { 85 | //load saved session 86 | session, err := readSession() 87 | if err == nil { 88 | //restore session 89 | session, err = wac.RestoreWithSession(session) 90 | if err != nil { 91 | //Maybe delete the file whatsappSession.gob over here 92 | return fmt.Errorf("restoring failed: %v\n", err) 93 | } 94 | } else { 95 | //no saved session -> regular login 96 | qr := make(chan string) 97 | go func() { 98 | terminal := qrcodeTerminal.New() 99 | terminal.Get(<-qr).Print() 100 | }() 101 | session, err = wac.Login(qr) 102 | if err != nil { 103 | return fmt.Errorf("error during login: %v\n", err) 104 | } 105 | } 106 | 107 | //save session 108 | err = writeSession(session) 109 | if err != nil { 110 | return fmt.Errorf("error saving session: %v\n", err) 111 | } 112 | return nil 113 | } 114 | func readSession() (whatsapp.Session, error) { 115 | //Sometimes the session would refer to an invalid connection. In that case you have to delete the session file 116 | session := whatsapp.Session{} 117 | file, err := os.Open("whatsappSession.gob") 118 | if err != nil { 119 | return session, err 120 | } 121 | defer file.Close() 122 | decoder := gob.NewDecoder(file) 123 | err = decoder.Decode(&session) 124 | if err != nil { 125 | return session, err 126 | } 127 | return session, nil 128 | } 129 | 130 | func writeSession(session whatsapp.Session) error { 131 | file, err := os.Create("whatsappSession.gob") 132 | if err != nil { 133 | return err 134 | } 135 | defer file.Close() 136 | encoder := gob.NewEncoder(file) 137 | err = encoder.Encode(session) 138 | if err != nil { 139 | return err 140 | } 141 | return nil 142 | } 143 | 144 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # vscode 7 | .vscode/ 8 | .idea/ 9 | 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ -------------------------------------------------------------------------------- /yocial/features/Instagram/Instagram.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | import urllib.parse 5 | 6 | import uuid as uuid 7 | 8 | import requests 9 | 10 | import yocial.features.PhoneNumber as PhoneNumber 11 | from yocial.features.BaseSocialApp import BaseSocialApp 12 | 13 | API_URL = 'https://i.instagram.com/api/v1/' 14 | USERS_LOOKUP_URL = API_URL + 'users/lookup/' 15 | SIG_KEY_VERSION = '4' 16 | IG_SIG_KEY = 'e6358aeede676184b9fe702b30f4fd35e71744605e39d2181a34cede076b3c33' 17 | 18 | 19 | def generate_uuid(type): 20 | gen_uuid = str(uuid.uuid4()) 21 | return gen_uuid if type else gen_uuid.replace('-', '') 22 | 23 | 24 | def generate_device_id(): 25 | return str(uuid.uuid4()).replace('-', '')[:16] 26 | 27 | 28 | def generate_signature(data): 29 | return 'ig_sig_key_version=' + SIG_KEY_VERSION + '&signed_body=' + hmac.new(IG_SIG_KEY.encode('utf-8'), 30 | data.encode('utf-8'), 31 | hashlib.sha256).hexdigest() + '.' \ 32 | + urllib.parse.quote_plus(data) 33 | 34 | 35 | def generate_headers(): 36 | # This is very basic headers and is literally copy pasted from burp. 37 | # TODO replace this with headers that is randomly generated 38 | return { 39 | "X-Pigeon-Session-Id": "3168f276-8c96-4db3-87af-1db1aa9cad09", 40 | "X-Pigeon-Rawclienttime": "1562978852"".860", 41 | "X-IG-Connection-Speed": "-1kbps", 42 | "X-IG-Bandwidth-Speed-KBPS": "-1.000", 43 | "X-IG-Bandwidth-TotalBytes-B": "0", 44 | "X-IG-Bandwidth-TotalTime-MS": "0", 45 | "X-Bloks-Version-Id": "c7aeefd59aab78fc0a703ea060ffb631e005e2b3948efb9d73ee6a346c446bf3", 46 | "X-IG-Connection-Type": "WIFI", 47 | "X-IG-Capabilities": "3brTvw==", 48 | "X-IG-App-ID": "567067343352427", 49 | "User-Agent": generate_user_agent(), 50 | "Accept-Language": "en-US", 51 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 52 | "Accept-Encoding": "gzip, deflate", 53 | 54 | "X-FB-HTTP-Engine": "Liger", 55 | "Connection": "close" 56 | } 57 | 58 | 59 | def generate_user_agent(): 60 | # TODO Generate random user agent 61 | return "Instagram 101.0.0.15.120 Android (26/8.0.0; 320dpi; 768x1184; Genymotion/Android; " \ 62 | "instagram_following_itsmoji; vbox86p; vbox86; en_US; 162439045) " 63 | 64 | 65 | class Instagram(BaseSocialApp): 66 | 67 | def __init__(self, phone_numbers): 68 | self.last_response = None 69 | self.current_session = None 70 | super().__init__(phone_numbers) 71 | 72 | def authenticate(self): 73 | pass 74 | 75 | def setup_session(self): 76 | if self.current_session is not None: 77 | return 78 | 79 | self.current_session = requests.Session() 80 | self.current_session.headers.update({'Connection': 'close', 81 | 'Accept': '*/*', 82 | 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 83 | 'Cookie2': '$Version=1', 84 | 'Accept-Language': 'en-US', 85 | 'User-Agent': generate_user_agent()}) 86 | 87 | def generate_data(self, phone_number_raw): 88 | # TODO look into whether there is a possibility of creating one request for lots of phone numbers 89 | data = {'phone_id': generate_uuid(True), 90 | 'guid': generate_uuid(True), 91 | 'device_id': generate_device_id(), 92 | 'login_attempt_count': '0', 93 | 'directly_sign_in': 'true', 94 | 'source': 'default', 95 | 'q': phone_number_raw, 96 | 'ig_sig_key_version': SIG_KEY_VERSION 97 | } 98 | return data 99 | 100 | def detect_single_number(self, phone_number): 101 | self.setup_session() 102 | data = self.generate_data(phone_number.phone_number) 103 | 104 | response = self.current_session.post(USERS_LOOKUP_URL, data=generate_signature(json.dumps(data))) 105 | self.last_response = response 106 | if response.ok: 107 | phone_number.set_app_state(self.get_name(), PhoneNumber.AppUsageEnum.USAGE) 108 | # Check if the user has whatsapp too 109 | if response.json()['can_wa_reset']: 110 | # TODO replace the hardcoded whatsapp string with a centralized strings file 111 | phone_number.set_app_state("Whatsapp", PhoneNumber.AppUsageEnum.USAGE) 112 | else: 113 | phone_number.set_app_state("Whatsapp", PhoneNumber.AppUsageEnum.NO_USAGE) 114 | elif response.status_code == 404: 115 | phone_number.set_app_state(self.get_name(), PhoneNumber.AppUsageEnum.NO_USAGE) 116 | else: 117 | # a generic bypass for generic failures for unexpected errors 118 | # for my case I faced `Too many requests` error 119 | phone_number.set_app_state(self.get_name(), response.reason) 120 | 121 | def detect_numbers(self, phone_numbers): 122 | for phone in phone_numbers: 123 | self.detect_single_number(phone) 124 | 125 | def process(self): 126 | # Making too many requests to Instagram will get the IP eventually blacklisted. You will need to invest in 127 | # private proxies If you do so, you should implement them in this function 128 | self.detect_numbers(self.phone_numbers_to_detect) 129 | 130 | def get_name(self): 131 | return "Instagram" 132 | --------------------------------------------------------------------------------