fleetmesh-node 0.2.0__py3-none-any.whl
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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""ActivityStreams 2.0 object and activity models.
|
|
2
|
+
|
|
3
|
+
Standard library only. Defines:
|
|
4
|
+
- Core ActivityStreams 2.0 objects: Note, Article, Image, Document.
|
|
5
|
+
- Core ActivityPub activities: Create, Follow, Accept, Undo, Delete, Announce, Like.
|
|
6
|
+
- Serialization and parsing helpers with schema validation.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Mapping, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
from fleetmesh_core.conformance.errors import FleetmeshError
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'ActivityPubError',
|
|
17
|
+
'AS_CONTEXT',
|
|
18
|
+
'SECURITY_CONTEXT',
|
|
19
|
+
'Activity',
|
|
20
|
+
'Note',
|
|
21
|
+
'parse_activity',
|
|
22
|
+
'build_note',
|
|
23
|
+
'build_create_activity',
|
|
24
|
+
'build_follow_activity',
|
|
25
|
+
'build_accept_activity',
|
|
26
|
+
'build_undo_activity',
|
|
27
|
+
'build_delete_activity',
|
|
28
|
+
'build_announce_activity',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
AS_CONTEXT = "https://www.w3.org/ns/activitystreams"
|
|
32
|
+
SECURITY_CONTEXT = "https://w3id.org/security/v1"
|
|
33
|
+
PUBLIC_COLLECTION = "https://www.w3.org/ns/activitystreams#Public"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ActivityPubError(FleetmeshError):
|
|
37
|
+
"""Base error for ActivityPub data and parsing."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
from node.protocols.isotime import now_iso as _now_iso # noqa: E402
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Note:
|
|
44
|
+
"""An ActivityStreams Note object (microblog post)."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
id: str,
|
|
49
|
+
attributed_to: str,
|
|
50
|
+
content: str,
|
|
51
|
+
to: Optional[Sequence[str]] = None,
|
|
52
|
+
cc: Optional[Sequence[str]] = None,
|
|
53
|
+
published: Optional[str] = None,
|
|
54
|
+
in_reply_to: Optional[str] = None,
|
|
55
|
+
attachment: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
56
|
+
url: Optional[str] = None,
|
|
57
|
+
):
|
|
58
|
+
self.id = id
|
|
59
|
+
self.attributed_to = attributed_to
|
|
60
|
+
self.content = content
|
|
61
|
+
self.to = list(to) if to is not None else [PUBLIC_COLLECTION]
|
|
62
|
+
self.cc = list(cc) if cc is not None else []
|
|
63
|
+
self.published = published or _now_iso()
|
|
64
|
+
self.in_reply_to = in_reply_to
|
|
65
|
+
self.attachment = list(attachment) if attachment is not None else []
|
|
66
|
+
self.url = url or id
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Mapping[str, Any]:
|
|
69
|
+
doc = {
|
|
70
|
+
'@context': AS_CONTEXT,
|
|
71
|
+
'id': self.id,
|
|
72
|
+
'type': 'Note',
|
|
73
|
+
'attributedTo': self.attributed_to,
|
|
74
|
+
'content': self.content,
|
|
75
|
+
'to': self.to,
|
|
76
|
+
'cc': self.cc,
|
|
77
|
+
'published': self.published,
|
|
78
|
+
'url': self.url,
|
|
79
|
+
**({'inReplyTo': self.in_reply_to} if self.in_reply_to else {}),
|
|
80
|
+
**({'attachment': self.attachment} if self.attachment else {}),
|
|
81
|
+
}
|
|
82
|
+
return doc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Activity:
|
|
86
|
+
"""An ActivityStreams Activity (Create, Follow, Accept, Undo, etc.)."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
id: str,
|
|
91
|
+
type: str,
|
|
92
|
+
actor: str,
|
|
93
|
+
object: Union[str, Mapping[str, Any]],
|
|
94
|
+
to: Optional[Sequence[str]] = None,
|
|
95
|
+
cc: Optional[Sequence[str]] = None,
|
|
96
|
+
published: Optional[str] = None,
|
|
97
|
+
):
|
|
98
|
+
self.id = id
|
|
99
|
+
self.type = type
|
|
100
|
+
self.actor = actor
|
|
101
|
+
self.object = object
|
|
102
|
+
self.to = list(to) if to is not None else [PUBLIC_COLLECTION]
|
|
103
|
+
self.cc = list(cc) if cc is not None else []
|
|
104
|
+
self.published = published or _now_iso()
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def object_id(self) -> str:
|
|
108
|
+
"""Extract object ID whether object is a string URI or embedded dict."""
|
|
109
|
+
if isinstance(self.object, dict):
|
|
110
|
+
return self.object.get('id', '')
|
|
111
|
+
return str(self.object)
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> Mapping[str, Any]:
|
|
114
|
+
return {
|
|
115
|
+
'@context': AS_CONTEXT,
|
|
116
|
+
'id': self.id,
|
|
117
|
+
'type': self.type,
|
|
118
|
+
'actor': self.actor,
|
|
119
|
+
'object': self.object,
|
|
120
|
+
'to': self.to,
|
|
121
|
+
'cc': self.cc,
|
|
122
|
+
'published': self.published,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_activity(data: Union[str, bytes, Mapping[str, Any]]) -> Activity:
|
|
127
|
+
"""Parse JSON or dict into an Activity instance."""
|
|
128
|
+
if isinstance(data, (str, bytes)):
|
|
129
|
+
try:
|
|
130
|
+
doc = json.loads(data)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise ActivityPubError(f"invalid JSON: {e}") from e
|
|
133
|
+
else:
|
|
134
|
+
doc = dict(data)
|
|
135
|
+
|
|
136
|
+
if not isinstance(doc, dict):
|
|
137
|
+
raise ActivityPubError("activity must be a JSON object")
|
|
138
|
+
|
|
139
|
+
act_type = doc.get('type')
|
|
140
|
+
if not act_type or not isinstance(act_type, str):
|
|
141
|
+
raise ActivityPubError("activity lacks valid type")
|
|
142
|
+
|
|
143
|
+
actor = doc.get('actor')
|
|
144
|
+
if not actor:
|
|
145
|
+
raise ActivityPubError("activity lacks actor")
|
|
146
|
+
|
|
147
|
+
act_id = doc.get('id', '')
|
|
148
|
+
obj = doc.get('object', '')
|
|
149
|
+
to = doc.get('to')
|
|
150
|
+
cc = doc.get('cc')
|
|
151
|
+
pub = doc.get('published')
|
|
152
|
+
|
|
153
|
+
to_list = [to] if isinstance(to, str) else list(to) if isinstance(to, list) else None
|
|
154
|
+
cc_list = [cc] if isinstance(cc, str) else list(cc) if isinstance(cc, list) else None
|
|
155
|
+
|
|
156
|
+
return Activity(
|
|
157
|
+
id=act_id,
|
|
158
|
+
type=act_type,
|
|
159
|
+
actor=actor,
|
|
160
|
+
object=obj,
|
|
161
|
+
to=to_list,
|
|
162
|
+
cc=cc_list,
|
|
163
|
+
published=pub,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_note(
|
|
168
|
+
id: str,
|
|
169
|
+
attributed_to: str,
|
|
170
|
+
content: str,
|
|
171
|
+
to: Optional[Sequence[str]] = None,
|
|
172
|
+
cc: Optional[Sequence[str]] = None,
|
|
173
|
+
in_reply_to: Optional[str] = None,
|
|
174
|
+
attachment: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
175
|
+
) -> Mapping[str, Any]:
|
|
176
|
+
"""Helper to build a standard Note dictionary."""
|
|
177
|
+
return Note(
|
|
178
|
+
id=id,
|
|
179
|
+
attributed_to=attributed_to,
|
|
180
|
+
content=content,
|
|
181
|
+
to=to,
|
|
182
|
+
cc=cc,
|
|
183
|
+
in_reply_to=in_reply_to,
|
|
184
|
+
attachment=attachment,
|
|
185
|
+
).to_dict()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_create_activity(
|
|
189
|
+
activity_id: str,
|
|
190
|
+
actor_uri: str,
|
|
191
|
+
note_or_object: Mapping[str, Any],
|
|
192
|
+
to: Optional[Sequence[str]] = None,
|
|
193
|
+
cc: Optional[Sequence[str]] = None,
|
|
194
|
+
) -> Mapping[str, Any]:
|
|
195
|
+
"""Wrap an object in a Create activity."""
|
|
196
|
+
return Activity(
|
|
197
|
+
id=activity_id,
|
|
198
|
+
type='Create',
|
|
199
|
+
actor=actor_uri,
|
|
200
|
+
object=note_or_object,
|
|
201
|
+
to=to or note_or_object.get('to'),
|
|
202
|
+
cc=cc or note_or_object.get('cc'),
|
|
203
|
+
).to_dict()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_follow_activity(
|
|
207
|
+
activity_id: str,
|
|
208
|
+
actor_uri: str,
|
|
209
|
+
target_actor_uri: str,
|
|
210
|
+
) -> Mapping[str, Any]:
|
|
211
|
+
"""Build a Follow activity targeting another actor."""
|
|
212
|
+
return Activity(
|
|
213
|
+
id=activity_id,
|
|
214
|
+
type='Follow',
|
|
215
|
+
actor=actor_uri,
|
|
216
|
+
object=target_actor_uri,
|
|
217
|
+
to=[target_actor_uri],
|
|
218
|
+
cc=[],
|
|
219
|
+
).to_dict()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def build_accept_activity(
|
|
223
|
+
activity_id: str,
|
|
224
|
+
actor_uri: str,
|
|
225
|
+
follow_activity: Union[str, Mapping[str, Any]],
|
|
226
|
+
recipient_uri: str,
|
|
227
|
+
) -> Mapping[str, Any]:
|
|
228
|
+
"""Build an Accept activity responding to a Follow."""
|
|
229
|
+
return Activity(
|
|
230
|
+
id=activity_id,
|
|
231
|
+
type='Accept',
|
|
232
|
+
actor=actor_uri,
|
|
233
|
+
object=follow_activity,
|
|
234
|
+
to=[recipient_uri],
|
|
235
|
+
cc=[],
|
|
236
|
+
).to_dict()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_undo_activity(
|
|
240
|
+
activity_id: str,
|
|
241
|
+
actor_uri: str,
|
|
242
|
+
activity_to_undo: Union[str, Mapping[str, Any]],
|
|
243
|
+
recipient_uri: Optional[str] = None,
|
|
244
|
+
) -> Mapping[str, Any]:
|
|
245
|
+
"""Build an Undo activity (e.g. unfollow or unlike)."""
|
|
246
|
+
to_list = [recipient_uri] if recipient_uri else [PUBLIC_COLLECTION]
|
|
247
|
+
return Activity(
|
|
248
|
+
id=activity_id,
|
|
249
|
+
type='Undo',
|
|
250
|
+
actor=actor_uri,
|
|
251
|
+
object=activity_to_undo,
|
|
252
|
+
to=to_list,
|
|
253
|
+
cc=[],
|
|
254
|
+
).to_dict()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_delete_activity(
|
|
258
|
+
activity_id: str,
|
|
259
|
+
actor_uri: str,
|
|
260
|
+
object_uri: str,
|
|
261
|
+
) -> Mapping[str, Any]:
|
|
262
|
+
"""Build a Delete activity removing a previously published object."""
|
|
263
|
+
return Activity(
|
|
264
|
+
id=activity_id,
|
|
265
|
+
type='Delete',
|
|
266
|
+
actor=actor_uri,
|
|
267
|
+
object=object_uri,
|
|
268
|
+
to=[PUBLIC_COLLECTION],
|
|
269
|
+
cc=[],
|
|
270
|
+
).to_dict()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_announce_activity(
|
|
274
|
+
activity_id: str,
|
|
275
|
+
actor_uri: str,
|
|
276
|
+
object_uri: str,
|
|
277
|
+
) -> Mapping[str, Any]:
|
|
278
|
+
"""Build an Announce (boost/repost) activity."""
|
|
279
|
+
return Activity(
|
|
280
|
+
id=activity_id,
|
|
281
|
+
type='Announce',
|
|
282
|
+
actor=actor_uri,
|
|
283
|
+
object=object_uri,
|
|
284
|
+
to=[PUBLIC_COLLECTION],
|
|
285
|
+
cc=[],
|
|
286
|
+
).to_dict()
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""fleetmesh-protocol-activitypub: ActivityPub & ActivityStreams 2.0 package.
|
|
2
|
+
|
|
3
|
+
Conforms to ProtocolPackage (fleetmesh_core.wire.interfaces.ProtocolPackage).
|
|
4
|
+
Mounts WebFinger (RFC 7033), Actor profiles, S2S Inbox federation,
|
|
5
|
+
Outbox feeds, and Blossom unified media onto ProtocolHostRouter.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import urllib.parse
|
|
11
|
+
from typing import Any, Mapping, Optional
|
|
12
|
+
|
|
13
|
+
from fleetmesh_core.wire.interfaces import (
|
|
14
|
+
ProtocolHealth,
|
|
15
|
+
ProtocolHostRouter,
|
|
16
|
+
ProtocolIdentityAdapter,
|
|
17
|
+
)
|
|
18
|
+
from node.protocols.activitypub.actor import Actor, WebFingerService, ed25519_pubkey_to_pem
|
|
19
|
+
from node.protocols.activitypub.federation import FederationService, StrainScoreKeeper
|
|
20
|
+
from node.protocols.activitypub.media import ActivityPubMediaManager
|
|
21
|
+
from node.protocols.activitypub.store import ActivityPubStore
|
|
22
|
+
from node.protocols.base import BaseProtocolPackage, Ed25519IdentityAdapter
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'ActivityPubIdentityAdapter',
|
|
26
|
+
'ActivityPubProtocolPackage',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ActivityPubIdentityAdapter(Ed25519IdentityAdapter):
|
|
31
|
+
"""Translates between Fleetmesh Ed25519 identities and ActivityPub Actor URIs."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, actor: Actor, private_key: Any = None):
|
|
34
|
+
super().__init__(private_key=private_key)
|
|
35
|
+
self.actor = actor
|
|
36
|
+
|
|
37
|
+
def resolve_id(self, uri: str) -> Optional[str]:
|
|
38
|
+
if uri in (self.actor.acct_uri, self.actor.actor_url):
|
|
39
|
+
return self.actor.actor_url
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def verify_assertion(self, payload: bytes, signature: bytes, pubkey: str) -> bool:
|
|
43
|
+
try:
|
|
44
|
+
raw_pub = bytes.fromhex(pubkey) if len(pubkey) == 64 else pubkey.encode('utf-8')
|
|
45
|
+
except ValueError:
|
|
46
|
+
return False
|
|
47
|
+
return self._verify_ed25519_bytes(payload, signature, raw_pub)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ActivityPubProtocolPackage(BaseProtocolPackage):
|
|
51
|
+
"""The ActivityPub & ActivityStreams 2.0 suite for Fleetmesh."""
|
|
52
|
+
|
|
53
|
+
name: str = 'activitypub'
|
|
54
|
+
version: str = '1.0.0'
|
|
55
|
+
standards: tuple[str, ...] = (
|
|
56
|
+
'activitypub',
|
|
57
|
+
'activitystreams-2.0',
|
|
58
|
+
'RFC-7033',
|
|
59
|
+
'RFC-9421',
|
|
60
|
+
'rfc7033-webfinger',
|
|
61
|
+
'draft-cavage-http-signatures',
|
|
62
|
+
'rfc9421',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
domain: str = "node.fleetmesh.local",
|
|
68
|
+
username: str = "node",
|
|
69
|
+
actor: Optional[Actor] = None,
|
|
70
|
+
store: Optional[ActivityPubStore] = None,
|
|
71
|
+
private_key: Optional[Any] = None,
|
|
72
|
+
blob_store: Optional[Any] = None,
|
|
73
|
+
strain_ledger: Optional[Any] = None,
|
|
74
|
+
keystore: Optional[Any] = None,
|
|
75
|
+
):
|
|
76
|
+
self.domain = domain
|
|
77
|
+
self.username = username
|
|
78
|
+
self.keystore = keystore
|
|
79
|
+
self.private_key = private_key
|
|
80
|
+
self.blob_store = blob_store
|
|
81
|
+
|
|
82
|
+
# Storage
|
|
83
|
+
self.store = store or ActivityPubStore(":memory:")
|
|
84
|
+
|
|
85
|
+
# Actor Document
|
|
86
|
+
pub_pem = ""
|
|
87
|
+
if private_key is not None:
|
|
88
|
+
try:
|
|
89
|
+
pub_bytes = private_key.public_key().public_bytes_raw()
|
|
90
|
+
pub_pem = ed25519_pubkey_to_pem(pub_bytes)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
self.actor = actor or Actor(
|
|
95
|
+
domain=domain,
|
|
96
|
+
username=username,
|
|
97
|
+
public_key_pem=pub_pem,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# WebFinger Service
|
|
101
|
+
self.webfinger = WebFingerService(self.actor)
|
|
102
|
+
|
|
103
|
+
# Media Manager
|
|
104
|
+
self.media = ActivityPubMediaManager(
|
|
105
|
+
base_url=f"{self.actor.scheme}://{self.actor.domain}",
|
|
106
|
+
blob_store=blob_store,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Federation Service
|
|
110
|
+
self.federation = FederationService(
|
|
111
|
+
store=self.store,
|
|
112
|
+
local_actor=self.actor,
|
|
113
|
+
private_key=private_key,
|
|
114
|
+
strain_score_keeper=StrainScoreKeeper(strain_ledger=strain_ledger),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.config: Mapping[str, Any] = {}
|
|
118
|
+
self._enabled: bool = True
|
|
119
|
+
self._host: Optional[ProtocolHostRouter] = None
|
|
120
|
+
|
|
121
|
+
def configure(self, config: Mapping[str, Any]) -> None:
|
|
122
|
+
self.config = dict(config)
|
|
123
|
+
self._enabled = self.config.get('enabled', True)
|
|
124
|
+
if 'domain' in self.config:
|
|
125
|
+
self.domain = self.config['domain']
|
|
126
|
+
if 'username' in self.config:
|
|
127
|
+
self.username = self.config['username']
|
|
128
|
+
|
|
129
|
+
def identity_adapter(self, keystore: Any) -> Optional[ProtocolIdentityAdapter]:
|
|
130
|
+
return ActivityPubIdentityAdapter(actor=self.actor, private_key=self.private_key)
|
|
131
|
+
|
|
132
|
+
def mount_ingress(self, host: ProtocolHostRouter) -> None:
|
|
133
|
+
if not self._enabled:
|
|
134
|
+
return
|
|
135
|
+
self._host = host
|
|
136
|
+
|
|
137
|
+
from node.transport import ws
|
|
138
|
+
|
|
139
|
+
# 1. WebFinger RFC 7033
|
|
140
|
+
def _handle_webfinger(conn: Any, req: Any) -> bool:
|
|
141
|
+
uri = getattr(req, 'path', '')
|
|
142
|
+
parsed = urllib.parse.urlparse(uri)
|
|
143
|
+
qparams = urllib.parse.parse_qs(parsed.query)
|
|
144
|
+
status, headers, body = self.webfinger.handle_webfinger_request(qparams)
|
|
145
|
+
ws.http_response(
|
|
146
|
+
conn,
|
|
147
|
+
"200 OK" if status == 200 else f"{status} Error",
|
|
148
|
+
body,
|
|
149
|
+
headers.get('Content-Type', 'application/jrd+json'),
|
|
150
|
+
)
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
host.mount_route('/.well-known/webfinger', _handle_webfinger, methods=('GET',), owner=self.name)
|
|
154
|
+
|
|
155
|
+
# 2. Shared Inbox & Personal Inbox POST
|
|
156
|
+
def _handle_inbox_post(conn: Any, req: Any) -> bool:
|
|
157
|
+
method = getattr(req, 'method', 'POST')
|
|
158
|
+
path = getattr(req, 'path', '')
|
|
159
|
+
raw_headers = getattr(req, 'headers', {}) or {}
|
|
160
|
+
headers = {
|
|
161
|
+
(k.decode('latin-1') if isinstance(k, bytes) else str(k)): (
|
|
162
|
+
v.decode('latin-1') if isinstance(v, bytes) else str(v)
|
|
163
|
+
)
|
|
164
|
+
for k, v in raw_headers.items()
|
|
165
|
+
}
|
|
166
|
+
body = getattr(req, 'body', None)
|
|
167
|
+
if body is None:
|
|
168
|
+
body = ws.read_body(conn, req)
|
|
169
|
+
status, resp_headers, resp_body = self.federation.handle_inbox(method, path, headers, body)
|
|
170
|
+
ws.http_response(
|
|
171
|
+
conn,
|
|
172
|
+
f"{status} OK" if status in (200, 202) else f"{status} Error",
|
|
173
|
+
resp_body,
|
|
174
|
+
resp_headers.get('Content-Type', 'application/json'),
|
|
175
|
+
)
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
host.mount_route('/inbox', _handle_inbox_post, methods=('POST',), owner=self.name)
|
|
179
|
+
host.mount_route(f"/users/{self.actor.username}/inbox", _handle_inbox_post, methods=('POST',), owner=self.name)
|
|
180
|
+
|
|
181
|
+
# 3. User Actor Profile, Outbox, Followers, Following
|
|
182
|
+
def _handle_user_routes(conn: Any, req: Any) -> bool:
|
|
183
|
+
path = getattr(req, 'path', '').split('?')[0].rstrip('/')
|
|
184
|
+
user_prefix = f"/users/{self.actor.username}"
|
|
185
|
+
|
|
186
|
+
if path == user_prefix:
|
|
187
|
+
# Actor JSON-LD
|
|
188
|
+
body = json.dumps(self.actor.to_jsonld(), indent=2).encode('utf-8')
|
|
189
|
+
ws.http_response(conn, "200 OK", body, "application/activity+json")
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
if path == f"{user_prefix}/outbox":
|
|
193
|
+
# Outbox OrderedCollection
|
|
194
|
+
acts = self.store.list_outbox(self.actor.actor_url)
|
|
195
|
+
coll = self.store.as_ordered_collection(acts, len(acts), f"{self.actor.actor_url}/outbox")
|
|
196
|
+
body = json.dumps(coll, indent=2).encode('utf-8')
|
|
197
|
+
ws.http_response(conn, "200 OK", body, "application/activity+json")
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
if path == f"{user_prefix}/followers":
|
|
201
|
+
followers = self.store.get_followers(self.actor.actor_url)
|
|
202
|
+
coll = self.store.as_ordered_collection(followers, len(followers), f"{self.actor.actor_url}/followers")
|
|
203
|
+
body = json.dumps(coll, indent=2).encode('utf-8')
|
|
204
|
+
ws.http_response(conn, "200 OK", body, "application/activity+json")
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
if path == f"{user_prefix}/following":
|
|
208
|
+
following = self.store.get_following(self.actor.actor_url)
|
|
209
|
+
coll = self.store.as_ordered_collection(following, len(following), f"{self.actor.actor_url}/following")
|
|
210
|
+
body = json.dumps(coll, indent=2).encode('utf-8')
|
|
211
|
+
ws.http_response(conn, "200 OK", body, "application/activity+json")
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
ws.http_response(conn, "404 Not Found", b'{"error": "not found"}', "application/json")
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
host.mount_route(f"/users/{self.actor.username}", _handle_user_routes, methods=('GET',), owner=self.name)
|
|
218
|
+
|
|
219
|
+
# 4. Media Attachments
|
|
220
|
+
def _handle_media(conn: Any, req: Any) -> bool:
|
|
221
|
+
path = getattr(req, 'path', '')
|
|
222
|
+
status, headers, body = self.media.handle_media_request(path)
|
|
223
|
+
ws.http_response(
|
|
224
|
+
conn,
|
|
225
|
+
"200 OK" if status == 200 else f"{status} Error",
|
|
226
|
+
body,
|
|
227
|
+
headers.get('Content-Type', 'application/octet-stream'),
|
|
228
|
+
)
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
host.mount_route('/media', _handle_media, methods=('GET',), owner=self.name)
|
|
232
|
+
|
|
233
|
+
def transport_rung(self, context: Any) -> Optional[Any]:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def compartments(self) -> tuple[type[Any], ...]:
|
|
237
|
+
return ()
|
|
238
|
+
|
|
239
|
+
def start(self) -> None:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def stop(self) -> None:
|
|
243
|
+
self.store.close()
|
|
244
|
+
|
|
245
|
+
def health(self) -> ProtocolHealth:
|
|
246
|
+
if not self._enabled:
|
|
247
|
+
return ProtocolHealth(
|
|
248
|
+
healthy=True,
|
|
249
|
+
status="disabled",
|
|
250
|
+
connections_active=0,
|
|
251
|
+
messages_in=0,
|
|
252
|
+
messages_out=0,
|
|
253
|
+
errors=(),
|
|
254
|
+
)
|
|
255
|
+
return ProtocolHealth(
|
|
256
|
+
healthy=True,
|
|
257
|
+
status="serving",
|
|
258
|
+
connections_active=self.store.count_followers(self.actor.actor_url),
|
|
259
|
+
messages_in=self.store.count_inbox(),
|
|
260
|
+
messages_out=self.store.count_outbox(),
|
|
261
|
+
errors=(),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
|
|
265
|
+
if not self._enabled:
|
|
266
|
+
return ()
|
|
267
|
+
return (
|
|
268
|
+
('c', 'activitypub'),
|
|
269
|
+
('c', 'activitystreams-2.0'),
|
|
270
|
+
('c', 'webfinger'),
|
|
271
|
+
)
|