Merge pull request #12 from bellingcat/package-it

refactoring and moving to poetry
This commit is contained in:
Miguel Sozinho Ramalho
2024-01-25 16:51:41 +00:00
committed by GitHub
10 changed files with 402 additions and 165 deletions

18
.github/workflows/pypi-publish.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Upload Python Package
on:
release:
types: [published]
jobs:
release:
name: Release
runs-on: ubuntu-latest
needs: [checks]
steps:
- uses: actions/checkout@v4
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v1.17
with:
pypi_token: ${{ secrets.PYPI_TOKEN }}
python_version: 3.9

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.env
*.session
*.json
dist/

13
Pipfile
View File

@@ -1,13 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
telethon = "*"
python-dotenv = "*"
[dev-packages]
[requires]
python_version = "3.9"

69
Pipfile.lock generated
View File

@@ -1,69 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "0c8df074fc457e527734bfb860a87923e5f8f806319557e83d22cbfb7ceea9c4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"pyaes": {
"hashes": [
"sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"
],
"version": "==1.6.1"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"python-dotenv": {
"hashes": [
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544",
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"
],
"index": "pypi",
"version": "==0.17.1"
},
"rsa": {
"hashes": [
"sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2",
"sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"
],
"markers": "python_version >= '3.5' and python_version < '4'",
"version": "==4.7.2"
},
"telethon": {
"hashes": [
"sha256:993c837ef745addf972a27d7bfba0ce518a2863d1a50e0737255b764d23e0ef2",
"sha256:df643fc988708ad16d16de834ffa12ad4bfa3f956473d835c8158e2283b885ea"
],
"index": "pypi",
"version": "==1.21.1"
}
},
"develop": {}
}

View File

