ReticulumTelemetryHub 0.1.0__py3-none-any.whl → 0.143.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 (108) hide show
  1. reticulum_telemetry_hub/api/__init__.py +23 -0
  2. reticulum_telemetry_hub/api/models.py +323 -0
  3. reticulum_telemetry_hub/api/service.py +836 -0
  4. reticulum_telemetry_hub/api/storage.py +528 -0
  5. reticulum_telemetry_hub/api/storage_base.py +156 -0
  6. reticulum_telemetry_hub/api/storage_models.py +118 -0
  7. reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
  8. reticulum_telemetry_hub/atak_cot/base.py +277 -0
  9. reticulum_telemetry_hub/atak_cot/chat.py +506 -0
  10. reticulum_telemetry_hub/atak_cot/detail.py +235 -0
  11. reticulum_telemetry_hub/atak_cot/event.py +181 -0
  12. reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
  13. reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
  14. reticulum_telemetry_hub/config/__init__.py +25 -0
  15. reticulum_telemetry_hub/config/constants.py +7 -0
  16. reticulum_telemetry_hub/config/manager.py +515 -0
  17. reticulum_telemetry_hub/config/models.py +215 -0
  18. reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
  19. reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
  20. reticulum_telemetry_hub/internal_api/__init__.py +21 -0
  21. reticulum_telemetry_hub/internal_api/bus.py +344 -0
  22. reticulum_telemetry_hub/internal_api/core.py +690 -0
  23. reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
  24. reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
  25. reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
  26. reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
  27. reticulum_telemetry_hub/internal_api/versioning.py +63 -0
  28. reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
  29. reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
  30. reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
  31. reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
  32. reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
  33. reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
  34. reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
  35. reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
  36. reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
  37. reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
  38. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
  39. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
  40. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
  41. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
  42. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
  43. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
  44. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
  45. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
  46. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
  47. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
  48. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
  49. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
  50. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
  51. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
  52. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
  53. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
  54. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
  55. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
  56. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
  57. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
  58. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
  59. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
  60. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
  61. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
  62. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
  63. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
  64. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
  65. reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
  66. reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
  67. reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
  68. reticulum_telemetry_hub/northbound/__init__.py +5 -0
  69. reticulum_telemetry_hub/northbound/app.py +195 -0
  70. reticulum_telemetry_hub/northbound/auth.py +119 -0
  71. reticulum_telemetry_hub/northbound/gateway.py +310 -0
  72. reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
  73. reticulum_telemetry_hub/northbound/models.py +213 -0
  74. reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
  75. reticulum_telemetry_hub/northbound/routes_files.py +119 -0
  76. reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
  77. reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
  78. reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
  79. reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
  80. reticulum_telemetry_hub/northbound/serializers.py +72 -0
  81. reticulum_telemetry_hub/northbound/services.py +373 -0
  82. reticulum_telemetry_hub/northbound/websocket.py +855 -0
  83. reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
  84. reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
  85. reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
  86. reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
  87. reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
  88. reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
  89. reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
  90. reticulum_telemetry_hub/reticulum_server/services.py +422 -0
  91. reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
  92. reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
  93. {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
  94. reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
  95. lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
  96. lxmf_telemetry/model/persistance/__init__.py +0 -3
  97. lxmf_telemetry/model/persistance/sensors/location.py +0 -69
  98. lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
  99. lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
  100. lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
  101. lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
  102. lxmf_telemetry/telemetry_controller.py +0 -124
  103. reticulum_server/main.py +0 -182
  104. reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
  105. reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
  106. {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
  107. {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
  108. {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
@@ -0,0 +1,1006 @@
1
+ import RNS
2
+ import RNS.vendor.umsgpack as msgpack
3
+
4
+ import os
5
+ import time
6
+ import base64
7
+ import multiprocessing
8
+ from typing import Any
9
+
10
+ from . import LXStamper
11
+ from .LXMF import APP_NAME
12
+
13
+
14
+ class LXMessage:
15
+ GENERATING = 0x00
16
+ OUTBOUND = 0x01
17
+ SENDING = 0x02
18
+ SENT = 0x04
19
+ DELIVERED = 0x08
20
+ REJECTED = 0xFD
21
+ CANCELLED = 0xFE
22
+ FAILED = 0xFF
23
+ states = [
24
+ GENERATING,
25
+ OUTBOUND,
26
+ SENDING,
27
+ SENT,
28
+ DELIVERED,
29
+ REJECTED,
30
+ CANCELLED,
31
+ FAILED,
32
+ ]
33
+
34
+ UNKNOWN = 0x00
35
+ PACKET = 0x01
36
+ RESOURCE = 0x02
37
+ representations = [UNKNOWN, PACKET, RESOURCE]
38
+
39
+ OPPORTUNISTIC = 0x01
40
+ DIRECT = 0x02
41
+ PROPAGATED = 0x03
42
+ PAPER = 0x05
43
+ valid_methods = [OPPORTUNISTIC, DIRECT, PROPAGATED, PAPER]
44
+
45
+ SOURCE_UNKNOWN = 0x01
46
+ SIGNATURE_INVALID = 0x02
47
+ unverified_reasons = [SOURCE_UNKNOWN, SIGNATURE_INVALID]
48
+
49
+ DESTINATION_LENGTH = RNS.Identity.TRUNCATED_HASHLENGTH // 8
50
+ SIGNATURE_LENGTH = RNS.Identity.SIGLENGTH // 8
51
+ TICKET_LENGTH = RNS.Identity.TRUNCATED_HASHLENGTH // 8
52
+
53
+ # Default ticket expiry is 3 weeks, with an
54
+ # additional grace period of 5 days, allowing
55
+ # for timekeeping inaccuracies. Tickets will
56
+ # automatically renew when there is less than
57
+ # 14 days to expiry.
58
+ TICKET_EXPIRY = 21 * 24 * 60 * 60
59
+ TICKET_GRACE = 5 * 24 * 60 * 60
60
+ TICKET_RENEW = 14 * 24 * 60 * 60
61
+ TICKET_INTERVAL = 1 * 24 * 60 * 60
62
+ COST_TICKET = 0x100
63
+
64
+ # LXMF overhead is 112 bytes per message:
65
+ # 16 bytes for destination hash
66
+ # 16 bytes for source hash
67
+ # 64 bytes for Ed25519 signature
68
+ # 8 bytes for timestamp
69
+ # 8 bytes for msgpack structure
70
+ TIMESTAMP_SIZE = 8
71
+ STRUCT_OVERHEAD = 8
72
+ LXMF_OVERHEAD = (
73
+ 2 * DESTINATION_LENGTH + SIGNATURE_LENGTH + TIMESTAMP_SIZE + STRUCT_OVERHEAD
74
+ )
75
+
76
+ # With an MTU of 500, the maximum amount of data
77
+ # we can send in a single encrypted packet is
78
+ # 391 bytes.
79
+ ENCRYPTED_PACKET_MDU = RNS.Packet.ENCRYPTED_MDU + TIMESTAMP_SIZE
80
+
81
+ # The max content length we can fit in LXMF message
82
+ # inside a single RNS packet is the encrypted MDU, minus
83
+ # the LXMF overhead. We can optimise a bit though, by
84
+ # inferring the destination hash from the destination
85
+ # field of the packet, therefore we also add the length
86
+ # of a destination hash to the calculation. With default
87
+ # RNS and LXMF parameters, the largest single-packet
88
+ # LXMF message we can send is 295 bytes. If a message
89
+ # is larger than that, a Reticulum link will be used.
90
+ ENCRYPTED_PACKET_MAX_CONTENT = (
91
+ ENCRYPTED_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
92
+ )
93
+
94
+ # Links can carry a larger MDU, due to less overhead per
95
+ # packet. The link MDU with default Reticulum parameters
96
+ # is 431 bytes.
97
+ LINK_PACKET_MDU = RNS.Link.MDU
98
+
99
+ # Which means that we can deliver single-packet LXMF
100
+ # messages with content of up to 319 bytes over a link.
101
+ # If a message is larger than that, LXMF will sequence
102
+ # and transfer it as a RNS resource over the link instead.
103
+ LINK_PACKET_MAX_CONTENT = LINK_PACKET_MDU - LXMF_OVERHEAD
104
+
105
+ # For plain packets without encryption, we can
106
+ # fit up to 368 bytes of content.
107
+ PLAIN_PACKET_MDU = RNS.Packet.PLAIN_MDU
108
+ PLAIN_PACKET_MAX_CONTENT = PLAIN_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
109
+
110
+ # Descriptive strings regarding transport encryption
111
+ ENCRYPTION_DESCRIPTION_AES = "AES-128"
112
+ ENCRYPTION_DESCRIPTION_EC = "Curve25519"
113
+ ENCRYPTION_DESCRIPTION_UNENCRYPTED = "Unencrypted"
114
+
115
+ # Constants for QR/URI encoding LXMs
116
+ URI_SCHEMA = "lxm"
117
+ QR_ERROR_CORRECTION = "ERROR_CORRECT_L"
118
+ QR_MAX_STORAGE = 2953
119
+ PAPER_MDU = ((QR_MAX_STORAGE - (len(URI_SCHEMA) + len("://"))) * 6) // 8
120
+
121
+ def __str__(self):
122
+ if self.hash != None:
123
+ return "<LXMessage " + RNS.hexrep(self.hash, delimit=False) + ">"
124
+ else:
125
+ return "<LXMessage>"
126
+
127
+ def __init__(
128
+ self,
129
+ destination,
130
+ source,
131
+ content="",
132
+ title="",
133
+ fields=None,
134
+ desired_method=None,
135
+ destination_hash=None,
136
+ source_hash=None,
137
+ stamp_cost=None,
138
+ include_ticket=False,
139
+ ):
140
+
141
+ if isinstance(destination, RNS.Destination) or destination == None:
142
+ self.__destination = destination
143
+ if destination != None:
144
+ self.destination_hash = destination.hash
145
+ else:
146
+ self.destination_hash = destination_hash
147
+ else:
148
+ raise ValueError("LXMessage initialised with invalid destination")
149
+
150
+ if isinstance(source, RNS.Destination) or source == None:
151
+ self.__source = source
152
+ if source != None:
153
+ self.source_hash = source.hash
154
+ else:
155
+ self.source_hash = source_hash
156
+ else:
157
+ raise ValueError("LXMessage initialised with invalid source")
158
+
159
+ if title == None:
160
+ title = ""
161
+
162
+ if type(title) == bytes:
163
+ self.set_title_from_bytes(title)
164
+ else:
165
+ self.set_title_from_string(title)
166
+
167
+ if type(content) == bytes:
168
+ self.set_content_from_bytes(content)
169
+ else:
170
+ self.set_content_from_string(content)
171
+
172
+ self.set_fields(fields)
173
+
174
+ self.payload = None
175
+ self.timestamp = None
176
+ self.signature = None
177
+ self.hash = None
178
+ self.transient_id = None
179
+ self.packed = None
180
+ self.state = LXMessage.GENERATING
181
+ self.method = LXMessage.UNKNOWN
182
+ self.progress = 0.0
183
+ self.rssi = None
184
+ self.snr = None
185
+ self.q = None
186
+
187
+ self.stamp = None
188
+ self.stamp_cost = stamp_cost
189
+ self.stamp_value = None
190
+ self.stamp_valid = False
191
+ self.stamp_checked = False
192
+ self.propagation_stamp = None
193
+ self.propagation_stamp_value = None
194
+ self.propagation_stamp_valid = False
195
+ self.propagation_target_cost = None
196
+ self.defer_stamp = True
197
+ self.defer_propagation_stamp = True
198
+ self.outbound_ticket = None
199
+ self.include_ticket = include_ticket
200
+
201
+ self.propagation_packed = None
202
+ self.paper_packed = None
203
+
204
+ self.incoming = False
205
+ self.signature_validated = False
206
+ self.unverified_reason = None
207
+ self.ratchet_id = None
208
+
209
+ self.representation = LXMessage.UNKNOWN
210
+ self.desired_method = desired_method
211
+ self.delivery_attempts = 0
212
+ self.transport_encrypted = False
213
+ self.transport_encryption = None
214
+ self.ratchet_id = None
215
+ self.packet_representation = None
216
+ self.resource_representation = None
217
+ self.__delivery_destination = None
218
+ self.__delivery_callback = None
219
+ self.__pn_encrypted_data = None
220
+ self.failed_callback = None
221
+
222
+ self.deferred_stamp_generating = False
223
+
224
+ def set_title_from_string(self, title_string):
225
+ self.title = title_string.encode("utf-8")
226
+
227
+ def set_title_from_bytes(self, title_bytes):
228
+ self.title = title_bytes
229
+
230
+ def title_as_string(self):
231
+ return self.title.decode("utf-8")
232
+
233
+ def set_content_from_string(self, content_string):
234
+ self.content = content_string.encode("utf-8")
235
+
236
+ def set_content_from_bytes(self, content_bytes):
237
+ self.content = content_bytes
238
+
239
+ def content_as_string(self):
240
+ try:
241
+ return self.content.decode("utf-8")
242
+ except Exception as e:
243
+ RNS.log(f"{self} could not decode message content as string: {e}")
244
+ return None
245
+
246
+ def set_fields(self, fields):
247
+ if isinstance(fields, dict) or fields == None:
248
+ self.fields = fields or {}
249
+ else:
250
+ raise ValueError('LXMessage property "fields" can only be dict or None')
251
+
252
+ def get_fields(self):
253
+ return self.fields
254
+
255
+ @property
256
+ def destination(self):
257
+ return self.__destination
258
+
259
+ @destination.setter
260
+ def destination(self, destination):
261
+ self.set_destination(destination)
262
+
263
+ def get_destination(self):
264
+ return self.destination
265
+
266
+ def set_destination(self, destination):
267
+ if self.destination == None:
268
+ if isinstance(destination, RNS.Destination):
269
+ self.__destination = destination
270
+ else:
271
+ raise ValueError("Invalid destination set on LXMessage")
272
+ else:
273
+ raise ValueError("Cannot reassign destination on LXMessage")
274
+
275
+ @property
276
+ def source(self):
277
+ return self.__source
278
+
279
+ @source.setter
280
+ def source(self, source):
281
+ self.set_source(source)
282
+
283
+ def get_source(self):
284
+ return self.source
285
+
286
+ def set_source(self, source):
287
+ if self.source == None:
288
+ if isinstance(source, RNS.Destination):
289
+ self.__source = source
290
+ else:
291
+ raise ValueError("Invalid source set on LXMessage")
292
+ else:
293
+ raise ValueError("Cannot reassign source on LXMessage")
294
+
295
+ def set_delivery_destination(self, delivery_destination):
296
+ self.__delivery_destination = delivery_destination
297
+
298
+ def register_delivery_callback(self, callback):
299
+ self.__delivery_callback = callback
300
+
301
+ def register_failed_callback(self, callback):
302
+ self.failed_callback = callback
303
+
304
+ def validate_stamp(self, target_cost, tickets=None):
305
+ if tickets != None:
306
+ for ticket in tickets:
307
+ try:
308
+ if self.stamp == RNS.Identity.truncated_hash(
309
+ ticket + self.message_id
310
+ ):
311
+ RNS.log(
312
+ f"Stamp on {self} validated by inbound ticket",
313
+ RNS.LOG_DEBUG,
314
+ ) # TODO: Remove at some point
315
+ self.stamp_value = LXMessage.COST_TICKET
316
+ return True
317
+ except Exception as e:
318
+ RNS.log(f"Error while validating ticket: {e}", RNS.LOG_ERROR)
319
+ RNS.trace_exception(e)
320
+
321
+ if self.stamp == None:
322
+ return False
323
+ else:
324
+ workblock = LXStamper.stamp_workblock(self.message_id)
325
+ if LXStamper.stamp_valid(self.stamp, target_cost, workblock):
326
+ RNS.log(
327
+ f"Stamp on {self} validated", RNS.LOG_DEBUG
328
+ ) # TODO: Remove at some point
329
+ self.stamp_value = LXStamper.stamp_value(workblock, self.stamp)
330
+ return True
331
+ else:
332
+ return False
333
+
334
+ def get_stamp(self, timeout=None):
335
+ # If an outbound ticket exists, use this for
336
+ # generating a valid stamp.
337
+ if (
338
+ self.outbound_ticket != None
339
+ and type(self.outbound_ticket) == bytes
340
+ and len(self.outbound_ticket) == LXMessage.TICKET_LENGTH
341
+ ):
342
+ generated_stamp = RNS.Identity.truncated_hash(
343
+ self.outbound_ticket + self.message_id
344
+ )
345
+ self.stamp_value = LXMessage.COST_TICKET
346
+ RNS.log(
347
+ f"Generated stamp with outbound ticket {RNS.hexrep(self.outbound_ticket)} for {self}",
348
+ RNS.LOG_DEBUG,
349
+ ) # TODO: Remove at some point
350
+ return generated_stamp
351
+
352
+ # If no stamp cost is required, we can just
353
+ # return immediately.
354
+ elif self.stamp_cost == None:
355
+ self.stamp_value = None
356
+ return None
357
+
358
+ # If a stamp was already generated, return
359
+ # it immediately.
360
+ elif self.stamp != None:
361
+ return self.stamp
362
+
363
+ # Otherwise, we will need to generate a
364
+ # valid stamp according to the cost that
365
+ # the receiver has specified.
366
+ else:
367
+ generated_stamp, value = LXStamper.generate_stamp(
368
+ self.message_id, self.stamp_cost
369
+ )
370
+ if generated_stamp:
371
+ self.stamp_value = value
372
+ self.stamp_valid = True
373
+ return generated_stamp
374
+
375
+ else:
376
+ return None
377
+
378
+ def get_propagation_stamp(self, target_cost, timeout=None):
379
+ # If a stamp was already generated, return
380
+ # it immediately.
381
+ if self.propagation_stamp != None:
382
+ return self.propagation_stamp
383
+
384
+ # Otherwise, we will need to generate a
385
+ # valid stamp according to the cost that
386
+ # the propagation node has specified.
387
+ else:
388
+ self.propagation_target_cost = target_cost
389
+ if self.propagation_target_cost == None:
390
+ raise ValueError(
391
+ "Cannot generate propagation stamp without configured target propagation cost"
392
+ )
393
+
394
+ if not self.transient_id:
395
+ self.pack()
396
+ generated_stamp, value = LXStamper.generate_stamp(
397
+ self.transient_id,
398
+ target_cost,
399
+ expand_rounds=LXStamper.WORKBLOCK_EXPAND_ROUNDS_PN,
400
+ )
401
+ if generated_stamp:
402
+ self.propagation_stamp = generated_stamp
403
+ self.propagation_stamp_value = value
404
+ self.propagation_stamp_valid = True
405
+ return generated_stamp
406
+
407
+ else:
408
+ return None
409
+
410
+ def pack(self, payload_updated=False):
411
+ if not self.packed:
412
+ if self.timestamp == None:
413
+ self.timestamp = time.time()
414
+
415
+ self.propagation_packed = None
416
+ self.paper_packed = None
417
+
418
+ self.payload = [self.timestamp, self.title, self.content, self.fields]
419
+
420
+ hashed_part = b""
421
+ hashed_part += self.__destination.hash
422
+ hashed_part += self.__source.hash
423
+ hashed_part += msgpack.packb(self.payload)
424
+ self.hash = RNS.Identity.full_hash(hashed_part)
425
+ self.message_id = self.hash
426
+
427
+ if not self.defer_stamp:
428
+ self.stamp = self.get_stamp()
429
+ if self.stamp != None:
430
+ self.payload.append(self.stamp)
431
+
432
+ signed_part = b""
433
+ signed_part += hashed_part
434
+ signed_part += self.hash
435
+ self.signature = self.__source.sign(signed_part)
436
+ self.signature_validated = True
437
+
438
+ packed_payload = msgpack.packb(self.payload)
439
+ self.packed = b""
440
+ self.packed += self.__destination.hash
441
+ self.packed += self.__source.hash
442
+ self.packed += self.signature
443
+ self.packed += packed_payload
444
+ self.packed_size = len(self.packed)
445
+ content_size = (
446
+ len(packed_payload)
447
+ - LXMessage.TIMESTAMP_SIZE
448
+ - LXMessage.STRUCT_OVERHEAD
449
+ )
450
+
451
+ # If no desired delivery method has been defined,
452
+ # one will be chosen according to these rules:
453
+ if self.desired_method == None:
454
+ self.desired_method = LXMessage.DIRECT
455
+
456
+ # If opportunistic delivery was requested, check
457
+ # that message will fit within packet size limits
458
+ if self.desired_method == LXMessage.OPPORTUNISTIC:
459
+ if self.__destination.type == RNS.Destination.SINGLE:
460
+ if content_size > LXMessage.ENCRYPTED_PACKET_MAX_CONTENT:
461
+ RNS.log(
462
+ "Opportunistic delivery was requested for "
463
+ f"{self}, but content of length {content_size} exceeds packet size limit. "
464
+ "Falling back to link-based delivery.",
465
+ RNS.LOG_DEBUG,
466
+ )
467
+ self.desired_method = LXMessage.DIRECT
468
+
469
+ # Set delivery parameters according to delivery method
470
+ if self.desired_method == LXMessage.OPPORTUNISTIC:
471
+ if self.__destination.type == RNS.Destination.SINGLE:
472
+ single_packet_content_limit = LXMessage.ENCRYPTED_PACKET_MAX_CONTENT
473
+ elif self.__destination.type == RNS.Destination.PLAIN:
474
+ single_packet_content_limit = LXMessage.PLAIN_PACKET_MAX_CONTENT
475
+ else:
476
+ raise TypeError(
477
+ "LXMessage desired opportunistic delivery method, but "
478
+ f"destination type {self.__destination.type} is unsupported."
479
+ )
480
+
481
+ if content_size > single_packet_content_limit:
482
+ raise TypeError(
483
+ "LXMessage desired opportunistic delivery method, but "
484
+ f"content of length {content_size} exceeds single-packet content limit of "
485
+ f"{single_packet_content_limit}."
486
+ )
487
+ else:
488
+ self.method = LXMessage.OPPORTUNISTIC
489
+ self.representation = LXMessage.PACKET
490
+ self.__delivery_destination = self.__destination
491
+
492
+ elif self.desired_method == LXMessage.DIRECT:
493
+ single_packet_content_limit = LXMessage.LINK_PACKET_MAX_CONTENT
494
+ if content_size <= single_packet_content_limit:
495
+ self.method = self.desired_method
496
+ self.representation = LXMessage.PACKET
497
+ else:
498
+ self.method = self.desired_method
499
+ self.representation = LXMessage.RESOURCE
500
+
501
+ elif self.desired_method == LXMessage.PROPAGATED:
502
+ single_packet_content_limit = LXMessage.LINK_PACKET_MAX_CONTENT
503
+
504
+ if self.__pn_encrypted_data == None or payload_updated:
505
+ self.__pn_encrypted_data = self.__destination.encrypt(
506
+ self.packed[LXMessage.DESTINATION_LENGTH :]
507
+ )
508
+ self.ratchet_id = self.__destination.latest_ratchet_id
509
+
510
+ lxmf_data = (
511
+ self.packed[: LXMessage.DESTINATION_LENGTH]
512
+ + self.__pn_encrypted_data
513
+ )
514
+ self.transient_id = RNS.Identity.full_hash(lxmf_data)
515
+ if self.propagation_stamp != None:
516
+ lxmf_data += self.propagation_stamp
517
+ self.propagation_packed = msgpack.packb([time.time(), [lxmf_data]])
518
+
519
+ content_size = len(self.propagation_packed)
520
+ if content_size <= single_packet_content_limit:
521
+ self.method = self.desired_method
522
+ self.representation = LXMessage.PACKET
523
+ else:
524
+ self.method = self.desired_method
525
+ self.representation = LXMessage.RESOURCE
526
+
527
+ elif self.desired_method == LXMessage.PAPER:
528
+ paper_content_limit = LXMessage.PAPER_MDU
529
+
530
+ encrypted_data = self.__destination.encrypt(
531
+ self.packed[LXMessage.DESTINATION_LENGTH :]
532
+ )
533
+ self.ratchet_id = self.__destination.latest_ratchet_id
534
+ self.paper_packed = (
535
+ self.packed[: LXMessage.DESTINATION_LENGTH] + encrypted_data
536
+ )
537
+
538
+ content_size = len(self.paper_packed)
539
+ if content_size <= paper_content_limit:
540
+ self.method = self.desired_method
541
+ self.representation = LXMessage.PAPER
542
+ else:
543
+ raise TypeError(
544
+ "LXMessage desired paper delivery method, but content exceeds paper message maximum size."
545
+ )
546
+
547
+ else:
548
+ raise ValueError(
549
+ "Attempt to re-pack LXMessage " + str(self) + " that was already packed"
550
+ )
551
+
552
+ def send(self):
553
+ self.determine_transport_encryption()
554
+
555
+ if self.method == LXMessage.OPPORTUNISTIC:
556
+ lxm_packet = self.__as_packet()
557
+ receipt: Any = lxm_packet.send()
558
+ if not isinstance(receipt, bool) and hasattr(
559
+ receipt, "set_delivery_callback"
560
+ ):
561
+ receipt.set_delivery_callback( # pylint: disable=no-member
562
+ self.__mark_delivered
563
+ )
564
+ self.progress = 0.50
565
+ self.ratchet_id = lxm_packet.ratchet_id
566
+ self.state = LXMessage.SENT
567
+
568
+ elif self.method == LXMessage.DIRECT:
569
+ self.state = LXMessage.SENDING
570
+
571
+ if self.representation == LXMessage.PACKET:
572
+ lxm_packet = self.__as_packet()
573
+ receipt: Any = lxm_packet.send()
574
+ receipt_obj: Any | None = None
575
+ if not isinstance(receipt, bool):
576
+ receipt_obj = receipt
577
+ self.ratchet_id = self.__delivery_destination.link_id
578
+ if receipt_obj:
579
+ receipt_obj.set_delivery_callback( # pylint: disable=no-member
580
+ self.__mark_delivered
581
+ )
582
+ receipt_obj.set_timeout_callback(self.__link_packet_timed_out)
583
+ self.progress = 0.50
584
+ else:
585
+ if self.__delivery_destination:
586
+ self.__delivery_destination.teardown()
587
+
588
+ elif self.representation == LXMessage.RESOURCE:
589
+ self.resource_representation = self.__as_resource()
590
+ self.ratchet_id = self.__delivery_destination.link_id
591
+ self.progress = 0.10
592
+
593
+ elif self.method == LXMessage.PROPAGATED:
594
+ self.state = LXMessage.SENDING
595
+
596
+ if self.representation == LXMessage.PACKET:
597
+ receipt = self.__as_packet().send()
598
+ if receipt:
599
+ receipt.set_delivery_callback(self.__mark_propagated)
600
+ receipt.set_timeout_callback(self.__link_packet_timed_out)
601
+ self.progress = 0.50
602
+ else:
603
+ self.__delivery_destination.teardown()
604
+
605
+ elif self.representation == LXMessage.RESOURCE:
606
+ self.resource_representation = self.__as_resource()
607
+ self.progress = 0.10
608
+
609
+ def determine_transport_encryption(self):
610
+ # TODO: These descriptions are old and outdated.
611
+ # Update the transport encryption descriptions to
612
+ # account for ratchets and other changes.
613
+ if self.method == LXMessage.OPPORTUNISTIC:
614
+ if self.__destination.type == RNS.Destination.SINGLE:
615
+ self.transport_encrypted = True
616
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
617
+ elif self.__destination.type == RNS.Destination.GROUP:
618
+ self.transport_encrypted = True
619
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
620
+ else:
621
+ self.transport_encrypted = False
622
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
623
+ elif self.method == LXMessage.DIRECT:
624
+ self.transport_encrypted = True
625
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
626
+ elif self.method == LXMessage.PROPAGATED:
627
+ if self.__destination.type == RNS.Destination.SINGLE:
628
+ self.transport_encrypted = True
629
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
630
+ elif self.__destination.type == RNS.Destination.GROUP:
631
+ self.transport_encrypted = True
632
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
633
+ else:
634
+ self.transport_encrypted = False
635
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
636
+ elif self.method == LXMessage.PAPER:
637
+ if self.__destination.type == RNS.Destination.SINGLE:
638
+ self.transport_encrypted = True
639
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
640
+ elif self.__destination.type == RNS.Destination.GROUP:
641
+ self.transport_encrypted = True
642
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
643
+ else:
644
+ self.transport_encrypted = False
645
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
646
+ else:
647
+ self.transport_encrypted = False
648
+ self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
649
+
650
+ def __mark_delivered(self, receipt=None):
651
+ RNS.log("Received delivery notification for " + str(self), RNS.LOG_DEBUG)
652
+ self.state = LXMessage.DELIVERED
653
+ self.progress = 1.0
654
+
655
+ if self.__delivery_callback != None and callable(self.__delivery_callback):
656
+ try:
657
+ self.__delivery_callback(self)
658
+ except Exception as e:
659
+ RNS.log(
660
+ "An error occurred in the external delivery callback for "
661
+ + str(self),
662
+ RNS.LOG_ERROR,
663
+ )
664
+ RNS.trace_exception(e)
665
+
666
+ def __mark_propagated(self, receipt=None):
667
+ RNS.log(
668
+ "Received propagation success notification for " + str(self), RNS.LOG_DEBUG
669
+ )
670
+ self.state = LXMessage.SENT
671
+ self.progress = 1.0
672
+
673
+ if self.__delivery_callback != None and callable(self.__delivery_callback):
674
+ try:
675
+ self.__delivery_callback(self)
676
+ except Exception as e:
677
+ RNS.log(
678
+ "An error occurred in the external delivery callback for "
679
+ + str(self),
680
+ RNS.LOG_ERROR,
681
+ )
682
+ RNS.trace_exception(e)
683
+
684
+ def __mark_paper_generated(self, receipt=None):
685
+ RNS.log("Paper message generation succeeded for " + str(self), RNS.LOG_DEBUG)
686
+ self.state = LXMessage.PAPER
687
+ self.progress = 1.0
688
+
689
+ if self.__delivery_callback != None and callable(self.__delivery_callback):
690
+ try:
691
+ self.__delivery_callback(self)
692
+ except Exception as e:
693
+ RNS.log(
694
+ "An error occurred in the external delivery callback for "
695
+ + str(self),
696
+ RNS.LOG_ERROR,
697
+ )
698
+ RNS.trace_exception(e)
699
+
700
+ def __resource_concluded(self, resource):
701
+ if resource.status == RNS.Resource.COMPLETE:
702
+ self.__mark_delivered()
703
+ else:
704
+ if resource.status == RNS.Resource.REJECTED:
705
+ self.state = LXMessage.REJECTED
706
+
707
+ elif self.state != LXMessage.CANCELLED:
708
+ resource.link.teardown()
709
+ self.state = LXMessage.OUTBOUND
710
+
711
+ def __propagation_resource_concluded(self, resource):
712
+ if resource.status == RNS.Resource.COMPLETE:
713
+ self.__mark_propagated()
714
+ else:
715
+ if self.state != LXMessage.CANCELLED:
716
+ resource.link.teardown()
717
+ self.state = LXMessage.OUTBOUND
718
+
719
+ def __link_packet_timed_out(self, packet_receipt):
720
+ if self.state != LXMessage.CANCELLED:
721
+ if packet_receipt:
722
+ packet_receipt.destination.teardown()
723
+
724
+ self.state = LXMessage.OUTBOUND
725
+
726
+ def __update_transfer_progress(self, resource):
727
+ self.progress = 0.10 + (resource.get_progress() * 0.90)
728
+
729
+ def __as_packet(self):
730
+ if not self.packed:
731
+ self.pack()
732
+
733
+ if not self.__delivery_destination:
734
+ raise ValueError(
735
+ "Can't synthesize packet for LXMF message before delivery destination is known"
736
+ )
737
+
738
+ if self.method == LXMessage.OPPORTUNISTIC:
739
+ return RNS.Packet(
740
+ self.__delivery_destination, self.packed[LXMessage.DESTINATION_LENGTH :]
741
+ )
742
+ elif self.method == LXMessage.DIRECT:
743
+ return RNS.Packet(self.__delivery_destination, self.packed)
744
+ elif self.method == LXMessage.PROPAGATED:
745
+ return RNS.Packet(self.__delivery_destination, self.propagation_packed)
746
+
747
+ def __as_resource(self):
748
+ if not self.packed:
749
+ self.pack()
750
+
751
+ if not self.__delivery_destination:
752
+ raise ValueError(
753
+ "Can't synthesize resource for LXMF message before delivery destination is known"
754
+ )
755
+
756
+ if not self.__delivery_destination.type == RNS.Destination.LINK:
757
+ raise TypeError(
758
+ "Tried to synthesize resource for LXMF message on a delivery destination that was not a link"
759
+ )
760
+
761
+ if not self.__delivery_destination.status == RNS.Link.ACTIVE:
762
+ raise ConnectionError(
763
+ "Tried to synthesize resource for LXMF message on a link that was not active"
764
+ )
765
+
766
+ if self.method == LXMessage.DIRECT:
767
+ return RNS.Resource(
768
+ self.packed,
769
+ self.__delivery_destination,
770
+ callback=self.__resource_concluded,
771
+ progress_callback=self.__update_transfer_progress,
772
+ )
773
+ elif self.method == LXMessage.PROPAGATED:
774
+ return RNS.Resource(
775
+ self.propagation_packed,
776
+ self.__delivery_destination,
777
+ callback=self.__propagation_resource_concluded,
778
+ progress_callback=self.__update_transfer_progress,
779
+ )
780
+ else:
781
+ return None
782
+
783
+ def packed_container(self):
784
+ if not self.packed:
785
+ self.pack()
786
+
787
+ container = {
788
+ "state": self.state,
789
+ "lxmf_bytes": self.packed,
790
+ "transport_encrypted": self.transport_encrypted,
791
+ "transport_encryption": self.transport_encryption,
792
+ "method": self.method,
793
+ }
794
+
795
+ return msgpack.packb(container)
796
+
797
+ def write_to_directory(self, directory_path):
798
+ file_name = RNS.hexrep(self.hash, delimit=False)
799
+ file_path = directory_path + "/" + file_name
800
+
801
+ try:
802
+ file = open(file_path, "wb")
803
+ file.write(self.packed_container())
804
+ file.close()
805
+
806
+ return file_path
807
+
808
+ except Exception as e:
809
+ RNS.log(
810
+ 'Error while writing LXMF message to file "'
811
+ + str(file_path)
812
+ + '". The contained exception was: '
813
+ + str(e),
814
+ RNS.LOG_ERROR,
815
+ )
816
+ return None
817
+
818
+ def as_uri(self, finalise=True):
819
+ if not self.packed:
820
+ self.pack()
821
+
822
+ if self.desired_method == LXMessage.PAPER and self.paper_packed != None:
823
+ # Encode packed LXM with URL-safe base64 and remove padding
824
+ encoded_bytes = base64.urlsafe_b64encode(self.paper_packed)
825
+
826
+ # Add protocol specifier and return
827
+ lxm_uri = (
828
+ LXMessage.URI_SCHEMA
829
+ + "://"
830
+ + encoded_bytes.decode("utf-8").replace("=", "")
831
+ )
832
+
833
+ if finalise:
834
+ self.determine_transport_encryption()
835
+ self.__mark_paper_generated()
836
+
837
+ return lxm_uri
838
+
839
+ else:
840
+ raise TypeError(
841
+ "Attempt to represent LXM with non-paper delivery method as URI"
842
+ )
843
+
844
+ def as_qr(self):
845
+ if not self.packed:
846
+ self.pack()
847
+
848
+ if self.desired_method == LXMessage.PAPER and self.paper_packed != None:
849
+ import importlib
850
+
851
+ if importlib.util.find_spec("qrcode") != None:
852
+ import qrcode
853
+
854
+ qr = qrcode.make(
855
+ error_correction=qrcode.constants.__dict__[
856
+ LXMessage.QR_ERROR_CORRECTION
857
+ ],
858
+ border=1,
859
+ data=self.as_uri(finalise=False),
860
+ )
861
+
862
+ self.determine_transport_encryption()
863
+ self.__mark_paper_generated()
864
+
865
+ return qr
866
+
867
+ else:
868
+ RNS.log(
869
+ 'Generating QR-code representanions of LXMs requires the "qrcode" module to be installed.',
870
+ RNS.LOG_CRITICAL,
871
+ )
872
+ RNS.log(
873
+ "You can install it with the command: python3 -m pip install qrcode",
874
+ RNS.LOG_CRITICAL,
875
+ )
876
+ return None
877
+
878
+ else:
879
+ raise TypeError(
880
+ "Attempt to represent LXM with non-paper delivery method as QR-code"
881
+ )
882
+
883
+ @staticmethod
884
+ def unpack_from_bytes(lxmf_bytes, original_method=None):
885
+ destination_hash = lxmf_bytes[: LXMessage.DESTINATION_LENGTH]
886
+ source_hash = lxmf_bytes[
887
+ LXMessage.DESTINATION_LENGTH : 2 * LXMessage.DESTINATION_LENGTH
888
+ ]
889
+ signature = lxmf_bytes[
890
+ 2 * LXMessage.DESTINATION_LENGTH : 2 * LXMessage.DESTINATION_LENGTH
891
+ + LXMessage.SIGNATURE_LENGTH
892
+ ]
893
+ packed_payload = lxmf_bytes[
894
+ 2 * LXMessage.DESTINATION_LENGTH + LXMessage.SIGNATURE_LENGTH :
895
+ ]
896
+ unpacked_payload = msgpack.unpackb(packed_payload)
897
+
898
+ # Extract stamp from payload if included
899
+ if len(unpacked_payload) > 4:
900
+ stamp = unpacked_payload[4]
901
+ unpacked_payload = unpacked_payload[:4]
902
+ packed_payload = msgpack.packb(unpacked_payload)
903
+ else:
904
+ stamp = None
905
+
906
+ hashed_part = b"" + destination_hash + source_hash + packed_payload
907
+ message_hash = RNS.Identity.full_hash(hashed_part)
908
+ signed_part = b"" + hashed_part + message_hash
909
+ timestamp = unpacked_payload[0]
910
+ title_bytes = unpacked_payload[1]
911
+ content_bytes = unpacked_payload[2]
912
+ fields = unpacked_payload[3]
913
+
914
+ destination_identity = RNS.Identity.recall(destination_hash)
915
+ if destination_identity != None:
916
+ destination = RNS.Destination(
917
+ destination_identity,
918
+ RNS.Destination.OUT,
919
+ RNS.Destination.SINGLE,
920
+ APP_NAME,
921
+ "delivery",
922
+ )
923
+ else:
924
+ destination = None
925
+
926
+ source_identity = RNS.Identity.recall(source_hash)
927
+ if source_identity != None:
928
+ source = RNS.Destination(
929
+ source_identity,
930
+ RNS.Destination.OUT,
931
+ RNS.Destination.SINGLE,
932
+ APP_NAME,
933
+ "delivery",
934
+ )
935
+ else:
936
+ source = None
937
+
938
+ message = LXMessage(
939
+ destination=destination,
940
+ source=source,
941
+ content="",
942
+ title="",
943
+ fields=fields,
944
+ destination_hash=destination_hash,
945
+ source_hash=source_hash,
946
+ desired_method=original_method,
947
+ )
948
+
949
+ message.hash = message_hash
950
+ message.message_id = message.hash
951
+ message.signature = signature
952
+ message.stamp = stamp
953
+ message.incoming = True
954
+ message.timestamp = timestamp
955
+ message.packed = lxmf_bytes
956
+ message.packed_size = len(lxmf_bytes)
957
+ message.set_title_from_bytes(title_bytes)
958
+ message.set_content_from_bytes(content_bytes)
959
+
960
+ try:
961
+ if source:
962
+ if source.identity.validate(signature, signed_part):
963
+ message.signature_validated = True
964
+ else:
965
+ message.signature_validated = False
966
+ message.unverified_reason = LXMessage.SIGNATURE_INVALID
967
+ else:
968
+ signature_validated = False
969
+ message.unverified_reason = LXMessage.SOURCE_UNKNOWN
970
+ RNS.log(
971
+ "Unpacked LXMF message signature could not be validated, since source identity is unknown",
972
+ RNS.LOG_DEBUG,
973
+ )
974
+ except Exception as e:
975
+ message.signature_validated = False
976
+ RNS.log(
977
+ "Error while validating LXMF message signature. The contained exception was: "
978
+ + str(e),
979
+ RNS.LOG_ERROR,
980
+ )
981
+
982
+ return message
983
+
984
+ @staticmethod
985
+ def unpack_from_file(lxmf_file_handle):
986
+ try:
987
+ container = msgpack.unpackb(lxmf_file_handle.read())
988
+ lxm = LXMessage.unpack_from_bytes(container["lxmf_bytes"])
989
+
990
+ if "state" in container:
991
+ lxm.state = container["state"]
992
+ if "transport_encrypted" in container:
993
+ lxm.transport_encrypted = container["transport_encrypted"]
994
+ if "transport_encryption" in container:
995
+ lxm.transport_encryption = container["transport_encryption"]
996
+ if "method" in container:
997
+ lxm.method = container["method"]
998
+
999
+ return lxm
1000
+ except Exception as e:
1001
+ RNS.log(
1002
+ "Could not unpack LXMessage from file. The contained exception was: "
1003
+ + str(e),
1004
+ RNS.LOG_ERROR,
1005
+ )
1006
+ return None