active-boxes 0.0.1.dev2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/LICENSE +2 -2
- active_boxes-0.2.0/PKG-INFO +323 -0
- active_boxes-0.2.0/README.md +294 -0
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/active_boxes/__version__.py +1 -1
- active_boxes-0.2.0/active_boxes/activitypub.py +1637 -0
- active_boxes-0.2.0/active_boxes/backend.py +252 -0
- active_boxes-0.2.0/active_boxes/collection.py +396 -0
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/active_boxes/content_helper.py +21 -19
- active_boxes-0.2.0/active_boxes/data_integrity.py +214 -0
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/active_boxes/errors.py +4 -5
- active_boxes-0.2.0/active_boxes/http_client.py +549 -0
- active_boxes-0.2.0/active_boxes/httpsig.py +933 -0
- active_boxes-0.2.0/active_boxes/key.py +205 -0
- active_boxes-0.2.0/active_boxes/linked_data_sig.py +129 -0
- active_boxes-0.2.0/active_boxes/plugin.py +359 -0
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/active_boxes/urlutils.py +19 -9
- active_boxes-0.2.0/active_boxes/webfinger.py +172 -0
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/pyproject.toml +32 -33
- active_boxes-0.0.1.dev2/PKG-INFO +0 -45
- active_boxes-0.0.1.dev2/README.md +0 -19
- active_boxes-0.0.1.dev2/active_boxes/activitypub.py +0 -984
- active_boxes-0.0.1.dev2/active_boxes/backend.py +0 -128
- active_boxes-0.0.1.dev2/active_boxes/collection.py +0 -70
- active_boxes-0.0.1.dev2/active_boxes/httpsig.py +0 -145
- active_boxes-0.0.1.dev2/active_boxes/key.py +0 -63
- active_boxes-0.0.1.dev2/active_boxes/linked_data_sig.py +0 -83
- active_boxes-0.0.1.dev2/active_boxes/webfinger.py +0 -92
- {active_boxes-0.0.1.dev2 → active_boxes-0.2.0}/active_boxes/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2018, Thomas Sileo
|
|
4
|
-
Copyright (c) 2025, Chaiwat Suttipongsakul
|
|
4
|
+
Copyright (c) 2025-2026, Chaiwat Suttipongsakul
|
|
5
5
|
|
|
6
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
19
19
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
20
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
21
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR PERFORMANCE OF THE
|
|
22
|
-
SOFTWARE.
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: active-boxes
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Tiny ActivityPub framework written in Python, both database and server agnostic.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Chaiwat Suttipongsakul
|
|
8
|
+
Author-email: cwt@bashell.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Requires-Dist: aiohttp (>=3.9.0)
|
|
18
|
+
Requires-Dist: bleach (>=6.0.0)
|
|
19
|
+
Requires-Dist: html2text (>=2020.1.16)
|
|
20
|
+
Requires-Dist: markdown (>=3.4.0)
|
|
21
|
+
Requires-Dist: mdx_linkify (>=1.5.0)
|
|
22
|
+
Requires-Dist: pycryptodome (>=3.18.0)
|
|
23
|
+
Requires-Dist: pyld (>=2.0.0)
|
|
24
|
+
Requires-Dist: regex (>=2023.0.0)
|
|
25
|
+
Requires-Dist: requests (>=2.31.0)
|
|
26
|
+
Project-URL: Homepage, https://github.com/cwt/active-boxes
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Active Boxes (Modernized Little Boxes)
|
|
30
|
+
|
|
31
|
+
This project is a fork of [Little Boxes](https://github.com/tsileo/little-boxes) that has been modernized and relicensed from ISC to MIT.
|
|
32
|
+
|
|
33
|
+
**Modernization Complete, ActivityPub Compliance In Progress**
|
|
34
|
+
|
|
35
|
+
This project has been successfully modernized and updated to current Python packaging standards and Python 3.10+ features. Core ActivityPub functionality is implemented, with federation delivery features under development.
|
|
36
|
+
|
|
37
|
+
The original README can be found in [ORIGINAL-README.md](ORIGINAL-README.md).
|
|
38
|
+
|
|
39
|
+
## Current Status
|
|
40
|
+
|
|
41
|
+
- [x] Migrated from `setup.py` to `pyproject.toml`
|
|
42
|
+
- [x] Moved development dependencies to `pyproject.toml`
|
|
43
|
+
- [x] Switched to Poetry for dependency management and building
|
|
44
|
+
- [x] Updated to require Python 3.10+
|
|
45
|
+
- [x] Created comprehensive modernization plans
|
|
46
|
+
- [x] Modernized codebase to leverage Python 3.10+ features
|
|
47
|
+
- [x] Created comprehensive test suite
|
|
48
|
+
- [x] ActivityPub protocol compliance - Core 11 activities, Extended activities
|
|
49
|
+
- [x] Updated documentation and examples
|
|
50
|
+
- [x] Prepared for stable release
|
|
51
|
+
- [x] **Async-by-default API** with sync wrappers for Flask/Django compatibility
|
|
52
|
+
|
|
53
|
+
## Modernization Features
|
|
54
|
+
|
|
55
|
+
### Python 3.10+ Features
|
|
56
|
+
|
|
57
|
+
- Structural Pattern Matching (match/case statements)
|
|
58
|
+
- Modern Union Types (`X | Y` syntax instead of `Union[X, Y]`)
|
|
59
|
+
- Parenthesized context managers
|
|
60
|
+
- Improved type hinting throughout the codebase
|
|
61
|
+
- Walrus operator usage where appropriate
|
|
62
|
+
- Modern string formatting with f-strings
|
|
63
|
+
|
|
64
|
+
### Code Quality
|
|
65
|
+
|
|
66
|
+
- 100% type hinting coverage
|
|
67
|
+
- Comprehensive test suite with ~89% code coverage
|
|
68
|
+
- Modern code formatting with Black
|
|
69
|
+
- Strict linting with Ruff
|
|
70
|
+
- Type checking with MyPy
|
|
71
|
+
|
|
72
|
+
### Testing
|
|
73
|
+
|
|
74
|
+
- ActivityPub protocol compliance testing (core + extended activities)
|
|
75
|
+
- Integration tests with mock servers
|
|
76
|
+
- Property-based testing for robustness
|
|
77
|
+
- Security-focused test suite (~89% coverage)
|
|
78
|
+
|
|
79
|
+
## Implemented ActivityPub Features
|
|
80
|
+
|
|
81
|
+
### Core Activities [x]
|
|
82
|
+
|
|
83
|
+
Create, Update, Delete, Follow, Accept, Reject, Add, Remove, Like, Block, Undo, Announce
|
|
84
|
+
|
|
85
|
+
### Extended Activities [x]
|
|
86
|
+
|
|
87
|
+
Flag, Move, Join, Leave, View, Listen, Read, Write, Travel, Arrive
|
|
88
|
+
|
|
89
|
+
### Actor Properties [x]
|
|
90
|
+
|
|
91
|
+
inbox, outbox, following, followers, preferredUsername, endpoints (sharedInbox)
|
|
92
|
+
|
|
93
|
+
### Collections [x]
|
|
94
|
+
|
|
95
|
+
Collection, OrderedCollection, CollectionPage, OrderedCollectionPage
|
|
96
|
+
|
|
97
|
+
### Security [x]
|
|
98
|
+
|
|
99
|
+
HTTP Signatures (generation/verification), Linked Data Signatures
|
|
100
|
+
|
|
101
|
+
### Plugin Interface [x]
|
|
102
|
+
|
|
103
|
+
`active_boxes.plugin.ActivityPubPlugin` - Protocol defining app responsibilities
|
|
104
|
+
|
|
105
|
+
### Missing (Under Development)
|
|
106
|
+
|
|
107
|
+
- Per-object Likes/Shares collections
|
|
108
|
+
- Backward pagination in collections
|
|
109
|
+
- Featured collection support
|
|
110
|
+
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
**This is an async-first library** - the primary API uses `async`/`await`. Sync wrappers (e.g., `fetch_iri_sync()`) are available for Flask/Django compatibility.
|
|
114
|
+
|
|
115
|
+
### 1. Implement the Plugin Protocol
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from active_boxes import activitypub as ap
|
|
119
|
+
from active_boxes.plugin import ActivityPubPlugin
|
|
120
|
+
|
|
121
|
+
class MyAppPlugin(ActivityPubPlugin):
|
|
122
|
+
BASE_URL = "https://myapp.example"
|
|
123
|
+
|
|
124
|
+
# Required: URL generation
|
|
125
|
+
def base_url(self) -> str:
|
|
126
|
+
return self.BASE_URL
|
|
127
|
+
|
|
128
|
+
def activity_url(self, obj_id: str) -> str:
|
|
129
|
+
return f"{self.BASE_URL}/activity/{obj_id}"
|
|
130
|
+
|
|
131
|
+
def note_url(self, obj_id: str) -> str:
|
|
132
|
+
return f"{self.BASE_URL}/note/{obj_id}"
|
|
133
|
+
|
|
134
|
+
# Required: Deliver activities to remote inboxes
|
|
135
|
+
async def deliver_activity(
|
|
136
|
+
self,
|
|
137
|
+
activity: dict,
|
|
138
|
+
inbox: str,
|
|
139
|
+
actor: dict,
|
|
140
|
+
) -> bool:
|
|
141
|
+
signed = self.sign_request(activity, actor)
|
|
142
|
+
async with httpx.AsyncClient() as client:
|
|
143
|
+
resp = await client.post(inbox, json=signed)
|
|
144
|
+
return resp.status_code in (200, 201, 202)
|
|
145
|
+
|
|
146
|
+
# Required: Process incoming activities
|
|
147
|
+
async def receive_activity(
|
|
148
|
+
self,
|
|
149
|
+
activity: dict,
|
|
150
|
+
source_inbox: str | None = None,
|
|
151
|
+
) -> bool:
|
|
152
|
+
if self.is_duplicate(activity["id"]):
|
|
153
|
+
return False # Skip duplicate
|
|
154
|
+
await self.store_activity(activity, source_inbox)
|
|
155
|
+
await self.process_activity(activity)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
# Required: Deduplication
|
|
159
|
+
def is_duplicate(self, activity_id: str) -> bool:
|
|
160
|
+
return self.redis.exists(f"activity:{activity_id}")
|
|
161
|
+
|
|
162
|
+
# Optional: Add extra recipients for all activities
|
|
163
|
+
def extra_inboxes(self) -> list[str]:
|
|
164
|
+
return [] # Or add a shared inbox
|
|
165
|
+
|
|
166
|
+
def sign_request(self, activity: dict, actor: dict) -> dict:
|
|
167
|
+
# Your HTTP signature logic here
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. Initialize the Backend
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from active_boxes import activitypub as ap
|
|
175
|
+
|
|
176
|
+
plugin = MyAppPlugin()
|
|
177
|
+
ap.use_backend(plugin)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3. Create and Send Activities
|
|
181
|
+
|
|
182
|
+
**Async (Recommended for FastAPI, aiohttp, etc.):**
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Create a note
|
|
186
|
+
note = ap.Note(
|
|
187
|
+
content="Hello, federation!",
|
|
188
|
+
attributedTo="https://myapp.example/user/alice",
|
|
189
|
+
to=[ap.AS_PUBLIC],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Create the activity wrapping the note
|
|
193
|
+
create = note.build_create()
|
|
194
|
+
create.set_id("https://myapp.example/activity/abc123", "abc123")
|
|
195
|
+
|
|
196
|
+
# Get recipients and deliver
|
|
197
|
+
recipients = create.recipients() # Computed by library
|
|
198
|
+
for inbox in recipients:
|
|
199
|
+
actor = await fetch_actor(create.get_actor().id)
|
|
200
|
+
await plugin.deliver_activity(create.to_dict(), inbox, actor)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Sync (For Flask, Django sync views):**
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Create a note
|
|
207
|
+
note = ap.Note(
|
|
208
|
+
content="Hello, federation!",
|
|
209
|
+
attributedTo="https://myapp.example/user/alice",
|
|
210
|
+
to=[ap.AS_PUBLIC],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Create the activity wrapping the note
|
|
214
|
+
create = note.build_create()
|
|
215
|
+
create.set_id("https://myapp.example/activity/abc123", "abc123")
|
|
216
|
+
|
|
217
|
+
# Get recipients and deliver (sync wrapper)
|
|
218
|
+
recipients = create.recipients()
|
|
219
|
+
for inbox in recipients:
|
|
220
|
+
actor = fetch_actor_sync(create.get_actor_sync().id)
|
|
221
|
+
plugin.deliver_activity(create.to_dict(), inbox, actor)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 4. Receive Activities
|
|
225
|
+
|
|
226
|
+
**Async (FastAPI, aiohttp):**
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# In your inbox endpoint handler
|
|
230
|
+
async def inbox_handler(request):
|
|
231
|
+
activity = await request.json()
|
|
232
|
+
await plugin.receive_activity(activity, source_inbox=str(request.url))
|
|
233
|
+
return web.Response(status=202)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Sync (Flask, Django sync views):**
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
# In your Flask route
|
|
240
|
+
@app.post("/inbox")
|
|
241
|
+
def inbox():
|
|
242
|
+
activity = request.get_json()
|
|
243
|
+
plugin.receive_activity_sync(activity, source_inbox=request.url)
|
|
244
|
+
return "", 202
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 5. Working with Actors
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# Create a person actor
|
|
251
|
+
person = ap.Person(
|
|
252
|
+
id="https://myapp.example/user/alice",
|
|
253
|
+
inbox="https://myapp.example/user/alice/inbox",
|
|
254
|
+
outbox="https://myapp.example/user/alice/outbox",
|
|
255
|
+
followers="https://myapp.example/user/alice/followers",
|
|
256
|
+
preferredUsername="alice",
|
|
257
|
+
publicKey={
|
|
258
|
+
"id": "https://myapp.example/user/alice#main-key",
|
|
259
|
+
"owner": "https://myapp.example/user/alice",
|
|
260
|
+
"publicKeyPem": "-----BEGIN PUBLIC KEY-----...",
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 6. Collection Pagination
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
# Build a paginated outbox
|
|
269
|
+
outbox = ap.OrderedCollection(
|
|
270
|
+
id="https://myapp.example/user/alice/outbox",
|
|
271
|
+
totalItems=42,
|
|
272
|
+
first="https://myapp.example/user/alice/outbox?page=1",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Library handles parsing remote collections (async)
|
|
276
|
+
items = await backend.parse_collection(url="https://example.com/user/bob/outbox")
|
|
277
|
+
|
|
278
|
+
# Or use sync wrapper
|
|
279
|
+
items = backend.parse_collection_sync(url="https://example.com/user/bob/outbox")
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## API Naming Convention
|
|
283
|
+
|
|
284
|
+
The library uses an **async-first** naming convention:
|
|
285
|
+
|
|
286
|
+
| Operation | Async (Primary) | Sync Wrapper |
|
|
287
|
+
|-----------|----------------|--------------|
|
|
288
|
+
| Fetch IRI | `fetch_iri()` | `fetch_iri_sync()` |
|
|
289
|
+
| Fetch JSON | `fetch_json()` | `fetch_json_sync()` |
|
|
290
|
+
| Get Actor | `get_actor()` | `get_actor_sync()` |
|
|
291
|
+
| Get Object | `get_object()` | `get_object_sync()` |
|
|
292
|
+
| WebFinger | `webfinger()` | `webfinger_sync()` |
|
|
293
|
+
| Verify Signature | `verify_request()` | `verify_request_sync()` |
|
|
294
|
+
| Parse Collection | `parse_collection()` | `parse_collection_sync()` |
|
|
295
|
+
|
|
296
|
+
**Guideline:** Use async methods by default. Use `_sync()` variants only when integrating with sync frameworks like Flask or Django sync views.
|
|
297
|
+
|
|
298
|
+
## Plugin Responsibilities
|
|
299
|
+
|
|
300
|
+
| What Library Does | What Your App Does |
|
|
301
|
+
|-------------------|-------------------|
|
|
302
|
+
| Activity/Object serialization | HTTP client setup (httpx, aiohttp, etc.) |
|
|
303
|
+
| Computing recipients | Signing outgoing requests (HTTP Signatures) |
|
|
304
|
+
| HTTP Signature generation | Delivering to remote inboxes |
|
|
305
|
+
| HTTP Signature verification | Receiving from remote inboxes |
|
|
306
|
+
| Collection pagination | Storing activities persistently |
|
|
307
|
+
| Activity vocabulary (Create, Follow, etc.) | Deduplication |
|
|
308
|
+
| WebFinger support | Retry/backoff logic |
|
|
309
|
+
|
|
310
|
+
## Modernization Plans
|
|
311
|
+
|
|
312
|
+
Detailed planning documents have been created to guide the modernization effort:
|
|
313
|
+
|
|
314
|
+
- [MODERNIZE_PLAN.md](documents/MODERNIZE_PLAN.md) - Overall modernization strategy
|
|
315
|
+
- [PYTHON_310_MODERNIZATION.md](documents/PYTHON_310_MODERNIZATION.md) - Python 3.10+ feature implementation
|
|
316
|
+
- [TEST_SUITE_IMPROVEMENTS.md](documents/TEST_SUITE_IMPROVEMENTS.md) - Test suite enhancement plans
|
|
317
|
+
- [ACTIVITYPUB_COMPLIANCE.md](documents/ACTIVITYPUB_COMPLIANCE.md) - ActivityPub protocol compliance requirements
|
|
318
|
+
- [IMPLEMENTATION_PLAN.md](documents/IMPLEMENTATION_PLAN.md) - Detailed 8-week implementation timeline
|
|
319
|
+
|
|
320
|
+
## Original Project
|
|
321
|
+
|
|
322
|
+
For information about the original project, please refer to [ORIGINAL-README.md](ORIGINAL-README.md).
|
|
323
|
+
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Active Boxes (Modernized Little Boxes)
|
|
2
|
+
|
|
3
|
+
This project is a fork of [Little Boxes](https://github.com/tsileo/little-boxes) that has been modernized and relicensed from ISC to MIT.
|
|
4
|
+
|
|
5
|
+
**Modernization Complete, ActivityPub Compliance In Progress**
|
|
6
|
+
|
|
7
|
+
This project has been successfully modernized and updated to current Python packaging standards and Python 3.10+ features. Core ActivityPub functionality is implemented, with federation delivery features under development.
|
|
8
|
+
|
|
9
|
+
The original README can be found in [ORIGINAL-README.md](ORIGINAL-README.md).
|
|
10
|
+
|
|
11
|
+
## Current Status
|
|
12
|
+
|
|
13
|
+
- [x] Migrated from `setup.py` to `pyproject.toml`
|
|
14
|
+
- [x] Moved development dependencies to `pyproject.toml`
|
|
15
|
+
- [x] Switched to Poetry for dependency management and building
|
|
16
|
+
- [x] Updated to require Python 3.10+
|
|
17
|
+
- [x] Created comprehensive modernization plans
|
|
18
|
+
- [x] Modernized codebase to leverage Python 3.10+ features
|
|
19
|
+
- [x] Created comprehensive test suite
|
|
20
|
+
- [x] ActivityPub protocol compliance - Core 11 activities, Extended activities
|
|
21
|
+
- [x] Updated documentation and examples
|
|
22
|
+
- [x] Prepared for stable release
|
|
23
|
+
- [x] **Async-by-default API** with sync wrappers for Flask/Django compatibility
|
|
24
|
+
|
|
25
|
+
## Modernization Features
|
|
26
|
+
|
|
27
|
+
### Python 3.10+ Features
|
|
28
|
+
|
|
29
|
+
- Structural Pattern Matching (match/case statements)
|
|
30
|
+
- Modern Union Types (`X | Y` syntax instead of `Union[X, Y]`)
|
|
31
|
+
- Parenthesized context managers
|
|
32
|
+
- Improved type hinting throughout the codebase
|
|
33
|
+
- Walrus operator usage where appropriate
|
|
34
|
+
- Modern string formatting with f-strings
|
|
35
|
+
|
|
36
|
+
### Code Quality
|
|
37
|
+
|
|
38
|
+
- 100% type hinting coverage
|
|
39
|
+
- Comprehensive test suite with ~89% code coverage
|
|
40
|
+
- Modern code formatting with Black
|
|
41
|
+
- Strict linting with Ruff
|
|
42
|
+
- Type checking with MyPy
|
|
43
|
+
|
|
44
|
+
### Testing
|
|
45
|
+
|
|
46
|
+
- ActivityPub protocol compliance testing (core + extended activities)
|
|
47
|
+
- Integration tests with mock servers
|
|
48
|
+
- Property-based testing for robustness
|
|
49
|
+
- Security-focused test suite (~89% coverage)
|
|
50
|
+
|
|
51
|
+
## Implemented ActivityPub Features
|
|
52
|
+
|
|
53
|
+
### Core Activities [x]
|
|
54
|
+
|
|
55
|
+
Create, Update, Delete, Follow, Accept, Reject, Add, Remove, Like, Block, Undo, Announce
|
|
56
|
+
|
|
57
|
+
### Extended Activities [x]
|
|
58
|
+
|
|
59
|
+
Flag, Move, Join, Leave, View, Listen, Read, Write, Travel, Arrive
|
|
60
|
+
|
|
61
|
+
### Actor Properties [x]
|
|
62
|
+
|
|
63
|
+
inbox, outbox, following, followers, preferredUsername, endpoints (sharedInbox)
|
|
64
|
+
|
|
65
|
+
### Collections [x]
|
|
66
|
+
|
|
67
|
+
Collection, OrderedCollection, CollectionPage, OrderedCollectionPage
|
|
68
|
+
|
|
69
|
+
### Security [x]
|
|
70
|
+
|
|
71
|
+
HTTP Signatures (generation/verification), Linked Data Signatures
|
|
72
|
+
|
|
73
|
+
### Plugin Interface [x]
|
|
74
|
+
|
|
75
|
+
`active_boxes.plugin.ActivityPubPlugin` - Protocol defining app responsibilities
|
|
76
|
+
|
|
77
|
+
### Missing (Under Development)
|
|
78
|
+
|
|
79
|
+
- Per-object Likes/Shares collections
|
|
80
|
+
- Backward pagination in collections
|
|
81
|
+
- Featured collection support
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
**This is an async-first library** - the primary API uses `async`/`await`. Sync wrappers (e.g., `fetch_iri_sync()`) are available for Flask/Django compatibility.
|
|
86
|
+
|
|
87
|
+
### 1. Implement the Plugin Protocol
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from active_boxes import activitypub as ap
|
|
91
|
+
from active_boxes.plugin import ActivityPubPlugin
|
|
92
|
+
|
|
93
|
+
class MyAppPlugin(ActivityPubPlugin):
|
|
94
|
+
BASE_URL = "https://myapp.example"
|
|
95
|
+
|
|
96
|
+
# Required: URL generation
|
|
97
|
+
def base_url(self) -> str:
|
|
98
|
+
return self.BASE_URL
|
|
99
|
+
|
|
100
|
+
def activity_url(self, obj_id: str) -> str:
|
|
101
|
+
return f"{self.BASE_URL}/activity/{obj_id}"
|
|
102
|
+
|
|
103
|
+
def note_url(self, obj_id: str) -> str:
|
|
104
|
+
return f"{self.BASE_URL}/note/{obj_id}"
|
|
105
|
+
|
|
106
|
+
# Required: Deliver activities to remote inboxes
|
|
107
|
+
async def deliver_activity(
|
|
108
|
+
self,
|
|
109
|
+
activity: dict,
|
|
110
|
+
inbox: str,
|
|
111
|
+
actor: dict,
|
|
112
|
+
) -> bool:
|
|
113
|
+
signed = self.sign_request(activity, actor)
|
|
114
|
+
async with httpx.AsyncClient() as client:
|
|
115
|
+
resp = await client.post(inbox, json=signed)
|
|
116
|
+
return resp.status_code in (200, 201, 202)
|
|
117
|
+
|
|
118
|
+
# Required: Process incoming activities
|
|
119
|
+
async def receive_activity(
|
|
120
|
+
self,
|
|
121
|
+
activity: dict,
|
|
122
|
+
source_inbox: str | None = None,
|
|
123
|
+
) -> bool:
|
|
124
|
+
if self.is_duplicate(activity["id"]):
|
|
125
|
+
return False # Skip duplicate
|
|
126
|
+
await self.store_activity(activity, source_inbox)
|
|
127
|
+
await self.process_activity(activity)
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
# Required: Deduplication
|
|
131
|
+
def is_duplicate(self, activity_id: str) -> bool:
|
|
132
|
+
return self.redis.exists(f"activity:{activity_id}")
|
|
133
|
+
|
|
134
|
+
# Optional: Add extra recipients for all activities
|
|
135
|
+
def extra_inboxes(self) -> list[str]:
|
|
136
|
+
return [] # Or add a shared inbox
|
|
137
|
+
|
|
138
|
+
def sign_request(self, activity: dict, actor: dict) -> dict:
|
|
139
|
+
# Your HTTP signature logic here
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 2. Initialize the Backend
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from active_boxes import activitypub as ap
|
|
147
|
+
|
|
148
|
+
plugin = MyAppPlugin()
|
|
149
|
+
ap.use_backend(plugin)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. Create and Send Activities
|
|
153
|
+
|
|
154
|
+
**Async (Recommended for FastAPI, aiohttp, etc.):**
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Create a note
|
|
158
|
+
note = ap.Note(
|
|
159
|
+
content="Hello, federation!",
|
|
160
|
+
attributedTo="https://myapp.example/user/alice",
|
|
161
|
+
to=[ap.AS_PUBLIC],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Create the activity wrapping the note
|
|
165
|
+
create = note.build_create()
|
|
166
|
+
create.set_id("https://myapp.example/activity/abc123", "abc123")
|
|
167
|
+
|
|
168
|
+
# Get recipients and deliver
|
|
169
|
+
recipients = create.recipients() # Computed by library
|
|
170
|
+
for inbox in recipients:
|
|
171
|
+
actor = await fetch_actor(create.get_actor().id)
|
|
172
|
+
await plugin.deliver_activity(create.to_dict(), inbox, actor)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Sync (For Flask, Django sync views):**
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# Create a note
|
|
179
|
+
note = ap.Note(
|
|
180
|
+
content="Hello, federation!",
|
|
181
|
+
attributedTo="https://myapp.example/user/alice",
|
|
182
|
+
to=[ap.AS_PUBLIC],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Create the activity wrapping the note
|
|
186
|
+
create = note.build_create()
|
|
187
|
+
create.set_id("https://myapp.example/activity/abc123", "abc123")
|
|
188
|
+
|
|
189
|
+
# Get recipients and deliver (sync wrapper)
|
|
190
|
+
recipients = create.recipients()
|
|
191
|
+
for inbox in recipients:
|
|
192
|
+
actor = fetch_actor_sync(create.get_actor_sync().id)
|
|
193
|
+
plugin.deliver_activity(create.to_dict(), inbox, actor)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 4. Receive Activities
|
|
197
|
+
|
|
198
|
+
**Async (FastAPI, aiohttp):**
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# In your inbox endpoint handler
|
|
202
|
+
async def inbox_handler(request):
|
|
203
|
+
activity = await request.json()
|
|
204
|
+
await plugin.receive_activity(activity, source_inbox=str(request.url))
|
|
205
|
+
return web.Response(status=202)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Sync (Flask, Django sync views):**
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# In your Flask route
|
|
212
|
+
@app.post("/inbox")
|
|
213
|
+
def inbox():
|
|
214
|
+
activity = request.get_json()
|
|
215
|
+
plugin.receive_activity_sync(activity, source_inbox=request.url)
|
|
216
|
+
return "", 202
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 5. Working with Actors
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# Create a person actor
|
|
223
|
+
person = ap.Person(
|
|
224
|
+
id="https://myapp.example/user/alice",
|
|
225
|
+
inbox="https://myapp.example/user/alice/inbox",
|
|
226
|
+
outbox="https://myapp.example/user/alice/outbox",
|
|
227
|
+
followers="https://myapp.example/user/alice/followers",
|
|
228
|
+
preferredUsername="alice",
|
|
229
|
+
publicKey={
|
|
230
|
+
"id": "https://myapp.example/user/alice#main-key",
|
|
231
|
+
"owner": "https://myapp.example/user/alice",
|
|
232
|
+
"publicKeyPem": "-----BEGIN PUBLIC KEY-----...",
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 6. Collection Pagination
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# Build a paginated outbox
|
|
241
|
+
outbox = ap.OrderedCollection(
|
|
242
|
+
id="https://myapp.example/user/alice/outbox",
|
|
243
|
+
totalItems=42,
|
|
244
|
+
first="https://myapp.example/user/alice/outbox?page=1",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Library handles parsing remote collections (async)
|
|
248
|
+
items = await backend.parse_collection(url="https://example.com/user/bob/outbox")
|
|
249
|
+
|
|
250
|
+
# Or use sync wrapper
|
|
251
|
+
items = backend.parse_collection_sync(url="https://example.com/user/bob/outbox")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## API Naming Convention
|
|
255
|
+
|
|
256
|
+
The library uses an **async-first** naming convention:
|
|
257
|
+
|
|
258
|
+
| Operation | Async (Primary) | Sync Wrapper |
|
|
259
|
+
|-----------|----------------|--------------|
|
|
260
|
+
| Fetch IRI | `fetch_iri()` | `fetch_iri_sync()` |
|
|
261
|
+
| Fetch JSON | `fetch_json()` | `fetch_json_sync()` |
|
|
262
|
+
| Get Actor | `get_actor()` | `get_actor_sync()` |
|
|
263
|
+
| Get Object | `get_object()` | `get_object_sync()` |
|
|
264
|
+
| WebFinger | `webfinger()` | `webfinger_sync()` |
|
|
265
|
+
| Verify Signature | `verify_request()` | `verify_request_sync()` |
|
|
266
|
+
| Parse Collection | `parse_collection()` | `parse_collection_sync()` |
|
|
267
|
+
|
|
268
|
+
**Guideline:** Use async methods by default. Use `_sync()` variants only when integrating with sync frameworks like Flask or Django sync views.
|
|
269
|
+
|
|
270
|
+
## Plugin Responsibilities
|
|
271
|
+
|
|
272
|
+
| What Library Does | What Your App Does |
|
|
273
|
+
|-------------------|-------------------|
|
|
274
|
+
| Activity/Object serialization | HTTP client setup (httpx, aiohttp, etc.) |
|
|
275
|
+
| Computing recipients | Signing outgoing requests (HTTP Signatures) |
|
|
276
|
+
| HTTP Signature generation | Delivering to remote inboxes |
|
|
277
|
+
| HTTP Signature verification | Receiving from remote inboxes |
|
|
278
|
+
| Collection pagination | Storing activities persistently |
|
|
279
|
+
| Activity vocabulary (Create, Follow, etc.) | Deduplication |
|
|
280
|
+
| WebFinger support | Retry/backoff logic |
|
|
281
|
+
|
|
282
|
+
## Modernization Plans
|
|
283
|
+
|
|
284
|
+
Detailed planning documents have been created to guide the modernization effort:
|
|
285
|
+
|
|
286
|
+
- [MODERNIZE_PLAN.md](documents/MODERNIZE_PLAN.md) - Overall modernization strategy
|
|
287
|
+
- [PYTHON_310_MODERNIZATION.md](documents/PYTHON_310_MODERNIZATION.md) - Python 3.10+ feature implementation
|
|
288
|
+
- [TEST_SUITE_IMPROVEMENTS.md](documents/TEST_SUITE_IMPROVEMENTS.md) - Test suite enhancement plans
|
|
289
|
+
- [ACTIVITYPUB_COMPLIANCE.md](documents/ACTIVITYPUB_COMPLIANCE.md) - ActivityPub protocol compliance requirements
|
|
290
|
+
- [IMPLEMENTATION_PLAN.md](documents/IMPLEMENTATION_PLAN.md) - Detailed 8-week implementation timeline
|
|
291
|
+
|
|
292
|
+
## Original Project
|
|
293
|
+
|
|
294
|
+
For information about the original project, please refer to [ORIGINAL-README.md](ORIGINAL-README.md).
|