Using the Client¶
The apkit.client module provides clients for communicating with other ActivityPub servers. There are two types of clients available:
- Asynchronous Client (
apkit.client.asyncio): This client is based onaiohttpand is suitable for applications that require non-blocking I/O operations. It's the recommended choice for web servers and other high-concurrency applications. - Synchronous Client (
apkit.client.sync): This client is based onhttpcore(the foundation ofhttpx) and provides a blocking, synchronous interface. It's easier to use in scripts or applications that don't require the complexity ofasyncio.
The choice between the two depends on your application's architecture. The asynchronous client offers better performance for I/O-bound tasks, while the synchronous client is simpler to integrate into traditional, linear programs.
This is a very simple example:
import asyncio
from apkit.client.asyncio import ActivityPubClient
async def main():
async with ActivityPubClient() as client:
# Fetch a remote Actor or Object
actor = await client.actor.fetch("https://example.com/users/someuser")
if actor:
print(f"Fetched actor: {actor.name}")
if __name__ == "__main__":
asyncio.run(main())
from apkit.client.sync import ActivityPubClient
def main():
with ActivityPubClient() as client:
# Fetch a remote Actor or Object
actor = client.actor.fetch("https://example.com/users/someuser")
if actor:
print(f"Fetched actor: {actor.name}")
if __name__ == "__main__":
main()
When sending activities to another server, this server usually wants to verify the signature.
This requires some interaction. Therefore, it is easiest to start the minimal server from the tutorial.
It will take care to answer the WebFinger requests, send the required application/activity+json documents and public key.
Create a Note¶
This is a simple example to send a Note to another ActivityPub server. First, some preparations need to be made, which must be done only once before all activities.
- The programs loads or creates a private key required to sign activities.
- A Person resource is created that will be used as the Actor of activities.
The function send_note contains the code to create a Note.
- Find the address of the receiver's inbox.
- Create a Note object.
- Create a Create activity that contains the Note.
- Deliver the Create activity to the receiver's inbox.
import asyncio
import logging
import os
import uuid
from apkit.client.asyncio import ActivityPubClient
from apkit.models import Person, Note, CryptographicKey, Create, Delete
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization as crypto_serialization
from datetime import datetime, UTC
HOST="social.example.com" # <<< Change this to your domain name
USER_ID="demo"
TARGET_ID="https://example.org/users/alice" # <<< Change this to the URI of the account you want to send something to
# --- Logging Setup ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Key Persistence ---
KEY_FILE = "private_key.pem"
if os.path.exists(KEY_FILE):
logger.info(f"Loading existing private key from {KEY_FILE}.")
with open(KEY_FILE, "rb") as f:
private_key = crypto_serialization.load_pem_private_key(f.read(), password=None)
else:
logger.info(f"No key file found. Generating new private key and saving to {KEY_FILE}.")
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(KEY_FILE, "wb") as f:
f.write(private_key.private_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PrivateFormat.PKCS8,
encryption_algorithm=crypto_serialization.NoEncryption()
))
public_key_pem = private_key.public_key().public_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
# --- Create actor ---
actor = Person(
id=f"https://{HOST}/users/{USER_ID}",
name="apkit Demo",
preferredUsername="demo",
summary="This is a demo actor powered by apkit!",
inbox=f"https://{HOST}/users/{USER_ID}/inbox",
outbox=f"https://{HOST}/users/{USER_ID}/outbox",
publicKey=CryptographicKey(
id=f"https://{HOST}/users/{USER_ID}#main-key",
owner=f"https://{HOST}/users/{USER_ID}",
publicKeyPem=public_key_pem
)
)
async def send_note():
async with ActivityPubClient() as client:
# Fetch a remote Actor
target_actor = await client.actor.fetch(TARGET_ID)
print(f"Fetched actor: {target_actor.name}")
# Get the inbox URL from the actor's profile
inbox_url = target_actor.inbox
if not inbox_url:
raise Exception("Could not find actor's inbox URL")
logger.info(f"Found actor's inbox: {inbox_url}")
# --- Create note ---
note = Note(
id=f"https://{HOST}/notes/{uuid.uuid4()}",
attributedTo=actor.id,
content=f"<p>Hello from apkit</p>",
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=[target_actor.id],
cc=["https://www.w3.org/ns/activitystreams#Public"],
)
# --- Create activity ---
create = Create(
id=f"https://{HOST}/creates/{uuid.uuid4()}",
actor=actor.id,
object=note.to_json(), # embed the note into the activity
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=note.to, # re-use the information from the note
cc=note.cc
)
# Deliver the activity
logger.info("Delivering activity...")
# If you are interested in the actual data, uncomment this line.
# print(create.to_json())
resp = await client.post(
inbox_url, # address of the receiver's inbox
key_id=actor.publicKey.id, # the id of our public key
signature=private_key, # this is our private key
json=create # the activity to send
)
logger.info(f"Delivery result: {resp.status}")
logger.info(f"Note id: {note.id}")
if __name__ == "__main__":
asyncio.run(send_note())
import logging
import os
import uuid
from apkit.client.sync import ActivityPubClient
from apkit.models import Person, Note, CryptographicKey, Create, Delete
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization as crypto_serialization
from datetime import datetime, UTC
HOST="social.example.com" # <<< Change this to your domain name
USER_ID="demo"
TARGET_ID="https://example.org/users/alice" # <<< Change this to the URI of the account you want to send something to
# --- Logging Setup ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Key Persistence ---
KEY_FILE = "private_key.pem"
if os.path.exists(KEY_FILE):
logger.info(f"Loading existing private key from {KEY_FILE}.")
with open(KEY_FILE, "rb") as f:
private_key = crypto_serialization.load_pem_private_key(f.read(), password=None)
else:
logger.info(f"No key file found. Generating new private key and saving to {KEY_FILE}.")
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(KEY_FILE, "wb") as f:
f.write(private_key.private_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PrivateFormat.PKCS8,
encryption_algorithm=crypto_serialization.NoEncryption()
))
public_key_pem = private_key.public_key().public_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
# --- Create actor ---
actor = Person(
id=f"https://{HOST}/users/{USER_ID}",
name="apkit Demo",
preferredUsername="demo",
summary="This is a demo actor powered by apkit!",
inbox=f"https://{HOST}/users/{USER_ID}/inbox",
outbox=f"https://{HOST}/users/{USER_ID}/outbox",
publicKey=CryptographicKey(
id=f"https://{HOST}/users/{USER_ID}#main-key",
owner=f"https://{HOST}/users/{USER_ID}",
publicKeyPem=public_key_pem
)
)
def send_note():
with ActivityPubClient() as client:
# Fetch a remote Actor
target_actor = client.actor.fetch(TARGET_ID)
print(f"Fetched actor: {target_actor.name}")
# Get the inbox URL from the actor's profile
inbox_url = target_actor.inbox
if not inbox_url:
raise Exception("Could not find actor's inbox URL")
logger.info(f"Found actor's inbox: {inbox_url}")
# --- Create note ---
note = Note(
id=f"https://{HOST}/notes/{uuid.uuid4()}",
attributedTo=actor.id,
content=f"<p>Hello from apkit</p>",
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=[target_actor.id],
cc=["https://www.w3.org/ns/activitystreams#Public"],
)
# --- Create activity ---
create = Create(
id=f"https://{HOST}/creates/{uuid.uuid4()}",
actor=actor.id,
object=note.to_json(), # embed the note into the activity
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=note.to, # re-use the information from the note
cc=note.cc
)
# Deliver the activity
logger.info("Delivering activity...")
# If you are interested in the actual data, uncomment this line.
# print(create.to_json())
resp = client.post(
inbox_url, # address of the receiver's inbox
key_id=actor.publicKey.id, # the id of our public key
signature=private_key, # this is our private key
json=create # the activity to send
)
logger.info(f"Delivery result: {resp.status}")
logger.info(f"Note id: {note.id}")
if __name__ == "__main__":
send_note()
In a productiv environment you will want to store the content of the note and its URI into some kind of persistent storage. Then, for example, likes could be correctly assigned.
Delete something¶
The last information the function to create a Note writes on the terminal is the URI by which the Note can be identified. It can also be used to remove it from another ActivityPub server. Here is some demo code for that:
async def delete_note():
URI = "https://social.example.com/notes/6de49020-85a0-4546-b63e-36fe23271f71" # <<< change this URI
async with ActivityPubClient() as client:
# Fetch a remote Actor
target_actor = await client.actor.fetch(TARGET_ID)
print(f"Fetched actor: {target_actor.name}")
# Get the inbox URL from the actor's profile
inbox_url = target_actor.inbox
if not inbox_url:
raise Exception("Could not find actor's inbox URL")
logger.info(f"Found actor's inbox: {inbox_url}")
# Delete activity
delete = Delete(
id=f"https://{HOST}/activities/{uuid.uuid4()}",
actor=actor.id,
object=URI,
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=[target_actor.id],
cc=["https://www.w3.org/ns/activitystreams#Public"],
)
# Deliver the activity
logger.info("Delivering activity...")
resp = await client.post(
inbox_url,
key_id=actor.publicKey.id,
signature=private_key,
json=delete
)
logger.info(f"Delivery result: {resp.status}")
def delete_note():
URI = "https://social.example.com/notes/6de49020-85a0-4546-b63e-36fe23271f71" # <<< change this URI
with ActivityPubClient() as client:
# Fetch a remote Actor
target_actor = client.actor.fetch(TARGET_ID)
print(f"Fetched actor: {target_actor.name}")
# Get the inbox URL from the actor's profile
inbox_url = target_actor.inbox
if not inbox_url:
raise Exception("Could not find actor's inbox URL")
logger.info(f"Found actor's inbox: {inbox_url}")
# Delete activity
delete = Delete(
id=f"https://{HOST}/activities/{uuid.uuid4()}",
actor=actor.id,
object=URI,
published=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
to=[target_actor.id],
cc=["https://www.w3.org/ns/activitystreams#Public"],
)
# Deliver the activity
logger.info("Delivering activity...")
resp = client.post(
inbox_url,
key_id=actor.publicKey.id,
signature=private_key,
json=delete
)
logger.info(f"Delivery result: {resp.status}")