diff --git a/examples/note_example.py b/examples/note_example.py index 714d8708fa43c590ae506b7e01d8d1b09fed6b92..9cdc809bb5488eae439b362d83fcb3808fed15ff 100644 --- a/examples/note_example.py +++ b/examples/note_example.py @@ -65,6 +65,7 @@ async def create_note_example(): logger.info(f"Active key: {active_key}") # Initialize HTTP signature verifier with the active key paths + logger.info(f"Using key paths: {key_manager.keys_path}/{active_key.key_id}_private.pem and {key_manager.keys_path}/{active_key.key_id}_public.pem") signature_verifier = HTTPSignatureVerifier( private_key_path=f"{key_manager.keys_path}/{active_key.key_id}_private.pem", public_key_path=f"{key_manager.keys_path}/{active_key.key_id}_public.pem" @@ -128,11 +129,18 @@ async def create_note_example(): # logger.info(f"Activity: {to_json(create_activity, indent=2)}") # Sign the request + parsed_url = urlparse(inbox_url) + headers = { + "Accept": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "Content-Type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "User-Agent": "PyFed/1.0", + "Host": parsed_url.netloc + } logger.debug(f"Resolved host: {urlparse(inbox_url).netloc}") signed_headers = await signature_verifier.sign_request( method="POST", path="/inbox", - headers={"Host": urlparse(inbox_url).netloc}, + headers=headers, body=activity_dict ) diff --git a/examples/send_to_mastodon.py b/examples/send_to_mastodon.py new file mode 100644 index 0000000000000000000000000000000000000000..962ff48b79bd74c5d86598683a6cf6067d93b912 --- /dev/null +++ b/examples/send_to_mastodon.py @@ -0,0 +1,94 @@ +""" +Example of sending a message to Mastodon following ActivityPub spec. +""" + +import asyncio +import logging +from pathlib import Path +import sys +# Add src directory to Python path +src_path = Path(__file__).parent.parent / "src" +sys.path.insert(0, str(src_path)) + +from pyfed.federation.delivery import ActivityDelivery +from pyfed.federation.discovery import InstanceDiscovery +from pyfed.security.key_management import KeyManager + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +async def send_activity_to_mastodon(): + # Initialize components + key_manager = KeyManager( + domain="localhost:8000", + keys_path="example_keys" + ) + + discovery = InstanceDiscovery() + delivery = ActivityDelivery(key_manager=key_manager) + + await discovery.initialize() + await delivery.initialize() + + try: + # 1. First, perform WebFinger lookup to get the actor's inbox + logger.info("Performing WebFinger lookup...") + webfinger_result = await discovery.webfinger( + resource="acct:kene29@mastodon.social" + ) + logger.info(f"WebFinger result: {webfinger_result}") + + if not webfinger_result: + raise Exception("Could not find user through WebFinger") + + # Find ActivityPub actor URL from WebFinger result + actor_url = None + for link in webfinger_result.get('links', []): + if link.get('rel') == 'self' and link.get('type') == 'application/activity+json': + actor_url = link.get('href') + break + + if not actor_url: + raise Exception("Could not find ActivityPub actor URL") + + # 2. Create the Activity + note_activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Create", + "actor": f"https://localhost:8000/users/testuser", + "object": { + "type": "Note", + "content": "Hello @kene29@mastodon.social! This is a test message from PyFed.", + "attributedTo": f"https://localhost:8000/users/testuser", + "to": [actor_url], + "cc": ["https://www.w3.org/ns/activitystreams#Public"] + }, + "to": [actor_url], + "cc": ["https://www.w3.org/ns/activitystreams#Public"] + } + + # 3. Deliver the activity + logger.info(f"Delivering activity: {note_activity}") + result = await delivery.deliver_activity( + activity=note_activity, + recipients=[actor_url] + ) + + if result.success: + logger.info("Activity delivered successfully!") + logger.info(f"Delivered to: {result.success}") + else: + logger.error("Activity delivery failed!") + logger.error(f"Failed recipients: {result.failed}") + logger.error(f"Error: {result.error_message}") + + except Exception as e: + logger.error(f"Error: {e}") + + finally: + # Clean up + await discovery.close() + await delivery.close() + +if __name__ == "__main__": + asyncio.run(send_activity_to_mastodon()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c8fb7e92494ece87076a27f43ca73e70b0a92c52..b8ed079bfed22762bcfbcc43515920d1dbeca305 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,4 +47,5 @@ soupsieve==2.6 SQLAlchemy==2.0.36 starlette==0.41.2 typing_extensions==4.12.2 -yarl==1.15.2 \ No newline at end of file +yarl==1.15.2 +certifi \ No newline at end of file diff --git a/src/pyfed/.DS_Store b/src/pyfed/.DS_Store index c6932cc849686664d20de6776ac3859c5ece9817..0366676234840b2cc46b344ce97f71b49bb555c8 100644 Binary files a/src/pyfed/.DS_Store and b/src/pyfed/.DS_Store differ diff --git a/src/pyfed/federation/delivery.py b/src/pyfed/federation/delivery.py index cc1e94e33093774088d25ea67989fc063262c52b..edf76e01225971704112005e2f9b80c915f2a735 100644 --- a/src/pyfed/federation/delivery.py +++ b/src/pyfed/federation/delivery.py @@ -127,8 +127,8 @@ class ActivityDelivery: # Prepare headers parsed_url = urlparse(inbox_url) headers = { - "Content-Type": "application/activity+json", - "Accept": "application/activity+json", + "Accept": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "Content-Type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "User-Agent": "PyFed/1.0", "Host": parsed_url.netloc } diff --git a/src/pyfed/federation/discovery.py b/src/pyfed/federation/discovery.py index 85ed45b8dbc78e0ed7ba5507e1b6e92078447fe1..1f7056884709f543c4fe589a6b9b02f7c6cf5fc7 100644 --- a/src/pyfed/federation/discovery.py +++ b/src/pyfed/federation/discovery.py @@ -16,6 +16,8 @@ from urllib.parse import urlparse, urljoin from datetime import datetime import asyncio from dataclasses import dataclass +import certifi +import ssl from ..utils.exceptions import DiscoveryError from ..utils.logging import get_logger @@ -59,12 +61,14 @@ class InstanceDiscovery: async def initialize(self) -> None: """Initialize HTTP session.""" + ssl_context = ssl.create_default_context(cafile=certifi.where()) self.session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=self.timeout), headers={ "User-Agent": "PyFed/1.0", "Accept": "application/activity+json" - } + }, + connector=aiohttp.TCPConnector(ssl=ssl_context) ) async def discover_instance(self, domain: str) -> InstanceInfo: @@ -172,7 +176,7 @@ class InstanceDiscovery: ] headers = { - "Accept": "application/activity+json" + "Accept": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" } for url in locations: