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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. 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']