20 Commits
v1.0.2 ... main

Author SHA1 Message Date
Miguel Sozinho Ramalho
8164ef17b6 Merge pull request #51 from olmonotarianni/feature/username_search
feat: add username search functionality
2026-01-06 13:07:55 +00:00
olmo
2402db662d feat: add username search functionality 2025-10-04 18:12:06 +02:00
Miguel Sozinho Ramalho
66b6b81be8 Update README.md 2024-07-09 12:44:11 +01:00
Galen Reich
35e5714d6a Add onward journeys to README.md 2024-06-24 15:56:28 +01:00
Douglas
6ac1cac601 Added photo download flag (#38)
* Added photo download flag
* Added logging, docstrings, bash cmd in README
* Black formatting
* Use logger for stdout results
2024-06-24 15:48:47 +01:00
Miguel Sozinho Ramalho
80d5eb1723 Merge pull request #25 from jordan-gillard/improve-user-was-online
Improve user last online, introduce pytest to CI, and use asyncio
2024-04-15 12:39:59 +01:00
msramalho
49f657f90c ensures telegram client is disconnected and the event loop can close 2024-04-15 12:29:51 +01:00
Jordan Gillard
874b5ef902 Improve user last online & make program async 2024-04-06 17:16:43 -04:00
Jordan Gillard
22ab68a59b Add GitHub action for pytest and write one test 2024-04-06 16:23:16 -04:00
Jordan Gillard
cee577fd79 Add pytest dependency & generate req. files 2024-04-06 16:21:00 -04:00
Dmitry
2782ce8f82 Format code with Black
Makes code PEP8 compliant
2024-03-18 11:07:13 +00:00
lowaCase
3636963d1a return richer account information (#21)
* added more fields for user information
* Increment version to 1.1.0 (reflecting the new functionality)

---------

Co-authored-by: Galen Reich <54807169+GalenReich@users.noreply.github.com>
2024-03-05 15:09:33 +00:00
Jordan Gillard
ac84e34d38 Extend --help text to include info for acquiring Telegram API ID/hash (#20)
* Fix Poetry mismatch b/w pyproject.toml/poetry.lock

* docs: poetry run command to use pyproject script

* Update .gitignore

* Add show_default kwarg to --output option

* Add envvar kwarg to options and move load_dotenv()

* Add info on creating/storing api credentials

* Bump version

---------

Co-authored-by: Galen Reich <54807169+GalenReich@users.noreply.github.com>
2024-03-05 09:57:06 +00:00
omstaendlig
1284bd1ae7 Updated docstrings and added type hints (#19) 2024-02-21 17:17:43 +00:00
Galen Reich
b9380e001a Fix missing info (#17)
* Use the user object from user deletion instead of user import
* Bump version
2024-02-13 15:23:52 +00:00
Galen Reich
fe57ce1443 Match accounts by id instead of username (#15)
* Replace account matching by (optional) username with (mandatory) id field
* Use get instead of direct key retrieval
* Bump version
2024-02-12 15:03:12 +00:00
msramalho
4011e916c8 removes unused imports 2024-02-08 17:50:41 +00:00
msramalho
2a503856c2 fix poetry build default env file is empty 2024-02-08 15:08:35 +00:00
msramalho
d7f5415a4e fixes .env not working from CLI 2024-02-08 12:52:50 +00:00
msramalho
9ed496d690 remove whitespace in phone numbers 2024-01-30 22:37:00 +00:00
9 changed files with 816 additions and 87 deletions

29
.github/workflows/run-pytest.yml vendored Normal file
View 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
View File

@@ -1,4 +1,15 @@
.env
*.session
*.json
dist/
dist/
# JetBrains IDEs
.idea/
# Byte-compiled / optimized / DLL files
__pycache__/
**/*.py[cod]
# Images downloaded
*.jpg
*.jpeg

View File

@@ -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
[![PyPI - Version](https://img.shields.io/pypi/v/telegram-phone-number-checker)
@@ -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.
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
View File

@@ -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"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "telegram-phone-number-checker"
version = "1.0.2"
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
View 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

View File

@@ -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

View File

@@ -1,100 +1,462 @@
import os, json
from telethon.sync import TelegramClient, errors
from telethon.tl.types import InputPhoneContact
from telethon import functions
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
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_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.
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:
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})
# Create a contact
contact = types.InputPhoneContact(
client_id=0, phone=phone_number, first_name="", last_name=""
)
# Attempt to add the contact from the address book
contacts = await client(functions.contacts.ImportContactsRequest([contact]))
users = contacts.to_dict().get("users", [])
number_of_matches = len(users)
if number_of_matches == 0:
result.update(
{
"error": "No response, the phone number is not on Telegram or has blocked contact adding."
}
)
elif number_of_matches == 1:
# 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.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": """This phone number matched multiple Telegram accounts,
which is unexpected. Please contact the developer: contact-tech@bellingcat.com"""
}
)
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."})
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.
'''
if not phone_numbers or not len(phone_numbers):
phone_numbers = input('Enter the phone numbers to check, separated by commas: ')
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 = {}
phones = [p.strip() for p in phone_numbers.split(",")]
# 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: ")
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"""
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()

View 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