roomkit 0.1.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.
- roomkit/AGENTS.md +362 -0
- roomkit/__init__.py +372 -0
- roomkit/_version.py +1 -0
- roomkit/ai_docs.py +93 -0
- roomkit/channels/__init__.py +194 -0
- roomkit/channels/ai.py +238 -0
- roomkit/channels/base.py +66 -0
- roomkit/channels/transport.py +115 -0
- roomkit/channels/websocket.py +85 -0
- roomkit/core/__init__.py +0 -0
- roomkit/core/_channel_ops.py +252 -0
- roomkit/core/_helpers.py +296 -0
- roomkit/core/_inbound.py +435 -0
- roomkit/core/_room_lifecycle.py +275 -0
- roomkit/core/circuit_breaker.py +84 -0
- roomkit/core/event_router.py +401 -0
- roomkit/core/framework.py +793 -0
- roomkit/core/hooks.py +232 -0
- roomkit/core/inbound_router.py +57 -0
- roomkit/core/locks.py +66 -0
- roomkit/core/rate_limiter.py +67 -0
- roomkit/core/retry.py +49 -0
- roomkit/core/router.py +24 -0
- roomkit/core/transcoder.py +85 -0
- roomkit/identity/__init__.py +0 -0
- roomkit/identity/base.py +27 -0
- roomkit/identity/mock.py +49 -0
- roomkit/llms.txt +52 -0
- roomkit/models/__init__.py +104 -0
- roomkit/models/channel.py +99 -0
- roomkit/models/context.py +35 -0
- roomkit/models/delivery.py +76 -0
- roomkit/models/enums.py +170 -0
- roomkit/models/event.py +203 -0
- roomkit/models/framework_event.py +19 -0
- roomkit/models/hook.py +68 -0
- roomkit/models/identity.py +81 -0
- roomkit/models/participant.py +34 -0
- roomkit/models/room.py +33 -0
- roomkit/models/task.py +36 -0
- roomkit/providers/__init__.py +0 -0
- roomkit/providers/ai/__init__.py +0 -0
- roomkit/providers/ai/base.py +140 -0
- roomkit/providers/ai/mock.py +33 -0
- roomkit/providers/anthropic/__init__.py +6 -0
- roomkit/providers/anthropic/ai.py +145 -0
- roomkit/providers/anthropic/config.py +14 -0
- roomkit/providers/elasticemail/__init__.py +6 -0
- roomkit/providers/elasticemail/config.py +16 -0
- roomkit/providers/elasticemail/email.py +97 -0
- roomkit/providers/email/__init__.py +0 -0
- roomkit/providers/email/base.py +46 -0
- roomkit/providers/email/mock.py +34 -0
- roomkit/providers/gemini/__init__.py +6 -0
- roomkit/providers/gemini/ai.py +153 -0
- roomkit/providers/gemini/config.py +14 -0
- roomkit/providers/http/__init__.py +15 -0
- roomkit/providers/http/base.py +33 -0
- roomkit/providers/http/config.py +14 -0
- roomkit/providers/http/mock.py +21 -0
- roomkit/providers/http/provider.py +105 -0
- roomkit/providers/http/webhook.py +33 -0
- roomkit/providers/messenger/__init__.py +15 -0
- roomkit/providers/messenger/base.py +33 -0
- roomkit/providers/messenger/config.py +17 -0
- roomkit/providers/messenger/facebook.py +95 -0
- roomkit/providers/messenger/mock.py +21 -0
- roomkit/providers/messenger/webhook.py +42 -0
- roomkit/providers/openai/__init__.py +6 -0
- roomkit/providers/openai/ai.py +155 -0
- roomkit/providers/openai/config.py +24 -0
- roomkit/providers/pydantic_ai/__init__.py +5 -0
- roomkit/providers/pydantic_ai/config.py +14 -0
- roomkit/providers/rcs/__init__.py +9 -0
- roomkit/providers/rcs/base.py +95 -0
- roomkit/providers/rcs/mock.py +78 -0
- roomkit/providers/sendgrid/__init__.py +5 -0
- roomkit/providers/sendgrid/config.py +13 -0
- roomkit/providers/sinch/__init__.py +6 -0
- roomkit/providers/sinch/config.py +22 -0
- roomkit/providers/sinch/sms.py +192 -0
- roomkit/providers/sms/__init__.py +15 -0
- roomkit/providers/sms/base.py +67 -0
- roomkit/providers/sms/meta.py +401 -0
- roomkit/providers/sms/mock.py +24 -0
- roomkit/providers/sms/phone.py +77 -0
- roomkit/providers/telnyx/__init__.py +21 -0
- roomkit/providers/telnyx/config.py +14 -0
- roomkit/providers/telnyx/rcs.py +352 -0
- roomkit/providers/telnyx/sms.py +231 -0
- roomkit/providers/twilio/__init__.py +18 -0
- roomkit/providers/twilio/config.py +19 -0
- roomkit/providers/twilio/rcs.py +183 -0
- roomkit/providers/twilio/sms.py +200 -0
- roomkit/providers/voicemeup/__init__.py +15 -0
- roomkit/providers/voicemeup/config.py +21 -0
- roomkit/providers/voicemeup/sms.py +374 -0
- roomkit/providers/whatsapp/__init__.py +0 -0
- roomkit/providers/whatsapp/base.py +44 -0
- roomkit/providers/whatsapp/mock.py +21 -0
- roomkit/py.typed +0 -0
- roomkit/realtime/__init__.py +17 -0
- roomkit/realtime/base.py +111 -0
- roomkit/realtime/memory.py +158 -0
- roomkit/sources/__init__.py +35 -0
- roomkit/sources/base.py +207 -0
- roomkit/sources/websocket.py +260 -0
- roomkit/store/__init__.py +0 -0
- roomkit/store/base.py +230 -0
- roomkit/store/memory.py +293 -0
- roomkit-0.1.0.dist-info/METADATA +567 -0
- roomkit-0.1.0.dist-info/RECORD +114 -0
- roomkit-0.1.0.dist-info/WHEEL +4 -0
- roomkit-0.1.0.dist-info/licenses/LICENSE +21 -0
roomkit/store/base.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Abstract base class for conversation storage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from roomkit.models.channel import ChannelBinding
|
|
9
|
+
from roomkit.models.event import RoomEvent
|
|
10
|
+
from roomkit.models.identity import Identity
|
|
11
|
+
from roomkit.models.participant import Participant
|
|
12
|
+
from roomkit.models.room import Room
|
|
13
|
+
from roomkit.models.task import Observation, Task
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConversationStore(ABC):
|
|
17
|
+
"""Persistent storage for rooms, events, bindings, and participants.
|
|
18
|
+
|
|
19
|
+
Implement this ABC to plug in any storage backend (SQL, Redis, etc.).
|
|
20
|
+
The library ships with `InMemoryStore` for development and testing.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Room operations
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def create_room(self, room: Room) -> Room:
|
|
27
|
+
"""Persist a new room."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def get_room(self, room_id: str) -> Room | None:
|
|
32
|
+
"""Get a room by ID, or ``None`` if it doesn't exist."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def update_room(self, room: Room) -> Room:
|
|
37
|
+
"""Update an existing room."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def delete_room(self, room_id: str) -> bool:
|
|
42
|
+
"""Delete a room. Returns ``True`` if the room existed."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def list_rooms(self, offset: int = 0, limit: int = 50) -> list[Room]:
|
|
47
|
+
"""List rooms with pagination."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def find_rooms(
|
|
52
|
+
self,
|
|
53
|
+
organization_id: str | None = None,
|
|
54
|
+
status: str | None = None,
|
|
55
|
+
metadata_filter: dict[str, Any] | None = None,
|
|
56
|
+
) -> list[Room]:
|
|
57
|
+
"""Find rooms matching the given filters."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def find_latest_room(
|
|
62
|
+
self,
|
|
63
|
+
participant_id: str,
|
|
64
|
+
channel_type: str | None = None,
|
|
65
|
+
status: str | None = None,
|
|
66
|
+
) -> Room | None:
|
|
67
|
+
"""Find the most recent room for a participant."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def find_room_id_by_channel(
|
|
72
|
+
self, channel_id: str, status: str | None = None
|
|
73
|
+
) -> str | None:
|
|
74
|
+
"""Find a room ID that has a binding for the given channel_id."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
# Event operations
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
async def add_event(self, event: RoomEvent) -> RoomEvent:
|
|
81
|
+
"""Store a new event."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
async def get_event(self, event_id: str) -> RoomEvent | None:
|
|
86
|
+
"""Get an event by ID."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def list_events(
|
|
91
|
+
self,
|
|
92
|
+
room_id: str,
|
|
93
|
+
offset: int = 0,
|
|
94
|
+
limit: int = 50,
|
|
95
|
+
visibility_filter: str | None = None,
|
|
96
|
+
) -> list[RoomEvent]:
|
|
97
|
+
"""List events in a room with pagination and optional visibility filter."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def check_idempotency(self, room_id: str, key: str) -> bool:
|
|
102
|
+
"""Check if an idempotency key has been seen. Returns ``True`` if duplicate."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def get_event_count(self, room_id: str) -> int:
|
|
107
|
+
"""Return the total number of events in a room."""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
# Binding operations
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
async def add_binding(self, binding: ChannelBinding) -> ChannelBinding:
|
|
114
|
+
"""Attach a channel binding to a room."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def get_binding(self, room_id: str, channel_id: str) -> ChannelBinding | None:
|
|
119
|
+
"""Get a channel binding, or ``None`` if not attached."""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def update_binding(self, binding: ChannelBinding) -> ChannelBinding:
|
|
124
|
+
"""Update an existing channel binding."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def remove_binding(self, room_id: str, channel_id: str) -> bool:
|
|
129
|
+
"""Detach a channel from a room. Returns ``True`` if it was attached."""
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
async def list_bindings(self, room_id: str) -> list[ChannelBinding]:
|
|
134
|
+
"""List all channel bindings for a room."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
# Participant operations
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
async def add_participant(self, participant: Participant) -> Participant:
|
|
141
|
+
"""Add a participant to a room."""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
async def get_participant(self, room_id: str, participant_id: str) -> Participant | None:
|
|
146
|
+
"""Get a participant by ID within a room."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def update_participant(self, participant: Participant) -> Participant:
|
|
151
|
+
"""Update a participant."""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
async def list_participants(self, room_id: str) -> list[Participant]:
|
|
156
|
+
"""List all participants in a room."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
# Identity operations
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
async def create_identity(self, identity: Identity) -> Identity:
|
|
163
|
+
"""Create a new identity record."""
|
|
164
|
+
...
|
|
165
|
+
|
|
166
|
+
@abstractmethod
|
|
167
|
+
async def get_identity(self, identity_id: str) -> Identity | None:
|
|
168
|
+
"""Get an identity by ID."""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
@abstractmethod
|
|
172
|
+
async def resolve_identity(self, channel_type: str, address: str) -> Identity | None:
|
|
173
|
+
"""Look up an identity by channel type and address."""
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def link_address(self, identity_id: str, channel_type: str, address: str) -> None:
|
|
178
|
+
"""Link a channel address to an identity."""
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
# Task operations
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
async def add_task(self, task: Task) -> Task:
|
|
185
|
+
"""Store a new task."""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
async def get_task(self, task_id: str) -> Task | None:
|
|
190
|
+
"""Get a task by ID."""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
async def list_tasks(self, room_id: str, status: str | None = None) -> list[Task]:
|
|
195
|
+
"""List tasks for a room, optionally filtered by status."""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
async def update_task(self, task: Task) -> Task:
|
|
200
|
+
"""Update a task."""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
# Observation operations
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
async def add_observation(self, observation: Observation) -> Observation:
|
|
207
|
+
"""Store a new observation."""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
async def list_observations(self, room_id: str) -> list[Observation]:
|
|
212
|
+
"""List all observations for a room."""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
# Read tracking
|
|
216
|
+
|
|
217
|
+
@abstractmethod
|
|
218
|
+
async def mark_read(self, room_id: str, channel_id: str, event_id: str) -> None:
|
|
219
|
+
"""Mark an event as read for a channel."""
|
|
220
|
+
...
|
|
221
|
+
|
|
222
|
+
@abstractmethod
|
|
223
|
+
async def mark_all_read(self, room_id: str, channel_id: str) -> None:
|
|
224
|
+
"""Mark all events as read for a channel."""
|
|
225
|
+
...
|
|
226
|
+
|
|
227
|
+
@abstractmethod
|
|
228
|
+
async def get_unread_count(self, room_id: str, channel_id: str) -> int:
|
|
229
|
+
"""Return the number of unread events for a channel in a room."""
|
|
230
|
+
...
|
roomkit/store/memory.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""In-memory implementation of ConversationStore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roomkit.models.channel import ChannelBinding
|
|
8
|
+
from roomkit.models.event import RoomEvent
|
|
9
|
+
from roomkit.models.identity import Identity
|
|
10
|
+
from roomkit.models.participant import Participant
|
|
11
|
+
from roomkit.models.room import Room
|
|
12
|
+
from roomkit.models.task import Observation, Task
|
|
13
|
+
from roomkit.store.base import ConversationStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InMemoryStore(ConversationStore):
|
|
17
|
+
"""Dict-based in-memory store for development and testing."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._rooms: dict[str, Room] = {}
|
|
21
|
+
self._events: dict[str, RoomEvent] = {}
|
|
22
|
+
self._room_events: dict[str, list[str]] = {}
|
|
23
|
+
self._bindings: dict[str, dict[str, ChannelBinding]] = {}
|
|
24
|
+
self._participants: dict[str, dict[str, Participant]] = {}
|
|
25
|
+
self._idempotency: dict[str, set[str]] = {}
|
|
26
|
+
self._read_markers: dict[str, dict[str, str]] = {}
|
|
27
|
+
self._identities: dict[str, Identity] = {}
|
|
28
|
+
self._address_index: dict[tuple[str, str], str] = {}
|
|
29
|
+
self._tasks: dict[str, Task] = {}
|
|
30
|
+
self._room_tasks: dict[str, list[str]] = {}
|
|
31
|
+
self._observations: dict[str, Observation] = {}
|
|
32
|
+
self._room_observations: dict[str, list[str]] = {}
|
|
33
|
+
|
|
34
|
+
# Room operations
|
|
35
|
+
|
|
36
|
+
async def create_room(self, room: Room) -> Room:
|
|
37
|
+
self._rooms[room.id] = room
|
|
38
|
+
self._room_events.setdefault(room.id, [])
|
|
39
|
+
self._bindings.setdefault(room.id, {})
|
|
40
|
+
self._participants.setdefault(room.id, {})
|
|
41
|
+
self._idempotency.setdefault(room.id, set())
|
|
42
|
+
self._read_markers.setdefault(room.id, {})
|
|
43
|
+
return room
|
|
44
|
+
|
|
45
|
+
async def get_room(self, room_id: str) -> Room | None:
|
|
46
|
+
room = self._rooms.get(room_id)
|
|
47
|
+
return room.model_copy() if room is not None else None
|
|
48
|
+
|
|
49
|
+
async def update_room(self, room: Room) -> Room:
|
|
50
|
+
self._rooms[room.id] = room
|
|
51
|
+
return room
|
|
52
|
+
|
|
53
|
+
async def delete_room(self, room_id: str) -> bool:
|
|
54
|
+
if room_id not in self._rooms:
|
|
55
|
+
return False
|
|
56
|
+
del self._rooms[room_id]
|
|
57
|
+
# Clean up events
|
|
58
|
+
event_ids = self._room_events.pop(room_id, [])
|
|
59
|
+
for eid in event_ids:
|
|
60
|
+
self._events.pop(eid, None)
|
|
61
|
+
# Clean up tasks
|
|
62
|
+
task_ids = self._room_tasks.pop(room_id, [])
|
|
63
|
+
for tid in task_ids:
|
|
64
|
+
self._tasks.pop(tid, None)
|
|
65
|
+
# Clean up observations
|
|
66
|
+
obs_ids = self._room_observations.pop(room_id, [])
|
|
67
|
+
for oid in obs_ids:
|
|
68
|
+
self._observations.pop(oid, None)
|
|
69
|
+
self._bindings.pop(room_id, None)
|
|
70
|
+
self._participants.pop(room_id, None)
|
|
71
|
+
self._idempotency.pop(room_id, None)
|
|
72
|
+
self._read_markers.pop(room_id, None)
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
async def list_rooms(self, offset: int = 0, limit: int = 50) -> list[Room]:
|
|
76
|
+
rooms = list(self._rooms.values())
|
|
77
|
+
return [r.model_copy() for r in rooms[offset : offset + limit]]
|
|
78
|
+
|
|
79
|
+
async def find_rooms(
|
|
80
|
+
self,
|
|
81
|
+
organization_id: str | None = None,
|
|
82
|
+
status: str | None = None,
|
|
83
|
+
metadata_filter: dict[str, Any] | None = None,
|
|
84
|
+
) -> list[Room]:
|
|
85
|
+
results: list[Room] = []
|
|
86
|
+
for room in self._rooms.values():
|
|
87
|
+
if organization_id is not None and room.organization_id != organization_id:
|
|
88
|
+
continue
|
|
89
|
+
if status is not None and room.status.value != status:
|
|
90
|
+
continue
|
|
91
|
+
if metadata_filter and not all(
|
|
92
|
+
room.metadata.get(k) == v for k, v in metadata_filter.items()
|
|
93
|
+
):
|
|
94
|
+
continue
|
|
95
|
+
results.append(room.model_copy())
|
|
96
|
+
return results
|
|
97
|
+
|
|
98
|
+
async def find_latest_room(
|
|
99
|
+
self,
|
|
100
|
+
participant_id: str,
|
|
101
|
+
channel_type: str | None = None,
|
|
102
|
+
status: str | None = None,
|
|
103
|
+
) -> Room | None:
|
|
104
|
+
best: Room | None = None
|
|
105
|
+
for room in self._rooms.values():
|
|
106
|
+
if status is not None and room.status.value != status:
|
|
107
|
+
continue
|
|
108
|
+
# Check if participant is in this room
|
|
109
|
+
if participant_id not in self._participants.get(room.id, {}):
|
|
110
|
+
# Also check bindings for participant_id
|
|
111
|
+
found = False
|
|
112
|
+
for binding in self._bindings.get(room.id, {}).values():
|
|
113
|
+
if binding.participant_id == participant_id and (
|
|
114
|
+
channel_type is None or binding.channel_type.value == channel_type
|
|
115
|
+
):
|
|
116
|
+
found = True
|
|
117
|
+
break
|
|
118
|
+
if not found:
|
|
119
|
+
continue
|
|
120
|
+
if best is None or room.created_at > best.created_at:
|
|
121
|
+
best = room
|
|
122
|
+
return best.model_copy() if best is not None else None
|
|
123
|
+
|
|
124
|
+
async def find_room_id_by_channel(
|
|
125
|
+
self, channel_id: str, status: str | None = None
|
|
126
|
+
) -> str | None:
|
|
127
|
+
for room_id, bindings in self._bindings.items():
|
|
128
|
+
if channel_id in bindings:
|
|
129
|
+
if status is not None:
|
|
130
|
+
room = self._rooms.get(room_id)
|
|
131
|
+
if room is None or room.status.value != status:
|
|
132
|
+
continue
|
|
133
|
+
return room_id
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
# Event operations
|
|
137
|
+
|
|
138
|
+
async def add_event(self, event: RoomEvent) -> RoomEvent:
|
|
139
|
+
self._events[event.id] = event
|
|
140
|
+
self._room_events.setdefault(event.room_id, []).append(event.id)
|
|
141
|
+
if event.idempotency_key:
|
|
142
|
+
self._idempotency.setdefault(event.room_id, set()).add(event.idempotency_key)
|
|
143
|
+
return event
|
|
144
|
+
|
|
145
|
+
async def get_event(self, event_id: str) -> RoomEvent | None:
|
|
146
|
+
event = self._events.get(event_id)
|
|
147
|
+
return event.model_copy() if event is not None else None
|
|
148
|
+
|
|
149
|
+
async def list_events(
|
|
150
|
+
self,
|
|
151
|
+
room_id: str,
|
|
152
|
+
offset: int = 0,
|
|
153
|
+
limit: int = 50,
|
|
154
|
+
visibility_filter: str | None = None,
|
|
155
|
+
) -> list[RoomEvent]:
|
|
156
|
+
event_ids = self._room_events.get(room_id, [])
|
|
157
|
+
events = [self._events[eid] for eid in event_ids if eid in self._events]
|
|
158
|
+
if visibility_filter is not None:
|
|
159
|
+
events = [e for e in events if e.visibility == visibility_filter]
|
|
160
|
+
return [e.model_copy() for e in events[offset : offset + limit]]
|
|
161
|
+
|
|
162
|
+
async def check_idempotency(self, room_id: str, key: str) -> bool:
|
|
163
|
+
return key in self._idempotency.get(room_id, set())
|
|
164
|
+
|
|
165
|
+
async def get_event_count(self, room_id: str) -> int:
|
|
166
|
+
return len(self._room_events.get(room_id, []))
|
|
167
|
+
|
|
168
|
+
# Binding operations
|
|
169
|
+
|
|
170
|
+
async def add_binding(self, binding: ChannelBinding) -> ChannelBinding:
|
|
171
|
+
self._bindings.setdefault(binding.room_id, {})[binding.channel_id] = binding
|
|
172
|
+
return binding
|
|
173
|
+
|
|
174
|
+
async def get_binding(self, room_id: str, channel_id: str) -> ChannelBinding | None:
|
|
175
|
+
binding = self._bindings.get(room_id, {}).get(channel_id)
|
|
176
|
+
return binding.model_copy() if binding is not None else None
|
|
177
|
+
|
|
178
|
+
async def update_binding(self, binding: ChannelBinding) -> ChannelBinding:
|
|
179
|
+
self._bindings.setdefault(binding.room_id, {})[binding.channel_id] = binding
|
|
180
|
+
return binding
|
|
181
|
+
|
|
182
|
+
async def remove_binding(self, room_id: str, channel_id: str) -> bool:
|
|
183
|
+
room_bindings = self._bindings.get(room_id, {})
|
|
184
|
+
if channel_id not in room_bindings:
|
|
185
|
+
return False
|
|
186
|
+
del room_bindings[channel_id]
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
async def list_bindings(self, room_id: str) -> list[ChannelBinding]:
|
|
190
|
+
return [b.model_copy() for b in self._bindings.get(room_id, {}).values()]
|
|
191
|
+
|
|
192
|
+
# Participant operations
|
|
193
|
+
|
|
194
|
+
async def add_participant(self, participant: Participant) -> Participant:
|
|
195
|
+
self._participants.setdefault(participant.room_id, {})[participant.id] = participant
|
|
196
|
+
return participant
|
|
197
|
+
|
|
198
|
+
async def get_participant(self, room_id: str, participant_id: str) -> Participant | None:
|
|
199
|
+
participant = self._participants.get(room_id, {}).get(participant_id)
|
|
200
|
+
return participant.model_copy() if participant is not None else None
|
|
201
|
+
|
|
202
|
+
async def update_participant(self, participant: Participant) -> Participant:
|
|
203
|
+
self._participants.setdefault(participant.room_id, {})[participant.id] = participant
|
|
204
|
+
return participant
|
|
205
|
+
|
|
206
|
+
async def list_participants(self, room_id: str) -> list[Participant]:
|
|
207
|
+
return [p.model_copy() for p in self._participants.get(room_id, {}).values()]
|
|
208
|
+
|
|
209
|
+
# Read tracking
|
|
210
|
+
|
|
211
|
+
async def mark_read(self, room_id: str, channel_id: str, event_id: str) -> None:
|
|
212
|
+
self._read_markers.setdefault(room_id, {})[channel_id] = event_id
|
|
213
|
+
|
|
214
|
+
async def mark_all_read(self, room_id: str, channel_id: str) -> None:
|
|
215
|
+
event_ids = self._room_events.get(room_id, [])
|
|
216
|
+
if event_ids:
|
|
217
|
+
self._read_markers.setdefault(room_id, {})[channel_id] = event_ids[-1]
|
|
218
|
+
|
|
219
|
+
async def get_unread_count(self, room_id: str, channel_id: str) -> int:
|
|
220
|
+
event_ids = self._room_events.get(room_id, [])
|
|
221
|
+
last_read = self._read_markers.get(room_id, {}).get(channel_id)
|
|
222
|
+
if last_read is None:
|
|
223
|
+
return len(event_ids)
|
|
224
|
+
try:
|
|
225
|
+
idx = event_ids.index(last_read)
|
|
226
|
+
return len(event_ids) - idx - 1
|
|
227
|
+
except ValueError:
|
|
228
|
+
return len(event_ids)
|
|
229
|
+
|
|
230
|
+
# Identity operations
|
|
231
|
+
|
|
232
|
+
async def create_identity(self, identity: Identity) -> Identity:
|
|
233
|
+
self._identities[identity.id] = identity
|
|
234
|
+
for ch_type, addresses in identity.channel_addresses.items():
|
|
235
|
+
for addr in addresses:
|
|
236
|
+
self._address_index[(ch_type, addr)] = identity.id
|
|
237
|
+
return identity
|
|
238
|
+
|
|
239
|
+
async def get_identity(self, identity_id: str) -> Identity | None:
|
|
240
|
+
identity = self._identities.get(identity_id)
|
|
241
|
+
return identity.model_copy() if identity is not None else None
|
|
242
|
+
|
|
243
|
+
async def resolve_identity(self, channel_type: str, address: str) -> Identity | None:
|
|
244
|
+
identity_id = self._address_index.get((channel_type, address))
|
|
245
|
+
if identity_id is None:
|
|
246
|
+
return None
|
|
247
|
+
return self._identities.get(identity_id)
|
|
248
|
+
|
|
249
|
+
async def link_address(self, identity_id: str, channel_type: str, address: str) -> None:
|
|
250
|
+
identity = self._identities.get(identity_id)
|
|
251
|
+
if identity is None:
|
|
252
|
+
return
|
|
253
|
+
current = identity.channel_addresses.get(channel_type, [])
|
|
254
|
+
if address not in current:
|
|
255
|
+
new_addresses = {**identity.channel_addresses, channel_type: [*current, address]}
|
|
256
|
+
self._identities[identity_id] = identity.model_copy(
|
|
257
|
+
update={"channel_addresses": new_addresses}
|
|
258
|
+
)
|
|
259
|
+
self._address_index[(channel_type, address)] = identity_id
|
|
260
|
+
|
|
261
|
+
# Task operations
|
|
262
|
+
|
|
263
|
+
async def add_task(self, task: Task) -> Task:
|
|
264
|
+
self._tasks[task.id] = task
|
|
265
|
+
self._room_tasks.setdefault(task.room_id, []).append(task.id)
|
|
266
|
+
return task
|
|
267
|
+
|
|
268
|
+
async def get_task(self, task_id: str) -> Task | None:
|
|
269
|
+
return self._tasks.get(task_id)
|
|
270
|
+
|
|
271
|
+
async def list_tasks(self, room_id: str, status: str | None = None) -> list[Task]:
|
|
272
|
+
task_ids = self._room_tasks.get(room_id, [])
|
|
273
|
+
tasks = [self._tasks[tid] for tid in task_ids if tid in self._tasks]
|
|
274
|
+
if status is not None:
|
|
275
|
+
tasks = [t for t in tasks if t.status == status]
|
|
276
|
+
return [t.model_copy() for t in tasks]
|
|
277
|
+
|
|
278
|
+
async def update_task(self, task: Task) -> Task:
|
|
279
|
+
self._tasks[task.id] = task
|
|
280
|
+
return task
|
|
281
|
+
|
|
282
|
+
# Observation operations
|
|
283
|
+
|
|
284
|
+
async def add_observation(self, observation: Observation) -> Observation:
|
|
285
|
+
self._observations[observation.id] = observation
|
|
286
|
+
self._room_observations.setdefault(observation.room_id, []).append(observation.id)
|
|
287
|
+
return observation
|
|
288
|
+
|
|
289
|
+
async def list_observations(self, room_id: str) -> list[Observation]:
|
|
290
|
+
obs_ids = self._room_observations.get(room_id, [])
|
|
291
|
+
return [
|
|
292
|
+
self._observations[oid].model_copy() for oid in obs_ids if oid in self._observations
|
|
293
|
+
]
|