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,644 @@
|
|
|
1
|
+
"""Cross-protocol translation engine for Fleetmesh.
|
|
2
|
+
|
|
3
|
+
Provides bidirectional translation across the four supported social protocols:
|
|
4
|
+
- Nostr (NIP-01 Kind 1 / e-tag threading)
|
|
5
|
+
- AT Protocol (app.bsky.feed.post)
|
|
6
|
+
- ActivityPub (ActivityStreams 2.0 Create / Note)
|
|
7
|
+
- Secure Scuttlebutt (post)
|
|
8
|
+
|
|
9
|
+
Uses UnifiedPost as the canonical intermediate representation for maximum fidelity.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
import datetime
|
|
15
|
+
import html
|
|
16
|
+
import re
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Mapping, MutableMapping, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class UnifiedPost:
|
|
23
|
+
"""Canonical intermediate representation of a social microblog post."""
|
|
24
|
+
text: str
|
|
25
|
+
author: str = ""
|
|
26
|
+
created_at: int = 0
|
|
27
|
+
attachments: list[str] = field(default_factory=list)
|
|
28
|
+
reply_to: Optional[str] = None
|
|
29
|
+
root: Optional[str] = None
|
|
30
|
+
tags: list[str] = field(default_factory=list)
|
|
31
|
+
extra: Mapping[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unix_to_iso(ts: int) -> str:
|
|
35
|
+
"""Format unix seconds to ISO 8601 UTC string."""
|
|
36
|
+
dt = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
|
|
37
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def iso_to_unix(iso_str: str) -> int:
|
|
41
|
+
"""Parse ISO 8601 string to unix seconds."""
|
|
42
|
+
clean = iso_str.strip()
|
|
43
|
+
if clean.endswith('Z'):
|
|
44
|
+
clean = clean[:-1] + '+00:00'
|
|
45
|
+
try:
|
|
46
|
+
dt = datetime.datetime.fromisoformat(clean)
|
|
47
|
+
return int(dt.timestamp())
|
|
48
|
+
except Exception:
|
|
49
|
+
return int(time.time())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def strip_html(text: str) -> str:
|
|
53
|
+
"""Strip HTML tags, scripts, and unescape HTML entities for clean text representation."""
|
|
54
|
+
# First completely remove <script> and <style> blocks
|
|
55
|
+
s = re.sub(r'<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>', '', text, flags=re.IGNORECASE)
|
|
56
|
+
s = re.sub(r'<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>', '', s, flags=re.IGNORECASE)
|
|
57
|
+
# Replace <br> and <p> with newlines
|
|
58
|
+
s = re.sub(r'<br\s*/?>', '\n', s, flags=re.IGNORECASE)
|
|
59
|
+
s = re.sub(r'</p>', '\n', s, flags=re.IGNORECASE)
|
|
60
|
+
s = re.sub(r'<[^>]+>', '', s)
|
|
61
|
+
s = html.unescape(s)
|
|
62
|
+
return s.strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def text_to_html(text: str) -> str:
|
|
66
|
+
"""Convert plain text to safe HTML paragraphs and line breaks."""
|
|
67
|
+
escaped = html.escape(text)
|
|
68
|
+
paragraphs = escaped.split('\n\n')
|
|
69
|
+
formatted = []
|
|
70
|
+
for p in paragraphs:
|
|
71
|
+
lines = p.split('\n')
|
|
72
|
+
formatted.append(f"<p>{'<br/>'.join(lines)}</p>")
|
|
73
|
+
return ''.join(formatted)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -----------------------------------------------------------------------------
|
|
77
|
+
# Nostr (NIP-01 / e-tag threading) Translation
|
|
78
|
+
# -----------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def nostr_to_unified(event: Mapping[str, Any]) -> UnifiedPost:
|
|
81
|
+
"""Translate a Nostr Kind 1 event to UnifiedPost."""
|
|
82
|
+
text = str(event.get('content', ''))
|
|
83
|
+
author = str(event.get('pubkey', ''))
|
|
84
|
+
created_at = int(event.get('created_at', 0)) or int(time.time())
|
|
85
|
+
|
|
86
|
+
tags = event.get('tags', [])
|
|
87
|
+
reply_to = None
|
|
88
|
+
root = None
|
|
89
|
+
hashtags: list[str] = []
|
|
90
|
+
attachments: list[str] = []
|
|
91
|
+
|
|
92
|
+
# Parse e-tag reply/root markers
|
|
93
|
+
e_tags = [t for t in tags if len(t) >= 2 and t[0] == 'e']
|
|
94
|
+
for t in e_tags:
|
|
95
|
+
eid = t[1]
|
|
96
|
+
marker = t[3] if len(t) >= 4 else None
|
|
97
|
+
if marker == 'reply':
|
|
98
|
+
reply_to = eid
|
|
99
|
+
elif marker == 'root':
|
|
100
|
+
root = eid
|
|
101
|
+
elif marker is None:
|
|
102
|
+
# Positional fallback (deprecated positional conventions)
|
|
103
|
+
if not root:
|
|
104
|
+
root = eid
|
|
105
|
+
else:
|
|
106
|
+
reply_to = eid
|
|
107
|
+
|
|
108
|
+
if reply_to is None and root is not None:
|
|
109
|
+
reply_to = root
|
|
110
|
+
|
|
111
|
+
for t in tags:
|
|
112
|
+
if len(t) >= 2:
|
|
113
|
+
if t[0] == 't':
|
|
114
|
+
hashtags.append(t[1].lower())
|
|
115
|
+
elif t[0] in ('r', 'image', 'imeta'):
|
|
116
|
+
if t[0] == 'imeta':
|
|
117
|
+
for part in t[1:]:
|
|
118
|
+
if part.startswith('url '):
|
|
119
|
+
attachments.append(part[4:])
|
|
120
|
+
else:
|
|
121
|
+
attachments.append(t[1])
|
|
122
|
+
|
|
123
|
+
# Extract image URLs from content if not explicitly tagged
|
|
124
|
+
url_matches = re.findall(r'https?://\S+\.(?:png|jpg|jpeg|gif|webp)', text, re.IGNORECASE)
|
|
125
|
+
for u in url_matches:
|
|
126
|
+
if u not in attachments:
|
|
127
|
+
attachments.append(u)
|
|
128
|
+
|
|
129
|
+
return UnifiedPost(
|
|
130
|
+
text=text,
|
|
131
|
+
author=author,
|
|
132
|
+
created_at=created_at,
|
|
133
|
+
attachments=attachments,
|
|
134
|
+
reply_to=reply_to,
|
|
135
|
+
root=root,
|
|
136
|
+
tags=hashtags,
|
|
137
|
+
extra={"nostr_id": event.get("id")},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def unified_to_nostr(post: UnifiedPost, pubkey: Optional[str] = None) -> Mapping[str, Any]:
|
|
142
|
+
"""Translate UnifiedPost to a Nostr Kind 1 event structure."""
|
|
143
|
+
author = pubkey or post.author
|
|
144
|
+
tags: list[list[str]] = []
|
|
145
|
+
|
|
146
|
+
if post.root and post.reply_to and post.root != post.reply_to:
|
|
147
|
+
tags.append(["e", post.root, "", "root"])
|
|
148
|
+
tags.append(["e", post.reply_to, "", "reply"])
|
|
149
|
+
elif post.reply_to:
|
|
150
|
+
tags.append(["e", post.reply_to, "", "root"])
|
|
151
|
+
|
|
152
|
+
for tag in post.tags:
|
|
153
|
+
tags.append(["t", tag.lstrip('#')])
|
|
154
|
+
|
|
155
|
+
for att in post.attachments:
|
|
156
|
+
tags.append(["r", att])
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"kind": 1,
|
|
160
|
+
"pubkey": author,
|
|
161
|
+
"created_at": post.created_at or int(time.time()),
|
|
162
|
+
"tags": tags,
|
|
163
|
+
"content": post.text,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# -----------------------------------------------------------------------------
|
|
168
|
+
# AT Protocol (app.bsky.feed.post) Translation
|
|
169
|
+
# -----------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def atproto_to_unified(record: Mapping[str, Any], author: str = "") -> UnifiedPost:
|
|
172
|
+
"""Translate an ATProto app.bsky.feed.post record to UnifiedPost."""
|
|
173
|
+
text = str(record.get('text', ''))
|
|
174
|
+
created_str = record.get('createdAt', '')
|
|
175
|
+
created_at = iso_to_unix(created_str) if created_str else int(time.time())
|
|
176
|
+
|
|
177
|
+
reply_info = record.get('reply', {})
|
|
178
|
+
reply_to = None
|
|
179
|
+
root = None
|
|
180
|
+
if isinstance(reply_info, dict):
|
|
181
|
+
parent_ref = reply_info.get('parent', {})
|
|
182
|
+
root_ref = reply_info.get('root', {})
|
|
183
|
+
if isinstance(parent_ref, dict):
|
|
184
|
+
reply_to = parent_ref.get('uri')
|
|
185
|
+
if isinstance(root_ref, dict):
|
|
186
|
+
root = root_ref.get('uri')
|
|
187
|
+
|
|
188
|
+
attachments: list[str] = []
|
|
189
|
+
embed = record.get('embed', {})
|
|
190
|
+
if isinstance(embed, dict):
|
|
191
|
+
images = embed.get('images', [])
|
|
192
|
+
for img in images:
|
|
193
|
+
if isinstance(img, dict):
|
|
194
|
+
blob = img.get('image', {})
|
|
195
|
+
if isinstance(blob, dict):
|
|
196
|
+
link = blob.get('ref', {}).get('$link') or blob.get('cid')
|
|
197
|
+
if link:
|
|
198
|
+
attachments.append(str(link))
|
|
199
|
+
|
|
200
|
+
# Tags from facets
|
|
201
|
+
tags: list[str] = []
|
|
202
|
+
facets = record.get('facets', [])
|
|
203
|
+
if isinstance(facets, list):
|
|
204
|
+
for facet in facets:
|
|
205
|
+
features = facet.get('features', [])
|
|
206
|
+
for feat in features:
|
|
207
|
+
if feat.get('$type') == 'app.bsky.richtext.facet#tag':
|
|
208
|
+
tag_name = feat.get('tag')
|
|
209
|
+
if tag_name:
|
|
210
|
+
tags.append(tag_name.lower())
|
|
211
|
+
|
|
212
|
+
return UnifiedPost(
|
|
213
|
+
text=text,
|
|
214
|
+
author=author,
|
|
215
|
+
created_at=created_at,
|
|
216
|
+
attachments=attachments,
|
|
217
|
+
reply_to=reply_to,
|
|
218
|
+
root=root,
|
|
219
|
+
tags=tags,
|
|
220
|
+
extra={"atproto_record": dict(record)},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def unified_to_atproto(post: UnifiedPost) -> Mapping[str, Any]:
|
|
225
|
+
"""Translate UnifiedPost to an ATProto app.bsky.feed.post record."""
|
|
226
|
+
record: MutableMapping[str, Any] = {
|
|
227
|
+
"$type": "app.bsky.feed.post",
|
|
228
|
+
"text": post.text,
|
|
229
|
+
"createdAt": unix_to_iso(post.created_at or int(time.time())),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if post.reply_to:
|
|
233
|
+
root_uri = post.root or post.reply_to
|
|
234
|
+
record["reply"] = {
|
|
235
|
+
"root": {"uri": root_uri, "cid": post.extra.get("root_cid", "")},
|
|
236
|
+
"parent": {"uri": post.reply_to, "cid": post.extra.get("reply_cid", "")},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if post.attachments:
|
|
240
|
+
images = []
|
|
241
|
+
for att in post.attachments:
|
|
242
|
+
images.append({
|
|
243
|
+
"alt": "",
|
|
244
|
+
"image": {
|
|
245
|
+
"$type": "blob",
|
|
246
|
+
"ref": {"$link": att},
|
|
247
|
+
"mimeType": "image/jpeg",
|
|
248
|
+
"size": 0,
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
record["embed"] = {
|
|
252
|
+
"$type": "app.bsky.embed.images",
|
|
253
|
+
"images": images,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if post.tags:
|
|
257
|
+
facets = []
|
|
258
|
+
for tag in post.tags:
|
|
259
|
+
facets.append({
|
|
260
|
+
"$type": "app.bsky.richtext.facet",
|
|
261
|
+
"features": [{
|
|
262
|
+
"$type": "app.bsky.richtext.facet#tag",
|
|
263
|
+
"tag": tag.lstrip('#'),
|
|
264
|
+
}],
|
|
265
|
+
})
|
|
266
|
+
record["facets"] = facets
|
|
267
|
+
|
|
268
|
+
return record
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# -----------------------------------------------------------------------------
|
|
272
|
+
# ActivityPub (Note / Create) Translation
|
|
273
|
+
# -----------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
def activitypub_to_unified(doc: Mapping[str, Any]) -> UnifiedPost:
|
|
276
|
+
"""Translate an ActivityPub Note or Create activity to UnifiedPost."""
|
|
277
|
+
obj: Mapping[str, Any] = doc
|
|
278
|
+
if doc.get('type') == 'Create' and isinstance(doc.get('object'), dict):
|
|
279
|
+
obj = doc['object']
|
|
280
|
+
|
|
281
|
+
content_html = str(obj.get('content', ''))
|
|
282
|
+
text = strip_html(content_html)
|
|
283
|
+
author = str(obj.get('attributedTo') or doc.get('actor', ''))
|
|
284
|
+
pub_str = obj.get('published') or doc.get('published', '')
|
|
285
|
+
created_at = iso_to_unix(pub_str) if pub_str else int(time.time())
|
|
286
|
+
|
|
287
|
+
reply_to = obj.get('inReplyTo')
|
|
288
|
+
if isinstance(reply_to, dict):
|
|
289
|
+
reply_to = reply_to.get('id')
|
|
290
|
+
|
|
291
|
+
attachments: list[str] = []
|
|
292
|
+
raw_atts = obj.get('attachment', [])
|
|
293
|
+
if isinstance(raw_atts, list):
|
|
294
|
+
for att in raw_atts:
|
|
295
|
+
if isinstance(att, dict):
|
|
296
|
+
url = att.get('url')
|
|
297
|
+
if isinstance(url, dict):
|
|
298
|
+
url = url.get('href')
|
|
299
|
+
if url:
|
|
300
|
+
attachments.append(str(url))
|
|
301
|
+
|
|
302
|
+
tags: list[str] = []
|
|
303
|
+
raw_tags = obj.get('tag', [])
|
|
304
|
+
if isinstance(raw_tags, list):
|
|
305
|
+
for t in raw_tags:
|
|
306
|
+
if isinstance(t, dict):
|
|
307
|
+
name = t.get('name', '')
|
|
308
|
+
if name:
|
|
309
|
+
tags.append(name.lstrip('#').lower())
|
|
310
|
+
|
|
311
|
+
return UnifiedPost(
|
|
312
|
+
text=text,
|
|
313
|
+
author=author,
|
|
314
|
+
created_at=created_at,
|
|
315
|
+
attachments=attachments,
|
|
316
|
+
reply_to=str(reply_to) if reply_to else None,
|
|
317
|
+
tags=tags,
|
|
318
|
+
extra={"activitypub_id": obj.get("id") or doc.get("id")},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def unified_to_activitypub(
|
|
323
|
+
post: UnifiedPost,
|
|
324
|
+
domain: str = "node.fleetmesh.local",
|
|
325
|
+
wrap_in_create: bool = True,
|
|
326
|
+
) -> Mapping[str, Any]:
|
|
327
|
+
"""Translate UnifiedPost to an ActivityPub Note (or Create(Note))."""
|
|
328
|
+
obj_id = f"https://{domain}/objects/{post.extra.get('nostr_id') or post.created_at}"
|
|
329
|
+
author_uri = post.author if post.author.startswith("https://") else f"https://{domain}/users/{post.author or 'node'}"
|
|
330
|
+
|
|
331
|
+
note: MutableMapping[str, Any] = {
|
|
332
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
333
|
+
"id": obj_id,
|
|
334
|
+
"type": "Note",
|
|
335
|
+
"attributedTo": author_uri,
|
|
336
|
+
"content": text_to_html(post.text),
|
|
337
|
+
"published": unix_to_iso(post.created_at or int(time.time())),
|
|
338
|
+
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if post.reply_to:
|
|
342
|
+
note["inReplyTo"] = post.reply_to
|
|
343
|
+
|
|
344
|
+
if post.attachments:
|
|
345
|
+
note["attachment"] = [
|
|
346
|
+
{"type": "Document", "mediaType": "image/jpeg", "url": att}
|
|
347
|
+
for att in post.attachments
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
if post.tags:
|
|
351
|
+
note["tag"] = [
|
|
352
|
+
{"type": "Hashtag", "name": f"#{t.lstrip('#')}"}
|
|
353
|
+
for t in post.tags
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
if not wrap_in_create:
|
|
357
|
+
return note
|
|
358
|
+
|
|
359
|
+
activity_id = f"{obj_id}/activity"
|
|
360
|
+
return {
|
|
361
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
362
|
+
"id": activity_id,
|
|
363
|
+
"type": "Create",
|
|
364
|
+
"actor": author_uri,
|
|
365
|
+
"published": note["published"],
|
|
366
|
+
"to": note["to"],
|
|
367
|
+
"object": note,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# -----------------------------------------------------------------------------
|
|
372
|
+
# Secure Scuttlebutt (SSB post) Translation
|
|
373
|
+
# -----------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
def ssb_to_unified(msg: Mapping[str, Any]) -> UnifiedPost:
|
|
376
|
+
"""Translate an SSB message document or post content to UnifiedPost."""
|
|
377
|
+
val = msg.get('value', msg)
|
|
378
|
+
content = val.get('content', msg)
|
|
379
|
+
|
|
380
|
+
text = str(content.get('text', ''))
|
|
381
|
+
author = str(val.get('author', ''))
|
|
382
|
+
ts = val.get('timestamp') or msg.get('timestamp', 0)
|
|
383
|
+
created_at = int(ts / 1000) if ts > 10000000000 else int(ts) or int(time.time())
|
|
384
|
+
|
|
385
|
+
reply_to = content.get('branch')
|
|
386
|
+
root = content.get('root')
|
|
387
|
+
|
|
388
|
+
tags: list[str] = []
|
|
389
|
+
channel = content.get('channel')
|
|
390
|
+
if channel:
|
|
391
|
+
tags.append(str(channel).lstrip('#').lower())
|
|
392
|
+
|
|
393
|
+
attachments: list[str] = []
|
|
394
|
+
mentions = content.get('mentions', [])
|
|
395
|
+
if isinstance(mentions, list):
|
|
396
|
+
for m in mentions:
|
|
397
|
+
if isinstance(m, dict) and m.get('link'):
|
|
398
|
+
attachments.append(str(m['link']))
|
|
399
|
+
|
|
400
|
+
return UnifiedPost(
|
|
401
|
+
text=text,
|
|
402
|
+
author=author,
|
|
403
|
+
created_at=created_at,
|
|
404
|
+
attachments=attachments,
|
|
405
|
+
reply_to=str(reply_to) if reply_to else None,
|
|
406
|
+
root=str(root) if root else None,
|
|
407
|
+
tags=tags,
|
|
408
|
+
extra={"ssb_key": msg.get("key")},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def unified_to_ssb(post: UnifiedPost) -> Mapping[str, Any]:
|
|
413
|
+
"""Translate UnifiedPost to an SSB post content structure."""
|
|
414
|
+
content: MutableMapping[str, Any] = {
|
|
415
|
+
"type": "post",
|
|
416
|
+
"text": post.text,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if post.root and post.reply_to and post.root != post.reply_to:
|
|
420
|
+
content["root"] = post.root
|
|
421
|
+
content["branch"] = post.reply_to
|
|
422
|
+
elif post.reply_to:
|
|
423
|
+
content["root"] = post.reply_to
|
|
424
|
+
content["branch"] = post.reply_to
|
|
425
|
+
|
|
426
|
+
if post.tags:
|
|
427
|
+
content["channel"] = post.tags[0].lstrip('#')
|
|
428
|
+
|
|
429
|
+
if post.attachments:
|
|
430
|
+
content["mentions"] = [{"link": att} for att in post.attachments]
|
|
431
|
+
|
|
432
|
+
return content
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# -----------------------------------------------------------------------------
|
|
436
|
+
# p2panda (append-only operation log) Translation
|
|
437
|
+
# -----------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def p2panda_to_unified(doc: Mapping[str, Any]) -> UnifiedPost:
|
|
440
|
+
"""Translate a p2panda operation dict or payload mapping to UnifiedPost."""
|
|
441
|
+
header = doc.get('header')
|
|
442
|
+
hdr_map: Mapping[str, Any] = header if isinstance(header, Mapping) else {}
|
|
443
|
+
raw_body = doc.get('body', doc)
|
|
444
|
+
|
|
445
|
+
body_map: Mapping[str, Any] = {}
|
|
446
|
+
text = ""
|
|
447
|
+
if isinstance(raw_body, Mapping) and raw_body is not doc:
|
|
448
|
+
body_map = raw_body
|
|
449
|
+
text = str(body_map.get('text') or body_map.get('content') or '')
|
|
450
|
+
elif isinstance(raw_body, (bytes, bytearray)):
|
|
451
|
+
decoded = bytes(raw_body).decode('utf-8', errors='replace')
|
|
452
|
+
text = decoded
|
|
453
|
+
elif isinstance(raw_body, str):
|
|
454
|
+
text = raw_body
|
|
455
|
+
else:
|
|
456
|
+
body_map = doc
|
|
457
|
+
text = str(doc.get('text') or doc.get('content') or '')
|
|
458
|
+
|
|
459
|
+
author = str(hdr_map.get('public_key') or body_map.get('author') or doc.get('author') or '')
|
|
460
|
+
ts = hdr_map.get('timestamp') or body_map.get('timestamp') or doc.get('created_at') or 0
|
|
461
|
+
created_at = int(ts) if isinstance(ts, (int, float)) and ts > 0 else int(time.time())
|
|
462
|
+
|
|
463
|
+
ext = hdr_map.get('extensions')
|
|
464
|
+
ext_map: Mapping[str, Any] = ext if isinstance(ext, Mapping) else {}
|
|
465
|
+
reply_to = body_map.get('reply_to') or ext_map.get('reply_to') or hdr_map.get('backlink')
|
|
466
|
+
root = body_map.get('root') or ext_map.get('root')
|
|
467
|
+
|
|
468
|
+
raw_tags = body_map.get('tags', [])
|
|
469
|
+
tags = [str(t).lstrip('#').lower() for t in raw_tags if isinstance(t, str)] if isinstance(raw_tags, list) else []
|
|
470
|
+
|
|
471
|
+
raw_atts = body_map.get('attachments', [])
|
|
472
|
+
attachments = [str(a) for a in raw_atts if isinstance(a, str)] if isinstance(raw_atts, list) else []
|
|
473
|
+
|
|
474
|
+
return UnifiedPost(
|
|
475
|
+
text=text,
|
|
476
|
+
author=author,
|
|
477
|
+
created_at=created_at,
|
|
478
|
+
attachments=attachments,
|
|
479
|
+
reply_to=str(reply_to) if reply_to else None,
|
|
480
|
+
root=str(root) if root else None,
|
|
481
|
+
tags=tags,
|
|
482
|
+
extra={"p2panda_seq": hdr_map.get("seq_num")},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def unified_to_p2panda(post: UnifiedPost) -> Mapping[str, Any]:
|
|
487
|
+
"""Translate UnifiedPost to a p2panda operation payload structure."""
|
|
488
|
+
payload: MutableMapping[str, Any] = {
|
|
489
|
+
"type": "p2panda.post",
|
|
490
|
+
"text": post.text,
|
|
491
|
+
"author": post.author,
|
|
492
|
+
"timestamp": post.created_at or int(time.time()),
|
|
493
|
+
}
|
|
494
|
+
if post.reply_to:
|
|
495
|
+
payload["reply_to"] = post.reply_to
|
|
496
|
+
if post.root:
|
|
497
|
+
payload["root"] = post.root
|
|
498
|
+
if post.tags:
|
|
499
|
+
payload["tags"] = [t.lstrip('#') for t in post.tags]
|
|
500
|
+
if post.attachments:
|
|
501
|
+
payload["attachments"] = list(post.attachments)
|
|
502
|
+
return payload
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# -----------------------------------------------------------------------------
|
|
506
|
+
# Rhio (cluster message & blob sync) Translation
|
|
507
|
+
# -----------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
def rhio_to_unified(doc: Mapping[str, Any]) -> UnifiedPost:
|
|
510
|
+
"""Translate a Rhio cluster envelope or payload mapping to UnifiedPost."""
|
|
511
|
+
inner = doc.get('payload')
|
|
512
|
+
payload_map: Mapping[str, Any] = inner if isinstance(inner, Mapping) else doc
|
|
513
|
+
|
|
514
|
+
text = str(payload_map.get('text') or payload_map.get('content') or '')
|
|
515
|
+
author = str(payload_map.get('author') or doc.get('author') or '')
|
|
516
|
+
ts = payload_map.get('timestamp') or doc.get('timestamp') or doc.get('created_at') or 0
|
|
517
|
+
created_at = int(ts) if isinstance(ts, (int, float)) and ts > 0 else int(time.time())
|
|
518
|
+
|
|
519
|
+
reply_to = payload_map.get('reply_to')
|
|
520
|
+
root = payload_map.get('root')
|
|
521
|
+
|
|
522
|
+
raw_tags = payload_map.get('tags', [])
|
|
523
|
+
tags = [str(t).lstrip('#').lower() for t in raw_tags if isinstance(t, str)] if isinstance(raw_tags, list) else []
|
|
524
|
+
|
|
525
|
+
raw_atts = payload_map.get('attachments', [])
|
|
526
|
+
attachments = [str(a) for a in raw_atts if isinstance(a, str)] if isinstance(raw_atts, list) else []
|
|
527
|
+
blob_hash = payload_map.get('blob_hash') or doc.get('hash')
|
|
528
|
+
if isinstance(blob_hash, str) and blob_hash and blob_hash not in attachments:
|
|
529
|
+
attachments.append(blob_hash)
|
|
530
|
+
|
|
531
|
+
return UnifiedPost(
|
|
532
|
+
text=text,
|
|
533
|
+
author=author,
|
|
534
|
+
created_at=created_at,
|
|
535
|
+
attachments=attachments,
|
|
536
|
+
reply_to=str(reply_to) if reply_to else None,
|
|
537
|
+
root=str(root) if root else None,
|
|
538
|
+
tags=tags,
|
|
539
|
+
extra={"rhio_subject": doc.get("subject", "fleetmesh.posts")},
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def unified_to_rhio(post: UnifiedPost, subject: str = "fleetmesh.posts") -> Mapping[str, Any]:
|
|
544
|
+
"""Translate UnifiedPost to a Rhio JetStream publication envelope."""
|
|
545
|
+
payload: MutableMapping[str, Any] = {
|
|
546
|
+
"type": "rhio.post",
|
|
547
|
+
"text": post.text,
|
|
548
|
+
"author": post.author,
|
|
549
|
+
"timestamp": post.created_at or int(time.time()),
|
|
550
|
+
}
|
|
551
|
+
if post.reply_to:
|
|
552
|
+
payload["reply_to"] = post.reply_to
|
|
553
|
+
if post.root:
|
|
554
|
+
payload["root"] = post.root
|
|
555
|
+
if post.tags:
|
|
556
|
+
payload["tags"] = [t.lstrip('#') for t in post.tags]
|
|
557
|
+
if post.attachments:
|
|
558
|
+
payload["attachments"] = list(post.attachments)
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
"subject": subject,
|
|
562
|
+
"timestamp": payload["timestamp"],
|
|
563
|
+
"payload": payload,
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# -----------------------------------------------------------------------------
|
|
568
|
+
# Unified Protocol Translator Matrix
|
|
569
|
+
# -----------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
class ProtocolTranslator:
|
|
572
|
+
"""Dispatches bidirectional translations across all supported protocols."""
|
|
573
|
+
|
|
574
|
+
INGEST_MAP = {
|
|
575
|
+
'nostr': nostr_to_unified,
|
|
576
|
+
'atproto': atproto_to_unified,
|
|
577
|
+
'activitypub': activitypub_to_unified,
|
|
578
|
+
'ssb': ssb_to_unified,
|
|
579
|
+
'p2panda': p2panda_to_unified,
|
|
580
|
+
'rhio': rhio_to_unified,
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
EMIT_MAP = {
|
|
584
|
+
'nostr': unified_to_nostr,
|
|
585
|
+
'atproto': unified_to_atproto,
|
|
586
|
+
'activitypub': unified_to_activitypub,
|
|
587
|
+
'ssb': unified_to_ssb,
|
|
588
|
+
'p2panda': unified_to_p2panda,
|
|
589
|
+
'rhio': unified_to_rhio,
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@classmethod
|
|
593
|
+
def to_unified(cls, source_protocol: str, payload: Mapping[str, Any], **kwargs: Any) -> UnifiedPost:
|
|
594
|
+
"""Parse source protocol payload into UnifiedPost."""
|
|
595
|
+
proto = source_protocol.lower().strip()
|
|
596
|
+
if proto == 'nostr':
|
|
597
|
+
return nostr_to_unified(payload)
|
|
598
|
+
elif proto == 'atproto':
|
|
599
|
+
return atproto_to_unified(payload, author=kwargs.get('author', ''))
|
|
600
|
+
elif proto == 'activitypub':
|
|
601
|
+
return activitypub_to_unified(payload)
|
|
602
|
+
elif proto == 'ssb':
|
|
603
|
+
return ssb_to_unified(payload)
|
|
604
|
+
elif proto == 'p2panda':
|
|
605
|
+
return p2panda_to_unified(payload)
|
|
606
|
+
elif proto == 'rhio':
|
|
607
|
+
return rhio_to_unified(payload)
|
|
608
|
+
else:
|
|
609
|
+
raise ValueError(f"Unsupported source protocol: {source_protocol}")
|
|
610
|
+
|
|
611
|
+
@classmethod
|
|
612
|
+
def from_unified(cls, target_protocol: str, post: UnifiedPost, **kwargs: Any) -> Mapping[str, Any]:
|
|
613
|
+
"""Format UnifiedPost into target protocol payload."""
|
|
614
|
+
proto = target_protocol.lower().strip()
|
|
615
|
+
if proto == 'nostr':
|
|
616
|
+
return unified_to_nostr(post, pubkey=kwargs.get('pubkey'))
|
|
617
|
+
elif proto == 'atproto':
|
|
618
|
+
return unified_to_atproto(post)
|
|
619
|
+
elif proto == 'activitypub':
|
|
620
|
+
return unified_to_activitypub(
|
|
621
|
+
post,
|
|
622
|
+
domain=kwargs.get('domain', 'node.fleetmesh.local'),
|
|
623
|
+
wrap_in_create=kwargs.get('wrap_in_create', True),
|
|
624
|
+
)
|
|
625
|
+
elif proto == 'ssb':
|
|
626
|
+
return unified_to_ssb(post)
|
|
627
|
+
elif proto == 'p2panda':
|
|
628
|
+
return unified_to_p2panda(post)
|
|
629
|
+
elif proto == 'rhio':
|
|
630
|
+
return unified_to_rhio(post, subject=kwargs.get('subject', 'fleetmesh.posts'))
|
|
631
|
+
else:
|
|
632
|
+
raise ValueError(f"Unsupported target protocol: {target_protocol}")
|
|
633
|
+
|
|
634
|
+
@classmethod
|
|
635
|
+
def translate(
|
|
636
|
+
cls,
|
|
637
|
+
source_protocol: str,
|
|
638
|
+
target_protocol: str,
|
|
639
|
+
payload: Mapping[str, Any],
|
|
640
|
+
**kwargs,
|
|
641
|
+
) -> Mapping[str, Any]:
|
|
642
|
+
"""Direct translation between any two supported protocols."""
|
|
643
|
+
post = cls.to_unified(source_protocol, payload, **kwargs)
|
|
644
|
+
return cls.from_unified(target_protocol, post, **kwargs)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""gossip/1.0 over the wire: group-topic epidemic flood, riding the transport
|
|
2
|
+
ladder.
|
|
3
|
+
|
|
4
|
+
A package by seam: envelope.py (PIURI + wire vocabulary + message builders),
|
|
5
|
+
mesh.py (the seen-id and membership stores, which know nothing of the
|
|
6
|
+
Compartment), core.py (the GossipMesh Compartment: membership, graft/prune,
|
|
7
|
+
publish/flood, tick-driven maintenance). No Shape Protocol: unlike budget/
|
|
8
|
+
and compute/, nothing here is a mixin on GossipMesh, so there is no `self`
|
|
9
|
+
to describe. Every name the module exports is re-exported here.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .core import GossipMesh, GOSSIP_LAZY_DEGREE, MCACHE_SECONDS
|
|
14
|
+
from .envelope import (DEFAULT_MESH_DEGREE, GRAFT_TYPE, GossipError, IHAVE_TYPE, IWANT_TYPE,
|
|
15
|
+
MEMBERSHIP_KIND, MESSAGES, PIURI, PRUNE_TYPE, PUBLISH_TYPE, make_graft,
|
|
16
|
+
make_ihave, make_iwant, make_prune, make_publish)
|
|
17
|
+
from .mesh import SeenStore
|
|
18
|
+
|
|
19
|
+
__all__ = ['GossipMesh', 'GOSSIP_LAZY_DEGREE', 'MCACHE_SECONDS', 'DEFAULT_MESH_DEGREE', 'GRAFT_TYPE', 'GossipError',
|
|
20
|
+
'IHAVE_TYPE', 'IWANT_TYPE', 'MEMBERSHIP_KIND', 'MESSAGES', 'PIURI', 'PRUNE_TYPE',
|
|
21
|
+
'PUBLISH_TYPE', 'make_graft', 'make_ihave', 'make_iwant', 'make_prune', 'make_publish',
|
|
22
|
+
'SeenStore']
|