mirror of
https://github.com/bellingcat/telegram-phone-number-checker.git
synced 2026-06-08 03:18:30 +03:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8164ef17b6 | ||
|
|
2402db662d | ||
|
|
66b6b81be8 | ||
|
|
35e5714d6a | ||
|
|
6ac1cac601 | ||
|
|
80d5eb1723 | ||
|
|
49f657f90c | ||
|
|
874b5ef902 | ||
|
|
22ab68a59b | ||
|
|
cee577fd79 | ||
|
|
2782ce8f82 | ||
|
|
3636963d1a | ||
|
|
ac84e34d38 | ||
|
|
1284bd1ae7 | ||
|
|
b9380e001a |
29
.github/workflows/run-pytest.yml
vendored
Normal file
29
.github/workflows/run-pytest.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Run Pytest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,4 +1,15 @@
|
||||
.env
|
||||
*.session
|
||||
*.json
|
||||
dist/
|
||||
dist/
|
||||
|
||||
# JetBrains IDEs
|
||||
.idea/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
**/*.py[cod]
|
||||
|
||||
# Images downloaded
|
||||
*.jpg
|
||||
*.jpeg
|
||||
|
||||
62
README.md
62
README.md
@@ -1,7 +1,13 @@
|
||||
# telegram-phone-number-checker
|
||||
|
||||
<a href="https://www.bellingcat.com"><img alt="Bellingcat logo: Discover Bellingcat" src="https://img.shields.io/badge/Discover%20Bellingcat-%20?style=for-the-badge&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAA4AAAAYCAYAAADKx8xXAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TS0UqDnZQEcxQneyiIo6likWwUNoKrTqYXPoFTRqSFBdHwbXg4Mdi1cHFWVcHV0EQ%2FABxdnBSdJES%2F5cUWsR4cNyPd%2Fced%2B8AoVllqtkTA1TNMtKJuJjLr4rBVwQwhhBEDEvM1JOZxSw8x9c9fHy9i%2FIs73N%2Fjn6lYDLAJxLHmG5YxBvEs5uWznmfOMzKkkJ8Tjxp0AWJH7kuu%2FzGueSwwDPDRjY9TxwmFktdLHcxKxsq8QxxRFE1yhdyLiuctzir1Tpr35O%2FMFTQVjJcpzmKBJaQRIo6klFHBVVYiNKqkWIiTftxD%2F%2BI40%2BRSyZXBYwcC6hBheT4wf%2Fgd7dmcXrKTQrFgcCLbX%2BMA8FdoNWw7e9j226dAP5n4Err%2BGtNYO6T9EZHixwBA9vAxXVHk%2FeAyx1g6EmXDMmR%2FDSFYhF4P6NvygODt0Dfmttbex%2BnD0CWulq%2BAQ4OgYkSZa97vLu3u7d%2Fz7T7%2BwHEU3LHAa%2FQ6gAAAAZiS0dEAAAAAAAA%2BUO7fwAAAAlwSFlzAAAuIwAALiMBeKU%2FdgAAAAd0SU1FB%2BgFHwwiMH4odB4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAA50lEQVQ4y82SvWpCQRCFz25ERSJiCNqlUiS1b5AuEEiZIq1NOsGXCKms0wXSp9T6dskDiFikyiPc%2FrMZyf3FXSGQ0%2BzuzPl2ZoeVKgQ0gQ2wBVpVHlcDkjM5V%2FJ5nag6sJ%2FZX%2Bh%2FC7gEhqeAFKf7p1M9aB3b5oN1OomB7g1axUBPBr3GQHODHmOgqUF3MZAzKI2d4LWBV4H%2BMXDuJd1a7Cew1k7SwksaHC4LqNaw7aeX9GWHXkC1G1sTAS17Y3Kk2lnp4wNLiz0DrgLq8qt2MfmSSabAO%2FBBXp26dtrADPjOmN%2BAUdG7B3cE61l5hOZiAAAAAElFTkSuQmCC&logoColor=%23fff&color=%23000"></a><!--
|
||||
--><a href="https://discord.gg/bellingcat"><img alt="Discord logo: Join our community" src="https://img.shields.io/badge/Join%20our%20community-%20?style=for-the-badge&logo=discord&logoColor=%23fff&color=%235865F2"></a><!--
|
||||
--><a href="https://colab.research.google.com/github/bellingcat/open-source-research-notebooks/blob/main/notebooks/bellingcat/telegram-phone-number-checker.ipynb"><img alt="Colab icon: Try it on Colab" src="https://img.shields.io/badge/Try%20it%20on%20Colab-%20?style=for-the-badge&logo=googlecolab&logoColor=fff&logoSize=auto&color=e8710a"></a>
|
||||
|
||||
Python tool/script to check if phone numbers are connected to Telegram accounts. Retrieving username, name, and IDs where available.
|
||||
|
||||
> ⚠️ NB: We advise you **not to use your personal account** for automations as telegram may block it. A fresh account works best from residential IPs rather than known VPN IPs.
|
||||
|
||||
## Installation
|
||||
|
||||
[
|
||||
@@ -13,7 +19,7 @@ You can install this tool directly from the [official pypi release](https://pypi
|
||||
pip install telegram-phone-number-checker
|
||||
```
|
||||
|
||||
You can also install it and run it directly from github as a script.
|
||||
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
|
||||
@@ -35,6 +41,8 @@ PHONE_NUMBER=
|
||||
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
|
||||
|
||||
### Search by phone number
|
||||
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:
|
||||
@@ -43,6 +51,9 @@ See the examples below:
|
||||
# single phone number
|
||||
telegram-phone-number-checker --phone-numbers +1234567890
|
||||
|
||||
# single phone number, download profile photo
|
||||
telegram-phone-number-checker --phone-numbers +1234567890 --download-profile-photos
|
||||
|
||||
# multiple phone numbers
|
||||
telegram-phone-number-checker --phone-numbers +1234567890,+9876543210,+111111111
|
||||
|
||||
@@ -53,14 +64,32 @@ telegram-phone-number-checker
|
||||
telegram-phone-number-checker --api-id YOUR_API_KEY --api-hash YOUR_API_HASH --api-phone-number YOUR_PHONE_NUMBER --phone-numbers +1234567890
|
||||
```
|
||||
|
||||
### Search by Username
|
||||
You can also search by Telegram username(s):
|
||||
|
||||
```bash
|
||||
# single username
|
||||
telegram-phone-number-checker --usernames john
|
||||
|
||||
# multiple usernames
|
||||
telegram-phone-number-checker --usernames john,jane
|
||||
|
||||
# combine phone numbers and usernames
|
||||
telegram-phone-number-checker --phone-numbers +1234567890 --usernames johndoe
|
||||
|
||||
# with profile photo download
|
||||
telegram-phone-number-checker --usernames johndoe --download-profile-photos
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
For each phone number, you can expect the following possible responses:
|
||||
For each phone number or username, 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.
|
||||
4. For username searches: The username may not exist, or it may belong to a channel/group rather than a user account. In the latter case, an output such as '@group_name is a channel or supergroup ('Group'), not a user account. This tool is for searching user accounts only'
|
||||
5. Or: another error occurred.
|
||||
|
||||
|
||||
## Development
|
||||
@@ -77,6 +106,9 @@ 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
|
||||
# install poetry if you haven't already
|
||||
pip install poetry
|
||||
|
||||
# with poetry
|
||||
poetry install
|
||||
|
||||
@@ -87,8 +119,28 @@ 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
|
||||
poetry run telegram-phone-number-checker
|
||||
|
||||
# with pip installation
|
||||
python3 telegram_phone_number_checker/main.py
|
||||
```
|
||||
```
|
||||
|
||||
### Generating `requirements.txt` & `requirements-dev.txt`
|
||||
|
||||
Poetry is used to generate both of these files. `requirements.txt` contains only those dependencies necessary for
|
||||
running the CLI. `requirements-dev.txt` contains all dependencies including those used for running tests, linters, etc.
|
||||
|
||||
To generate `requirements.txt`:
|
||||
|
||||
```shell
|
||||
poetry export --output=requirements.txt --without-urls
|
||||
```
|
||||
|
||||
To generate `requirements-dev.txt`:
|
||||
|
||||
```shell
|
||||
poetry export --output=requirements-dev.txt --without-urls --with=dev
|
||||
```
|
||||
|
||||
💡 `--without-urls` is for users who install from their own private package repository
|
||||
instead of pypi.org
|
||||
|
||||
171
poetry.lock
generated
171
poetry.lock
generated
@@ -22,6 +22,52 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
|
||||
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 = "black"
|
||||
version = "24.3.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
@@ -72,6 +118,94 @@ files = [
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
|
||||
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
|
||||
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.4.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
|
||||
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pyaes"
|
||||
version = "1.6.1"
|
||||
@@ -93,6 +227,28 @@ files = [
|
||||
{file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.1.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
|
||||
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.4,<2.0"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
@@ -149,6 +305,17 @@ rsa = "*"
|
||||
[package.extras]
|
||||
cryptg = ["cryptg"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
@@ -162,5 +329,5 @@ files = [
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "1cf8b9ab78b1cf2860f38fc9e50e373b857e6d10ed6a397cb4f29cc556e14c95"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "e2ac4216301383f37ab71c523d371ed38b7c3a282b676277c8fb155219d2de42"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "telegram-phone-number-checker"
|
||||
version = "1.0.6"
|
||||
version = "1.2.1"
|
||||
description = "Check if phone numbers are connected to Telegram accounts."
|
||||
authors = ["Bellingcat"]
|
||||
license = "MIT"
|
||||
@@ -28,6 +28,16 @@ telethon = "1.33.1"
|
||||
typing-extensions = "4.9.0"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.1.1"
|
||||
black = "24.3.0"
|
||||
isort = "^5.13.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = [
|
||||
"."
|
||||
]
|
||||
|
||||
49
requirements-dev.txt
Normal file
49
requirements-dev.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
anyio==4.2.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \
|
||||
--hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f
|
||||
click==8.1.7 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32") \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
|
||||
--hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
|
||||
idna==3.6 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||
iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
|
||||
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
|
||||
packaging==24.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
|
||||
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
|
||||
pluggy==1.4.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \
|
||||
--hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be
|
||||
pyaes==1.6.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
pyasn1==0.5.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58 \
|
||||
--hash=sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c
|
||||
pytest==8.1.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7 \
|
||||
--hash=sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044
|
||||
python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
|
||||
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
|
||||
rsa==4.9 ; python_version >= "3.9" and python_version < "4" \
|
||||
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
|
||||
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
|
||||
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
|
||||
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
|
||||
telethon==1.33.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:377107d77fd95f8c2bd7fce77f9d78da84c1ff58023f59e8d06035a4548bd36b
|
||||
tomli==2.0.1 ; python_version >= "3.9" and python_version < "3.11" \
|
||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
|
||||
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
|
||||
typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
|
||||
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
|
||||
@@ -1,11 +1,34 @@
|
||||
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
|
||||
anyio==4.2.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \
|
||||
--hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f
|
||||
click==8.1.7 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
|
||||
--hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
|
||||
idna==3.6 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||
pyaes==1.6.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
pyasn1==0.5.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58 \
|
||||
--hash=sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c
|
||||
python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
|
||||
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
|
||||
rsa==4.9 ; python_version >= "3.9" and python_version < "4" \
|
||||
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
|
||||
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
|
||||
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
|
||||
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
|
||||
telethon==1.33.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:377107d77fd95f8c2bd7fce77f9d78da84c1ff58023f59e8d06035a4548bd36b
|
||||
typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
|
||||
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
|
||||
|
||||
@@ -1,105 +1,462 @@
|
||||
import os, json, re
|
||||
from telethon.sync import TelegramClient, errors, functions
|
||||
from telethon.tl.types import InputPhoneContact
|
||||
from dotenv import load_dotenv
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from getpass import getpass
|
||||
import click
|
||||
import logging
|
||||
|
||||
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.
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
from telethon.sync import TelegramClient, errors, functions
|
||||
from telethon.tl import types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def get_human_readable_user_status(status: types.TypeUserStatus):
|
||||
match status:
|
||||
case types.UserStatusOnline():
|
||||
return "Currently online"
|
||||
case types.UserStatusOffline():
|
||||
return status.was_online.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
case types.UserStatusRecently():
|
||||
return "Last seen recently"
|
||||
case types.UserStatusLastWeek():
|
||||
return "Last seen last week"
|
||||
case types.UserStatusLastMonth():
|
||||
return "Last seen last month"
|
||||
case _:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
async def get_names(
|
||||
client: TelegramClient, phone_number: str, download_profile_photos: bool = False
|
||||
) -> dict:
|
||||
"""Take in a phone number and returns the associated user information 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.
|
||||
---
|
||||
client, TelegramClient : Telegram client used to generate API call(s)
|
||||
phone_number, str : Phone number associated with a given Telegram account (including country code, example format '+11232223333')
|
||||
download_profile_photos, bool : Flag for whether to download a profile's associated account photo; defaults to False.
|
||||
"""
|
||||
result = {}
|
||||
print(f'Checking: {phone_number=} ...', end="", flush=True)
|
||||
logging.info(f"Checking: {phone_number=} ...")
|
||||
try:
|
||||
# Create a contact
|
||||
contact = InputPhoneContact(client_id = 0, phone = phone_number, first_name="", last_name="")
|
||||
contact = types.InputPhoneContact(
|
||||
client_id=0, phone=phone_number, first_name="", last_name=""
|
||||
)
|
||||
# Attempt to add the contact from the address book
|
||||
contacts = client(functions.contacts.ImportContactsRequest([contact]))
|
||||
contacts = await client(functions.contacts.ImportContactsRequest([contact]))
|
||||
|
||||
users = contacts.to_dict().get('users', [])
|
||||
users = contacts.to_dict().get("users", [])
|
||||
number_of_matches = len(users)
|
||||
|
||||
if number_of_matches == 0:
|
||||
result.update({"error": f'No response, the phone number is not on Telegram or has blocked contact adding.'})
|
||||
result.update(
|
||||
{
|
||||
"error": "No response, the phone number is not on Telegram or has blocked contact adding."
|
||||
}
|
||||
)
|
||||
elif number_of_matches == 1:
|
||||
user = users[0]
|
||||
# Attempt to remove the contact from the address book
|
||||
client(functions.contacts.DeleteContactsRequest(id=[user.get('id')]))
|
||||
# Attempt to remove the contact from the address book.
|
||||
# The response from DeleteContactsRequest contains more information than from ImportContactsRequest
|
||||
updates_response: types.Updates = await client(
|
||||
functions.contacts.DeleteContactsRequest(id=[users[0].get("id")])
|
||||
)
|
||||
user = updates_response.users[0]
|
||||
# getting more information about the user
|
||||
result.update({
|
||||
"id": user.get('id'),
|
||||
"username": user.get('username'),
|
||||
"first_name": user.get('first_name'),
|
||||
"last_name": user.get('last_name')
|
||||
})
|
||||
result.update(
|
||||
{
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"usernames": [u.username for u in (user.usernames or [])] if user.usernames else None,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"fake": user.fake,
|
||||
"verified": user.verified,
|
||||
"premium": user.premium,
|
||||
"mutual_contact": user.mutual_contact,
|
||||
"bot": user.bot,
|
||||
"bot_chat_history": user.bot_chat_history,
|
||||
"restricted": user.restricted,
|
||||
"restriction_reason": user.restriction_reason,
|
||||
"user_was_online": get_human_readable_user_status(user.status),
|
||||
"phone": user.phone,
|
||||
}
|
||||
)
|
||||
if download_profile_photos is True:
|
||||
try:
|
||||
photo_output_path = Path("{}_{}_photo.jpeg".format(user.id, phone_number))
|
||||
logging.info(
|
||||
"Attempting to download profile photo for %s (%s)",
|
||||
str(user.id),
|
||||
str(phone_number),
|
||||
)
|
||||
photo = await client.download_profile_photo(
|
||||
user, file=photo_output_path, download_big=True
|
||||
)
|
||||
if photo is not None:
|
||||
logging.info("Downloaded photo at '%s'", photo)
|
||||
else:
|
||||
logging.info(
|
||||
"No photo found for %s (%s)", str(user.id), str(phone_number)
|
||||
)
|
||||
# We don't want the script to fail if download I/O fails locally, file format error, etc.
|
||||
# TODO : Add handling for ind. exceptions
|
||||
except Exception as e:
|
||||
logging.exception(
|
||||
"---\nUnable to download profile photo for %s. Exception provided below.\n---\n%s\n---\n",
|
||||
str(phone_number),
|
||||
str(e),
|
||||
)
|
||||
|
||||
else:
|
||||
result.update({"error": f'This phone number matched multiple Telegram accounts, which is unexpected. Please contact the developer: contact-tech@bellingcat.com'})
|
||||
result.update(
|
||||
{
|
||||
"error": """This phone number matched multiple Telegram accounts,
|
||||
which is unexpected. Please contact the developer: contact-tech@bellingcat.com"""
|
||||
}
|
||||
)
|
||||
|
||||
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."})
|
||||
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.")
|
||||
logging.info("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.
|
||||
'''
|
||||
async def get_user_by_username(
|
||||
client: TelegramClient, username: str, download_profile_photos: bool = False
|
||||
) -> dict:
|
||||
"""Take in a username and returns the associated user information if the user exists.
|
||||
|
||||
Uses Telegram's get_entity to look up users by their username.
|
||||
---
|
||||
client, TelegramClient : Telegram client used to generate API call(s)
|
||||
username, str : Username to search (with or without @ symbol, e.g. 'username' or '@username')
|
||||
download_profile_photos, bool : Flag for whether to download a profile's associated account photo; defaults to False.
|
||||
"""
|
||||
result = {}
|
||||
# Remove @ symbol if present
|
||||
clean_username = username.lstrip('@')
|
||||
logging.info(f"Checking username: @{clean_username} ...")
|
||||
|
||||
try:
|
||||
# Get entity by username
|
||||
entity = await client.get_entity(clean_username)
|
||||
|
||||
# Check if it's a User (not a Channel or Chat)
|
||||
if isinstance(entity, types.User):
|
||||
result.update(
|
||||
{
|
||||
"id": entity.id,
|
||||
"username": entity.username,
|
||||
"usernames": [u.username for u in (entity.usernames or [])] if entity.usernames else None,
|
||||
"first_name": entity.first_name,
|
||||
"last_name": entity.last_name,
|
||||
"fake": entity.fake,
|
||||
"verified": entity.verified,
|
||||
"premium": entity.premium,
|
||||
"mutual_contact": entity.mutual_contact,
|
||||
"bot": entity.bot,
|
||||
"bot_chat_history": entity.bot_chat_history,
|
||||
"restricted": entity.restricted,
|
||||
"restriction_reason": entity.restriction_reason,
|
||||
"user_was_online": get_human_readable_user_status(entity.status),
|
||||
"phone": entity.phone,
|
||||
}
|
||||
)
|
||||
|
||||
if download_profile_photos is True:
|
||||
try:
|
||||
photo_output_path = Path(f"{entity.id}_{clean_username}_photo.jpeg")
|
||||
logging.info(
|
||||
"Attempting to download profile photo for @%s (%s)",
|
||||
clean_username,
|
||||
str(entity.id),
|
||||
)
|
||||
photo = await client.download_profile_photo(
|
||||
entity, file=photo_output_path, download_big=True
|
||||
)
|
||||
if photo is not None:
|
||||
logging.info("Downloaded photo at '%s'", photo)
|
||||
else:
|
||||
logging.info(
|
||||
"No photo found for @%s (%s)", clean_username, str(entity.id)
|
||||
)
|
||||
except Exception as e:
|
||||
logging.exception(
|
||||
"---\nUnable to download profile photo for @%s. Exception provided below.\n---\n%s\n---\n",
|
||||
clean_username,
|
||||
str(e),
|
||||
)
|
||||
elif isinstance(entity, types.Channel):
|
||||
result.update(
|
||||
{
|
||||
"error": f"@{clean_username} is a channel or supergroup ('{entity.title}'), not a user account. This tool is for searching user accounts only."
|
||||
}
|
||||
)
|
||||
elif isinstance(entity, types.Chat):
|
||||
result.update(
|
||||
{
|
||||
"error": f"@{clean_username} is a group chat ('{entity.title}'), not a user account. This tool is for searching user accounts only."
|
||||
}
|
||||
)
|
||||
else:
|
||||
result.update(
|
||||
{
|
||||
"error": f"@{clean_username} returned an unexpected entity type: {type(entity).__name__}"
|
||||
}
|
||||
)
|
||||
|
||||
except errors.UsernameNotOccupiedError:
|
||||
result.update({"error": f"Username @{clean_username} does not exist on Telegram."})
|
||||
except errors.UsernameInvalidError:
|
||||
result.update({"error": f"Username @{clean_username} is invalid."})
|
||||
except ValueError as e:
|
||||
result.update({"error": f"Could not find username @{clean_username}: {e}"})
|
||||
except Exception as e:
|
||||
result.update({"error": f"Unexpected error while searching for @{clean_username}: {e}."})
|
||||
raise
|
||||
|
||||
logging.info("Done.")
|
||||
return result
|
||||
|
||||
|
||||
async def validate_users(
|
||||
client: TelegramClient, phone_numbers: str, download_profile_photos: bool
|
||||
) -> dict:
|
||||
"""
|
||||
Take in a string of comma separated phone numbers and try to get the user information associated with each phone number.
|
||||
"""
|
||||
if not phone_numbers or not len(phone_numbers):
|
||||
phone_numbers = input('Enter the phone numbers to check, separated by commas: ')
|
||||
phone_numbers = input("Enter the phone numbers to check, separated by commas: ")
|
||||
result = {}
|
||||
phones = [re.sub(r"\s+", "", p, flags=re.UNICODE) for p in phone_numbers.split(",")]
|
||||
try:
|
||||
for phone in phones:
|
||||
if phone not in result:
|
||||
result[phone] = get_names(client, phone)
|
||||
result[phone] = await get_names(client, phone, download_profile_photos)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.error(e)
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def login(api_id, api_hash, phone_number):
|
||||
async def validate_usernames(
|
||||
client: TelegramClient, usernames: str, download_profile_photos: bool
|
||||
) -> dict:
|
||||
"""
|
||||
Take in a string of comma separated usernames and try to get the user information associated with each username.
|
||||
"""
|
||||
if not usernames or not len(usernames):
|
||||
usernames = input("Enter the usernames to check, separated by commas: ")
|
||||
result = {}
|
||||
username_list = [re.sub(r"\s+", "", u, flags=re.UNICODE) for u in usernames.split(",")]
|
||||
try:
|
||||
for username in username_list:
|
||||
if username not in result:
|
||||
result[username] = await get_user_by_username(client, username, download_profile_photos)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
async def login(
|
||||
api_id: str | None, api_hash: str | None, phone_number: str | None
|
||||
) -> TelegramClient:
|
||||
"""Create a telethon session or reuse existing one"""
|
||||
print('Logging in...', end="", flush=True)
|
||||
API_ID = api_id or os.getenv('API_ID') or input('Enter your API ID: ')
|
||||
API_HASH = api_hash or os.getenv('API_HASH') or input('Enter your API HASH: ')
|
||||
PHONE_NUMBER = phone_number or os.getenv('PHONE_NUMBER') or input('Enter your phone number: ')
|
||||
logging.info("Logging in...")
|
||||
API_ID = api_id or os.getenv("API_ID") or input("Enter your API ID: ")
|
||||
API_HASH = api_hash or os.getenv("API_HASH") or input("Enter your API HASH: ")
|
||||
PHONE_NUMBER = (
|
||||
phone_number or os.getenv("PHONE_NUMBER") or input("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)
|
||||
await client.connect()
|
||||
if not await client.is_user_authorized():
|
||||
await client.send_code_request(PHONE_NUMBER)
|
||||
try:
|
||||
client.sign_in(PHONE_NUMBER, input('Enter the code (sent on telegram): '))
|
||||
await 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.")
|
||||
pw = getpass(
|
||||
"Two-Step Verification enabled. Please enter your account password: "
|
||||
)
|
||||
await client.sign_in(password=pw)
|
||||
logging.info("Done.")
|
||||
return client
|
||||
|
||||
def show_results(output, res):
|
||||
print(json.dumps(res, indent=4))
|
||||
with open(output, 'w') as f:
|
||||
|
||||
def show_results(output: str, res: dict) -> None:
|
||||
logging.info(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"""
|
||||
load_dotenv(".env")
|
||||
client = login(api_id, api_hash, api_phone_number)
|
||||
res = validate_users(client, phone_numbers)
|
||||
show_results(output, res)
|
||||
logging.info(f"Results saved to {output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@click.command(
|
||||
epilog="Check out the docs at github.com/bellingcat/telegram-phone-number-checker for more information."
|
||||
)
|
||||
@click.option(
|
||||
"--phone-numbers",
|
||||
"-p",
|
||||
help="List of phone numbers to check, separated by commas",
|
||||
type=str,
|
||||
)
|
||||
@click.option(
|
||||
"--usernames",
|
||||
"-u",
|
||||
help="List of usernames to check, separated by commas (e.g. 'username' or '@username')",
|
||||
type=str,
|
||||
)
|
||||
@click.option(
|
||||
"--api-id",
|
||||
help="Your Telegram app api_id",
|
||||
type=str,
|
||||
prompt="Enter your Telegram App app_id",
|
||||
envvar="API_ID",
|
||||
show_envvar=True,
|
||||
)
|
||||
@click.option(
|
||||
"--api-hash",
|
||||
help="Your Telegram app api_hash",
|
||||
type=str,
|
||||
prompt="Enter your Telegram App api_hash",
|
||||
hide_input=True,
|
||||
envvar="API_HASH",
|
||||
show_envvar=True,
|
||||
)
|
||||
@click.option(
|
||||
"--api-phone-number",
|
||||
help="Your phone number",
|
||||
type=str,
|
||||
prompt="Enter the number associated with your Telegram account",
|
||||
envvar="PHONE_NUMBER",
|
||||
show_envvar=True,
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
help="Filename to store results",
|
||||
default="results.json",
|
||||
show_default=True,
|
||||
type=str,
|
||||
)
|
||||
@click.option(
|
||||
"--download-profile-photos",
|
||||
help="Download the user profile photo associated with requested Telegram account",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
show_default=True,
|
||||
)
|
||||
def main_entrypoint(
|
||||
phone_numbers: str,
|
||||
usernames: str,
|
||||
api_id: str,
|
||||
api_hash: str,
|
||||
api_phone_number: str,
|
||||
output: str,
|
||||
download_profile_photos: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Check to see if one or more phone numbers or usernames belong to a valid Telegram account.
|
||||
|
||||
\b
|
||||
Prerequisites:
|
||||
1. A Telegram account with an active phone number
|
||||
2. A Telegram App api_id and App api_hash, which you can get by creating
|
||||
a Telegram App @ https://my.telegram.org/apps
|
||||
|
||||
\b
|
||||
Note:
|
||||
If you do not want to enter the API ID, API hash, or phone number associated with
|
||||
your Telegram account on the command line, you can store these values in a `.env`
|
||||
file located within the same directory you run this command from.
|
||||
|
||||
\b
|
||||
// .env file example:
|
||||
API_ID=12345678
|
||||
API_HASH=1234abcd5678efgh1234abcd567
|
||||
PHONE_NUMBER=+15555555555
|
||||
|
||||
See the official Telegram docs at https://core.telegram.org/api/obtaining_api_id
|
||||
for more information on obtaining an API ID.
|
||||
|
||||
\b
|
||||
Recommendations:
|
||||
Telegram recommends entering phone numbers in international format
|
||||
+(country code)(city or carrier code)(your number)
|
||||
i.e. +491234567891
|
||||
|
||||
"""
|
||||
asyncio.run(
|
||||
run_program(
|
||||
phone_numbers,
|
||||
usernames,
|
||||
api_id,
|
||||
api_hash,
|
||||
api_phone_number,
|
||||
output,
|
||||
download_profile_photos,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def run_program(
|
||||
phone_numbers: str,
|
||||
usernames: str,
|
||||
api_id: str,
|
||||
api_hash: str,
|
||||
api_phone_number: str,
|
||||
output: str,
|
||||
download_profile_photos: bool = False,
|
||||
):
|
||||
"""
|
||||
Get all args passed from Click parser, pass them into the script.
|
||||
"""
|
||||
client = await login(api_id, api_hash, api_phone_number)
|
||||
|
||||
results = {}
|
||||
|
||||
# Search by phone numbers if provided
|
||||
if phone_numbers:
|
||||
phone_results = await validate_users(client, phone_numbers, download_profile_photos)
|
||||
results.update(phone_results)
|
||||
|
||||
# Search by usernames if provided
|
||||
if usernames:
|
||||
username_results = await validate_usernames(client, usernames, download_profile_photos)
|
||||
results.update(username_results)
|
||||
|
||||
# If neither provided, prompt for input
|
||||
if not phone_numbers and not usernames:
|
||||
choice = input("Search by (p)hone numbers or (u)sernames? [p/u]: ").lower()
|
||||
if choice == 'u':
|
||||
usernames = input("Enter the usernames to check, separated by commas: ")
|
||||
username_results = await validate_usernames(client, usernames, download_profile_photos)
|
||||
results.update(username_results)
|
||||
else:
|
||||
phone_numbers = input("Enter the phone numbers to check, separated by commas: ")
|
||||
phone_results = await validate_users(client, phone_numbers, download_profile_photos)
|
||||
results.update(phone_results)
|
||||
|
||||
show_results(output, results)
|
||||
client.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_entrypoint()
|
||||
26
tests/test_user_was_online.py
Normal file
26
tests/test_user_was_online.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from telethon.tl import types
|
||||
|
||||
from telegram_phone_number_checker import main
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_status, readable_string",
|
||||
[
|
||||
(types.UserStatusEmpty(), "Unknown"),
|
||||
(types.UserStatusOnline(expires=None), "Currently online"),
|
||||
(
|
||||
types.UserStatusOffline(
|
||||
was_online=datetime(2024, 4, 6, 12, 30, 1, tzinfo=timezone.utc)
|
||||
),
|
||||
"2024-04-06 12:30:01 UTC",
|
||||
),
|
||||
(types.UserStatusRecently(), "Last seen recently"),
|
||||
(types.UserStatusLastWeek(), "Last seen last week"),
|
||||
(types.UserStatusLastMonth(), "Last seen last month"),
|
||||
],
|
||||
)
|
||||
def test_should_return_correct_status_string(user_status, readable_string):
|
||||
assert main.get_human_readable_user_status(user_status) == readable_string
|
||||
Reference in New Issue
Block a user