quantumflow-sdk 0.2.1__py3-none-any.whl → 0.4.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.
- api/main.py +34 -3
- api/models.py +41 -0
- api/routes/algorithm_routes.py +1029 -0
- api/routes/chat_routes.py +565 -0
- api/routes/pipeline_routes.py +578 -0
- db/models.py +357 -0
- quantumflow/algorithms/machine_learning/__init__.py +14 -2
- quantumflow/algorithms/machine_learning/vqe.py +355 -3
- quantumflow/core/__init__.py +10 -1
- quantumflow/core/quantum_compressor.py +379 -1
- quantumflow/integrations/domain_agents.py +617 -0
- quantumflow/pipeline/__init__.py +29 -0
- quantumflow/pipeline/anomaly_detector.py +521 -0
- quantumflow/pipeline/base_pipeline.py +602 -0
- quantumflow/pipeline/checkpoint_manager.py +587 -0
- quantumflow/pipeline/finance/__init__.py +5 -0
- quantumflow/pipeline/finance/portfolio_optimization.py +595 -0
- quantumflow/pipeline/healthcare/__init__.py +5 -0
- quantumflow/pipeline/healthcare/protein_folding.py +994 -0
- quantumflow/pipeline/temporal_memory.py +577 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/METADATA +3 -3
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/RECORD +25 -12
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QChat API Routes - Quantum-Secure Messaging
|
|
3
|
+
|
|
4
|
+
P2P encrypted messaging using QuantumFlow as relay.
|
|
5
|
+
Backend only sees encrypted blobs - cannot decrypt.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Depends, WebSocket, WebSocketDisconnect
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
from typing import Optional, List
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
import uuid
|
|
13
|
+
import hashlib
|
|
14
|
+
import asyncio
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
|
+
from sqlalchemy import select, or_, and_
|
|
19
|
+
from sqlalchemy.orm import selectinload
|
|
20
|
+
|
|
21
|
+
from db.database import get_async_db
|
|
22
|
+
from db.models import ChatUser, QuantumChannel, ChatMessage, ChannelStatus, MessageStatus
|
|
23
|
+
|
|
24
|
+
router = APIRouter(prefix="/chat", tags=["QChat"])
|
|
25
|
+
|
|
26
|
+
# ==================== WebSocket Connection Manager ====================
|
|
27
|
+
|
|
28
|
+
class ConnectionManager:
|
|
29
|
+
"""Manage WebSocket connections for real-time messaging."""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
# user_id -> WebSocket
|
|
33
|
+
self.active_connections: dict[str, WebSocket] = {}
|
|
34
|
+
# user_id -> list of pending messages
|
|
35
|
+
self.pending_messages: dict[str, list] = defaultdict(list)
|
|
36
|
+
|
|
37
|
+
async def connect(self, user_id: str, websocket: WebSocket):
|
|
38
|
+
await websocket.accept()
|
|
39
|
+
self.active_connections[user_id] = websocket
|
|
40
|
+
|
|
41
|
+
# Send any pending messages
|
|
42
|
+
if user_id in self.pending_messages:
|
|
43
|
+
for msg in self.pending_messages[user_id]:
|
|
44
|
+
await websocket.send_json(msg)
|
|
45
|
+
del self.pending_messages[user_id]
|
|
46
|
+
|
|
47
|
+
def disconnect(self, user_id: str):
|
|
48
|
+
if user_id in self.active_connections:
|
|
49
|
+
del self.active_connections[user_id]
|
|
50
|
+
|
|
51
|
+
async def send_to_user(self, user_id: str, message: dict):
|
|
52
|
+
if user_id in self.active_connections:
|
|
53
|
+
await self.active_connections[user_id].send_json(message)
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
# Store for later delivery
|
|
57
|
+
self.pending_messages[user_id].append(message)
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def is_online(self, user_id: str) -> bool:
|
|
61
|
+
return user_id in self.active_connections
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
manager = ConnectionManager()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ==================== Request/Response Models ====================
|
|
68
|
+
|
|
69
|
+
class RegisterUserRequest(BaseModel):
|
|
70
|
+
phone_number: str = Field(..., description="Phone number in E.164 format")
|
|
71
|
+
display_name: Optional[str] = None
|
|
72
|
+
firebase_uid: Optional[str] = None
|
|
73
|
+
device_token: Optional[str] = None
|
|
74
|
+
platform: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RegisterUserResponse(BaseModel):
|
|
78
|
+
user_id: str
|
|
79
|
+
phone_number: str
|
|
80
|
+
display_name: Optional[str]
|
|
81
|
+
created_at: datetime
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EstablishChannelRequest(BaseModel):
|
|
85
|
+
recipient_phone: str = Field(..., description="Recipient phone number")
|
|
86
|
+
sender_id: str = Field(..., description="Sender user ID")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ChannelResponse(BaseModel):
|
|
90
|
+
channel_id: str
|
|
91
|
+
status: str
|
|
92
|
+
bell_pairs_remaining: int
|
|
93
|
+
error_rate: float
|
|
94
|
+
created_at: datetime
|
|
95
|
+
established_at: Optional[datetime]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SendMessageRequest(BaseModel):
|
|
99
|
+
channel_id: str
|
|
100
|
+
sender_id: str
|
|
101
|
+
encrypted_content: str = Field(..., description="Encrypted message (base64)")
|
|
102
|
+
content_hash: str = Field(..., description="SHA-256 hash of plaintext")
|
|
103
|
+
compression_ratio: Optional[float] = None
|
|
104
|
+
bell_pair_id: Optional[str] = None
|
|
105
|
+
teleport_fidelity: Optional[float] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MessageResponse(BaseModel):
|
|
109
|
+
message_id: str
|
|
110
|
+
channel_id: str
|
|
111
|
+
sender_id: str
|
|
112
|
+
encrypted_content: str
|
|
113
|
+
status: str
|
|
114
|
+
compression_ratio: Optional[float]
|
|
115
|
+
eavesdrop_detected: bool
|
|
116
|
+
created_at: datetime
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class GetMessagesRequest(BaseModel):
|
|
120
|
+
channel_id: str
|
|
121
|
+
user_id: str
|
|
122
|
+
limit: int = 50
|
|
123
|
+
before_id: Optional[str] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ==================== User Registration ====================
|
|
127
|
+
|
|
128
|
+
@router.post("/register", response_model=RegisterUserResponse)
|
|
129
|
+
async def register_user(
|
|
130
|
+
request: RegisterUserRequest,
|
|
131
|
+
db: AsyncSession = Depends(get_async_db)
|
|
132
|
+
):
|
|
133
|
+
"""Register a new QChat user by phone number."""
|
|
134
|
+
|
|
135
|
+
# Check if user exists
|
|
136
|
+
result = await db.execute(
|
|
137
|
+
select(ChatUser).where(ChatUser.phone_number == request.phone_number)
|
|
138
|
+
)
|
|
139
|
+
existing = result.scalar_one_or_none()
|
|
140
|
+
|
|
141
|
+
if existing:
|
|
142
|
+
# Update existing user
|
|
143
|
+
existing.display_name = request.display_name or existing.display_name
|
|
144
|
+
existing.firebase_uid = request.firebase_uid or existing.firebase_uid
|
|
145
|
+
existing.device_token = request.device_token or existing.device_token
|
|
146
|
+
existing.platform = request.platform or existing.platform
|
|
147
|
+
existing.updated_at = datetime.utcnow()
|
|
148
|
+
await db.commit()
|
|
149
|
+
|
|
150
|
+
return RegisterUserResponse(
|
|
151
|
+
user_id=str(existing.id),
|
|
152
|
+
phone_number=existing.phone_number,
|
|
153
|
+
display_name=existing.display_name,
|
|
154
|
+
created_at=existing.created_at
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Create new user
|
|
158
|
+
user = ChatUser(
|
|
159
|
+
phone_number=request.phone_number,
|
|
160
|
+
display_name=request.display_name,
|
|
161
|
+
firebase_uid=request.firebase_uid,
|
|
162
|
+
device_token=request.device_token,
|
|
163
|
+
platform=request.platform
|
|
164
|
+
)
|
|
165
|
+
db.add(user)
|
|
166
|
+
await db.commit()
|
|
167
|
+
await db.refresh(user)
|
|
168
|
+
|
|
169
|
+
return RegisterUserResponse(
|
|
170
|
+
user_id=str(user.id),
|
|
171
|
+
phone_number=user.phone_number,
|
|
172
|
+
display_name=user.display_name,
|
|
173
|
+
created_at=user.created_at
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.get("/user/{phone_number}")
|
|
178
|
+
async def get_user_by_phone(
|
|
179
|
+
phone_number: str,
|
|
180
|
+
db: AsyncSession = Depends(get_async_db)
|
|
181
|
+
):
|
|
182
|
+
"""Look up user by phone number."""
|
|
183
|
+
|
|
184
|
+
result = await db.execute(
|
|
185
|
+
select(ChatUser).where(ChatUser.phone_number == phone_number)
|
|
186
|
+
)
|
|
187
|
+
user = result.scalar_one_or_none()
|
|
188
|
+
|
|
189
|
+
if not user:
|
|
190
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"user_id": str(user.id),
|
|
194
|
+
"phone_number": user.phone_number,
|
|
195
|
+
"display_name": user.display_name,
|
|
196
|
+
"is_online": manager.is_online(str(user.id))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ==================== Channel Management ====================
|
|
201
|
+
|
|
202
|
+
@router.post("/channel/establish", response_model=ChannelResponse)
|
|
203
|
+
async def establish_channel(
|
|
204
|
+
request: EstablishChannelRequest,
|
|
205
|
+
db: AsyncSession = Depends(get_async_db)
|
|
206
|
+
):
|
|
207
|
+
"""Establish a quantum-secure channel with another user."""
|
|
208
|
+
|
|
209
|
+
# Get sender
|
|
210
|
+
sender_result = await db.execute(
|
|
211
|
+
select(ChatUser).where(ChatUser.id == uuid.UUID(request.sender_id))
|
|
212
|
+
)
|
|
213
|
+
sender = sender_result.scalar_one_or_none()
|
|
214
|
+
if not sender:
|
|
215
|
+
raise HTTPException(status_code=404, detail="Sender not found")
|
|
216
|
+
|
|
217
|
+
# Get recipient by phone
|
|
218
|
+
recipient_result = await db.execute(
|
|
219
|
+
select(ChatUser).where(ChatUser.phone_number == request.recipient_phone)
|
|
220
|
+
)
|
|
221
|
+
recipient = recipient_result.scalar_one_or_none()
|
|
222
|
+
if not recipient:
|
|
223
|
+
raise HTTPException(status_code=404, detail="Recipient not found")
|
|
224
|
+
|
|
225
|
+
# Check for existing channel
|
|
226
|
+
channel_result = await db.execute(
|
|
227
|
+
select(QuantumChannel).where(
|
|
228
|
+
or_(
|
|
229
|
+
and_(
|
|
230
|
+
QuantumChannel.user_a_id == sender.id,
|
|
231
|
+
QuantumChannel.user_b_id == recipient.id
|
|
232
|
+
),
|
|
233
|
+
and_(
|
|
234
|
+
QuantumChannel.user_a_id == recipient.id,
|
|
235
|
+
QuantumChannel.user_b_id == sender.id
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
existing_channel = channel_result.scalar_one_or_none()
|
|
241
|
+
|
|
242
|
+
if existing_channel:
|
|
243
|
+
return ChannelResponse(
|
|
244
|
+
channel_id=str(existing_channel.id),
|
|
245
|
+
status=existing_channel.status.value,
|
|
246
|
+
bell_pairs_remaining=existing_channel.bell_pairs_remaining,
|
|
247
|
+
error_rate=existing_channel.error_rate,
|
|
248
|
+
created_at=existing_channel.created_at,
|
|
249
|
+
established_at=existing_channel.established_at
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Create new channel
|
|
253
|
+
channel = QuantumChannel(
|
|
254
|
+
user_a_id=sender.id,
|
|
255
|
+
user_b_id=recipient.id,
|
|
256
|
+
status=ChannelStatus.ESTABLISHING,
|
|
257
|
+
qkd_key_id=str(uuid.uuid4()),
|
|
258
|
+
bell_pairs_remaining=1000,
|
|
259
|
+
key_generated_at=datetime.utcnow(),
|
|
260
|
+
key_expires_at=datetime.utcnow() + timedelta(days=7)
|
|
261
|
+
)
|
|
262
|
+
db.add(channel)
|
|
263
|
+
|
|
264
|
+
# Simulate QKD exchange (in production, call actual QKD API)
|
|
265
|
+
channel.status = ChannelStatus.READY
|
|
266
|
+
channel.established_at = datetime.utcnow()
|
|
267
|
+
|
|
268
|
+
await db.commit()
|
|
269
|
+
await db.refresh(channel)
|
|
270
|
+
|
|
271
|
+
# Notify recipient
|
|
272
|
+
await manager.send_to_user(str(recipient.id), {
|
|
273
|
+
"type": "channel_established",
|
|
274
|
+
"channel_id": str(channel.id),
|
|
275
|
+
"from_user": sender.display_name or sender.phone_number
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return ChannelResponse(
|
|
279
|
+
channel_id=str(channel.id),
|
|
280
|
+
status=channel.status.value,
|
|
281
|
+
bell_pairs_remaining=channel.bell_pairs_remaining,
|
|
282
|
+
error_rate=channel.error_rate,
|
|
283
|
+
created_at=channel.created_at,
|
|
284
|
+
established_at=channel.established_at
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@router.get("/channels/{user_id}")
|
|
289
|
+
async def get_user_channels(
|
|
290
|
+
user_id: str,
|
|
291
|
+
db: AsyncSession = Depends(get_async_db)
|
|
292
|
+
):
|
|
293
|
+
"""Get all channels for a user."""
|
|
294
|
+
|
|
295
|
+
uid = uuid.UUID(user_id)
|
|
296
|
+
result = await db.execute(
|
|
297
|
+
select(QuantumChannel)
|
|
298
|
+
.where(
|
|
299
|
+
or_(
|
|
300
|
+
QuantumChannel.user_a_id == uid,
|
|
301
|
+
QuantumChannel.user_b_id == uid
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
.options(selectinload(QuantumChannel.user_a), selectinload(QuantumChannel.user_b))
|
|
305
|
+
)
|
|
306
|
+
channels = result.scalars().all()
|
|
307
|
+
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
"channel_id": str(ch.id),
|
|
311
|
+
"status": ch.status.value,
|
|
312
|
+
"bell_pairs_remaining": ch.bell_pairs_remaining,
|
|
313
|
+
"other_user": {
|
|
314
|
+
"id": str(ch.user_b.id if str(ch.user_a_id) == user_id else ch.user_a.id),
|
|
315
|
+
"phone": ch.user_b.phone_number if str(ch.user_a_id) == user_id else ch.user_a.phone_number,
|
|
316
|
+
"name": ch.user_b.display_name if str(ch.user_a_id) == user_id else ch.user_a.display_name
|
|
317
|
+
},
|
|
318
|
+
"last_message_at": ch.last_message_at
|
|
319
|
+
}
|
|
320
|
+
for ch in channels
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ==================== Messaging ====================
|
|
325
|
+
|
|
326
|
+
@router.post("/message/send", response_model=MessageResponse)
|
|
327
|
+
async def send_message(
|
|
328
|
+
request: SendMessageRequest,
|
|
329
|
+
db: AsyncSession = Depends(get_async_db)
|
|
330
|
+
):
|
|
331
|
+
"""Send an encrypted message through a quantum channel."""
|
|
332
|
+
|
|
333
|
+
# Get channel
|
|
334
|
+
channel_result = await db.execute(
|
|
335
|
+
select(QuantumChannel).where(QuantumChannel.id == uuid.UUID(request.channel_id))
|
|
336
|
+
)
|
|
337
|
+
channel = channel_result.scalar_one_or_none()
|
|
338
|
+
if not channel:
|
|
339
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
340
|
+
|
|
341
|
+
if channel.status != ChannelStatus.READY:
|
|
342
|
+
raise HTTPException(status_code=400, detail=f"Channel not ready: {channel.status.value}")
|
|
343
|
+
|
|
344
|
+
# Verify sender is in channel
|
|
345
|
+
sender_uuid = uuid.UUID(request.sender_id)
|
|
346
|
+
if sender_uuid not in [channel.user_a_id, channel.user_b_id]:
|
|
347
|
+
raise HTTPException(status_code=403, detail="Sender not in channel")
|
|
348
|
+
|
|
349
|
+
# Determine recipient
|
|
350
|
+
recipient_id = channel.user_b_id if sender_uuid == channel.user_a_id else channel.user_a_id
|
|
351
|
+
|
|
352
|
+
# Consume bell pair
|
|
353
|
+
if channel.bell_pairs_remaining > 0:
|
|
354
|
+
channel.bell_pairs_remaining -= 1
|
|
355
|
+
else:
|
|
356
|
+
raise HTTPException(status_code=400, detail="No bell pairs remaining. Regenerate keys.")
|
|
357
|
+
|
|
358
|
+
# Simulate eavesdrop detection (random ~2% chance for demo)
|
|
359
|
+
import random
|
|
360
|
+
eavesdrop_detected = random.random() < 0.02
|
|
361
|
+
|
|
362
|
+
if eavesdrop_detected:
|
|
363
|
+
channel.eavesdrop_detected_count += 1
|
|
364
|
+
channel.last_eavesdrop_at = datetime.utcnow()
|
|
365
|
+
channel.error_rate = min(0.25, channel.error_rate + 0.05)
|
|
366
|
+
|
|
367
|
+
if channel.error_rate >= 0.25:
|
|
368
|
+
channel.status = ChannelStatus.COMPROMISED
|
|
369
|
+
|
|
370
|
+
# Create message
|
|
371
|
+
message = ChatMessage(
|
|
372
|
+
channel_id=channel.id,
|
|
373
|
+
sender_id=sender_uuid,
|
|
374
|
+
encrypted_content=request.encrypted_content,
|
|
375
|
+
content_hash=request.content_hash,
|
|
376
|
+
compression_ratio=request.compression_ratio,
|
|
377
|
+
bell_pair_id=request.bell_pair_id,
|
|
378
|
+
teleport_fidelity=request.teleport_fidelity,
|
|
379
|
+
eavesdrop_detected=eavesdrop_detected,
|
|
380
|
+
status=MessageStatus.SENT,
|
|
381
|
+
sent_at=datetime.utcnow()
|
|
382
|
+
)
|
|
383
|
+
db.add(message)
|
|
384
|
+
|
|
385
|
+
channel.last_message_at = datetime.utcnow()
|
|
386
|
+
|
|
387
|
+
await db.commit()
|
|
388
|
+
await db.refresh(message)
|
|
389
|
+
|
|
390
|
+
# Send to recipient via WebSocket
|
|
391
|
+
delivered = await manager.send_to_user(str(recipient_id), {
|
|
392
|
+
"type": "new_message",
|
|
393
|
+
"message_id": str(message.id),
|
|
394
|
+
"channel_id": str(channel.id),
|
|
395
|
+
"sender_id": request.sender_id,
|
|
396
|
+
"encrypted_content": request.encrypted_content,
|
|
397
|
+
"content_hash": request.content_hash,
|
|
398
|
+
"compression_ratio": request.compression_ratio,
|
|
399
|
+
"eavesdrop_detected": eavesdrop_detected,
|
|
400
|
+
"created_at": message.created_at.isoformat()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
if delivered:
|
|
404
|
+
message.status = MessageStatus.DELIVERED
|
|
405
|
+
message.delivered_at = datetime.utcnow()
|
|
406
|
+
await db.commit()
|
|
407
|
+
|
|
408
|
+
return MessageResponse(
|
|
409
|
+
message_id=str(message.id),
|
|
410
|
+
channel_id=str(channel.id),
|
|
411
|
+
sender_id=str(message.sender_id),
|
|
412
|
+
encrypted_content=message.encrypted_content,
|
|
413
|
+
status=message.status.value,
|
|
414
|
+
compression_ratio=message.compression_ratio,
|
|
415
|
+
eavesdrop_detected=message.eavesdrop_detected,
|
|
416
|
+
created_at=message.created_at
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@router.get("/messages/{channel_id}")
|
|
421
|
+
async def get_messages(
|
|
422
|
+
channel_id: str,
|
|
423
|
+
user_id: str,
|
|
424
|
+
limit: int = 50,
|
|
425
|
+
db: AsyncSession = Depends(get_async_db)
|
|
426
|
+
):
|
|
427
|
+
"""Get messages from a channel."""
|
|
428
|
+
|
|
429
|
+
# Verify user is in channel
|
|
430
|
+
channel_result = await db.execute(
|
|
431
|
+
select(QuantumChannel).where(QuantumChannel.id == uuid.UUID(channel_id))
|
|
432
|
+
)
|
|
433
|
+
channel = channel_result.scalar_one_or_none()
|
|
434
|
+
if not channel:
|
|
435
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
436
|
+
|
|
437
|
+
user_uuid = uuid.UUID(user_id)
|
|
438
|
+
if user_uuid not in [channel.user_a_id, channel.user_b_id]:
|
|
439
|
+
raise HTTPException(status_code=403, detail="User not in channel")
|
|
440
|
+
|
|
441
|
+
# Get messages
|
|
442
|
+
result = await db.execute(
|
|
443
|
+
select(ChatMessage)
|
|
444
|
+
.where(ChatMessage.channel_id == uuid.UUID(channel_id))
|
|
445
|
+
.order_by(ChatMessage.created_at.desc())
|
|
446
|
+
.limit(limit)
|
|
447
|
+
)
|
|
448
|
+
messages = result.scalars().all()
|
|
449
|
+
|
|
450
|
+
# Mark as read
|
|
451
|
+
for msg in messages:
|
|
452
|
+
if msg.sender_id != user_uuid and msg.status != MessageStatus.READ:
|
|
453
|
+
msg.status = MessageStatus.READ
|
|
454
|
+
msg.read_at = datetime.utcnow()
|
|
455
|
+
await db.commit()
|
|
456
|
+
|
|
457
|
+
return [
|
|
458
|
+
{
|
|
459
|
+
"message_id": str(msg.id),
|
|
460
|
+
"sender_id": str(msg.sender_id),
|
|
461
|
+
"encrypted_content": msg.encrypted_content,
|
|
462
|
+
"content_hash": msg.content_hash,
|
|
463
|
+
"compression_ratio": msg.compression_ratio,
|
|
464
|
+
"eavesdrop_detected": msg.eavesdrop_detected,
|
|
465
|
+
"status": msg.status.value,
|
|
466
|
+
"created_at": msg.created_at.isoformat()
|
|
467
|
+
}
|
|
468
|
+
for msg in reversed(messages)
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@router.post("/channel/{channel_id}/regenerate-keys")
|
|
473
|
+
async def regenerate_keys(
|
|
474
|
+
channel_id: str,
|
|
475
|
+
db: AsyncSession = Depends(get_async_db)
|
|
476
|
+
):
|
|
477
|
+
"""Regenerate QKD keys for a compromised channel."""
|
|
478
|
+
|
|
479
|
+
result = await db.execute(
|
|
480
|
+
select(QuantumChannel).where(QuantumChannel.id == uuid.UUID(channel_id))
|
|
481
|
+
)
|
|
482
|
+
channel = result.scalar_one_or_none()
|
|
483
|
+
if not channel:
|
|
484
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
485
|
+
|
|
486
|
+
# Regenerate keys
|
|
487
|
+
channel.qkd_key_id = str(uuid.uuid4())
|
|
488
|
+
channel.bell_pairs_remaining = 1000
|
|
489
|
+
channel.key_generated_at = datetime.utcnow()
|
|
490
|
+
channel.key_expires_at = datetime.utcnow() + timedelta(days=7)
|
|
491
|
+
channel.status = ChannelStatus.READY
|
|
492
|
+
channel.error_rate = 0.0
|
|
493
|
+
|
|
494
|
+
await db.commit()
|
|
495
|
+
|
|
496
|
+
# Notify both users
|
|
497
|
+
await manager.send_to_user(str(channel.user_a_id), {
|
|
498
|
+
"type": "keys_regenerated",
|
|
499
|
+
"channel_id": channel_id
|
|
500
|
+
})
|
|
501
|
+
await manager.send_to_user(str(channel.user_b_id), {
|
|
502
|
+
"type": "keys_regenerated",
|
|
503
|
+
"channel_id": channel_id
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
"status": "success",
|
|
508
|
+
"channel_id": channel_id,
|
|
509
|
+
"new_key_id": channel.qkd_key_id,
|
|
510
|
+
"bell_pairs": channel.bell_pairs_remaining
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ==================== WebSocket ====================
|
|
515
|
+
|
|
516
|
+
@router.websocket("/ws/{user_id}")
|
|
517
|
+
async def websocket_endpoint(
|
|
518
|
+
websocket: WebSocket,
|
|
519
|
+
user_id: str,
|
|
520
|
+
db: AsyncSession = Depends(get_async_db)
|
|
521
|
+
):
|
|
522
|
+
"""WebSocket connection for real-time messaging."""
|
|
523
|
+
|
|
524
|
+
await manager.connect(user_id, websocket)
|
|
525
|
+
|
|
526
|
+
# Update user online status
|
|
527
|
+
result = await db.execute(
|
|
528
|
+
select(ChatUser).where(ChatUser.id == uuid.UUID(user_id))
|
|
529
|
+
)
|
|
530
|
+
user = result.scalar_one_or_none()
|
|
531
|
+
if user:
|
|
532
|
+
user.is_online = True
|
|
533
|
+
await db.commit()
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
while True:
|
|
537
|
+
data = await websocket.receive_json()
|
|
538
|
+
|
|
539
|
+
# Handle different message types
|
|
540
|
+
if data.get("type") == "ping":
|
|
541
|
+
await websocket.send_json({"type": "pong"})
|
|
542
|
+
|
|
543
|
+
elif data.get("type") == "typing":
|
|
544
|
+
# Forward typing indicator
|
|
545
|
+
channel_id = data.get("channel_id")
|
|
546
|
+
result = await db.execute(
|
|
547
|
+
select(QuantumChannel).where(QuantumChannel.id == uuid.UUID(channel_id))
|
|
548
|
+
)
|
|
549
|
+
channel = result.scalar_one_or_none()
|
|
550
|
+
if channel:
|
|
551
|
+
recipient_id = str(channel.user_b_id) if str(channel.user_a_id) == user_id else str(channel.user_a_id)
|
|
552
|
+
await manager.send_to_user(recipient_id, {
|
|
553
|
+
"type": "typing",
|
|
554
|
+
"channel_id": channel_id,
|
|
555
|
+
"user_id": user_id
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
except WebSocketDisconnect:
|
|
559
|
+
manager.disconnect(user_id)
|
|
560
|
+
|
|
561
|
+
# Update user offline status
|
|
562
|
+
if user:
|
|
563
|
+
user.is_online = False
|
|
564
|
+
user.last_seen = datetime.utcnow()
|
|
565
|
+
await db.commit()
|