forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient_device.py
More file actions
130 lines (106 loc) · 4.59 KB
/
Copy pathclient_device.py
File metadata and controls
130 lines (106 loc) · 4.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""Stable per-install client device identity (provenance only).
Contract (see backend/docs/memory/domain_model.md):
client_device_id = "{platform}_{hash}"
hash = first 8 hex chars of sha256(stable per-install id)
Headers: X-Device-Id-Hash + X-App-Platform (+ optional X-App-Version)
Absent headers => unknown device (all fields nullable).
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from typing import Any, Mapping, Optional
from starlette.requests import Request
class DeviceScopeValidationError(ValueError):
"""Invalid device_scope query value."""
@dataclass(frozen=True)
class DeviceScopeRequest:
"""Resolved device-scope filter for canonical memory reads."""
device_scope: str
client_device_id: Optional[str] = None
@classmethod
def resolve_from_headers(
cls,
*,
device_scope: str = "all",
client_device_id: Optional[str] = None,
x_app_platform: Optional[str] = None,
x_device_id_hash: Optional[str] = None,
) -> "DeviceScopeRequest":
scope = cls._normalize_device_scope(device_scope)
resolved_device_id = client_device_id
if scope == "current":
resolved_device_id = resolve_client_device(
x_app_platform=x_app_platform,
x_device_id_hash=x_device_id_hash,
).client_device_id
return cls(device_scope=scope, client_device_id=resolved_device_id)
@staticmethod
def _normalize_device_scope(device_scope: str) -> str:
scope = (device_scope or "all").strip().lower()
if scope not in ("all", "current", "explicit"):
raise DeviceScopeValidationError("device_scope must be one of: all, current, explicit")
return scope
@dataclass(frozen=True)
class ClientDeviceContext:
client_device_id: Optional[str] = None
platform: Optional[str] = None
device_hash: Optional[str] = None
app_version: Optional[str] = None
def build_client_device_id(platform: Optional[str], device_hash: Optional[str]) -> Optional[str]:
platform_norm = (platform or "").strip().lower()
hash_norm = (device_hash or "").strip().lower()
if platform_norm not in {'android', 'ios', 'linux', 'macos', 'web', 'windows'} or not re.fullmatch(
r'[0-9a-f]{8}', hash_norm
):
return None
return f"{platform_norm}_{hash_norm}"
def resolve_client_device(
*,
x_app_platform: Optional[str] = None,
x_device_id_hash: Optional[str] = None,
x_app_version: Optional[str] = None,
) -> ClientDeviceContext:
platform = (x_app_platform or "").strip().lower() or None
device_hash = (x_device_id_hash or "").strip().lower() or None
app_version = (x_app_version or "").strip() or None
return ClientDeviceContext(
client_device_id=build_client_device_id(platform, device_hash),
platform=platform,
device_hash=device_hash,
app_version=app_version,
)
def resolve_client_device_from_request(request: Request) -> ClientDeviceContext:
headers = request.headers
return resolve_client_device(
x_app_platform=headers.get("x-app-platform"),
x_device_id_hash=headers.get("x-device-id-hash"),
x_app_version=headers.get("x-app-version"),
)
def resolve_client_device_from_headers(headers: Mapping[str, str]) -> ClientDeviceContext:
return resolve_client_device(
x_app_platform=headers.get("x-app-platform") or headers.get("X-App-Platform"),
x_device_id_hash=headers.get("x-device-id-hash") or headers.get("X-Device-Id-Hash"),
x_app_version=headers.get("x-app-version") or headers.get("X-App-Version"),
)
def resolve_client_device_from_websocket_auth_message(message: Mapping[str, Any]) -> ClientDeviceContext:
"""Resolve web capture provenance sent in the first WebSocket auth message.
Browsers cannot attach arbitrary headers to a WebSocket upgrade. The web
listen client therefore sends its stable device hash beside the Firebase
token in the already-required first auth message. Platform is fixed to
``web`` rather than trusting a browser-provided platform value.
"""
text = message.get("text")
if not isinstance(text, str):
return ClientDeviceContext()
try:
auth_data = json.loads(text)
except (TypeError, json.JSONDecodeError):
return ClientDeviceContext()
if not isinstance(auth_data, dict):
return ClientDeviceContext()
device_hash = auth_data.get("device_id_hash")
return resolve_client_device(
x_app_platform="web",
x_device_id_hash=device_hash if isinstance(device_hash, str) else None,
)