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,161 @@
|
|
|
1
|
+
"""Inbound side of the relay rung: a wrap off a relay becomes a message, or a
|
|
2
|
+
drop with a reason. Dedup, unwrap, reassemble fragments, open the envelope
|
|
3
|
+
by the rule every rung shares (didcomm_inbound), then route to the thread
|
|
4
|
+
waiter or the inbox.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import queue
|
|
10
|
+
import time
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional
|
|
12
|
+
|
|
13
|
+
from fleetmesh_core.crypto import giftwrap
|
|
14
|
+
|
|
15
|
+
from .. import didcomm_inbound
|
|
16
|
+
from .. import const
|
|
17
|
+
from ..const import Inbound, _foreign
|
|
18
|
+
from .types import Partial
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .shape import RelayRungShape
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InboundMixin:
|
|
26
|
+
def _remember(self: RelayRungShape, wid: str, kind: Optional[int] = None) -> bool:
|
|
27
|
+
"""True if this wrap is new. The set is capped: a wrap older than the
|
|
28
|
+
cap has aged past the relay's replay horizon anyway. A stored wrap is
|
|
29
|
+
also remembered on disk, because it outlives this process."""
|
|
30
|
+
with self._lock:
|
|
31
|
+
if wid in self._seen:
|
|
32
|
+
return False
|
|
33
|
+
if kind == giftwrap.WRAP_STORED and self.seen_store is not None and self.seen_store[0](wid):
|
|
34
|
+
return False
|
|
35
|
+
self._seen.add(wid)
|
|
36
|
+
self._seen_order.append(wid)
|
|
37
|
+
while len(self._seen_order) > self.seen_max:
|
|
38
|
+
self._seen.discard(self._seen_order.popleft())
|
|
39
|
+
if kind == giftwrap.WRAP_STORED and self.seen_store is not None:
|
|
40
|
+
self.seen_store[1](wid, self.clock())
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def _deliver(self: RelayRungShape, wrap: Any) -> None:
|
|
44
|
+
if not isinstance(wrap, dict) or not isinstance(wrap.get('id'), str):
|
|
45
|
+
self._drop('wrap: not an event', None)
|
|
46
|
+
return
|
|
47
|
+
wid: str = wrap['id']
|
|
48
|
+
# Counted by kind before anything else can discard it. Ephemeral wraps
|
|
49
|
+
# (21059) carry receipts and sync; stored wraps (1059) carry the four
|
|
50
|
+
# messages that must survive an offline peer, an amended grant among
|
|
51
|
+
# them. When every peering on a mesh stopped taking up amends while
|
|
52
|
+
# receipts kept flowing, nothing in this node could answer whether a
|
|
53
|
+
# stored wrap had arrived at all, which is the first question.
|
|
54
|
+
self.wraps_in[wrap.get('kind')] += 1
|
|
55
|
+
if not self._remember(wid, wrap.get('kind')):
|
|
56
|
+
# A wrap already seen used to return silently. A relay replays
|
|
57
|
+
# stored wraps for two days, so this is ordinary - but it is also
|
|
58
|
+
# exactly how a message that was seen and never acted on becomes
|
|
59
|
+
# invisible for ever.
|
|
60
|
+
self.wraps_seen_again[wrap.get('kind')] += 1
|
|
61
|
+
return
|
|
62
|
+
try:
|
|
63
|
+
jwe, seal_pk = giftwrap.unwrap(wrap, self.ks.node_sk, now=self.clock())
|
|
64
|
+
except (giftwrap.GiftWrapError, ValueError, TypeError, KeyError) as e:
|
|
65
|
+
self._drop('wrap: %s' % e, wid)
|
|
66
|
+
return
|
|
67
|
+
if isinstance(jwe, dict) and 'frag' in jwe:
|
|
68
|
+
assembled = self._reassemble(jwe, seal_pk)
|
|
69
|
+
if assembled is None:
|
|
70
|
+
return # more fragments to come
|
|
71
|
+
jwe = assembled
|
|
72
|
+
if not isinstance(jwe, dict) or 'protected' not in jwe:
|
|
73
|
+
# Said apart from malformed traffic when it can be. NIP-59 does not
|
|
74
|
+
# namespace what is inside a wrap, so kind 21059 addressed to this
|
|
75
|
+
# key is shared ground with every other protocol that uses it - one
|
|
76
|
+
# machine running two of them under one key has each daemon's
|
|
77
|
+
# subscription receiving the other's envelopes. It fails closed
|
|
78
|
+
# either way, but one reason for both had an operator reading
|
|
79
|
+
# somebody else's healthy traffic as a peer sending malformed
|
|
80
|
+
# envelopes, and the count is what they read first.
|
|
81
|
+
self._drop(_foreign(jwe), wid)
|
|
82
|
+
return
|
|
83
|
+
# Decrypt and check that the names agree, by the one rule every rung
|
|
84
|
+
# shares (didcomm_inbound). The seal signer is the third name here.
|
|
85
|
+
inbound, why = didcomm_inbound.open_inbound(jwe, self.ks, self.peers, wid, seal_pk=seal_pk)
|
|
86
|
+
if inbound is None:
|
|
87
|
+
self._drop(why or 'envelope: refused', wid)
|
|
88
|
+
return
|
|
89
|
+
message = inbound.message
|
|
90
|
+
thid = message.get('thid')
|
|
91
|
+
# The three names agree, so this is the peer speaking. Everything
|
|
92
|
+
# below is the acknowledgment machinery, which lives here below the
|
|
93
|
+
# protocols because it is about the transport keeping its word.
|
|
94
|
+
self.heard[seal_pk] = self.clock()
|
|
95
|
+
if isinstance(message.get('ack'), list):
|
|
96
|
+
self._acknowledged(message['ack'])
|
|
97
|
+
asks = message.get('please_ack')
|
|
98
|
+
if (isinstance(asks, list) and ('' in asks or message['id'] in asks)
|
|
99
|
+
and message.get('type') != const.EMPTY_TYPE):
|
|
100
|
+
# Never honour a request that arrives on a pure ack, and never
|
|
101
|
+
# more than once per message - the wrap dedup above already
|
|
102
|
+
# makes a replay not a message.
|
|
103
|
+
self._acks.put((message['id'], seal_pk))
|
|
104
|
+
if message.get('type') == const.EMPTY_TYPE:
|
|
105
|
+
# Headers only, by definition. Consumed here; no handler wants it.
|
|
106
|
+
return
|
|
107
|
+
with self._lock:
|
|
108
|
+
waiter = self._waiting.get(thid) if thid else None
|
|
109
|
+
try:
|
|
110
|
+
(waiter if waiter is not None else self.inbox).put_nowait(inbound)
|
|
111
|
+
except queue.Full:
|
|
112
|
+
self._drop('inbox full', wid)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
def _reassemble(self: RelayRungShape, frag: Mapping[str, Any], seal_pk: str) -> Optional[Any]:
|
|
116
|
+
"""Collect fragments by id and sender; the JWE once all have arrived."""
|
|
117
|
+
try:
|
|
118
|
+
fid, i, n, data = frag['frag'], int(frag['i']), int(frag['n']), frag['data']
|
|
119
|
+
except (KeyError, ValueError, TypeError):
|
|
120
|
+
self._drop('malformed fragment', None)
|
|
121
|
+
return None
|
|
122
|
+
if not (0 < n <= const.MAX_FRAGMENTS and 0 <= i < n) or not isinstance(data, str):
|
|
123
|
+
self._drop('fragment out of range', None)
|
|
124
|
+
return None
|
|
125
|
+
if not isinstance(fid, str) or len(fid) > 64 or len(data) > const.FRAGMENT_BYTES + 16:
|
|
126
|
+
self._drop('fragment out of range', None)
|
|
127
|
+
return None
|
|
128
|
+
key = (seal_pk, fid)
|
|
129
|
+
with self._lock:
|
|
130
|
+
# drop assemblies nobody finished in ten minutes
|
|
131
|
+
for k in [k for k, v in self._partials.items() if time.time() - v['at'] > 600]:
|
|
132
|
+
self._partials.pop(k, None)
|
|
133
|
+
parts = self._partials.get(key)
|
|
134
|
+
if parts is None:
|
|
135
|
+
mine = sum(1 for k in self._partials if k[0] == seal_pk)
|
|
136
|
+
if mine >= const.PARTIALS_PER_SENDER or len(self._partials) >= const.PARTIALS_MAX:
|
|
137
|
+
self._drop('fragment: too many unfinished envelopes', None)
|
|
138
|
+
return None
|
|
139
|
+
parts = self._partials[key] = Partial(n=n, parts={}, at=time.time())
|
|
140
|
+
if parts['n'] != n:
|
|
141
|
+
self._drop('fragment: count changed mid-envelope', None)
|
|
142
|
+
self._partials.pop(key, None)
|
|
143
|
+
return None
|
|
144
|
+
parts['parts'][i] = data
|
|
145
|
+
if len(parts['parts']) < n:
|
|
146
|
+
return None
|
|
147
|
+
self._partials.pop(key, None)
|
|
148
|
+
try:
|
|
149
|
+
return json.loads(''.join(parts['parts'][k] for k in range(n)))
|
|
150
|
+
except ValueError:
|
|
151
|
+
self._drop('reassembled fragments are not JSON', None)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def receive(self: RelayRungShape, timeout: Optional[float] = None) -> Optional[Inbound]:
|
|
156
|
+
"""The next unsolicited message, or None on timeout."""
|
|
157
|
+
try:
|
|
158
|
+
return self.inbox.get(timeout=timeout)
|
|
159
|
+
except queue.Empty:
|
|
160
|
+
return None
|
|
161
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""The acknowledgment ledger: stored messages sent and not yet answered.
|
|
2
|
+
|
|
3
|
+
A relay's OK is its word about itself, not about delivery. Every stored
|
|
4
|
+
message asks for an ack (DIDComm `please_ack`) and is held here until one
|
|
5
|
+
arrives; `sweep()` turns silence from a peer that has since spoken into a
|
|
6
|
+
verdict the operator can read.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import queue
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
from .. import const
|
|
15
|
+
from ..const import TransportError
|
|
16
|
+
from .types import AckVerdict, Verdict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .shape import RelayRungShape
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LedgerMixin:
|
|
24
|
+
def _ack(self: RelayRungShape, mid: str, peer: str) -> None:
|
|
25
|
+
"""A pure acknowledgment: the empty message with an `ack` header."""
|
|
26
|
+
msg = {'id': 'ack-' + os.urandom(6).hex(), 'type': const.EMPTY_TYPE, 'thid': mid, 'ack': [mid], 'body': {}}
|
|
27
|
+
# Counted before the send and taken back if it raised, the way the
|
|
28
|
+
# relay face counts what it refuses. The peer's `sweep` clears on the
|
|
29
|
+
# ack itself rather than on this number, so nothing on the wire turns
|
|
30
|
+
# on the order - but health is read by another thread, and a caller
|
|
31
|
+
# that has seen the ack arrive and then reads 0 acks out has been told
|
|
32
|
+
# the wrong thing rather than told it late.
|
|
33
|
+
self.acks_out += 1
|
|
34
|
+
try:
|
|
35
|
+
self.send(msg, peer)
|
|
36
|
+
except TransportError as e:
|
|
37
|
+
self.acks_out -= 1
|
|
38
|
+
self._drop('ack not sent: %s' % e, None)
|
|
39
|
+
|
|
40
|
+
def _acker(self: RelayRungShape) -> None:
|
|
41
|
+
"""Acks are sent off the relay thread, because a send publishes to
|
|
42
|
+
every relay with a timeout each and the subscription must not wait."""
|
|
43
|
+
while not self._stop.is_set():
|
|
44
|
+
try:
|
|
45
|
+
mid, peer = self._acks.get(timeout=0.5)
|
|
46
|
+
except queue.Empty:
|
|
47
|
+
continue
|
|
48
|
+
self._ack(mid, peer)
|
|
49
|
+
|
|
50
|
+
def _acknowledged(self: RelayRungShape, ids: Sequence[Any]) -> None:
|
|
51
|
+
for mid in ids:
|
|
52
|
+
if not isinstance(mid, str):
|
|
53
|
+
continue
|
|
54
|
+
with self._lock:
|
|
55
|
+
entry = self.awaiting.pop(mid, None)
|
|
56
|
+
if entry is None:
|
|
57
|
+
continue
|
|
58
|
+
self.acked[entry['type']] += 1
|
|
59
|
+
self.acks_in += 1
|
|
60
|
+
if self.ack_store is not None:
|
|
61
|
+
self.ack_store.drop_awaiting(mid)
|
|
62
|
+
|
|
63
|
+
def sweep(self: RelayRungShape, now: Optional[int] = None) -> list[AckVerdict]:
|
|
64
|
+
"""Judge what is still unacknowledged, and return the new verdicts.
|
|
65
|
+
|
|
66
|
+
A message is undelivered when the peer has been heard from at least
|
|
67
|
+
ACK_SLACK after it was sent, that hearing is ACK_GRACE old, and no ack
|
|
68
|
+
has come: the peer was up, a relay serving the wrap would have handed
|
|
69
|
+
it over, and the peer said nothing about it. A peer not heard from is
|
|
70
|
+
not a verdict; it may simply be away. Past ACK_EXPIRE the relay would
|
|
71
|
+
not serve the wrap anyway, and the entry is dropped as expired.
|
|
72
|
+
"""
|
|
73
|
+
now_val = int(now if now is not None else self.clock())
|
|
74
|
+
out: list[AckVerdict] = []
|
|
75
|
+
with self._lock:
|
|
76
|
+
for mid, e in list(self.awaiting.items()):
|
|
77
|
+
heard = self.heard.get(e['peer'])
|
|
78
|
+
if heard is not None and heard >= e['at'] + const.ACK_SLACK:
|
|
79
|
+
e['heard_at'] = heard if e['heard_at'] is None else e['heard_at']
|
|
80
|
+
verdict: Optional[Verdict] = None
|
|
81
|
+
if e['heard_at'] is not None and now_val >= e['heard_at'] + const.ACK_GRACE:
|
|
82
|
+
verdict = 'unacknowledged'
|
|
83
|
+
elif now_val >= e['at'] + const.ACK_EXPIRE:
|
|
84
|
+
verdict = 'expired'
|
|
85
|
+
if verdict is None:
|
|
86
|
+
continue
|
|
87
|
+
del self.awaiting[mid]
|
|
88
|
+
v = AckVerdict(peer=e['peer'], type=e['type'], at=e['at'], relays=e['relays'],
|
|
89
|
+
heard_at=e['heard_at'], mid=mid, verdict=verdict, at_verdict=now_val)
|
|
90
|
+
self.unacked.append(v)
|
|
91
|
+
out.append(v)
|
|
92
|
+
self._unacked_new.extend(out)
|
|
93
|
+
if self.ack_store is not None:
|
|
94
|
+
for v in out:
|
|
95
|
+
self.ack_store.drop_awaiting(v['mid'])
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
def take_unacked(self: RelayRungShape) -> list[AckVerdict]:
|
|
99
|
+
"""The verdicts since the last call, for the compartment that records them."""
|
|
100
|
+
with self._lock:
|
|
101
|
+
out: list[AckVerdict] = self._unacked_new
|
|
102
|
+
self._unacked_new = list[AckVerdict]()
|
|
103
|
+
return out
|
|
104
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""The listening side of the relay rung: one subscription per relay for the
|
|
2
|
+
life of the node, with reconnects the operator can read about, and the
|
|
3
|
+
readiness check everything that is about to cause traffic waits on.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
10
|
+
|
|
11
|
+
from ...protocols.nostr import client as relay
|
|
12
|
+
from .. import const
|
|
13
|
+
from . import wire
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .shape import RelayRungShape
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ListenMixin:
|
|
21
|
+
def start(self: RelayRungShape, since: Optional[int] = None) -> Any:
|
|
22
|
+
# A wrap's created_at is skewed up to two days into the past, so a
|
|
23
|
+
# subscription that starts "now" misses most of them. Whatever the
|
|
24
|
+
# caller asks for, the filter reaches back at least that far.
|
|
25
|
+
floor = wire.since_floor(self.clock())
|
|
26
|
+
since_val = floor if since is None else min(since, floor)
|
|
27
|
+
for url in self.relays:
|
|
28
|
+
t = threading.Thread(target=self._listen, args=(url, since_val), daemon=True)
|
|
29
|
+
t.start()
|
|
30
|
+
self._threads.append(t)
|
|
31
|
+
t = threading.Thread(target=self._acker, daemon=True)
|
|
32
|
+
t.start()
|
|
33
|
+
self._threads.append(t)
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def stop(self: RelayRungShape) -> None:
|
|
37
|
+
self._stop.set()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _listen(self: RelayRungShape, url: str, since: int) -> None:
|
|
41
|
+
"""One relay's subscription, for the life of the node.
|
|
42
|
+
|
|
43
|
+
This used to swallow every error and retry once a second, silently,
|
|
44
|
+
with the same `since` it started with. A relay that was down for a
|
|
45
|
+
day was dialled eighty-six thousand times and nobody was told; one
|
|
46
|
+
that came back replayed everything since the node started. The
|
|
47
|
+
state is kept where the operator can read it, the retry backs off,
|
|
48
|
+
and each reconnect asks only for what the skew allowance requires.
|
|
49
|
+
"""
|
|
50
|
+
h = self.health[url]
|
|
51
|
+
backoff = 1.0
|
|
52
|
+
first = True
|
|
53
|
+
while not self._stop.is_set():
|
|
54
|
+
if not first:
|
|
55
|
+
since = wire.since_floor(self.clock())
|
|
56
|
+
first = False
|
|
57
|
+
filt = wire.subscription(self.ks.node_pk, since)
|
|
58
|
+
def _on_late_eose(_sid: str) -> None:
|
|
59
|
+
h['confirmed'] = True
|
|
60
|
+
if h.get('last_error') and 'no EOSE in' in str(h['last_error']):
|
|
61
|
+
h['last_error'] = None
|
|
62
|
+
|
|
63
|
+
r = relay.Relay(url, timeout=5.0, auth_signer=self._sign_auth, on_eose=_on_late_eose)
|
|
64
|
+
try:
|
|
65
|
+
sid = r.connect().subscribe([filt], ready=True)
|
|
66
|
+
# `connected` is the socket; `confirmed` is the relay's own
|
|
67
|
+
# EOSE. They are reported apart because a subscription that
|
|
68
|
+
# was sent and never acknowledged still carries events, so
|
|
69
|
+
# tearing it down would be worse than saying so.
|
|
70
|
+
h['connected'] = True
|
|
71
|
+
h['confirmed'] = r.confirmed(sid)
|
|
72
|
+
h['since'] = self.clock()
|
|
73
|
+
h['last_error'] = (None if r.confirmed(sid)
|
|
74
|
+
else 'the subscription was not acknowledged: no EOSE in %.0fs' % 5.0)
|
|
75
|
+
backoff = 1.0
|
|
76
|
+
for wrap in r.events(sid):
|
|
77
|
+
if self._stop.is_set():
|
|
78
|
+
break
|
|
79
|
+
h['last_event_at'] = self.clock()
|
|
80
|
+
try:
|
|
81
|
+
self._deliver(wrap)
|
|
82
|
+
except Exception as e: # a bug in delivery drops one wrap, not the relay
|
|
83
|
+
self._drop('delivery failed: %s: %s' % (type(e).__name__, e),
|
|
84
|
+
wrap.get('id') if isinstance(wrap, dict) else None)
|
|
85
|
+
if not self._stop.is_set():
|
|
86
|
+
h['last_error'] = 'subscription ended'
|
|
87
|
+
except Exception as e:
|
|
88
|
+
h['last_error'] = '%s: %s' % (type(e).__name__, str(e)[:120])
|
|
89
|
+
finally:
|
|
90
|
+
r.close()
|
|
91
|
+
h['confirmed'] = False
|
|
92
|
+
if h['connected']:
|
|
93
|
+
h['connected'] = False
|
|
94
|
+
h['reconnects'] += 1
|
|
95
|
+
if not self._stop.is_set():
|
|
96
|
+
self._stop.wait(backoff)
|
|
97
|
+
backoff = min(backoff * 2, const.BACKOFF_MAX)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def listening(self: RelayRungShape) -> int:
|
|
101
|
+
"""How many relays have acknowledged this node's own subscription."""
|
|
102
|
+
return sum(1 for h in self.health.values() if h['confirmed'])
|
|
103
|
+
|
|
104
|
+
def wait_listening(self: RelayRungShape, seconds: float = 10.0, at_least: int = 1) -> bool:
|
|
105
|
+
"""Block until `at_least` relays have acknowledged the subscription.
|
|
106
|
+
|
|
107
|
+
`start()` returns as soon as the threads exist, which is not the same as
|
|
108
|
+
being reachable. Between the two, a live wrap addressed to this node is
|
|
109
|
+
fanned out to nobody and no relay keeps a copy, because kind 21059 is
|
|
110
|
+
ephemeral - the node never learns there was anything to miss. Anything
|
|
111
|
+
about to cause traffic waits here instead of sleeping and hoping: a
|
|
112
|
+
node starting up, or a test that is about to publish.
|
|
113
|
+
|
|
114
|
+
False if the wait ran out, which is a relay problem and is already on
|
|
115
|
+
the health of the relay that caused it.
|
|
116
|
+
"""
|
|
117
|
+
deadline = time.time() + seconds
|
|
118
|
+
while time.time() < deadline and self.listening() < at_least:
|
|
119
|
+
time.sleep(0.05)
|
|
120
|
+
return self.listening() >= at_least
|
|
121
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Outbound side of the relay rung: address, encrypt, wrap, publish; and the
|
|
2
|
+
thread waiters request()/converse() use to wait for an answer.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import queue
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, cast
|
|
8
|
+
|
|
9
|
+
from fleetmesh_core.identity import didcomm, relaylist
|
|
10
|
+
from fleetmesh_core.identity.did import did_nostr
|
|
11
|
+
from ...protocols.nostr import client as relay
|
|
12
|
+
from .. import const
|
|
13
|
+
from ..const import Inbound, TransportError
|
|
14
|
+
from . import cooldown, wire
|
|
15
|
+
from .types import AwaitingEntry, OutboundMessage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .shape import RelayRungShape
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OutboundMixin:
|
|
23
|
+
def inbox_relays(self: RelayRungShape, to_pubkey: str) -> list[str]:
|
|
24
|
+
"""Where a stored wrap for this peer goes: the relays its kind 10050
|
|
25
|
+
names (NIP-17), and this node's own when it has published none or
|
|
26
|
+
they cannot be read. The peer's inbox first, because that is where
|
|
27
|
+
it will look; ours as well, because a peer with no list reads its
|
|
28
|
+
descriptor relays and those are usually ours."""
|
|
29
|
+
now = self.clock()
|
|
30
|
+
held = self._dm_relays.get(to_pubkey)
|
|
31
|
+
if held is not None and now - held[1] < const.DM_RELAYS_TTL:
|
|
32
|
+
theirs = held[0]
|
|
33
|
+
else:
|
|
34
|
+
theirs = []
|
|
35
|
+
if self.relays:
|
|
36
|
+
try:
|
|
37
|
+
found = relay.query_all(self.relays, [relaylist.filter_for(to_pubkey)], self.timeout)
|
|
38
|
+
newest = max(found, key=lambda e: int(e.get('created_at', 0)), default=None)
|
|
39
|
+
theirs = relaylist.dm_relays(newest) if newest is not None else []
|
|
40
|
+
except Exception:
|
|
41
|
+
theirs = []
|
|
42
|
+
self._dm_relays[to_pubkey] = (theirs, now)
|
|
43
|
+
return theirs + [r for r in self.relays if r not in theirs]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def send(self: RelayRungShape,
|
|
47
|
+
message: Mapping[str, Any],
|
|
48
|
+
to_pubkey: str,
|
|
49
|
+
anonymous: bool = False,
|
|
50
|
+
stored: bool = False,
|
|
51
|
+
) -> dict[str, tuple[bool, str]]:
|
|
52
|
+
"""Encrypt, wrap and publish. Returns {relay: (accepted, note)}.
|
|
53
|
+
|
|
54
|
+
`stored` sends kind 1059, which relays keep, for the messages the
|
|
55
|
+
specification says must survive the recipient being offline: a grant
|
|
56
|
+
amendment, a complaint notice, a window close. Everything else is
|
|
57
|
+
kind 21059 and gone if nobody is listening, which is the point.
|
|
58
|
+
"""
|
|
59
|
+
their = self.peers.agreement(to_pubkey)
|
|
60
|
+
if their is None:
|
|
61
|
+
raise TransportError('no live descriptor for %s; cannot address an envelope' % to_pubkey[:16])
|
|
62
|
+
msg_dict = cast(OutboundMessage, dict(message)) # the caller's message, addressed below
|
|
63
|
+
msg_dict.setdefault('from', self.ks.did)
|
|
64
|
+
msg_dict.setdefault('to', [did_nostr(to_pubkey)])
|
|
65
|
+
msg_dict.setdefault('created_time', self.clock())
|
|
66
|
+
if stored and isinstance(msg_dict.get('id'), str):
|
|
67
|
+
# "" is DIDComm's shorthand for this message. A pure ack never
|
|
68
|
+
# asks for one, so this cannot start a circle.
|
|
69
|
+
msg_dict['please_ack'] = ['']
|
|
70
|
+
kid = did_nostr(to_pubkey) + '#agree'
|
|
71
|
+
sender = None if anonymous else (self.ks.kid('agreement'), self.ks.agreement_priv)
|
|
72
|
+
jwe = didcomm.encrypt(msg_dict, [(kid, their)], sender=sender)
|
|
73
|
+
held = stored and isinstance(msg_dict.get('id'), str)
|
|
74
|
+
if held:
|
|
75
|
+
# Held before the first relay is asked, not after the last one
|
|
76
|
+
# answers: a peer on the first relay acknowledges in tens of
|
|
77
|
+
# milliseconds, and an entry registered after the second relay's
|
|
78
|
+
# OK was being registered after the ack it waited for had come
|
|
79
|
+
# and gone. Which relays took it is filled in below.
|
|
80
|
+
entry = AwaitingEntry(peer=to_pubkey, type=str(msg_dict.get('type', ''))[-40:],
|
|
81
|
+
at=self.clock(), relays=[], heard_at=None)
|
|
82
|
+
with self._lock:
|
|
83
|
+
self.awaiting[msg_dict['id']] = entry
|
|
84
|
+
results: dict[str, tuple[bool, str]] = {}
|
|
85
|
+
wanted = self.inbox_relays(to_pubkey) if stored else self.relays
|
|
86
|
+
targets = self.cooldown.open(wanted)
|
|
87
|
+
for url in wanted:
|
|
88
|
+
cooling = self.cooldown.holding(url)
|
|
89
|
+
if cooling is not None:
|
|
90
|
+
# Not asked, so not refused; the result says why it was skipped.
|
|
91
|
+
results[url] = (False, 'cooling until %d: %s' % (cooling['until'], cooling['why']))
|
|
92
|
+
for payload in wire.fragments(jwe):
|
|
93
|
+
wrap = wire.make_wrap(payload, self.ks.node_sk, to_pubkey, self.clock(), stored)
|
|
94
|
+
took = relay.publish_all(targets, wrap, self.timeout, auth_signer=self._sign_auth)
|
|
95
|
+
for url, r in took.items():
|
|
96
|
+
if r[0]:
|
|
97
|
+
self.cooldown.accepted(url)
|
|
98
|
+
else:
|
|
99
|
+
cooling = self.cooldown.refused(url, r[1] or '')
|
|
100
|
+
if cooling is not None:
|
|
101
|
+
self._drop('relay cooled: %s until %d (strike %d) %s'
|
|
102
|
+
% (url, cooling['until'], cooling['strikes'], r[1]), wrap['id'])
|
|
103
|
+
if url not in results:
|
|
104
|
+
results[url] = r
|
|
105
|
+
else:
|
|
106
|
+
prev_ok, prev_note = results[url]
|
|
107
|
+
curr_ok, curr_note = r
|
|
108
|
+
results[url] = (prev_ok and curr_ok, curr_note if not curr_ok else prev_note)
|
|
109
|
+
refused = {u: r[1] or 'no reason' for u, r in took.items()
|
|
110
|
+
if not r[0] and not cooldown.is_throttle(r[1] or '')}
|
|
111
|
+
if refused and not any(r[0] for r in took.values()):
|
|
112
|
+
# No relay took this fragment, so the envelope never leaves and
|
|
113
|
+
# nothing else in the protocol will ever say so: the peer times
|
|
114
|
+
# out and the session is merely aborted. One relay of several
|
|
115
|
+
# refusing while another accepts is that relay's weather, kept
|
|
116
|
+
# in its health row; a throttle is recorded above as the
|
|
117
|
+
# cooling it caused.
|
|
118
|
+
self._drop('publish refused: %s' % '; '.join('%s %s' % kv for kv in sorted(refused.items())), wrap['id'])
|
|
119
|
+
elif refused:
|
|
120
|
+
for url, why in refused.items():
|
|
121
|
+
self.faults.record('relay flaked: %s %s' % (url, why), wrap['id'])
|
|
122
|
+
if held:
|
|
123
|
+
accepted = sorted(u for u, r in results.items() if r[0])
|
|
124
|
+
with self._lock:
|
|
125
|
+
still = self.awaiting.get(msg_dict['id'])
|
|
126
|
+
if still is not None:
|
|
127
|
+
if accepted:
|
|
128
|
+
still['relays'] = accepted
|
|
129
|
+
else:
|
|
130
|
+
# Nowhere took it, so it never left; the refused
|
|
131
|
+
# publish above is the record and there is nothing
|
|
132
|
+
# to wait for.
|
|
133
|
+
del self.awaiting[msg_dict['id']]
|
|
134
|
+
still = None
|
|
135
|
+
if still is not None and self.ack_store is not None:
|
|
136
|
+
self.ack_store.put_awaiting(msg_dict['id'], to_pubkey, still['type'], still['at'], accepted)
|
|
137
|
+
return results
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def request(self: RelayRungShape,
|
|
141
|
+
message: Mapping[str, Any],
|
|
142
|
+
to_pubkey: str,
|
|
143
|
+
timeout: float = 30.0,
|
|
144
|
+
anonymous: bool = False,
|
|
145
|
+
) -> Optional[Inbound]:
|
|
146
|
+
"""Send and wait for the reply threaded on this message's id."""
|
|
147
|
+
return self.converse(message, to_pubkey, message['id'], timeout, anonymous)
|
|
148
|
+
|
|
149
|
+
def converse(self: RelayRungShape,
|
|
150
|
+
message: Mapping[str, Any],
|
|
151
|
+
to_pubkey: str,
|
|
152
|
+
thid: str,
|
|
153
|
+
timeout: float = 30.0,
|
|
154
|
+
anonymous: bool = False,
|
|
155
|
+
) -> Optional[Inbound]:
|
|
156
|
+
"""Send and wait for the next message on an existing thread. A sync
|
|
157
|
+
session keeps one thread open across many exchanges, so the waiter is
|
|
158
|
+
keyed by the thread and not by the message just sent."""
|
|
159
|
+
q = self.listen(thid)
|
|
160
|
+
try:
|
|
161
|
+
self.send(message, to_pubkey, anonymous=anonymous)
|
|
162
|
+
try:
|
|
163
|
+
return q.get(timeout=timeout)
|
|
164
|
+
except queue.Empty:
|
|
165
|
+
return None
|
|
166
|
+
finally:
|
|
167
|
+
self.unlisten(thid)
|
|
168
|
+
|
|
169
|
+
def listen(self: RelayRungShape, thid: str) -> queue.Queue[Inbound]:
|
|
170
|
+
"""Route messages on this thread to a private queue instead of the inbox."""
|
|
171
|
+
with self._lock:
|
|
172
|
+
q = self._waiting.get(thid)
|
|
173
|
+
if q is None:
|
|
174
|
+
q = self._waiting[thid] = queue.Queue()
|
|
175
|
+
return q
|
|
176
|
+
|
|
177
|
+
def unlisten(self: RelayRungShape, thid: str) -> None:
|
|
178
|
+
with self._lock:
|
|
179
|
+
self._waiting.pop(thid, None)
|
|
180
|
+
|
|
181
|
+
# ---- inbound -----------------------------------------------------------
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""What the relay rung's mixins may assume about `self`.
|
|
2
|
+
|
|
3
|
+
Declared once so outbound.py, listen.py, inbound.py and ledger.py can call
|
|
4
|
+
each other's methods and read the shared state without inheriting from one
|
|
5
|
+
another. core.RelayRung is the one class that satisfies it.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Any, Callable, Counter, Deque, Mapping, Optional, Protocol
|
|
12
|
+
|
|
13
|
+
from ..const import Inbound
|
|
14
|
+
from ...interfaces import NostrEvent, RelayHealthRow
|
|
15
|
+
from ..ladder import FaultLog
|
|
16
|
+
from .cooldown import RelayCooldown
|
|
17
|
+
from .types import AckVerdict, AwaitingEntry, Partial
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RelayRungShape(Protocol):
|
|
21
|
+
ks: Any
|
|
22
|
+
peers: Any
|
|
23
|
+
relays: list[str]
|
|
24
|
+
clock: Callable[[], int]
|
|
25
|
+
timeout: float
|
|
26
|
+
seen_max: int
|
|
27
|
+
inbox: queue.Queue[Inbound]
|
|
28
|
+
_waiting: dict[str, queue.Queue[Inbound]]
|
|
29
|
+
_lock: threading.Lock
|
|
30
|
+
_stop: threading.Event
|
|
31
|
+
_threads: list[threading.Thread]
|
|
32
|
+
health: dict[str, RelayHealthRow]
|
|
33
|
+
faults: FaultLog
|
|
34
|
+
cooldown: RelayCooldown
|
|
35
|
+
# ledger
|
|
36
|
+
ack_store: Optional[Any]
|
|
37
|
+
awaiting: dict[str, AwaitingEntry]
|
|
38
|
+
heard: dict[str, int]
|
|
39
|
+
acked: Counter[str]
|
|
40
|
+
acks_in: int
|
|
41
|
+
acks_out: int
|
|
42
|
+
unacked: Deque[AckVerdict]
|
|
43
|
+
_unacked_new: list[AckVerdict]
|
|
44
|
+
_acks: queue.Queue[tuple[str, str]]
|
|
45
|
+
# inbound
|
|
46
|
+
seen_store: Optional[tuple[Callable[[str], bool], Callable[[str, int], None]]]
|
|
47
|
+
wraps_in: Counter[Optional[int]]
|
|
48
|
+
wraps_seen_again: Counter[Optional[int]]
|
|
49
|
+
_seen: set[str]
|
|
50
|
+
_seen_order: Deque[str]
|
|
51
|
+
_partials: dict[tuple[str, str], Partial]
|
|
52
|
+
# outbound
|
|
53
|
+
_dm_relays: dict[str, tuple[list[str], int]]
|
|
54
|
+
|
|
55
|
+
def _drop(self, reason: str, wrap_id: Optional[str]) -> None: ...
|
|
56
|
+
def _sign_auth(self, relay_url: str, challenge: str) -> NostrEvent: ...
|
|
57
|
+
def send(self, message: Any, to_pubkey: str, anonymous: bool = False,
|
|
58
|
+
stored: bool = False) -> dict[str, tuple[bool, str]]: ...
|
|
59
|
+
def listen(self, thid: str) -> queue.Queue[Inbound]: ...
|
|
60
|
+
def unlisten(self, thid: str) -> None: ...
|
|
61
|
+
def _deliver(self, wrap: Any) -> None: ...
|
|
62
|
+
def _acknowledged(self, ids: Any) -> None: ...
|
|
63
|
+
def _ack(self, mid: str, peer: str) -> None: ...
|
|
64
|
+
def listening(self) -> int: ...
|
|
65
|
+
def _remember(self, wid: str, kind: Optional[int] = None) -> bool: ...
|
|
66
|
+
def _reassemble(self, frag: Mapping[str, Any], seal_pk: str) -> Optional[Any]: ...
|
|
67
|
+
def inbox_relays(self, to_pubkey: str) -> list[str]: ...
|
|
68
|
+
def converse(self, message: Any, to_pubkey: str, thid: str, timeout: float = 30.0,
|
|
69
|
+
anonymous: bool = False) -> Optional[Inbound]: ...
|
|
70
|
+
def _listen(self, url: str, since: int) -> None: ...
|
|
71
|
+
def _acker(self) -> None: ...
|