Skip to content
Snippets Groups Projects
Commit f72d9749 authored by supersonicwisd1's avatar supersonicwisd1
Browse files

added readme to run the send_to_mastodon.py

Signed-off-by: supersonicwisd1 <supersonicwisd1>
parent 459d8950
No related branches found
No related tags found
1 merge request!15added readme to run the send_to_mastodon.py
To run examples, use the following command:
`python <example.py>`
\ No newline at end of file
# PyFed Mastodon Integration Demo
This guide demonstrates how to use PyFed to interact with Mastodon using the ActivityPub protocol. The example shows how to send a message (Note) to a Mastodon user using federation.
## Prerequisites
1. Python 3.9 or higher
2. Virtual environment (recommended)
3. ngrok installed (for local testing)
4. A Mastodon account to send messages to
## Installation
1. Clone the repository and set up the environment:
```bash
# Create and activate virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
2. Set up ngrok for local testing:
```bash
# Start ngrok on port 8880
ngrok http 8880
```
Save the ngrok URL (e.g., "12f8-197-211-61-33.ngrok-free.app") for configuration.
## Configuration
1. Create a `config.py` file in the examples directory with your settings:
```python
CONFIG = {
"domain": "12f8-197-211-61-33.ngrok-free.app", # Your ngrok URL (without https://)
"user": "testuser",
"keys_path": "example_keys",
"port": 8880
}
```
2. Create the keys directory:
```bash
mkdir example_keys
```
3. Start the key server:
```bash
python examples/key_server.py
```
The key server will handle key management and HTTP signatures automatically.
## Running the Example
1. Ensure both ngrok and the key server are running
2. Run the example:
```bash
python examples/send_to_mastodon.py
```
3. Check the logs for the federation process:
- WebFinger lookup
- Actor discovery
- Message delivery
- HTTP signatures
## Troubleshooting
Common issues and solutions:
1. **Connection Issues**
- Verify ngrok is running and URL is correct
- Check if key server is running on port 8880
- Ensure ngrok URL is properly set in config
2. **WebFinger Lookup Fails**
- Verify the Mastodon username is correct
- Check if the instance is online
- Ensure proper network connectivity
3. **Delivery Errors**
- Verify your ngrok URL is current (they expire)
- Check key server logs for signature issues
- Ensure Mastodon instance is reachable
## Understanding the Code
The example demonstrates several key PyFed features:
1. **Key Management**
- Automatic key generation and management via key server
- Handles HTTP signatures for federation
2. **Federation**
- WebFinger protocol for user discovery
- ActivityPub protocol for message delivery
3. **Activity Creation**
- Creates ActivityPub Note objects
- Wraps them in Create activities
## Next Steps
1. Try sending different types of activities
2. Implement inbox handling for responses
3. Add error handling and retries
4. Explore other ActivityPub interactions
## Support
For issues and questions:
1. Check the PyFed documentation
2. Open an issue on GitHub
3. Join our community discussions
\ No newline at end of file
......@@ -6,10 +6,12 @@ from .delivery import ActivityDelivery
from .fetch import ResourceFetcher
from .resolver import ActivityPubResolver
from .discovery import InstanceDiscovery
from .webfinger import WebFingerClient
__all__ = [
'ActivityDelivery',
'ResourceFetcher',
'ActivityPubResolver',
'InstanceDiscovery'
'ActivityPubResolver',
'InstanceDiscovery',
'WebFingerClient'
]
\ No newline at end of file
......@@ -10,7 +10,7 @@ from urllib.parse import urlparse
from ..utils.exceptions import ResolverError
from ..utils.logging import get_logger
from ..cache.actor_cache import ActorCache
from ..protocols.webfinger import WebFingerClient
from .webfinger import WebFingerClient
logger = get_logger(__name__)
......
......@@ -13,7 +13,7 @@ from .collections import (
)
from .activities import (
APCreate, APUpdate, APDelete,
APFollow, APUndo, APLike, APAnnounce, APActivity
APFollow, APUndo, APLike, APAnnounce, APActivity, APAccept, APRemove, APBlock, APReject
)
from .links import APLink, APMention
......@@ -23,5 +23,5 @@ __all__ = [
'APActor', 'APPerson', 'APGroup', 'APOrganization', 'APApplication', 'APService',
'APEvent', 'APPlace', 'APProfile', 'APRelationship', 'APTombstone', 'APArticle', 'APAudio', 'APDocument', 'APImage', 'APNote', 'APPage', 'APVideo',
'APCollection', 'APOrderedCollection', 'APCollectionPage', 'APOrderedCollectionPage',
'APCreate', 'APUpdate', 'APDelete', 'APFollow', 'APUndo', 'APLike', 'APAnnounce', 'APMention'
'APCreate', 'APUpdate', 'APDelete', 'APFollow', 'APUndo', 'APLike', 'APAnnounce', 'APMention', 'APAccept', 'APRemove', 'APBlock', 'APReject'
]
......@@ -181,3 +181,76 @@ class APAnnounce(APActivity):
...,
description="The object being announced"
)
class APAccept(APActivity):
"""
Represents an Accept activity in ActivityPub.
Indicates that the actor accepts the object.
Attributes:
type (Literal["Accept"]): Always set to "Accept".
object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
Specification: https://www.w3.org/TR/activitystreams-vocabulary/#accept
"""
type: Literal["Accept"] = Field(default="Accept", description="Indicates that this object represents an accept activity")
object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
...,
description="The object being accepted"
)
class APRemove(APActivity):
"""
Represents a Remove activity in ActivityPub.
Indicates that the actor is removing the object.
Attributes:
type (Literal["Remove"]): Always set to "Remove".
object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
Specification: https://www.w3.org/TR/activitystreams-vocabulary/#remove
"""
type: Literal["Remove"] = Field(default="Remove", description="Indicates that this object represents a remove activity")
object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
...,
description="The object being removed"
)
class APBlock(APActivity):
"""
Represents a Block activity in ActivityPub.
Indicates that the actor is blocking the object.
Attributes:
type (Literal["Block"]): Always set to "Block".
object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
Specification: https://www.w3.org/TR/activitystreams-vocabulary/#block
"""
type: Literal["Block"] = Field(default="Block", description="Indicates that this object represents a block activity")
object: Union[str, HttpUrl, APObject] = Field(
...,
description="The object being blocked"
)
class APReject(APActivity):
"""
Represents a Reject activity in ActivityPub.
Indicates that the actor rejects the object.
This is typically used to reject incoming activities such as Follow requests.
Attributes:
type (Literal["Reject"]): Always set to "Reject".
object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
Specification: https://www.w3.org/TR/activitystreams-vocabulary/#reject
"""
type: Literal["Reject"] = Field(default="Reject", description="Indicates that this object represents a reject activity")
object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
...,
description="The object being rejected"
)
......@@ -9,7 +9,7 @@ https://www.w3.org/TR/activitystreams-core/#collections
from __future__ import annotations
from typing import Optional, List, Union, Literal, Dict, Any
from pydantic import Field, HttpUrl
from pydantic import Field, BaseModel, HttpUrl
from pyfed.models.objects import APObject
from pyfed.utils.logging import get_logger
......@@ -101,6 +101,26 @@ class APOrderedCollectionPage(APCollectionPage):
description="The index of the first item"
)
class APFollowersCollection(APOrderedCollection):
"""Collection of followers."""
type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
name: Literal["Followers"] = Field(default="Followers")
class APFollowingCollection(APOrderedCollection):
"""Collection of following."""
type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
name: Literal["Following"] = Field(default="Following")
class APLikedCollection(APOrderedCollection):
"""Collection of liked objects."""
type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
name: Literal["Liked"] = Field(default="Liked")
class APSharedCollection(APOrderedCollection):
"""Collection of shared objects."""
type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
name: Literal["Shared"] = Field(default="Shared")
async def fetch_collection(url: str) -> Union[APCollection, APOrderedCollection]:
"""
Fetch a collection from a given URL.
......
# from .c2s import C2SConfig, ClientToServerProtocol
# from .s2s import ServerToServerProtocol
"""
Protocol implementations for PyFed.
"""
from .base import (
BaseProtocol, BaseProtocolConfig, ProtocolVersion,
ProtocolError, ValidationError, SecurityError, TimeoutError
)
from .c2s import C2SConfig, ClientToServerProtocol
from .s2s import ServerToServerProtocol
# __all__ = [
# 'C2SConfig', 'ClientToServerProtocol', 'ServerToServerProtocol'
# ]
__all__ = [
# Base
'BaseProtocol',
'BaseProtocolConfig',
'ProtocolVersion',
'ProtocolError',
'ValidationError',
'SecurityError',
'TimeoutError',
# Client-to-Server
'C2SConfig',
'ClientToServerProtocol',
# Server-to-Server
'ServerToServerProtocol',
]
"""
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
\ No newline at end of file
aiofiles==24.1.0
aiohappyeyeballs==2.4.3
aiohttp==3.10.10
aioredis==2.0.1
aiosignal==1.3.1
aiosqlite==0.20.0
annotated-types==0.7.0
anyio==4.6.2.post1
async-timeout==5.0.0
asyncpg==0.30.0
attrs==24.2.0
backoff==2.2.1
beautifulsoup4==4.12.3
bs4==0.0.2
cachetools==5.5.0
cffi==1.17.1
cryptography==43.0.3
dnspython==2.7.0
factory_boy==3.3.1
Faker==30.8.0
fastapi==0.115.4
frozenlist==1.4.1
greenlet==3.1.1
idna==3.10
iniconfig==2.0.0
Markdown==3.7
motor==3.6.0
multidict==6.1.0
packaging==24.1
pluggy==1.5.0
prometheus_client==0.21.0
propcache==0.2.0
pycparser==2.22
pydantic==2.9.2
pydantic_core==2.23.4
PyJWT==2.9.0
pymongo==4.9.2
pytest==8.3.3
pytest-aiohttp==1.0.5
pytest-asyncio==0.24.0
python-dateutil==2.9.0.post0
PyYAML==6.0.2
redis==5.2.0
six==1.16.0
sniffio==1.3.1
soupsieve==2.6
SQLAlchemy==2.0.36
starlette==0.41.2
typing_extensions==4.12.2
yarl==1.15.2
certifi
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment