Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9de410e
chore: add ty
AmaseCocoa Feb 23, 2026
ca80b6b
Merge remote-tracking branch 'origin/main' into chore/pyrefly-to-ty
AmaseCocoa Feb 23, 2026
c1fe2f6
chore: auto fixes by prek
AmaseCocoa Feb 23, 2026
b9032da
fix: WebfingerResult.get always return List for type safety
AmaseCocoa Feb 23, 2026
ad71e48
fix(nodeinfo): use snake_case in NodeinfoBuilder
AmaseCocoa Feb 23, 2026
64e00f8
chore: use ty in Zed
AmaseCocoa Feb 23, 2026
7a640ac
test: replace pyrefly to ty
AmaseCocoa Feb 23, 2026
4885462
chore: fix typeerrors
AmaseCocoa Feb 23, 2026
3d12fd2
chore: remove pyrefly config from pyproject.toml
AmaseCocoa Feb 23, 2026
2707c2a
Merge branch 'chore/pyrefly-to-ty' of https://github.com/fedi-libs/ap…
AmaseCocoa Feb 23, 2026
bb32704
chore: auto fixes by prek
AmaseCocoa Feb 23, 2026
b89851e
remove _version.py
AmaseCocoa Feb 23, 2026
3cc6eb8
remove _version.py
AmaseCocoa Feb 23, 2026
b9098a6
fix: fix tests
AmaseCocoa Feb 23, 2026
70a12c4
Update test_models.py
AmaseCocoa Feb 23, 2026
a5d4b34
fix: fix type annotations
AmaseCocoa Feb 24, 2026
030ae29
camelCase to snake_case
AmaseCocoa Apr 7, 2026
d0292f7
fix: support apmodel 0.6.0
AmaseCocoa Apr 7, 2026
d4cbd5e
Merge remote-tracking branch 'origin/main' into chore/pyrefly-to-ty
AmaseCocoa Apr 7, 2026
b6b9df0
chore: auto fixes by prek
AmaseCocoa Apr 7, 2026
96b3515
fix: fix type errors
AmaseCocoa Apr 7, 2026
c00d5f3
Merge branch 'chore/pyrefly-to-ty' of https://github.com/fedi-libs/ap…
AmaseCocoa Apr 7, 2026
77da368
chore: auto fixes by prek
AmaseCocoa Apr 7, 2026
c04d8c4
chore
AmaseCocoa Apr 7, 2026
57d4c8b
Merge branch 'chore/pyrefly-to-ty' of https://github.com/fedi-libs/ap…
AmaseCocoa Apr 7, 2026
72711a3
chore: auto fixes by prek
AmaseCocoa Apr 7, 2026
f27a1d3
Update uv.lock
AmaseCocoa Apr 7, 2026
e8c9288
chore: remove webfinger test from apkit (migrated to apmodel)
AmaseCocoa Apr 7, 2026
2f706be
Merge branch 'chore/pyrefly-to-ty' of https://github.com/fedi-libs/ap…
AmaseCocoa Apr 7, 2026
5d7dd47
chore: auto fixes by prek
AmaseCocoa Apr 7, 2026
7179209
refactor: remove unused deps
AmaseCocoa Apr 7, 2026
ce4faf1
Merge branch 'chore/pyrefly-to-ty' of https://github.com/fedi-libs/ap…
AmaseCocoa Apr 7, 2026
bf912bb
fix: fix tests
AmaseCocoa Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,10 @@ __marimo__/
private_key*.pem

devel/
data/
_version.py

/*.py
data/

# auto-generated file
src/apkit/_version.py
19 changes: 1 addition & 18 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
{
"languages": {
"Python": {
"language_servers": ["ruff", "pyrefly"]
"language_servers": ["ruff", "ty"]
}
},
"lsp": {
"pyrefly": {
"binary": {
"path": ".venv/bin/pyrefly",
"arguments": ["lsp"]
},
"settings": {
"python": {
"pythonPath": ".venv/bin/python"
},
"pyrefly": {
"project_includes": ["src/**/*.py", "tests/**/*.py"],
"project_excludes": ["**/.[!/.]*", "**/*venv/**"],
"search_path": ["src"],
"ignore_errors_in_generated_code": true
}
}
},
"ruff": {
"initialization_options": {
"settings": {
Expand Down
7 changes: 5 additions & 2 deletions examples/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

from apkit.client.asyncio import ActivityPubClient
from apkit.client import ActivityPubClient
from apkit.models import CryptographicKey, Delete, Person

if len(sys.argv) < 3:
Expand Down Expand Up @@ -104,9 +104,12 @@ async def delete_note(recepient: str, object_id: str) -> None:
# Deliver the activity
logger.info("Delivering activity...")

if not actor.public_key or not actor.public_key.id:
raise ValueError("public_key.id not found")

resp = await client.post(
inbox_url,
key_id=actor.publicKey.id,
key_id=actor.public_key.id,
signature=private_key,
json=delete,
)
Expand Down
11 changes: 7 additions & 4 deletions examples/follow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

from apkit.client.asyncio import ActivityPubClient
from apkit.client.models import Resource as WebfingerResource
from apkit.client import ActivityPubClient
from apmodel.webfinger import Resource as WebfingerResource
from apkit.models import CryptographicKey, Follow, Person

if len(sys.argv) < 2:
Expand Down Expand Up @@ -70,10 +70,10 @@
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",
public_key_pem=CryptographicKey(
public_key=CryptographicKey(
id=f"https://{HOST}/users/{USER_ID}#main-key",
owner=f"https://{HOST}/users/{USER_ID}",
public_key=public_key_pem,
public_key_pem=public_key_pem,
),
)

Expand Down Expand Up @@ -113,6 +113,9 @@ async def follow(actor_id: str) -> None:
# Deliver the activity
logger.info("Delivering activity...")

if not actor.public_key or not actor.public_key.id:
raise ValueError("public_key.id not found")

resp = await client.post(
inbox_url,
key_id=actor.public_key.id,
Expand Down
20 changes: 16 additions & 4 deletions examples/like.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import uuid
from datetime import UTC, datetime

from apmodel.objects import Actor
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import ed25519, rsa

from apkit.client.asyncio import ActivityPubClient
from apkit.client import ActivityPubClient
from apkit.models import CryptographicKey, Like, Person
from apkit.types import ActorKey

if len(sys.argv) < 2:
print("USAGE: python like.py <OBJECT_ID>", file=sys.stderr)
Expand Down Expand Up @@ -89,6 +91,9 @@ async def like(object_id: str) -> None:
logger.info(f"Author of the object is: {sender_id}")

target_actor = await client.actor.fetch(sender_id)
if not isinstance(target_actor, Actor):
raise ValueError("Actor not found.")

# Get the inbox URL from the actor's profile
inbox_url = target_actor.inbox
if not inbox_url:
Expand All @@ -109,10 +114,17 @@ async def like(object_id: str) -> None:
# Deliver the activity
logger.info("Delivering activity...")

if not actor.public_key or not actor.public_key.id:
raise ValueError("Actor's publickey is missing")

if not isinstance(private_key, rsa.RSAPrivateKey) and not isinstance(
private_key, ed25519.Ed25519PrivateKey
):
raise ValueError("Invalid Key")

resp = await client.post(
inbox_url,
key_id=actor.publicKey.id,
signature=private_key,
signatures=[ActorKey(key_id=actor.public_key.id, private_key=private_key)],
json=activity,
)
logger.info(f"Delivery result: {resp.status}")
Expand Down
34 changes: 23 additions & 11 deletions examples/minimal_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
import logging
import os
import sys
import uuid

from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import ed25519, rsa
from fastapi import Request, Response
from fastapi.responses import JSONResponse

from apkit.client import WebfingerLink, WebfingerResource, WebfingerResult
from apkit.client.asyncio.client import ActivityPubClient
from apkit.client import (
ActivityPubClient,
WebfingerLink,
WebfingerResource,
WebfingerResult,
)
from apkit.models import (
Actor as APKitActor,
)
Expand Down Expand Up @@ -83,26 +88,33 @@
actor = Person(
id=f"https://{HOST}/users/{USER_ID}",
name="apkit Demo",
preferredUsername="demo",
preferred_username="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(
public_key=CryptographicKey(
id=f"https://{HOST}/users/{USER_ID}#main-key",
owner=f"https://{HOST}/users/{USER_ID}",
publicKeyPem=public_key_pem,
public_key_pem=public_key_pem,
),
)


# --- Server Initialization ---
app = ActivityPubServer()


# --- Key Retrieval Function ---
# This function provides the private key for signing outgoing activities.
def get_keys_for_actor(identifier: str) -> list[ActorKey]:
if identifier == USER_ID:
return [ActorKey(key_id=actor.publicKey.id, private_key=private_key)]
if not actor.public_key:
raise ValueError("PublicKey not found.")
if not isinstance(private_key, rsa.RSAPrivateKey) and not isinstance(
private_key, ed25519.Ed25519PrivateKey
):
raise ValueError("Invalid Key")
if identifier == USER_ID and actor.public_key.id:
return [ActorKey(key_id=actor.public_key.id, private_key=private_key)]
return []


Expand Down Expand Up @@ -177,7 +189,7 @@ async def outbox(ctx: Context):

if not ctx.request.query_params.get("page"):
outbox = OrderedCollection()
outbox.totalItems = 0 # No letter in the mail today.
outbox.total_items = 0 # No letter in the mail today.
outbox.id = f"https://{HOST}/users/{identifier}/outbox"
outbox.first = f"{outbox.id}?page=true"
outbox.last = f"{outbox.id}?min_id=0&page=true"
Expand All @@ -202,15 +214,15 @@ async def on_follow_activity(ctx: Context) -> Response:
elif isinstance(activity.actor, APKitActor):
follower_actor = activity.actor

if not follower_actor:
if not follower_actor or not isinstance(follower_actor, APKitActor):
return JSONResponse(
{"error": "Could not resolve follower actor"}, status_code=400
)

logger.info(f"🫂 {follower_actor.name} follows me.")

# Automatically accept the follow request
id_ = "https://{HOST}/activity/{uuid.uuid4()}"
id_ = f"https://{HOST}/activity/{uuid.uuid4()}"
accept_activity = activity.accept(id_, actor)

# Send the signed Accept activity back to the follower's inbox
Expand Down
46 changes: 37 additions & 9 deletions examples/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import uuid
from datetime import UTC, datetime

from apmodel.vocab.mention import Mention
import apmodel
from apmodel.core import Link
from apmodel.objects import Actor, Mention
from apmodel.webfinger import Resource as WebfingerResource
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import ed25519, rsa

from apkit.client.asyncio import ActivityPubClient
from apkit.client.models import Resource as WebfingerResource
from apkit.client import ActivityPubClient
from apkit.models import Create, CryptographicKey, Note, Person
from apkit.types import ActorKey

if len(sys.argv) < 2:
print("USAGE: python send_message.py <RECEPIENT_URI>", file=sys.stderr)
Expand Down Expand Up @@ -93,10 +96,16 @@ async def send_note(recepient: str) -> None:
webfinger_result = await client.actor.resolve(res.username, res.host)

# read the ActivityPub link from the result
recepient = webfinger_result.get("application/activity+json").href
res = webfinger_result.get("application/activity+json")[0].href
if not res:
raise ValueError("")
else:
recepient = res

# Fetch a remote Actor
target_actor = await client.actor.fetch(recepient)
if not isinstance(target_actor, Actor):
raise ValueError("Actor not found.")
logger.info(f"Fetched actor: {target_actor.name}")

# Get the inbox URL from the actor's profile
Expand All @@ -106,6 +115,14 @@ async def send_note(recepient: str) -> None:

logger.info(f"Found actor's inbox: {inbox_url}")

if (
actor.id is None
or target_actor.id is None
or target_actor.url is None
or isinstance(target_actor.url, Link)
):
raise ValueError("Actor ID or URL is missing")

# Create note
t = f'<p><span class="h-card" translate="no"><a href="{target_actor.url}" class="u-url mention">@<span>{target_actor.preferred_username}</span></a></span></p>'
note = Note(
Expand All @@ -117,7 +134,8 @@ async def send_note(recepient: str) -> None:
cc=["https://www.w3.org/ns/activitystreams#Public"],
tag=[
Mention(
href=target_actor.url, name=f"@{target_actor.preferred_username}"
href=target_actor.url,
name=f"@{target_actor.preferred_username}",
)
],
)
Expand All @@ -138,11 +156,21 @@ async def send_note(recepient: str) -> None:
# Uncomment the following line if you want to see the code of the activity.
# print(create.to_json())

if not actor.public_key:
raise ValueError("Actor's publickey is missing")

if not isinstance(private_key, rsa.RSAPrivateKey) and not isinstance(
private_key, ed25519.Ed25519PrivateKey
):
raise ValueError("Invalid Key")

if not actor.public_key.id:
raise ValueError("public_key.key_id is required")

resp = await client.post(
inbox_url,
key_id=actor.publicKey.id,
signature=private_key,
json=create.to_json(keep_object=True),
signatures=[ActorKey(key_id=actor.public_key.id, private_key=private_key)],
json=apmodel.to_dict(create),
)
logger.info(f"Delivery result: {resp.status}")

Expand Down
Loading
Loading