diff --git a/examples/config.py b/examples/config.py index 4b3c769a4149c383540235ed1074071e146ae8a8..c7d83c7365c15d6c0032e605c678435183574ace 100644 --- a/examples/config.py +++ b/examples/config.py @@ -3,8 +3,8 @@ Test configuration. """ CONFIG = { - "domain": "12f8-197-211-61-33.ngrok-free.app", - "user": "testuser", + "domain": "Ngrok forwarding url without https://", + "user": "username", "keys_path": "example_keys", - "port": 8880 + "port": 8880 #port number from ngrok } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b8ed079bfed22762bcfbcc43515920d1dbeca305..0c06fd5d8b238b859f39c4fe6fe40717ae911d53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,9 @@ backoff==2.2.1 beautifulsoup4==4.12.3 bs4==0.0.2 cachetools==5.5.0 +certifi==2024.8.30 cffi==1.17.1 +click==8.1.7 cryptography==43.0.3 dnspython==2.7.0 factory_boy==3.3.1 @@ -21,6 +23,7 @@ Faker==30.8.0 fastapi==0.115.4 frozenlist==1.4.1 greenlet==3.1.1 +h11==0.14.0 idna==3.10 iniconfig==2.0.0 Markdown==3.7 @@ -47,5 +50,5 @@ soupsieve==2.6 SQLAlchemy==2.0.36 starlette==0.41.2 typing_extensions==4.12.2 +uvicorn==0.32.1 yarl==1.15.2 -certifi \ No newline at end of file diff --git a/src/pyfed/federation/webfinger.py b/src/pyfed/federation/webfinger.py new file mode 100644 index 0000000000000000000000000000000000000000..7dcd164d0380f76603388bb53fa7491dfd0b7a56 --- /dev/null +++ b/src/pyfed/federation/webfinger.py @@ -0,0 +1,113 @@ +""" +WebFinger protocol implementation. +""" + +from typing import Dict, Any, Optional +import aiohttp +from urllib.parse import quote +import logging + +logger = logging.getLogger(__name__) + +class WebFingerClient: + """WebFinger client implementation.""" + + def __init__(self, verify_ssl: bool = True): + self.verify_ssl = verify_ssl + self.session = None + + async def initialize(self) -> None: + """Initialize client.""" + ssl = None if self.verify_ssl else False + self.session = aiohttp.ClientSession( + headers={"Accept": "application/jrd+json, application/json"}, + connector=aiohttp.TCPConnector(ssl=ssl) + ) + + async def finger(self, account: str) -> Optional[Dict[str, Any]]: + """ + Perform WebFinger lookup. + + Args: + account: Account to look up (e.g., user@domain.com) + + Returns: + WebFinger response if found + """ + try: + if not '@' in account: + return None + + # Ensure acct: prefix + if not account.startswith('acct:'): + account = f"acct:{account}" + + domain = account.split('@')[-1] + url = f"https://{domain}/.well-known/webfinger?resource={quote(account)}" + + response = await self.session.get(url) + async with response as resp: + if resp.status != 200: + return None + return await resp.json() + + except Exception as e: + logger.error(f"WebFinger lookup failed for {account}: {e}") + return None + + async def get_actor_url(self, account: str) -> Optional[str]: + """ + Get actor URL from WebFinger response. + + Args: + account: Account to look up + + Returns: + Actor URL if found + """ + try: + data = await self.finger(account) + if not data: + return None + + for link in data.get('links', []): + if (link.get('rel') == 'self' and + link.get('type') == 'application/activity+json'): + return link.get('href') + return None + + except Exception as e: + logger.error(f"Failed to get actor URL for {account}: {e}") + return None + + async def get_inbox_url(self, account: str) -> Optional[str]: + """ + Get inbox URL for account. + + Args: + account: Account to look up + + Returns: + Inbox URL if found + """ + try: + actor_url = await self.get_actor_url(account) + logger.debug(f"actor_url: {actor_url}") + if not actor_url: + return None + + response = await self.session.get(actor_url) + async with response as resp: + if resp.status != 200: + return None + data = await resp.json() + return data.get('inbox') + + except Exception as e: + logger.error(f"Failed to get inbox URL for {account}: {e}") + return None + + async def close(self) -> None: + """Clean up resources.""" + if self.session: + await self.session.close() \ No newline at end of file