mirror of
https://github.com/bellingcat/telegram-phone-number-checker.git
synced 2026-06-07 19:08:31 +03:00
Merge pull request #51 from olmonotarianni/feature/username_search
feat: add username search functionality
This commit is contained in:
24
README.md
24
README.md
@@ -41,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.
|
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
|
## 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.
|
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:
|
See the examples below:
|
||||||
@@ -62,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
|
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.
|
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.
|
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.
|
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
|
## Development
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async def get_names(
|
|||||||
{
|
{
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"usernames": user.usernames,
|
"usernames": [u.username for u in (user.usernames or [])] if user.usernames else None,
|
||||||
"first_name": user.first_name,
|
"first_name": user.first_name,
|
||||||
"last_name": user.last_name,
|
"last_name": user.last_name,
|
||||||
"fake": user.fake,
|
"fake": user.fake,
|
||||||
@@ -137,6 +137,104 @@ async def get_names(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
async def validate_users(
|
||||||
client: TelegramClient, phone_numbers: str, download_profile_photos: bool
|
client: TelegramClient, phone_numbers: str, download_profile_photos: bool
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -157,6 +255,26 @@ async def validate_users(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
async def login(
|
||||||
api_id: str | None, api_hash: str | None, phone_number: str | None
|
api_id: str | None, api_hash: str | None, phone_number: str | None
|
||||||
) -> TelegramClient:
|
) -> TelegramClient:
|
||||||
@@ -200,6 +318,12 @@ def show_results(output: str, res: dict) -> None:
|
|||||||
help="List of phone numbers to check, separated by commas",
|
help="List of phone numbers to check, separated by commas",
|
||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--usernames",
|
||||||
|
"-u",
|
||||||
|
help="List of usernames to check, separated by commas (e.g. 'username' or '@username')",
|
||||||
|
type=str,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--api-id",
|
"--api-id",
|
||||||
help="Your Telegram app api_id",
|
help="Your Telegram app api_id",
|
||||||
@@ -241,6 +365,7 @@ def show_results(output: str, res: dict) -> None:
|
|||||||
)
|
)
|
||||||
def main_entrypoint(
|
def main_entrypoint(
|
||||||
phone_numbers: str,
|
phone_numbers: str,
|
||||||
|
usernames: str,
|
||||||
api_id: str,
|
api_id: str,
|
||||||
api_hash: str,
|
api_hash: str,
|
||||||
api_phone_number: str,
|
api_phone_number: str,
|
||||||
@@ -248,7 +373,7 @@ def main_entrypoint(
|
|||||||
download_profile_photos: bool,
|
download_profile_photos: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Check to see if one or more phone numbers belong to a valid Telegram account.
|
Check to see if one or more phone numbers or usernames belong to a valid Telegram account.
|
||||||
|
|
||||||
\b
|
\b
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
@@ -281,6 +406,7 @@ def main_entrypoint(
|
|||||||
asyncio.run(
|
asyncio.run(
|
||||||
run_program(
|
run_program(
|
||||||
phone_numbers,
|
phone_numbers,
|
||||||
|
usernames,
|
||||||
api_id,
|
api_id,
|
||||||
api_hash,
|
api_hash,
|
||||||
api_phone_number,
|
api_phone_number,
|
||||||
@@ -292,6 +418,7 @@ def main_entrypoint(
|
|||||||
|
|
||||||
async def run_program(
|
async def run_program(
|
||||||
phone_numbers: str,
|
phone_numbers: str,
|
||||||
|
usernames: str,
|
||||||
api_id: str,
|
api_id: str,
|
||||||
api_hash: str,
|
api_hash: str,
|
||||||
api_phone_number: str,
|
api_phone_number: str,
|
||||||
@@ -302,10 +429,34 @@ async def run_program(
|
|||||||
Get all args passed from Click parser, pass them into the script.
|
Get all args passed from Click parser, pass them into the script.
|
||||||
"""
|
"""
|
||||||
client = await login(api_id, api_hash, api_phone_number)
|
client = await login(api_id, api_hash, api_phone_number)
|
||||||
res = await validate_users(client, phone_numbers, download_profile_photos)
|
|
||||||
show_results(output, res)
|
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()
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main_entrypoint()
|
main_entrypoint()
|
||||||
Reference in New Issue
Block a user