@@ -1,33 +1,93 @@
# telegram-phone-number-checker
This script lets you check whether a specific phone number is connected to a Telegram account.
Python tool/script toc heck if phone numbers are connected to Telegram accounts.
## Installation
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/telegram-phone-number-checker)](https://pypi.org/project/telegram-phone-number-checker/)
You can install this tool directly from the [official pypi release](https://pypi.org/project/telegram-phone-number-checker/).
```bash
pip install telegram-phone-number-checker
```
You can also install it and run it directly from github as a script.
```bash
git clone https://github.com/bellingcat/telegram-phone-number-checker
cd telegram-phone-number-checker
pip install -r requirements.txt
python telegram-phone-number-checker/main.py
```
## Requirements
To run it, you need:
1. A Telegram account with an active phone number;
2. Telegram 'API_ID' and 'API_HASH', which you can get by creating a developers account using this link: https://my.telegram.org/. Place these values in a .env file, along with the phone number of your Telegram account:
2. Telegram `API_ID` and `API_HASH`, which you can get by creating a developers account at https://my.telegram.org/. Place these values in a `.env` file, along with the phone number of your Telegram account:
```
API_ID=
API_HASH=
PHONE_NUMBER=
```
## Installing Dependencies
This project uses [pipenv](https://pipenv.pypa.io/en/latest/#install-pipenv-today) to manage dependencies. Install pipenv on your machine, and then this project's dependencies can be installed like so:
```sh
pipenv install
```
If you don't create this file, you can also provide these 3 values when calling the tool, or even be prompted for them interactively.
## Usage
```sh
pipenv run python telegram-phone-validation.py
The tool accepts a comma-separated list of phone numbers to check, you can pass this when you call the tool, or interactively.
See the examples below:
```bash
# single phone number
telegram-phone-number-checker --phone-numbers +1234567890
# multiple phone numbers
telegram-phone-number-checker --phone-numbers +1234567890,+9876543210,+111111111
# interactive version, you will be prompted for the phone-numbers
telegram-phone-number-checker
# overwrite the telegram API keys in .env (or if no .env is found)
telegram-phone-number-checker --api-id YOUR_API_KEY --api-hash YOUR_API_HASH --api-phone-number YOUR_PHONE_NUMBER --phone-numbers +1234567890
```
You can expect the following possible responses:
The result will be written to the console but also written as JSON to a `results.json` file, you can write it to another file by adding `--output your_filename.json` to the command.
1. If available, you will receive the Telegram Username that is connected with this number.
2. 'Response detected, but no user name returned by the API for the number: {phone_number}'. This means that it looks like the number was used to create a Telegram account but the user did not choose a Telegram Username. It is optional to create a Username on Telegram.
3. 'ERROR: there was no response for the phone number: {phone_number}': There can be several reasons for this response. Either the phone number has not been used to create a Telegram account. Or: The phone number is connected to a Telegram account but the user has restricted the option to find him/her via the phone number. Or: another error occurred.
For each phone number, you can expect the following possible responses:
1. If available, you will receive the Telegram Username,Name, and ID that are connected with this number.
2. 'no username detected'. This means that it looks like the number was used to create a Telegram account but the user did not choose a Telegram Username. It is optional to create a Username on Telegram.
3. 'ERROR: no response, the user does not exist or has blocked contact adding.': There can be several reasons for this response. Either the phone number has not been used to create a Telegram account. Or: The phone number is connected to a Telegram account but the user has restricted the option to find him/her via the phone number.
4. Or: another error occurred.
## Development
This section describes how to install the project in order to run it locally, for example if you want to build new features.
```bash
# clone the code
git clone https://github.com/bellingcat/telegram-phone-number-checker
# move into the project's folder
cd telegram-phone-number-checker
```
This project uses [poetry](https://python-poetry.org/) to manage dependencies. You can install dependencies via poetry, or use the up-to-date [requirements.txt](requirements.txt) file.
```bash
# with poetry
poetry install
# with pip
pip install -r requirements.txt
```
You can then run it with any of these:
```bash
# with poetry
poetry run python3 telegram_phone_number_checker/main.py
# with pip installation
python3 telegram_phone_number_checker/main.py
```

166
poetry.lock generated Normal file
View File

@@ -0,0 +1,166 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "anyio"
version = "4.2.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "pyaes"
version = "1.6.1"
description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
optional = false
python-versions = "*"
files = [
{file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"},
]
[[package]]
name = "pyasn1"
version = "0.5.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"},
{file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"},
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "telethon"
version = "1.33.1"
description = "Full-featured Telegram client library for Python 3"
optional = false
python-versions = ">=3.5"
files = [
{file = "Telethon-1.33.1.tar.gz", hash = "sha256:377107d77fd95f8c2bd7fce77f9d78da84c1ff58023f59e8d06035a4548bd36b"},
]
[package.dependencies]
pyaes = "*"
rsa = "*"
[package.extras]
cryptg = ["cryptg"]
[[package]]
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "1cf8b9ab78b1cf2860f38fc9e50e373b857e6d10ed6a397cb4f29cc556e14c95"

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[tool.poetry]
name = "telegram-phone-number-checker"
version = "1.0.0"
description = "Check if phone numbers are connected to Telegram accounts."
authors = ["Bellingcat"]
license = "MIT"
readme = "README.md"
[tool.poetry.scripts]
telegram-phone-number-checker = "telegram_phone_number_checker.main:main_entrypoint"
[tool.poetry.dependencies]
python = "^3.9"
anyio = "4.2.0"
click = "8.1.7"
exceptiongroup = "1.2.0"
idna = "3.6"
pyaes = "1.6.1"
pyasn1 = "0.5.1"
python-dotenv = "1.0.1"
rsa = "4.9"
sniffio = "1.3.0"
telethon = "1.33.1"
typing-extensions = "4.9.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

11
requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
anyio==4.2.0
click==8.1.7
exceptiongroup==1.2.0
idna==3.6
pyaes==1.6.1
pyasn1==0.5.1
python-dotenv==1.0.1
rsa==4.9
sniffio==1.3.0
Telethon==1.33.1
typing_extensions==4.9.0

View File

@@ -1,67 +0,0 @@
#!/usr/local/bin/python3
from telethon import TelegramClient, errors, events, sync
from telethon.tl.types import InputPhoneContact
from telethon import functions, types
from dotenv import load_dotenv
import argparse
import os
from getpass import getpass
load_dotenv()
result = {}
API_ID = os.getenv('API_ID')
API_HASH = os.getenv('API_HASH')
PHONE_NUMBER = os.getenv('PHONE_NUMBER')
def get_names(phone_number):
try:
contact = InputPhoneContact(client_id = 0, phone = phone_number, first_name="", last_name="")
contacts = client(functions.contacts.ImportContactsRequest([contact]))
username = contacts.to_dict()['users'][0]['username']
if not username:
print("*"*5 + f' Response detected, but no user name returned by the API for the number: {phone_number} ' + "*"*5)
del_usr = client(functions.contacts.DeleteContactsRequest(id=[username]))
return
else:
del_usr = client(functions.contacts.DeleteContactsRequest(id=[username]))
return username
except IndexError as e:
return f'ERROR: there was no response for the phone number: {phone_number}'
except TypeError as e:
return f"TypeError: {e}. --> The error might have occured due to the inability to delete the {phone_number} from the contact list."
except:
raise
def user_validator():
'''
The function uses the get_api_response function to first check if the user exists and if it does, then it returns the first user name and the last user name.
'''
input_phones = input("Phone numbers: ")
phones = input_phones.split()
try:
for phone in phones:
api_res = get_names(phone)
result[phone] = api_res
except:
raise
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Check to see if a phone number is a valid Telegram account')
args = parser.parse_args()
client = TelegramClient(PHONE_NUMBER, API_ID, API_HASH)
client.connect()
if not client.is_user_authorized():
client.send_code_request(PHONE_NUMBER)
try:
client.sign_in(PHONE_NUMBER, input('Enter the code (sent on telegram): '))
except errors.SessionPasswordNeededError:
pw = getpass('Two-Step Verification enabled. Please enter your account password: ')
client.sign_in(password=pw)
user_validator()
print(result)

View File

@@ -0,0 +1,100 @@
import os, json
from telethon.sync import TelegramClient, errors
from telethon.tl.types import InputPhoneContact
from telethon import functions
from dotenv import load_dotenv
from getpass import getpass
import click
load_dotenv()
def get_names(client, phone_number):
"""
This function takes in a phone number and returns the username first name and the last name of the user if the user exists. It does so by first adding the user's phones to the contact list, retrieving the information, and then deleting the user from the contact list.
"""
result = {}
print(f'Checking: {phone_number=} ...', end="", flush=True)
try:
contact = InputPhoneContact(client_id = 0, phone = phone_number, first_name="", last_name="")
contacts = client(functions.contacts.ImportContactsRequest([contact]))
username = contacts.to_dict()['users'][0]['username']
if not username:
result.update({"error": f'ERROR: no username detected'})
del_usr = client(functions.contacts.DeleteContactsRequest(id=[username]))
else:
result.update({"username": username})
del_usr = client(functions.contacts.DeleteContactsRequest(id=[username]))
# getting more information about the user
id = del_usr.to_dict()['users'][0]['id']
first_name = del_usr.to_dict()['users'][0]['first_name']
last_name = del_usr.to_dict()['users'][0]['last_name']
result.update({"first_name": first_name, "last_name": last_name, "id": id})
except IndexError as e:
result.update({"error": f'ERROR: no response, the user does not exist or has blocked contact adding.'})
except TypeError as e:
result.update({"error": f"TypeError: {e}. --> The error might have occurred due to the inability to delete the {phone_number=} from the contact list."})
except Exception as e:
result.update({"error": f"Unexpected error: {e}."})
raise
print("Done.")
return result
def validate_users(client, phone_numbers):
'''
The function uses the get_api_response function to first check if the user exists and if it does, then it returns the first user name and the last user name.
'''
if not phone_numbers or not len(phone_numbers):
phone_numbers = click.prompt('Enter the phone numbers to check, separated by commas')
result = {}
phones = [p.strip() for p in phone_numbers.split(",")]
try:
for phone in phones:
if phone not in result:
result[phone] = get_names(client, phone)
except Exception as e:
print(e)
raise
return result
def login(api_id, api_hash, phone_number):
"""Create a telethon session or reuse existing one"""
print('Logging in...', end="", flush=True)
API_ID = api_id or os.getenv('API_ID') or click.prompt('Enter your API ID')
API_HASH = api_hash or os.getenv('API_HASH') or click.prompt('Enter your API HASH')
PHONE_NUMBER = phone_number or os.getenv('PHONE_NUMBER') or click.prompt('Enter your phone number')
client = TelegramClient(PHONE_NUMBER, API_ID, API_HASH)
client.connect()
if not client.is_user_authorized():
client.send_code_request(PHONE_NUMBER)
try:
client.sign_in(PHONE_NUMBER, input('Enter the code (sent on telegram): '))
except errors.SessionPasswordNeededError:
pw = getpass('Two-Step Verification enabled. Please enter your account password: ')
client.sign_in(password=pw)
print("Done.")
return client
def show_results(output, res):
print(json.dumps(res, indent=4))
with open(output, 'w') as f:
json.dump(res, f, indent=4)
print(f"Results saved to {output}")
@click.command()
@click.option('--phone-numbers', '-p', help='List of phone numbers to check, separated by commas', type=str)
@click.option('--api-id', help='Your API_ID', type=str)
@click.option('--api-hash', help='Your API_HASH', type=str)
@click.option('--api-phone-number', help='Your phone_number', type=str)
@click.option('--output', help='results filename, default to results.json', default="results.json", type=str)
def main_entrypoint(phone_numbers, api_id, api_hash, api_phone_number, output):
"""Check to see if one or more phone numbers belong to a valid Telegram account"""
client = login(api_id, api_hash, api_phone_number)
res = validate_users(client, phone_numbers)
show_results(output, res)
if __name__ == '__main__':
main_entrypoint()