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
node/commands/core.py ADDED
@@ -0,0 +1,474 @@
1
+ """Core node lifecycle, health, identity, and operator diagnostics."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import signal
8
+ import stat
9
+ import time
10
+ from typing import Any, MutableMapping, Optional, TypedDict
11
+
12
+ from fleetmesh_core.identity import identity
13
+ from node import app as app_mod, keystore, storage
14
+ from node.app import socket as control
15
+ from fleetmesh_core.wire import validate
16
+ from node import release as release_mod
17
+
18
+
19
+ class WhoamiDescriptorSummary(TypedDict, total=False):
20
+ id: str
21
+ pow_bits: int
22
+ expires: Optional[int]
23
+ protocol: Optional[str]
24
+ implements: list[str]
25
+
26
+
27
+ class FaceDict(TypedDict, total=False):
28
+ host: str
29
+ port: int
30
+
31
+
32
+ class EndpointDict(TypedDict, total=False):
33
+ transport: str
34
+ address: str
35
+ ticket: str
36
+ priority: int
37
+
38
+
39
+ WhoamiReport = TypedDict('WhoamiReport', {
40
+ 'did': str,
41
+ 'pubkey': str,
42
+ 'npub': str,
43
+ 'agreement': str,
44
+ 'transport': str,
45
+ 'config_error': Optional[str],
46
+ 'descriptor': Optional[WhoamiDescriptorSummary],
47
+ 'running': bool,
48
+ 'class': Optional[str],
49
+ 'name': Optional[str],
50
+ 'operator': Optional[str],
51
+ 'relays': list[str],
52
+ 'face': FaceDict,
53
+ 'endpoints': list[EndpointDict],
54
+ }, total=False)
55
+ from .base import (
56
+ _ask,
57
+ _bech32,
58
+ _live,
59
+ _load_doc,
60
+ _short,
61
+ _status_file,
62
+ _table,
63
+ _trouble,
64
+ _when,
65
+ add_command,
66
+ emit,
67
+ fail,
68
+ status_health,
69
+ )
70
+
71
+
72
+ def cmd_init(a: argparse.Namespace) -> int:
73
+ cfg: MutableMapping[str, Any]
74
+ if a.manifest:
75
+ cfg = dict(app_mod.config_from_manifests(_load_doc(a.manifest), [_load_doc(p) for p in a.peering or []]))
76
+ else:
77
+ if not a.operator:
78
+ fail('--operator <pubkey> is required without a manifest')
79
+ cfg = dict(app_mod.defaults())
80
+ cfg.update({'class': a.node_class, 'operator': a.operator})
81
+ cfg['relays'] = list(a.relay or cfg.get('relays') or [])
82
+ if a.endpoint:
83
+ cfg['endpoints'] = []
84
+ for e in a.endpoint:
85
+ scheme, _, rest = e.partition('=')
86
+ addr, _, pref = rest.rpartition(':') if rest.count(':') > 1 and rest.rsplit(':', 1)[1].isdigit() else (rest, '', '50')
87
+ cfg['endpoints'].append({'transport': scheme, 'address': addr, 'priority': int(pref or 50)})
88
+ if a.face_port is not None:
89
+ cfg['face'] = {'host': a.face_host, 'port': a.face_port}
90
+ if a.pow_bits is not None:
91
+ cfg['pow_bits'] = a.pow_bits
92
+ cfg['name'] = a.name or os.path.basename(os.path.abspath(a.dir))
93
+ problems = app_mod.validate_config(cfg)
94
+ if problems:
95
+ fail('refusing to write a config the node could not run on:\n ' + '\n '.join(problems))
96
+ had_keys = os.path.exists(os.path.join(a.dir, keystore.FILE))
97
+ if had_keys and os.path.exists(os.path.join(a.dir, 'config.json')):
98
+ # A node already. init is what a deploy runs on every pass, and a pass
99
+ # that changes nothing must say so rather than re-mine and republish
100
+ # a descriptor the relays already hold.
101
+ ks = keystore.Keystore.load(a.dir)
102
+ return emit(a, {'action': 'init', 'changed': False, 'did': ks.did, 'pubkey': ks.node_pk,
103
+ 'config': os.path.join(a.dir, 'config.json')},
104
+ lambda _: print('already a node: %s (%s); config.json left as it is' % (ks.did, a.dir)))
105
+ app_mod.save_config(a.dir, cfg)
106
+ ks = keystore.Keystore.load_or_generate(a.dir)
107
+ told: dict[str, tuple[bool, str]] = {}
108
+ node = app_mod.NodeApp(a.dir, cfg)
109
+ node.face.start()
110
+ try:
111
+ desc = node.build_descriptor()
112
+ if cfg['relays']:
113
+ from node.protocols.nostr import client as relay
114
+ told = dict(relay.publish_all(cfg['relays'], desc))
115
+ finally:
116
+ node.face.stop()
117
+ nonce = [t for t in desc['tags'] if t[0] == 'nonce']
118
+
119
+ def human(_: Any) -> None:
120
+ print('keys %s' % os.path.join(a.dir, keystore.FILE))
121
+ print('did %s' % ks.did)
122
+ print('agreement %s' % ks.multikeys()['agreement'])
123
+ print('transport %s' % ks.multikeys()['transport'])
124
+ print('descriptor %s (%d tags, pow %s)' % (desc['id'], len(desc['tags']), nonce[0][2] if nonce else 'none'))
125
+ if told:
126
+ for url, (okay, note) in told.items():
127
+ print(' %-8s %s %s' % ('sent' if okay else 'FAILED', url, note))
128
+ else:
129
+ print(' no relays configured; the descriptor is not published yet')
130
+ print('config %s' % os.path.join(a.dir, 'config.json'))
131
+ return emit(a, {'action': 'init', 'changed': True, 'did': ks.did, 'pubkey': ks.node_pk,
132
+ 'descriptor': desc['id'], 'pow': nonce[0][2] if nonce else None,
133
+ 'published': {u: {'ok': okay, 'note': note} for u, (okay, note) in told.items()},
134
+ 'config': os.path.join(a.dir, 'config.json')}, human)
135
+
136
+
137
+ def cmd_run(a: argparse.Namespace) -> int:
138
+ if not os.path.isfile(os.path.join(a.dir, 'config.json')):
139
+ fail('%s has no config.json; run `meshnode init` first' % a.dir)
140
+ if control.running(a.dir):
141
+ try:
142
+ control.call(a.dir, 'health', timeout=5)
143
+ except control.ControlError:
144
+ pass # a stale socket from a node that died; the server replaces it
145
+ else:
146
+ fail('a node is already running in %s' % a.dir)
147
+ try:
148
+ node = app_mod.NodeApp(a.dir).start()
149
+ except (app_mod.ConfigError, PermissionError, ValueError) as e:
150
+ fail(str(e))
151
+ print('meshnode %s' % node.ks.did)
152
+ print(' relay face %s' % node.face.url)
153
+ print(' relays %s' % (', '.join(node.relays) or '(none)'))
154
+ print(' peers %d configured' % len(node.cfg.get('peers', [])))
155
+ print(' control %s' % node.control.path)
156
+ stop = lambda *_: node.stop()
157
+ signal.signal(signal.SIGTERM, stop)
158
+ signal.signal(signal.SIGINT, stop)
159
+ try:
160
+ node.run()
161
+ finally:
162
+ node.stop()
163
+ return 0
164
+
165
+
166
+ def cmd_status(a: argparse.Namespace) -> int:
167
+ st = _live(a, 'status') or _status_file(a.dir)
168
+ if a.json:
169
+ return emit(a, st)
170
+ age = int(time.time()) - int(st.get('at', 0))
171
+ print('%s class %s written %ds ago%s' % (st.get('did'), st.get('class'), age,
172
+ ' (STALE: is the node running?)' if age > 300 else ''))
173
+ rel = st.get('release') or {}
174
+ if rel:
175
+ print('%s %s (%s%s)' % (rel.get('name'), rel.get('version'), rel.get('source'),
176
+ ', ' + rel['commit'][:12] if rel.get('commit') else ''))
177
+ print('relay face %s relays %s' % (st.get('face'), ', '.join(st.get('relays') or []) or '(none)'))
178
+ print('descriptor %s events stored %s' % (_short(st.get('descriptor')) + '…', st.get('events_stored')))
179
+ rows = [(g['peer'], g['direction'], g['tier'], g['status']) for g in st.get('grants', [])]
180
+ if rows:
181
+ print('\ngrants')
182
+ _table(('peer', 'direction', 'tier', 'status'), rows)
183
+ if st.get('pending_grants'):
184
+ print('\nwaiting for the boundary')
185
+ _table(('peer', 'tier', 'from window'), [(p['peer'], p['tier'], '%s (%s UTC)' % (p['from_epoch'], _when(p['from_epoch'])))
186
+ for p in st['pending_grants']])
187
+ ev = st.get('evaluations') or []
188
+ if ev:
189
+ print('\nlast windows priced')
190
+ _table(('peer', 'window', 'closed', 'severity', 'state'),
191
+ [(e['peer'], e['epoch'], _when(e['epoch'] + 1), e['severity'], e['state']) for e in ev[-6:]])
192
+ rp = st.get('repriced') or []
193
+ if rp:
194
+ # A window read again because the subject's receipt for it arrived
195
+ # after the grace had run. Shown because an operator who sees a
196
+ # severity change with no new traffic has nowhere else to look.
197
+ print('\nrepriced after a late receipt')
198
+ _table(('peer', 'window', 'was', 'now', 'remedy'),
199
+ [(r['peer'], r['epoch'], r['was'], r['severity'],
200
+ 'cancelled' if r['remedy_cancelled'] else 'stands') for r in rp])
201
+ print('\nreports published %s findings pending %s sessions refused %s'
202
+ % (st.get('reports_published'), st.get('pending_findings'), st.get('refused_sessions')))
203
+ if st.get('last_tick'):
204
+ print('last tick: ' + '; '.join(' '.join(_short(x) if validate.hex64(x) else x for x in d) for d in st['last_tick']))
205
+ trouble = _trouble(st.get('health'))
206
+ print('\n' + ('\n'.join('!! ' + t for t in trouble) if trouble else 'no trouble reported'))
207
+ return 0
208
+
209
+
210
+ def cmd_health(a: argparse.Namespace) -> int:
211
+ h = _live(a, 'health')
212
+ live = h is not None
213
+ if h is None:
214
+ h = status_health(a.dir)
215
+ if a.json:
216
+ return emit(a, h)
217
+ print('from %s' % ('the running node' if live else 'status.json (the node is not answering)'))
218
+ for url, r in (h.get('relays') or {}).items():
219
+ print('relay %s: %s, reconnects %s%s%s' % (
220
+ url, 'up' if r.get('connected') else 'DOWN', r.get('reconnects'),
221
+ (', last event %ds ago' % (int(time.time()) - r['last_event_at'])) if r.get('last_event_at') else ', no event yet',
222
+ (', last error: %s' % r['last_error']) if r.get('last_error') else ''))
223
+ print('inbox %s unfinished envelopes %s sessions open %s refused %s awaiting ack %s' % (
224
+ h.get('inbox'), h.get('unfinished_envelopes'), h.get('sessions_open'), h.get('sessions_refused'),
225
+ len(h.get('awaiting_ack') or [])))
226
+ if h.get('unacknowledged'):
227
+ # The publish said OK and the peer, who has spoken since, never said
228
+ # it arrived. This is the relay dropping the stored path, and it is
229
+ # the one fault the transport could not see until it asked.
230
+ print('stored messages never acknowledged by a peer that has spoken since:')
231
+ for u in h['unacknowledged']:
232
+ print(' %s %s sent %s UTC, peer heard %s UTC, via %s' % (
233
+ u['peer'], u['type'].rpartition('/')[2],
234
+ time.strftime('%m-%d %H:%M', time.gmtime(u['at'])),
235
+ time.strftime('%m-%d %H:%M', time.gmtime(u['heard_at'])) if u.get('heard_at') else '?',
236
+ ', '.join(u.get('relays') or []) or 'no relay'))
237
+ if h.get('throttled'):
238
+ print('throttled, so quiet is expected until the window turns:')
239
+ for t in h['throttled']:
240
+ print(' %s %s: %s, until %s UTC' % (
241
+ t['peer'][:16], t.get('shard', ''), t.get('why', ''),
242
+ time.strftime('%H:%M', time.gmtime(t['until'])) if t.get('until') else '?'))
243
+ if h.get('receipts_refused'):
244
+ print('receipts refused because the two ends disagree about which grant is current:')
245
+ for r in h['receipts_refused']:
246
+ print(' %s window %s named %s, this node would take %s' % (
247
+ r['peer'], r['window'], r['named'], ' or '.join(r['acceptable']) or '(none)'))
248
+ for label, key in (('dropped by the transport', 'dropped'), ('rejected at the door', 'rejected'),
249
+ ('rejected by the budget', 'budget_rejected')):
250
+ if h.get(key):
251
+ print('%s:' % label)
252
+ for k, v in sorted(h[key].items(), key=lambda kv: -kv[1]):
253
+ print(' %6d %s' % (v, k))
254
+ print('handler failures %s tick failures %s%s' % (
255
+ h.get('handler_failures'), h.get('tick_failures'),
256
+ (' last: %s' % h['last_tick_error']) if h.get('last_tick_error') else ''))
257
+ if h.get('config_error'):
258
+ print('config.json REJECTED, last good one in force: %s' % h['config_error'])
259
+ trouble = _trouble(h)
260
+ print('\n' + ('\n'.join('!! ' + t for t in trouble) if trouble else 'no trouble reported'))
261
+ return 0
262
+
263
+
264
+ def cmd_whoami(a: argparse.Namespace) -> int:
265
+ try:
266
+ ks = keystore.Keystore.load(a.dir)
267
+ except (OSError, ValueError, PermissionError) as e:
268
+ fail('cannot read the keys: %s' % e)
269
+ cfg, cfg_error = None, None
270
+ try:
271
+ cfg = app_mod.load_config(a.dir)
272
+ except (OSError, ValueError) as e:
273
+ cfg_error = str(e)
274
+ who: WhoamiReport = {'did': ks.did, 'pubkey': ks.node_pk, 'npub': _bech32('npub', bytes.fromhex(ks.node_pk)),
275
+ 'agreement': ks.multikeys()['agreement'], 'transport': ks.multikeys()['transport'],
276
+ 'config_error': cfg_error, 'descriptor': None, 'running': control.running(a.dir)}
277
+ if cfg:
278
+ who.update({'class': cfg.get('class'), 'name': cfg.get('name'), 'operator': cfg.get('operator'),
279
+ 'relays': cfg.get('relays') or [], 'face': cfg.get('face', {}), 'endpoints': cfg.get('endpoints') or []})
280
+ dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
281
+ if os.path.exists(dpath):
282
+ with open(dpath, encoding='utf-8') as _fh:
283
+ d = json.load(_fh)
284
+ desc: WhoamiDescriptorSummary = {
285
+ 'id': d['id'], 'pow_bits': identity.pow_bits(d['id']),
286
+ 'expires': next((int(t[1]) for t in d['tags'] if t[0] == 'expiration'), None),
287
+ 'protocol': next((t[1] for t in d['tags'] if t[0] == 'protocol'), None),
288
+ 'implements': [t[1] for t in d['tags'] if t[0] == 'c' and len(t) > 1]}
289
+ who['descriptor'] = desc
290
+ if a.json:
291
+ return emit(a, who)
292
+ if cfg_error:
293
+ print('config unreadable: %s' % cfg_error)
294
+ print('did %s' % who['did'])
295
+ print('pubkey %s' % who['pubkey'])
296
+ print('npub %s' % who['npub'])
297
+ print('agreement %s' % who['agreement'])
298
+ print('transport %s' % who['transport'])
299
+ if cfg:
300
+ print('class %s name %s operator %s' % (cfg.get('class'), cfg.get('name'), _short(cfg.get('operator')) + '…'))
301
+ print('relays %s' % (', '.join(cfg.get('relays') or []) or '(none)'))
302
+ print('face ws://%s:%s' % (cfg.get('face', {}).get('host'), cfg.get('face', {}).get('port')))
303
+ print('endpoints %s' % (', '.join('%s=%s' % (e['transport'], e['address']) for e in cfg.get('endpoints') or []) or '(none: reachable over the nostr transport only)'))
304
+ shown: WhoamiDescriptorSummary | None = who['descriptor']
305
+ if shown:
306
+ print('descriptor %s pow %d bits expires %s protocol %s'
307
+ % (shown['id'], shown['pow_bits'],
308
+ time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(shown['expires'])) if shown['expires'] else '?',
309
+ (shown['protocol'] or '')[:8] + '…'))
310
+ print('implements %s' % ', '.join('/'.join(c.rstrip('/').rsplit('/', 2)[-2:]) if '/' in c else c for c in shown['implements']))
311
+ else:
312
+ print('descriptor none yet (mined on the first run)')
313
+ print('running %s' % ('yes' if who['running'] else 'no'))
314
+ return 0
315
+
316
+
317
+ def cmd_check(a: argparse.Namespace) -> int:
318
+ """Everything that would stop `run`, named at once. Exits 1 when it cannot."""
319
+ problems, notes = [], []
320
+ if not os.path.isdir(a.dir):
321
+ fail('%s is not a directory' % a.dir)
322
+ kpath = os.path.join(a.dir, keystore.FILE)
323
+ if not os.path.exists(kpath):
324
+ problems.append('no %s (run `meshnode init`)' % keystore.FILE)
325
+ else:
326
+ mode = stat.S_IMODE(os.stat(kpath).st_mode)
327
+ if mode & 0o077:
328
+ problems.append('%s is mode %o; it must be 0600' % (keystore.FILE, mode))
329
+ else:
330
+ try:
331
+ keystore.Keystore.load(a.dir)
332
+ notes.append('keys load')
333
+ except (ValueError, PermissionError, OSError) as e:
334
+ problems.append('keys do not load: %s' % e)
335
+ cpath = os.path.join(a.dir, 'config.json')
336
+ if not os.path.exists(cpath):
337
+ problems.append('no config.json (run `meshnode init`)')
338
+ else:
339
+ try:
340
+ cfg = app_mod.load_config(a.dir)
341
+ notes.append('config.json is valid: class %s, %d relay(s), %d peer(s), %d shard(s) claimed'
342
+ % (cfg['class'], len(cfg['relays']), len(cfg.get('peers', [])), len(cfg.get('coverage', []))))
343
+ if not cfg['relays']:
344
+ notes.append('no relays: the descriptor is not published and no peer can reach this node')
345
+ except app_mod.ConfigError as e:
346
+ problems.extend(str(e).replace('config.json: ', '').split('; '))
347
+ except ValueError as e:
348
+ problems.append('config.json is not JSON: %s' % e)
349
+ dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
350
+ if os.path.exists(dpath):
351
+ try:
352
+ with open(dpath, encoding='utf-8') as _fh:
353
+ d = json.load(_fh)
354
+ proto = next((t for t in d['tags'] if t[0] == 'protocol'), None)
355
+ from node.protocols.nostr import descriptor as _d
356
+ if proto and proto[2] != _d.DIGEST:
357
+ notes.append('descriptor.json declares digest %s…, this code is %s…: it will be re-mined on the next run'
358
+ % (proto[2][:8], _d.DIGEST[:8]))
359
+ else:
360
+ notes.append('descriptor.json matches this code\'s digest')
361
+ except (ValueError, KeyError, IndexError) as e:
362
+ problems.append('descriptor.json is unreadable: %s' % e)
363
+ # Soundness asked of the dialect rather than run as a PRAGMA here:
364
+ # `quick_check` is SQLite's, and a file-integrity check is a file-backed
365
+ # idea. A server backend answers for its own soundness.
366
+ for db in ('events.sqlite', 'grants.sqlite', 'accounts.sqlite'):
367
+ p = os.path.join(a.dir, db)
368
+ if os.path.exists(p):
369
+ try:
370
+ st = storage.SqliteStore(p, readonly=True)
371
+ try:
372
+ bad = st.integrity_check()
373
+ finally:
374
+ st.close()
375
+ if bad:
376
+ problems.append('%s: %s' % (db, bad))
377
+ except Exception as e:
378
+ problems.append('%s does not open: %s' % (db, e))
379
+ notes.append('a node is %srunning here' % ('' if control.running(a.dir) else 'not '))
380
+ if a.json:
381
+ emit(a, {'ok': not problems, 'problems': problems, 'notes': notes})
382
+ return 1 if problems else 0
383
+ for n in notes:
384
+ print(' ' + n)
385
+ for p in problems:
386
+ print('!! ' + p)
387
+ print('\n%s' % ('this directory can run' if not problems else '%d problem(s)' % len(problems)))
388
+ return 1 if problems else 0
389
+
390
+
391
+ def cmd_tick(a: argparse.Namespace) -> int:
392
+ did = _ask(a.dir, 'tick', timeout=120)
393
+
394
+ def human(did: list) -> None:
395
+ if not did:
396
+ print('nothing to do')
397
+ return
398
+ for d in did:
399
+ print(' ' + ' '.join(str(x)[:16] + '…' if validate.hex64(str(x)) else str(x) for x in d))
400
+ return emit(a, did, human)
401
+
402
+
403
+ def cmd_api(a: argparse.Namespace) -> int:
404
+ """The control socket described by itself."""
405
+ if control.running(a.dir):
406
+ try:
407
+ v = control.call(a.dir, 'version', timeout=10)
408
+ cmds = control.call(a.dir, 'commands', timeout=10)
409
+ live = True
410
+ except control.ControlError:
411
+ v, cmds, live = None, None, False
412
+ else:
413
+ v, cmds, live = None, None, False
414
+ if v is None:
415
+ v = {'protocol': control.PROTOCOL, 'fleetmesh': app_mod.descriptor.VERSION, 'digest': app_mod.descriptor.DIGEST,
416
+ 'release': release_mod.running().as_dict(), 'commands': list(app_mod.CONTROL)}
417
+ cmds = [dict(name=n, **spec) for n, spec in app_mod.CONTROL.items()]
418
+ if a.json:
419
+ return emit(a, {'version': v, 'commands': cmds, 'live': live})
420
+ print('%s over %s (%s)' % (v['protocol'], control.socket_path(a.dir),
421
+ 'from the running node' if live else 'no node running; from the table it would dispatch on'))
422
+ print('%s@%s' % (v['fleetmesh'], v['digest'][:16] + '…'))
423
+ rel = v.get('release') or {}
424
+ if rel:
425
+ print('%s %s (%s%s)' % (rel.get('name'), rel.get('version'), rel.get('source'),
426
+ ', ' + rel['commit'][:12] if rel.get('commit') else ''))
427
+ print('request {"cmd": name, "args": {...}} reply {"ok": true, "protocol": ..., "result": ...} | {"ok": false, "protocol": ..., "error": ...}')
428
+ print()
429
+ _table(('command', 'args', 'answers'),
430
+ [(c['name'], ' '.join('%s%s' % (k, '' if v_ == 'required' else '?') for k, v_ in c['args'].items()) or '-',
431
+ c['does']) for c in cmds])
432
+ return 0
433
+
434
+
435
+ from ..config.parameter import Parameter
436
+ from .adapter import add_parameter_argument
437
+
438
+ PARAM_INIT_CLASS = Parameter(key='node_class', cli_name='--class', name='node_class', description='node class', type=str, default='edge', choices=('leaf', 'edge', 'anchor'))
439
+ PARAM_INIT_OPERATOR = Parameter(key='operator', name='operator', description='operator pubkey, hex', type=str, default='')
440
+ PARAM_INIT_RELAY = Parameter(key='relay', name='relay', description='control-plane relay, repeatable', type=list, default=None)
441
+ PARAM_INIT_ENDPOINT = Parameter(key='endpoint', name='endpoint', description='scheme=address[:preference], repeatable', type=list, default=None)
442
+ PARAM_INIT_FACE_HOST = Parameter(key='face_host', cli_name='--face-host', name='face_host', description='relay face host', type=str, default='127.0.0.1')
443
+ PARAM_INIT_FACE_PORT = Parameter(key='face_port', cli_name='--face-port', name='face_port', description='relay face port', type=int, default=None)
444
+ PARAM_INIT_POW_BITS = Parameter(key='pow_bits', cli_name='--pow-bits', name='pow_bits', description='PoW difficulty bits', type=int, default=None)
445
+ PARAM_INIT_NAME = Parameter(key='name', name='name', description='node name', type=str, default='')
446
+ PARAM_INIT_MANIFEST = Parameter(key='manifest', name='manifest', description='a MeshNode manifest (yaml or json)', type=str, default='')
447
+ PARAM_INIT_PEERING = Parameter(key='peering', name='peering', description='a Peering manifest, repeatable', type=list, default=None)
448
+
449
+
450
+ class CoreCommandProvider:
451
+ """Registers core lifecycle, health, identity, and operator commands."""
452
+
453
+ @classmethod
454
+ def register(cls, sub: argparse._SubParsersAction) -> None:
455
+ i = add_command(sub, 'init', cmd_init, 'generate keys, write config, mine and publish the descriptor', js=True)
456
+ add_parameter_argument(i, PARAM_INIT_CLASS)
457
+ add_parameter_argument(i, PARAM_INIT_OPERATOR)
458
+ add_parameter_argument(i, PARAM_INIT_RELAY)
459
+ add_parameter_argument(i, PARAM_INIT_ENDPOINT)
460
+ add_parameter_argument(i, PARAM_INIT_FACE_HOST)
461
+ add_parameter_argument(i, PARAM_INIT_FACE_PORT)
462
+ add_parameter_argument(i, PARAM_INIT_POW_BITS)
463
+ add_parameter_argument(i, PARAM_INIT_NAME)
464
+ add_parameter_argument(i, PARAM_INIT_MANIFEST)
465
+ add_parameter_argument(i, PARAM_INIT_PEERING)
466
+
467
+ add_command(sub, 'run', cmd_run, 'start the node and tick until SIGTERM')
468
+ add_command(sub, 'status', cmd_status, 'what the running node last wrote, and its trouble', js=True)
469
+ add_command(sub, 'health', cmd_health, 'every error plane: relays, drops, rejections, failures', js=True)
470
+ add_command(sub, 'whoami', cmd_whoami, "the node's identity: DID, npub, keys, descriptor", js=True)
471
+
472
+ add_command(sub, 'check', cmd_check, 'is this directory a node that can run? exits 1 if not', js=True)
473
+ add_command(sub, 'tick', cmd_tick, 'one tick now, and what it did (needs the node running)', js=True)
474
+ add_command(sub, 'api', cmd_api, 'the control socket described by itself: protocol, commands, arguments, replies', js=True)
@@ -0,0 +1,106 @@
1
+ """Gossip topics: create and publish rosters, admit another admin's, leave,
2
+ and flood a sealed wrap into a mesh. Every verb is the running daemon's
3
+ (it holds the keys and the mesh); this module is the operator's words for
4
+ them."""
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+
9
+ from node.config.parameter import Parameter
10
+ from node.planes.services.gossip.core import GossipMesh
11
+ from .adapter import CommandDispatcher
12
+ from .base import _ask, _peer, _short, _table, add_command, emit, fail, load_json_arg
13
+
14
+
15
+ def _show(r: dict) -> None:
16
+ print('Gossip: %d topic(s), %d seen id(s), %d published, %d delivered, %d refused'
17
+ % (len(r.get('topics', [])), r.get('seen', 0), r.get('published', 0),
18
+ r.get('delivered', 0), r.get('refused', 0)))
19
+ for t in r.get('topics', []):
20
+ print(' %-40s mesh %d' % (t[:40], (r.get('mesh') or {}).get(t, 0)))
21
+
22
+
23
+ def _topics(rows: list) -> None:
24
+ _table(['topic', 'admin', 'members', 'mesh', 'expires'],
25
+ [[_short(r['admin']) + ':' + r['topic'].split(':', 1)[1] if ':' in r['topic'] else r['topic'],
26
+ _short(r['admin']), len(r['members']), len(r['mesh']), r['expires']] for r in rows])
27
+
28
+
29
+ def _cmd_create(a: argparse.Namespace) -> int:
30
+ if not a.name:
31
+ fail('create requires --name <topic-name>')
32
+ members = [_peer(a.dir, m) for m in (a.member or [])]
33
+ if not members:
34
+ fail('create requires at least one --member besides this node')
35
+ r = _ask(a.dir, 'gossip.create', name=a.name, members=members, ttl_seconds=a.ttl,
36
+ no_relay=bool(a.no_relay), timeout=60)
37
+
38
+ def created(r: dict) -> None:
39
+ print('topic %s' % r['topic'])
40
+ print('roster %s, %d member(s), expires %s' % (r['event']['id'][:16], len(members) + 1,
41
+ next(t[1] for t in r['event']['tags'] if t[0] == 'expiration')))
42
+ for url, (ok, why) in (r.get('relays') or {}).items():
43
+ print(' %-40s %s %s' % (url, 'accepted' if ok else 'REFUSED', why))
44
+ if not r.get('relays') and not a.no_relay:
45
+ print(' (no relays configured; give the roster to the members with --json)')
46
+ return emit(a, r, created)
47
+
48
+
49
+ def _cmd_admit(a: argparse.Namespace) -> int:
50
+ if not a.event:
51
+ fail('admit requires --event <json|@file>')
52
+ r = _ask(a.dir, 'gossip.admit', event=load_json_arg(a.event, '--event'), timeout=15)
53
+ return emit(a, r, lambda r: print('admitted %s' % r['topic']))
54
+
55
+
56
+ def _cmd_publish(a: argparse.Namespace) -> int:
57
+ if not a.topic or not a.event:
58
+ fail('publish requires --topic <id> and --event <sealed wrap json|@file>')
59
+ r = _ask(a.dir, 'gossip.publish', topic=a.topic, envelope=load_json_arg(a.event, '--event'), timeout=30)
60
+ return emit(a, r, lambda r: print('published %s into %s: sent to %d of %d mesh peer(s)'
61
+ % (r['event_id'][:16], r['topic'][:40], len(r['sent_to']), r['mesh'])))
62
+
63
+
64
+ _cmd_map = {c.key: c for c in GossipMesh.COMMANDS}
65
+ dispatcher = CommandDispatcher('gossip', default_action='status')
66
+ dispatcher.add_command('status', _cmd_map['show'], human=_show, timeout=10)
67
+ dispatcher.add_command('topics', _cmd_map['topics'], human=_topics, timeout=10)
68
+ dispatcher.add_command('leave', _cmd_map['leave'], human=lambda r, a: print('left %s, pruned %d peer(s)' % (r['topic'], r['pruned'])), timeout=15)
69
+ dispatcher.add_custom(
70
+ 'create',
71
+ handler=_cmd_create,
72
+ parameters=(
73
+ Parameter(key='name', name='name', description='topic name for create; the id becomes <this node pk>:<name>', type=str, default=''),
74
+ Parameter(key='member', name='member', description='a member pubkey/did/npub/prefix for create (repeatable)', type=list, default=list, cli_name='--member'),
75
+ Parameter(key='ttl', name='ttl', description='roster lifetime in seconds for create (default 30 days)', type=int, default=30 * 86400),
76
+ Parameter(key='no_relay', name='no_relay', description='create: do not publish the roster to the configured relays', type=bool, default=False, cli_name='--no-relay'),
77
+ ),
78
+ )
79
+ dispatcher.add_custom(
80
+ 'admit',
81
+ handler=_cmd_admit,
82
+ parameters=(
83
+ Parameter(key='event', name='event', description='JSON event, or @file: a kind 30804 roster for admit, a sealed wrap for publish', type=str, default=''),
84
+ ),
85
+ )
86
+ dispatcher.add_custom(
87
+ 'publish',
88
+ handler=_cmd_publish,
89
+ parameters=(
90
+ Parameter(key='topic', name='topic', description='topic id for leave / publish', type=str, default=''),
91
+ Parameter(key='event', name='event', description='JSON event, or @file: a kind 30804 roster for admit, a sealed wrap for publish', type=str, default=''),
92
+ ),
93
+ )
94
+
95
+
96
+ def cmd_gossip(a: argparse.Namespace) -> int:
97
+ return dispatcher.dispatch(a)
98
+
99
+
100
+ class GossipCommandProvider:
101
+ """Registers the gossip topic commands."""
102
+
103
+ @classmethod
104
+ def register(cls, sub: argparse._SubParsersAction) -> None:
105
+ g = add_command(sub, 'gossip', cmd_gossip, 'gossip/1.0 group topics: create and publish rosters, flood sealed wraps', js=True)
106
+ dispatcher.register_arguments(g, action_choices=('status', 'topics', 'create', 'admit', 'leave', 'publish'))