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.
- reticulum_telemetry_hub/api/__init__.py +23 -0
- reticulum_telemetry_hub/api/models.py +323 -0
- reticulum_telemetry_hub/api/service.py +836 -0
- reticulum_telemetry_hub/api/storage.py +528 -0
- reticulum_telemetry_hub/api/storage_base.py +156 -0
- reticulum_telemetry_hub/api/storage_models.py +118 -0
- reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
- reticulum_telemetry_hub/atak_cot/base.py +277 -0
- reticulum_telemetry_hub/atak_cot/chat.py +506 -0
- reticulum_telemetry_hub/atak_cot/detail.py +235 -0
- reticulum_telemetry_hub/atak_cot/event.py +181 -0
- reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
- reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
- reticulum_telemetry_hub/config/__init__.py +25 -0
- reticulum_telemetry_hub/config/constants.py +7 -0
- reticulum_telemetry_hub/config/manager.py +515 -0
- reticulum_telemetry_hub/config/models.py +215 -0
- reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
- reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
- reticulum_telemetry_hub/internal_api/__init__.py +21 -0
- reticulum_telemetry_hub/internal_api/bus.py +344 -0
- reticulum_telemetry_hub/internal_api/core.py +690 -0
- reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
- reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
- reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
- reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
- reticulum_telemetry_hub/internal_api/versioning.py +63 -0
- reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
- reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
- reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
- reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
- reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
- reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
- reticulum_telemetry_hub/northbound/__init__.py +5 -0
- reticulum_telemetry_hub/northbound/app.py +195 -0
- reticulum_telemetry_hub/northbound/auth.py +119 -0
- reticulum_telemetry_hub/northbound/gateway.py +310 -0
- reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
- reticulum_telemetry_hub/northbound/models.py +213 -0
- reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
- reticulum_telemetry_hub/northbound/routes_files.py +119 -0
- reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
- reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
- reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
- reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
- reticulum_telemetry_hub/northbound/serializers.py +72 -0
- reticulum_telemetry_hub/northbound/services.py +373 -0
- reticulum_telemetry_hub/northbound/websocket.py +855 -0
- reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
- reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
- reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
- reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
- reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
- reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
- reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
- reticulum_telemetry_hub/reticulum_server/services.py +422 -0
- reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
- reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
- {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
- reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
- lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
- lxmf_telemetry/model/persistance/__init__.py +0 -3
- lxmf_telemetry/model/persistance/sensors/location.py +0 -69
- lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
- lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
- lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
- lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
- lxmf_telemetry/telemetry_controller.py +0 -124
- reticulum_server/main.py +0 -182
- reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
- reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
- {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
- {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
- {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""High level Python API mirroring the ReticulumTelemetryHub OpenAPI spec."""
|
|
2
|
+
|
|
3
|
+
from .models import ChatAttachment
|
|
4
|
+
from .models import ChatMessage
|
|
5
|
+
from .models import Client
|
|
6
|
+
from .models import FileAttachment
|
|
7
|
+
from .models import IdentityStatus
|
|
8
|
+
from .models import ReticulumInfo
|
|
9
|
+
from .models import Subscriber
|
|
10
|
+
from .models import Topic
|
|
11
|
+
from .service import ReticulumTelemetryHubAPI
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Topic",
|
|
15
|
+
"Subscriber",
|
|
16
|
+
"Client",
|
|
17
|
+
"FileAttachment",
|
|
18
|
+
"ChatAttachment",
|
|
19
|
+
"ChatMessage",
|
|
20
|
+
"IdentityStatus",
|
|
21
|
+
"ReticulumInfo",
|
|
22
|
+
"ReticulumTelemetryHubAPI",
|
|
23
|
+
]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Data models for the Reticulum Telemetry Hub API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
from datetime import timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import Dict
|
|
14
|
+
from typing import List
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _now() -> datetime:
|
|
19
|
+
"""Return the current UTC timestamp with timezone information."""
|
|
20
|
+
|
|
21
|
+
return datetime.now(timezone.utc)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _json_safe_key(key: Any) -> str:
|
|
25
|
+
"""Return a JSON-safe dictionary key."""
|
|
26
|
+
|
|
27
|
+
if isinstance(key, (bytes, bytearray, memoryview)):
|
|
28
|
+
return bytes(key).hex()
|
|
29
|
+
if key is None:
|
|
30
|
+
return "null"
|
|
31
|
+
return str(key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _json_safe(value: Any) -> Any:
|
|
35
|
+
"""Return a JSON-safe version of ``value``."""
|
|
36
|
+
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return {_json_safe_key(k): _json_safe(v) for k, v in value.items()}
|
|
39
|
+
if isinstance(value, (list, tuple, set)):
|
|
40
|
+
return [_json_safe(item) for item in value]
|
|
41
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
42
|
+
return bytes(value).hex()
|
|
43
|
+
if isinstance(value, datetime):
|
|
44
|
+
return value.isoformat()
|
|
45
|
+
if isinstance(value, Path):
|
|
46
|
+
return str(value)
|
|
47
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
48
|
+
return value
|
|
49
|
+
return str(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Topic:
|
|
54
|
+
"""Topic subscription metadata for the Reticulum Telemetry Hub."""
|
|
55
|
+
|
|
56
|
+
topic_name: str
|
|
57
|
+
topic_path: str
|
|
58
|
+
topic_description: str = ""
|
|
59
|
+
topic_id: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
"""Serialize the topic into the external API schema."""
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"TopicID": self.topic_id,
|
|
66
|
+
"TopicName": self.topic_name,
|
|
67
|
+
"TopicPath": self.topic_path,
|
|
68
|
+
"TopicDescription": self.topic_description,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Topic":
|
|
73
|
+
"""Create a Topic from a dictionary with flexible casing.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data (Dict[str, Any]): Source mapping using either title-case or
|
|
77
|
+
snake_case keys.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Topic: Parsed topic instance with defaults for missing values.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
topic_name=data.get("TopicName") or data.get("topic_name") or "",
|
|
85
|
+
topic_path=data.get("TopicPath") or data.get("topic_path") or "",
|
|
86
|
+
topic_description=data.get("TopicDescription")
|
|
87
|
+
or data.get("topic_description")
|
|
88
|
+
or "",
|
|
89
|
+
topic_id=data.get("TopicID") or data.get("topic_id"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Subscriber:
|
|
95
|
+
"""Subscription details linking destinations to topics."""
|
|
96
|
+
|
|
97
|
+
destination: str
|
|
98
|
+
topic_id: Optional[str] = None
|
|
99
|
+
reject_tests: Optional[int] = None
|
|
100
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
subscriber_id: Optional[str] = None
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
"""Serialize the subscriber into the external API schema."""
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"SubscriberID": self.subscriber_id,
|
|
108
|
+
"Destination": self.destination,
|
|
109
|
+
"TopicID": self.topic_id,
|
|
110
|
+
"RejectTests": self.reject_tests,
|
|
111
|
+
"Metadata": _json_safe(self.metadata or None),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Subscriber":
|
|
116
|
+
"""Create a Subscriber from a dictionary with flexible casing.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
data (Dict[str, Any]): Source mapping using either title-case or
|
|
120
|
+
snake_case keys.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Subscriber: Parsed subscriber instance.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
reject_tests = None
|
|
127
|
+
if "RejectTests" in data:
|
|
128
|
+
reject_tests = data.get("RejectTests")
|
|
129
|
+
elif "reject_tests" in data:
|
|
130
|
+
reject_tests = data.get("reject_tests")
|
|
131
|
+
|
|
132
|
+
return cls(
|
|
133
|
+
destination=data.get("Destination") or data.get("destination") or "",
|
|
134
|
+
topic_id=data.get("TopicID") or data.get("topic_id"),
|
|
135
|
+
reject_tests=reject_tests,
|
|
136
|
+
metadata=data.get("Metadata") or data.get("metadata") or {},
|
|
137
|
+
subscriber_id=data.get("SubscriberID") or data.get("subscriber_id"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class Client:
|
|
143
|
+
"""Connected client state and metadata."""
|
|
144
|
+
|
|
145
|
+
identity: str
|
|
146
|
+
last_seen: datetime = field(default_factory=_now)
|
|
147
|
+
display_name: Optional[str] = None
|
|
148
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
149
|
+
|
|
150
|
+
def touch(self) -> None:
|
|
151
|
+
"""Update ``last_seen`` to the latest timestamp in UTC."""
|
|
152
|
+
|
|
153
|
+
now = _now()
|
|
154
|
+
if now <= self.last_seen:
|
|
155
|
+
now = self.last_seen + timedelta(microseconds=1)
|
|
156
|
+
self.last_seen = now
|
|
157
|
+
|
|
158
|
+
def to_dict(self) -> dict:
|
|
159
|
+
"""Serialize the client into primitive types for JSON responses."""
|
|
160
|
+
|
|
161
|
+
data = asdict(self)
|
|
162
|
+
data["last_seen"] = self.last_seen.isoformat()
|
|
163
|
+
data["metadata"] = _json_safe(self.metadata or {})
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
# pylint: disable=too-many-instance-attributes
|
|
169
|
+
class ReticulumInfo:
|
|
170
|
+
"""Application and environment metadata exposed by the API."""
|
|
171
|
+
|
|
172
|
+
is_transport_enabled: bool
|
|
173
|
+
is_connected_to_shared_instance: bool
|
|
174
|
+
reticulum_config_path: str
|
|
175
|
+
database_path: str
|
|
176
|
+
storage_path: str
|
|
177
|
+
file_storage_path: str
|
|
178
|
+
image_storage_path: str
|
|
179
|
+
app_name: str
|
|
180
|
+
rns_version: str
|
|
181
|
+
lxmf_version: str
|
|
182
|
+
app_version: str
|
|
183
|
+
app_description: str
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> dict:
|
|
186
|
+
"""Serialize the info model to a dictionary."""
|
|
187
|
+
|
|
188
|
+
data = asdict(self)
|
|
189
|
+
data["name"] = self.app_name
|
|
190
|
+
data["version"] = self.app_version
|
|
191
|
+
data["description"] = self.app_description
|
|
192
|
+
data["storage_paths"] = {
|
|
193
|
+
"storage": self.storage_path,
|
|
194
|
+
"database": self.database_path,
|
|
195
|
+
"reticulum_config": self.reticulum_config_path,
|
|
196
|
+
"files": self.file_storage_path,
|
|
197
|
+
"images": self.image_storage_path,
|
|
198
|
+
}
|
|
199
|
+
return data
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class FileAttachment:
|
|
204
|
+
"""Metadata for files or images stored by the hub."""
|
|
205
|
+
|
|
206
|
+
name: str
|
|
207
|
+
path: str
|
|
208
|
+
category: str
|
|
209
|
+
size: int
|
|
210
|
+
media_type: Optional[str] = None
|
|
211
|
+
topic_id: Optional[str] = None
|
|
212
|
+
created_at: datetime = field(default_factory=_now)
|
|
213
|
+
updated_at: datetime = field(default_factory=_now)
|
|
214
|
+
file_id: Optional[int] = None
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> dict:
|
|
217
|
+
"""Return a serialization friendly representation."""
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"FileID": self.file_id,
|
|
221
|
+
"Name": self.name,
|
|
222
|
+
"Path": self.path,
|
|
223
|
+
"Category": self.category,
|
|
224
|
+
"MediaType": self.media_type,
|
|
225
|
+
"TopicID": self.topic_id,
|
|
226
|
+
"Size": self.size,
|
|
227
|
+
"CreatedAt": self.created_at.isoformat(),
|
|
228
|
+
"UpdatedAt": self.updated_at.isoformat(),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class ChatAttachment:
|
|
234
|
+
"""Attachment metadata associated with a chat message."""
|
|
235
|
+
|
|
236
|
+
file_id: int
|
|
237
|
+
category: str
|
|
238
|
+
name: str
|
|
239
|
+
size: int
|
|
240
|
+
media_type: Optional[str] = None
|
|
241
|
+
|
|
242
|
+
def to_dict(self) -> dict:
|
|
243
|
+
"""Serialize the attachment reference for API responses."""
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"FileID": self.file_id,
|
|
247
|
+
"Category": self.category,
|
|
248
|
+
"Name": self.name,
|
|
249
|
+
"Size": self.size,
|
|
250
|
+
"MediaType": self.media_type,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ChatAttachment":
|
|
255
|
+
"""Create a ChatAttachment from a serialized dictionary."""
|
|
256
|
+
|
|
257
|
+
return cls(
|
|
258
|
+
file_id=int(data.get("FileID") or data.get("file_id") or 0),
|
|
259
|
+
category=str(data.get("Category") or data.get("category") or ""),
|
|
260
|
+
name=str(data.get("Name") or data.get("name") or ""),
|
|
261
|
+
size=int(data.get("Size") or data.get("size") or 0),
|
|
262
|
+
media_type=data.get("MediaType") or data.get("media_type"),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass
|
|
267
|
+
class ChatMessage:
|
|
268
|
+
"""Chat message metadata persisted by the hub."""
|
|
269
|
+
|
|
270
|
+
direction: str
|
|
271
|
+
scope: str
|
|
272
|
+
state: str
|
|
273
|
+
content: str
|
|
274
|
+
source: Optional[str] = None
|
|
275
|
+
destination: Optional[str] = None
|
|
276
|
+
topic_id: Optional[str] = None
|
|
277
|
+
attachments: List[ChatAttachment] = field(default_factory=list)
|
|
278
|
+
created_at: datetime = field(default_factory=_now)
|
|
279
|
+
updated_at: datetime = field(default_factory=_now)
|
|
280
|
+
message_id: Optional[str] = None
|
|
281
|
+
|
|
282
|
+
def to_dict(self) -> dict:
|
|
283
|
+
"""Serialize the chat message into API-friendly fields."""
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"MessageID": self.message_id,
|
|
287
|
+
"Direction": self.direction,
|
|
288
|
+
"Scope": self.scope,
|
|
289
|
+
"State": self.state,
|
|
290
|
+
"Content": self.content,
|
|
291
|
+
"Source": self.source,
|
|
292
|
+
"Destination": self.destination,
|
|
293
|
+
"TopicID": self.topic_id,
|
|
294
|
+
"Attachments": [attachment.to_dict() for attachment in self.attachments],
|
|
295
|
+
"CreatedAt": self.created_at.isoformat(),
|
|
296
|
+
"UpdatedAt": self.updated_at.isoformat(),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class IdentityStatus:
|
|
302
|
+
"""Current identity status for admin tooling."""
|
|
303
|
+
|
|
304
|
+
identity: str
|
|
305
|
+
status: str
|
|
306
|
+
last_seen: Optional[datetime] = None
|
|
307
|
+
display_name: Optional[str] = None
|
|
308
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
309
|
+
is_banned: bool = False
|
|
310
|
+
is_blackholed: bool = False
|
|
311
|
+
|
|
312
|
+
def to_dict(self) -> dict:
|
|
313
|
+
"""Serialize the identity status into JSON-friendly values."""
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"Identity": self.identity,
|
|
317
|
+
"DisplayName": self.display_name,
|
|
318
|
+
"Status": self.status,
|
|
319
|
+
"LastSeen": self.last_seen.isoformat() if self.last_seen else None,
|
|
320
|
+
"Metadata": _json_safe(self.metadata or {}),
|
|
321
|
+
"IsBanned": self.is_banned,
|
|
322
|
+
"IsBlackholed": self.is_blackholed,
|
|
323
|
+
}
|