├── 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 |
--------------------------------------------------------------------------------