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,564 @@
|
|
|
1
|
+
"""ActivityPub SQLite persistent store.
|
|
2
|
+
|
|
3
|
+
Standard library only. Manages persistence for:
|
|
4
|
+
- Local and remote actors (URI, public key, endpoints, JSON-LD profile).
|
|
5
|
+
- Inbound (inbox) and outbound (outbox) activities.
|
|
6
|
+
- ActivityStreams objects (Notes, Articles, Media, etc.).
|
|
7
|
+
- Follower and Following relational graphs.
|
|
8
|
+
- ActivityStreams OrderedCollection paging.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any, Mapping, Optional, Sequence, Union
|
|
15
|
+
|
|
16
|
+
from fleetmesh_core.conformance.errors import FleetmeshError
|
|
17
|
+
from node.compartment.schema import Column, Index, Table
|
|
18
|
+
from node.storage import SqliteStore
|
|
19
|
+
from node.protocols.activitypub.actor import Actor
|
|
20
|
+
from node.protocols.activitypub.models import AS_CONTEXT, Activity, Note
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'ActivityPubStoreError',
|
|
24
|
+
'ActivityPubStore',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ActivityPubStoreError(FleetmeshError):
|
|
29
|
+
"""Base error for ActivityPub storage operations."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from node.protocols.isotime import now_iso as _now_iso # noqa: E402
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ActivityPubStore(SqliteStore):
|
|
36
|
+
"""Storage for ActivityPub actor profiles, activities and the follow graph.
|
|
37
|
+
|
|
38
|
+
A `SqliteStore` rather than a hand-rolled connection: the base owns the
|
|
39
|
+
connection, the lock, the per-store schema version, transaction control and
|
|
40
|
+
the dialect that rewrites placeholders and upserts for another backend.
|
|
41
|
+
This store opened its own connection, ran its own PRAGMAs and declared its
|
|
42
|
+
schema as a SQL string, so it was SQLite-only and invisible to the
|
|
43
|
+
`Locator` - see docs/storage-backends.md.
|
|
44
|
+
|
|
45
|
+
The six indexes were declared `published DESC`. `Index` takes no direction,
|
|
46
|
+
and none is needed: both backends can scan an ascending index backwards, so
|
|
47
|
+
`(direction, published)` still serves `WHERE direction = ? ORDER BY
|
|
48
|
+
published DESC`.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
SCHEMA = (
|
|
52
|
+
Table('ap_actors', (
|
|
53
|
+
Column('uri', 'TEXT', primary_key=True),
|
|
54
|
+
Column('type', 'TEXT', not_null=True),
|
|
55
|
+
Column('username', 'TEXT'),
|
|
56
|
+
Column('public_key_pem', 'TEXT'),
|
|
57
|
+
Column('inbox', 'TEXT'),
|
|
58
|
+
Column('outbox', 'TEXT'),
|
|
59
|
+
Column('data_json', 'TEXT', not_null=True),
|
|
60
|
+
Column('updated_at', 'TEXT', not_null=True),
|
|
61
|
+
), indices=(
|
|
62
|
+
Index('idx_ap_actors_username', ('username',)),
|
|
63
|
+
)),
|
|
64
|
+
Table('ap_activities', (
|
|
65
|
+
Column('id', 'TEXT', primary_key=True),
|
|
66
|
+
Column('type', 'TEXT', not_null=True),
|
|
67
|
+
Column('actor', 'TEXT', not_null=True),
|
|
68
|
+
Column('target', 'TEXT'),
|
|
69
|
+
Column('published', 'TEXT', not_null=True),
|
|
70
|
+
Column('direction', 'TEXT', not_null=True),
|
|
71
|
+
Column('raw_json', 'TEXT', not_null=True),
|
|
72
|
+
), indices=(
|
|
73
|
+
Index('idx_ap_act_dir', ('direction', 'published')),
|
|
74
|
+
Index('idx_ap_act_actor_dir', ('actor', 'direction', 'published')),
|
|
75
|
+
)),
|
|
76
|
+
Table('ap_objects', (
|
|
77
|
+
Column('id', 'TEXT', primary_key=True),
|
|
78
|
+
Column('type', 'TEXT', not_null=True),
|
|
79
|
+
Column('attributed_to', 'TEXT'),
|
|
80
|
+
Column('published', 'TEXT'),
|
|
81
|
+
Column('content', 'TEXT'),
|
|
82
|
+
Column('raw_json', 'TEXT', not_null=True),
|
|
83
|
+
), indices=(
|
|
84
|
+
Index('idx_ap_obj_attr', ('attributed_to', 'published')),
|
|
85
|
+
)),
|
|
86
|
+
Table('ap_followers', (
|
|
87
|
+
Column('actor_uri', 'TEXT', not_null=True),
|
|
88
|
+
Column('follower_uri', 'TEXT', not_null=True),
|
|
89
|
+
Column('followed_at', 'TEXT', not_null=True),
|
|
90
|
+
), primary_key=('actor_uri', 'follower_uri'), indices=(
|
|
91
|
+
Index('idx_ap_followers_actor', ('actor_uri',)),
|
|
92
|
+
)),
|
|
93
|
+
Table('ap_following', (
|
|
94
|
+
Column('actor_uri', 'TEXT', not_null=True),
|
|
95
|
+
Column('target_uri', 'TEXT', not_null=True),
|
|
96
|
+
Column('followed_at', 'TEXT', not_null=True),
|
|
97
|
+
), primary_key=('actor_uri', 'target_uri'), indices=(
|
|
98
|
+
Index('idx_ap_following_actor', ('actor_uri',)),
|
|
99
|
+
)),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __init__(self, db_path: str = ":memory:"):
|
|
103
|
+
if db_path != ":memory:":
|
|
104
|
+
dirname = os.path.dirname(db_path)
|
|
105
|
+
if dirname:
|
|
106
|
+
os.makedirs(dirname, exist_ok=True)
|
|
107
|
+
super().__init__(db_path)
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
super().close()
|
|
111
|
+
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
# Actor Management
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def upsert_actor(self, actor: Union[Actor, Mapping[str, Any]]) -> None:
|
|
117
|
+
"""Persist or update an Actor document."""
|
|
118
|
+
if isinstance(actor, Actor):
|
|
119
|
+
doc = actor.to_jsonld()
|
|
120
|
+
elif isinstance(actor, (dict, Mapping)):
|
|
121
|
+
doc = dict(actor)
|
|
122
|
+
else:
|
|
123
|
+
raise ActivityPubStoreError("actor must be an Actor instance or dict")
|
|
124
|
+
|
|
125
|
+
uri = doc.get('id')
|
|
126
|
+
if not uri or not isinstance(uri, str):
|
|
127
|
+
raise ActivityPubStoreError("actor document missing 'id' URI")
|
|
128
|
+
|
|
129
|
+
act_type = doc.get('type', 'Person')
|
|
130
|
+
username = doc.get('preferredUsername')
|
|
131
|
+
inbox = doc.get('inbox')
|
|
132
|
+
outbox = doc.get('outbox')
|
|
133
|
+
|
|
134
|
+
pk_pem = None
|
|
135
|
+
pk_obj = doc.get('publicKey')
|
|
136
|
+
if isinstance(pk_obj, dict):
|
|
137
|
+
pk_pem = pk_obj.get('publicKeyPem')
|
|
138
|
+
|
|
139
|
+
data_json = json.dumps(doc, sort_keys=True)
|
|
140
|
+
updated_at = _now_iso()
|
|
141
|
+
|
|
142
|
+
with self._lock:
|
|
143
|
+
with self.transaction() as _cur:
|
|
144
|
+
_cur.execute(
|
|
145
|
+
"""
|
|
146
|
+
INSERT INTO ap_actors (uri, type, username, public_key_pem, inbox, outbox, data_json, updated_at)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
148
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
149
|
+
type=excluded.type,
|
|
150
|
+
username=excluded.username,
|
|
151
|
+
public_key_pem=excluded.public_key_pem,
|
|
152
|
+
inbox=excluded.inbox,
|
|
153
|
+
outbox=excluded.outbox,
|
|
154
|
+
data_json=excluded.data_json,
|
|
155
|
+
updated_at=excluded.updated_at
|
|
156
|
+
""",
|
|
157
|
+
(uri, act_type, username, pk_pem, inbox, outbox, data_json, updated_at),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def get_actor(self, uri: str) -> Optional[Mapping[str, Any]]:
|
|
161
|
+
"""Retrieve an Actor document by its URI."""
|
|
162
|
+
with self._lock:
|
|
163
|
+
row = self._one(
|
|
164
|
+
"SELECT data_json FROM ap_actors WHERE uri = ?",
|
|
165
|
+
(uri,),
|
|
166
|
+
)
|
|
167
|
+
if not row:
|
|
168
|
+
return None
|
|
169
|
+
return json.loads(row[0])
|
|
170
|
+
|
|
171
|
+
def get_actor_by_username(self, username: str) -> Optional[Mapping[str, Any]]:
|
|
172
|
+
"""Retrieve an Actor document by preferredUsername."""
|
|
173
|
+
with self._lock:
|
|
174
|
+
row = self._one(
|
|
175
|
+
"SELECT data_json FROM ap_actors WHERE LOWER(username) = LOWER(?)",
|
|
176
|
+
(username,),
|
|
177
|
+
)
|
|
178
|
+
if not row:
|
|
179
|
+
return None
|
|
180
|
+
return json.loads(row[0])
|
|
181
|
+
|
|
182
|
+
def get_actor_public_key_pem(self, uri: str) -> Optional[str]:
|
|
183
|
+
"""Directly query an actor's public key PEM string."""
|
|
184
|
+
with self._lock:
|
|
185
|
+
row = self._one(
|
|
186
|
+
"SELECT public_key_pem FROM ap_actors WHERE uri = ?",
|
|
187
|
+
(uri,),
|
|
188
|
+
)
|
|
189
|
+
if not row or not row[0]:
|
|
190
|
+
return None
|
|
191
|
+
return row[0]
|
|
192
|
+
|
|
193
|
+
# -------------------------------------------------------------------------
|
|
194
|
+
# Object Management (Notes, Articles, etc.)
|
|
195
|
+
# -------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def save_object(self, obj: Union[Note, Mapping[str, Any]]) -> None:
|
|
198
|
+
"""Persist or update an ActivityStreams object."""
|
|
199
|
+
if isinstance(obj, Note):
|
|
200
|
+
doc = obj.to_dict()
|
|
201
|
+
elif isinstance(obj, (dict, Mapping)):
|
|
202
|
+
doc = dict(obj)
|
|
203
|
+
else:
|
|
204
|
+
raise ActivityPubStoreError("object must be a Note or dict")
|
|
205
|
+
|
|
206
|
+
obj_id = doc.get('id')
|
|
207
|
+
if not obj_id or not isinstance(obj_id, str):
|
|
208
|
+
raise ActivityPubStoreError("object missing 'id'")
|
|
209
|
+
|
|
210
|
+
obj_type = doc.get('type', 'Object')
|
|
211
|
+
attributed_to = doc.get('attributedTo')
|
|
212
|
+
published = doc.get('published') or _now_iso()
|
|
213
|
+
content = doc.get('content')
|
|
214
|
+
raw_json = json.dumps(doc, sort_keys=True)
|
|
215
|
+
|
|
216
|
+
with self._lock:
|
|
217
|
+
with self.transaction() as _cur:
|
|
218
|
+
_cur.execute(
|
|
219
|
+
"""
|
|
220
|
+
INSERT INTO ap_objects (id, type, attributed_to, published, content, raw_json)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
222
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
223
|
+
type=excluded.type,
|
|
224
|
+
attributed_to=excluded.attributed_to,
|
|
225
|
+
published=excluded.published,
|
|
226
|
+
content=excluded.content,
|
|
227
|
+
raw_json=excluded.raw_json
|
|
228
|
+
""",
|
|
229
|
+
(obj_id, obj_type, attributed_to, published, content, raw_json),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def get_object(self, id: str) -> Optional[Mapping[str, Any]]:
|
|
233
|
+
"""Retrieve an ActivityStreams object by ID."""
|
|
234
|
+
with self._lock:
|
|
235
|
+
row = self._one(
|
|
236
|
+
"SELECT raw_json FROM ap_objects WHERE id = ?",
|
|
237
|
+
(id,),
|
|
238
|
+
)
|
|
239
|
+
if not row:
|
|
240
|
+
return None
|
|
241
|
+
return json.loads(row[0])
|
|
242
|
+
|
|
243
|
+
# -------------------------------------------------------------------------
|
|
244
|
+
# Activity Management (Inbox / Outbox)
|
|
245
|
+
# -------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
def record_activity(
|
|
248
|
+
self,
|
|
249
|
+
activity: Union[Activity, Mapping[str, Any]],
|
|
250
|
+
direction: str,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Record an inbound ('inbox') or outbound ('outbox') activity.
|
|
253
|
+
|
|
254
|
+
If the activity contains an embedded object dictionary, it will automatically
|
|
255
|
+
be persisted to the objects table.
|
|
256
|
+
"""
|
|
257
|
+
if direction not in ('inbox', 'outbox'):
|
|
258
|
+
raise ActivityPubStoreError("direction must be 'inbox' or 'outbox'")
|
|
259
|
+
|
|
260
|
+
if isinstance(activity, Activity):
|
|
261
|
+
doc = activity.to_dict()
|
|
262
|
+
elif isinstance(activity, dict):
|
|
263
|
+
doc = dict(activity)
|
|
264
|
+
else:
|
|
265
|
+
raise ActivityPubStoreError("activity must be an Activity or dict")
|
|
266
|
+
|
|
267
|
+
act_id = doc.get('id')
|
|
268
|
+
if not act_id or not isinstance(act_id, str):
|
|
269
|
+
raise ActivityPubStoreError("activity missing 'id'")
|
|
270
|
+
|
|
271
|
+
act_type = doc.get('type')
|
|
272
|
+
if not act_type or not isinstance(act_type, str):
|
|
273
|
+
raise ActivityPubStoreError("activity missing 'type'")
|
|
274
|
+
|
|
275
|
+
actor = doc.get('actor')
|
|
276
|
+
if not actor or not isinstance(actor, str):
|
|
277
|
+
raise ActivityPubStoreError("activity missing 'actor'")
|
|
278
|
+
|
|
279
|
+
target = None
|
|
280
|
+
obj = doc.get('object')
|
|
281
|
+
if isinstance(obj, dict):
|
|
282
|
+
target = obj.get('id')
|
|
283
|
+
# Auto-save embedded object
|
|
284
|
+
try:
|
|
285
|
+
self.save_object(obj)
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
elif isinstance(obj, str):
|
|
289
|
+
target = obj
|
|
290
|
+
|
|
291
|
+
published = doc.get('published') or _now_iso()
|
|
292
|
+
raw_json = json.dumps(doc, sort_keys=True)
|
|
293
|
+
|
|
294
|
+
with self._lock:
|
|
295
|
+
with self.transaction() as _cur:
|
|
296
|
+
_cur.execute(
|
|
297
|
+
"""
|
|
298
|
+
INSERT INTO ap_activities (id, type, actor, target, published, direction, raw_json)
|
|
299
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
300
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
301
|
+
type=excluded.type,
|
|
302
|
+
actor=excluded.actor,
|
|
303
|
+
target=excluded.target,
|
|
304
|
+
published=excluded.published,
|
|
305
|
+
direction=excluded.direction,
|
|
306
|
+
raw_json=excluded.raw_json
|
|
307
|
+
""",
|
|
308
|
+
(act_id, act_type, actor, target, published, direction, raw_json),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def get_activity(self, id: str) -> Optional[Mapping[str, Any]]:
|
|
312
|
+
"""Retrieve an activity by ID."""
|
|
313
|
+
with self._lock:
|
|
314
|
+
row = self._one(
|
|
315
|
+
"SELECT raw_json FROM ap_activities WHERE id = ?",
|
|
316
|
+
(id,),
|
|
317
|
+
)
|
|
318
|
+
if not row:
|
|
319
|
+
return None
|
|
320
|
+
return json.loads(row[0])
|
|
321
|
+
|
|
322
|
+
def list_inbox(
|
|
323
|
+
self,
|
|
324
|
+
actor_uri: Optional[str] = None,
|
|
325
|
+
limit: int = 50,
|
|
326
|
+
offset: int = 0,
|
|
327
|
+
) -> list[Mapping[str, Any]]:
|
|
328
|
+
"""List incoming activities, optionally filtered by actor."""
|
|
329
|
+
with self._lock:
|
|
330
|
+
if actor_uri:
|
|
331
|
+
rows = self._all(
|
|
332
|
+
"""
|
|
333
|
+
SELECT raw_json FROM ap_activities
|
|
334
|
+
WHERE direction = 'inbox' AND actor = ?
|
|
335
|
+
ORDER BY published DESC, id DESC
|
|
336
|
+
LIMIT ? OFFSET ?
|
|
337
|
+
""",
|
|
338
|
+
(actor_uri, limit, offset),
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
rows = self._all(
|
|
342
|
+
"""
|
|
343
|
+
SELECT raw_json FROM ap_activities
|
|
344
|
+
WHERE direction = 'inbox'
|
|
345
|
+
ORDER BY published DESC, id DESC
|
|
346
|
+
LIMIT ? OFFSET ?
|
|
347
|
+
""",
|
|
348
|
+
(limit, offset),
|
|
349
|
+
)
|
|
350
|
+
return [json.loads(r[0]) for r in rows]
|
|
351
|
+
|
|
352
|
+
def list_outbox(
|
|
353
|
+
self,
|
|
354
|
+
actor_uri: Optional[str] = None,
|
|
355
|
+
limit: int = 50,
|
|
356
|
+
offset: int = 0,
|
|
357
|
+
) -> list[Mapping[str, Any]]:
|
|
358
|
+
"""List outgoing activities, optionally filtered by actor."""
|
|
359
|
+
with self._lock:
|
|
360
|
+
if actor_uri:
|
|
361
|
+
rows = self._all(
|
|
362
|
+
"""
|
|
363
|
+
SELECT raw_json FROM ap_activities
|
|
364
|
+
WHERE direction = 'outbox' AND actor = ?
|
|
365
|
+
ORDER BY published DESC, id DESC
|
|
366
|
+
LIMIT ? OFFSET ?
|
|
367
|
+
""",
|
|
368
|
+
(actor_uri, limit, offset),
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
rows = self._all(
|
|
372
|
+
"""
|
|
373
|
+
SELECT raw_json FROM ap_activities
|
|
374
|
+
WHERE direction = 'outbox'
|
|
375
|
+
ORDER BY published DESC, id DESC
|
|
376
|
+
LIMIT ? OFFSET ?
|
|
377
|
+
""",
|
|
378
|
+
(limit, offset),
|
|
379
|
+
)
|
|
380
|
+
return [json.loads(r[0]) for r in rows]
|
|
381
|
+
|
|
382
|
+
def count_inbox(self, actor_uri: Optional[str] = None) -> int:
|
|
383
|
+
"""Count incoming activities."""
|
|
384
|
+
with self._lock:
|
|
385
|
+
if actor_uri:
|
|
386
|
+
row = self._one(
|
|
387
|
+
"SELECT COUNT(*) FROM ap_activities WHERE direction = 'inbox' AND actor = ?",
|
|
388
|
+
(actor_uri,),
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
row = self._one(
|
|
392
|
+
"SELECT COUNT(*) FROM ap_activities WHERE direction = 'inbox'"
|
|
393
|
+
)
|
|
394
|
+
return int(row[0]) if row else 0
|
|
395
|
+
|
|
396
|
+
def count_outbox(self, actor_uri: Optional[str] = None) -> int:
|
|
397
|
+
"""Count outgoing activities."""
|
|
398
|
+
with self._lock:
|
|
399
|
+
if actor_uri:
|
|
400
|
+
row = self._one(
|
|
401
|
+
"SELECT COUNT(*) FROM ap_activities WHERE direction = 'outbox' AND actor = ?",
|
|
402
|
+
(actor_uri,),
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
row = self._one(
|
|
406
|
+
"SELECT COUNT(*) FROM ap_activities WHERE direction = 'outbox'"
|
|
407
|
+
)
|
|
408
|
+
return int(row[0]) if row else 0
|
|
409
|
+
|
|
410
|
+
# -------------------------------------------------------------------------
|
|
411
|
+
# Social Graph (Followers & Following)
|
|
412
|
+
# -------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def add_follower(
|
|
415
|
+
self,
|
|
416
|
+
actor_uri: str,
|
|
417
|
+
follower_uri: str,
|
|
418
|
+
followed_at: Optional[str] = None,
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Record a follower for an actor."""
|
|
421
|
+
ts = followed_at or _now_iso()
|
|
422
|
+
with self._lock:
|
|
423
|
+
with self.transaction() as _cur:
|
|
424
|
+
_cur.execute(
|
|
425
|
+
"""
|
|
426
|
+
INSERT INTO ap_followers (actor_uri, follower_uri, followed_at)
|
|
427
|
+
VALUES (?, ?, ?)
|
|
428
|
+
ON CONFLICT(actor_uri, follower_uri) DO NOTHING
|
|
429
|
+
""",
|
|
430
|
+
(actor_uri, follower_uri, ts),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def remove_follower(self, actor_uri: str, follower_uri: str) -> None:
|
|
434
|
+
"""Remove a follower relationship."""
|
|
435
|
+
with self._lock:
|
|
436
|
+
with self.transaction() as _cur:
|
|
437
|
+
_cur.execute(
|
|
438
|
+
"DELETE FROM ap_followers WHERE actor_uri = ? AND follower_uri = ?",
|
|
439
|
+
(actor_uri, follower_uri),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def get_followers(
|
|
443
|
+
self,
|
|
444
|
+
actor_uri: str,
|
|
445
|
+
limit: int = 100,
|
|
446
|
+
offset: int = 0,
|
|
447
|
+
) -> list[str]:
|
|
448
|
+
"""List follower URIs for an actor."""
|
|
449
|
+
with self._lock:
|
|
450
|
+
rows = self._all(
|
|
451
|
+
"""
|
|
452
|
+
SELECT follower_uri FROM ap_followers
|
|
453
|
+
WHERE actor_uri = ?
|
|
454
|
+
ORDER BY followed_at DESC
|
|
455
|
+
LIMIT ? OFFSET ?
|
|
456
|
+
""",
|
|
457
|
+
(actor_uri, limit, offset),
|
|
458
|
+
)
|
|
459
|
+
return [r[0] for r in rows]
|
|
460
|
+
|
|
461
|
+
def count_followers(self, actor_uri: str) -> int:
|
|
462
|
+
"""Return the total number of followers for an actor."""
|
|
463
|
+
with self._lock:
|
|
464
|
+
row = self._one(
|
|
465
|
+
"SELECT COUNT(*) FROM ap_followers WHERE actor_uri = ?",
|
|
466
|
+
(actor_uri,),
|
|
467
|
+
)
|
|
468
|
+
return int(row[0]) if row else 0
|
|
469
|
+
|
|
470
|
+
def add_following(
|
|
471
|
+
self,
|
|
472
|
+
actor_uri: str,
|
|
473
|
+
target_uri: str,
|
|
474
|
+
followed_at: Optional[str] = None,
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Record an actor following a remote target."""
|
|
477
|
+
ts = followed_at or _now_iso()
|
|
478
|
+
with self._lock:
|
|
479
|
+
with self.transaction() as _cur:
|
|
480
|
+
_cur.execute(
|
|
481
|
+
"""
|
|
482
|
+
INSERT INTO ap_following (actor_uri, target_uri, followed_at)
|
|
483
|
+
VALUES (?, ?, ?)
|
|
484
|
+
ON CONFLICT(actor_uri, target_uri) DO NOTHING
|
|
485
|
+
""",
|
|
486
|
+
(actor_uri, target_uri, ts),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def remove_following(self, actor_uri: str, target_uri: str) -> None:
|
|
490
|
+
"""Remove a following relationship."""
|
|
491
|
+
with self._lock:
|
|
492
|
+
with self.transaction() as _cur:
|
|
493
|
+
_cur.execute(
|
|
494
|
+
"DELETE FROM ap_following WHERE actor_uri = ? AND target_uri = ?",
|
|
495
|
+
(actor_uri, target_uri),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def get_following(
|
|
499
|
+
self,
|
|
500
|
+
actor_uri: str,
|
|
501
|
+
limit: int = 100,
|
|
502
|
+
offset: int = 0,
|
|
503
|
+
) -> list[str]:
|
|
504
|
+
"""List target URIs that an actor follows."""
|
|
505
|
+
with self._lock:
|
|
506
|
+
rows = self._all(
|
|
507
|
+
"""
|
|
508
|
+
SELECT target_uri FROM ap_following
|
|
509
|
+
WHERE actor_uri = ?
|
|
510
|
+
ORDER BY followed_at DESC
|
|
511
|
+
LIMIT ? OFFSET ?
|
|
512
|
+
""",
|
|
513
|
+
(actor_uri, limit, offset),
|
|
514
|
+
)
|
|
515
|
+
return [r[0] for r in rows]
|
|
516
|
+
|
|
517
|
+
def count_following(self, actor_uri: str) -> int:
|
|
518
|
+
"""Return the total number of actors that an actor follows."""
|
|
519
|
+
with self._lock:
|
|
520
|
+
row = self._one(
|
|
521
|
+
"SELECT COUNT(*) FROM ap_following WHERE actor_uri = ?",
|
|
522
|
+
(actor_uri,),
|
|
523
|
+
)
|
|
524
|
+
return int(row[0]) if row else 0
|
|
525
|
+
|
|
526
|
+
# -------------------------------------------------------------------------
|
|
527
|
+
# OrderedCollection Serialization
|
|
528
|
+
# -------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def as_ordered_collection(
|
|
532
|
+
items: Sequence[Any],
|
|
533
|
+
total_items: int,
|
|
534
|
+
id_url: str,
|
|
535
|
+
page: Optional[int] = None,
|
|
536
|
+
page_size: int = 50,
|
|
537
|
+
) -> Mapping[str, Any]:
|
|
538
|
+
"""Format items as an ActivityStreams 2.0 OrderedCollection or OrderedCollectionPage."""
|
|
539
|
+
if page is None:
|
|
540
|
+
doc = {
|
|
541
|
+
"@context": AS_CONTEXT,
|
|
542
|
+
"id": id_url,
|
|
543
|
+
"type": "OrderedCollection",
|
|
544
|
+
"totalItems": total_items,
|
|
545
|
+
"first": f"{id_url}?page=1",
|
|
546
|
+
}
|
|
547
|
+
if total_items > 0:
|
|
548
|
+
last_page = max(1, (total_items + page_size - 1) // page_size)
|
|
549
|
+
doc["last"] = f"{id_url}?page={last_page}"
|
|
550
|
+
return doc
|
|
551
|
+
|
|
552
|
+
page_doc = {
|
|
553
|
+
"@context": AS_CONTEXT,
|
|
554
|
+
"id": f"{id_url}?page={page}",
|
|
555
|
+
"type": "OrderedCollectionPage",
|
|
556
|
+
"partOf": id_url,
|
|
557
|
+
"totalItems": total_items,
|
|
558
|
+
"orderedItems": list(items),
|
|
559
|
+
}
|
|
560
|
+
if page > 1:
|
|
561
|
+
page_doc["prev"] = f"{id_url}?page={page - 1}"
|
|
562
|
+
if (page * page_size) < total_items:
|
|
563
|
+
page_doc["next"] = f"{id_url}?page={page + 1}"
|
|
564
|
+
return page_doc
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""FleetMesh Node Arcade Protocol Package (`node.protocols.arcade`)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# The stores and route handlers live in the fleetmesh-protocols package
|
|
5
|
+
# (fleetmesh_protocols.arcade); this package re-exports them so node code and
|
|
6
|
+
# the protocol router keep their import paths.
|
|
7
|
+
|
|
8
|
+
from fleetmesh_protocols.arcade.routes import HttpResult, JSON_CT, handle_arcade_http
|
|
9
|
+
from fleetmesh_protocols.arcade.store import (
|
|
10
|
+
ArcadeError,
|
|
11
|
+
MAX_DESKTOP_JSON_BYTES,
|
|
12
|
+
MAX_RECORD_JSON_BYTES,
|
|
13
|
+
TOMBSTONE_TTL_SEC,
|
|
14
|
+
StandaloneArcadeStore,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ArcadeError",
|
|
19
|
+
"MAX_DESKTOP_JSON_BYTES",
|
|
20
|
+
"MAX_RECORD_JSON_BYTES",
|
|
21
|
+
"TOMBSTONE_TTL_SEC",
|
|
22
|
+
"StandaloneArcadeStore",
|
|
23
|
+
"HttpResult",
|
|
24
|
+
"JSON_CT",
|
|
25
|
+
"handle_arcade_http",
|
|
26
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""AT Protocol (atproto) package for Fleetmesh.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Repositories, MST storage, and CAR export/import.
|
|
5
|
+
- XRPC HTTP server and Firehose subscribeRepos WebSocket stream.
|
|
6
|
+
- Cross-protocol mapping between Nostr, ActivityPub, and ATProto records.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .package import AtprotoProtocolPackage
|
|
11
|
+
from .repo import Repo
|
|
12
|
+
|
|
13
|
+
__all__ = ['AtprotoProtocolPackage', 'Repo']
|