diff --git a/poetry.lock b/poetry.lock index 753e99d..f30bda7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" @@ -83,6 +129,31 @@ files = [ {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" @@ -94,6 +165,32 @@ files = [ {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" @@ -233,4 +330,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "17abe59706d0c666b46a35de6e4ea1dac59015cb0976edf4479d29d839546220" +content-hash = "e2ac4216301383f37ab71c523d371ed38b7c3a282b676277c8fb155219d2de42" diff --git a/pyproject.toml b/pyproject.toml index 83da0df..7331083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,14 @@ 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 = [ + "." +] diff --git a/telegram_phone_number_checker/main.py b/telegram_phone_number_checker/main.py index 29ef664..3e456c7 100644 --- a/telegram_phone_number_checker/main.py +++ b/telegram_phone_number_checker/main.py @@ -1,17 +1,34 @@ -import os +import asyncio import json +import os import re -from telethon.sync import TelegramClient, errors, functions -from telethon.tl.types import InputPhoneContact -from dotenv import load_dotenv from getpass import getpass -import click +import click +from dotenv import load_dotenv +from telethon.sync import TelegramClient, errors, functions +from telethon.tl import types load_dotenv() -def get_names(client: TelegramClient, phone_number: str) -> dict: +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) -> 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 @@ -21,11 +38,11 @@ def get_names(client: TelegramClient, phone_number: str) -> dict: print(f"Checking: {phone_number=} ...", end="", flush=True) try: # Create a contact - contact = InputPhoneContact( + 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", []) number_of_matches = len(users) @@ -39,31 +56,28 @@ def get_names(client: TelegramClient, phone_number: str) -> dict: elif number_of_matches == 1: # Attempt to remove the contact from the address book. # The response from DeleteContactsRequest contains more information than from ImportContactsRequest - del_user = client( + updates_response: types.Updates = await client( functions.contacts.DeleteContactsRequest(id=[users[0].get("id")]) ) - user = del_user.to_dict().get("users")[0] - user_was_online = user.get("status", {}).get("was_online") + 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"), - "fake": user.get("fake"), - "verified": user.get("verified"), - "premium": user.get("premium"), - "mutual_contact": user.get("mutual_contact"), - "bot": user.get("bot"), - "bot_chat_history": user.get("bot_chat_history"), - "restricted": user.get("restricted"), - "restriction_reason": user.get("restriction_reason"), - "user_was_online": ( - user_was_online.strftime("%Y-%m-%d %H:%M:%S %Z") - if user_was_online - else None - ), + "id": user.id, + "username": user.username, + "usernames": user.usernames, + "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, } ) else: @@ -87,7 +101,7 @@ def get_names(client: TelegramClient, phone_number: str) -> dict: return result -def validate_users(client: TelegramClient, phone_numbers: str) -> dict: +async def validate_users(client: TelegramClient, phone_numbers: str) -> dict: """ Take in a string of comma separated phone numbers and try to get the user information associated with each phone number. """ @@ -98,14 +112,14 @@ def validate_users(client: TelegramClient, phone_numbers: str) -> dict: try: for phone in phones: if phone not in result: - result[phone] = get_names(client, phone) + result[phone] = await get_names(client, phone) except Exception as e: print(e) raise return result -def login( +async def login( api_id: str | None, api_hash: str | None, phone_number: str | None ) -> TelegramClient: """Create a telethon session or reuse existing one""" @@ -116,16 +130,18 @@ def login( 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) + await client.sign_in(password=pw) print("Done.") return client @@ -212,8 +228,22 @@ def main_entrypoint( i.e. +491234567891 """ - client = login(api_id, api_hash, api_phone_number) - res = validate_users(client, phone_numbers) + asyncio.run( + run_program( + phone_numbers, + api_id, + api_hash, + api_phone_number, + output, + ) + ) + + +async def run_program( + phone_numbers: str, api_id: str, api_hash: str, api_phone_number: str, output: str +): + client = await login(api_id, api_hash, api_phone_number) + res = await validate_users(client, phone_numbers) show_results(output, res) diff --git a/tests/test_user_was_online.py b/tests/test_user_was_online.py index 74435c2..d9b9552 100644 --- a/tests/test_user_was_online.py +++ b/tests/test_user_was_online.py @@ -1,5 +1,26 @@ +from datetime import datetime, timezone + import pytest +from telethon.tl import types + +from telegram_phone_number_checker import main -def test_should_run_this_in_ci(): - assert True +@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