Skip to content

Commit b109758

Browse files
🎨 Send Socket.IO events whenever conversations are created, updated or deleted (#7977)
1 parent 98caa3a commit b109758

File tree

6 files changed

+239
-58
lines changed

6 files changed

+239
-58
lines changed

packages/models-library/src/models_library/conversations.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import TypeAlias
3+
from typing import Annotated, TypeAlias
44
from uuid import UUID
55

66
from models_library.groups import GroupID
77
from models_library.projects import ProjectID
8-
from pydantic import BaseModel, ConfigDict
8+
from pydantic import BaseModel, ConfigDict, StringConstraints
99

1010
from .products import ProductName
1111
from .utils.enums import StrAutoEnum
1212

1313
ConversationID: TypeAlias = UUID
14+
ConversationName: TypeAlias = Annotated[
15+
str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255)
16+
]
17+
1418
ConversationMessageID: TypeAlias = UUID
1519

1620

@@ -36,7 +40,7 @@ class ConversationMessageType(StrAutoEnum):
3640
class ConversationGetDB(BaseModel):
3741
conversation_id: ConversationID
3842
product_name: ProductName
39-
name: str
43+
name: ConversationName
4044
project_uuid: ProjectID | None
4145
user_group_id: GroupID
4246
type: ConversationType
@@ -63,7 +67,7 @@ class ConversationMessageGetDB(BaseModel):
6367

6468

6569
class ConversationPatchDB(BaseModel):
66-
name: str | None = None
70+
name: ConversationName | None = None
6771

6872

6973
class ConversationMessagePatchDB(BaseModel):

services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@
1616
from models_library.rest_pagination import PageTotalCount
1717
from models_library.users import UserID
1818

19-
from ..projects._groups_repository import list_project_groups
20-
from ..users._users_service import get_users_in_group
21-
2219
# Import or define SocketMessageDict
2320
from ..users.api import get_user_primary_group_id
2421
from . import _conversation_message_repository
22+
from ._conversation_service import _get_recipients
2523
from ._socketio import (
2624
notify_conversation_message_created,
2725
notify_conversation_message_deleted,
@@ -31,16 +29,6 @@
3129
_logger = logging.getLogger(__name__)
3230

3331

34-
async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]:
35-
groups = await list_project_groups(app, project_id=project_id)
36-
return {
37-
user
38-
for group in groups
39-
if group.read
40-
for user in await get_users_in_group(app, gid=group.gid)
41-
}
42-
43-
4432
async def create_message(
4533
app: web.Application,
4634
*,

services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
from models_library.rest_pagination import PageTotalCount
1717
from models_library.users import UserID
1818

19+
from ..conversations._socketio import (
20+
notify_conversation_created,
21+
notify_conversation_deleted,
22+
notify_conversation_updated,
23+
)
24+
from ..projects._groups_repository import list_project_groups
25+
from ..users._users_service import get_users_in_group
1926
from ..users.api import get_user_primary_group_id
2027
from . import _conversation_repository
2128

2229
_logger = logging.getLogger(__name__)
2330

2431

32+
async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]:
33+
groups = await list_project_groups(app, project_id=project_id)
34+
return {
35+
user
36+
for group in groups
37+
if group.read
38+
for user in await get_users_in_group(app, gid=group.gid)
39+
}
40+
41+
2542
async def create_conversation(
2643
app: web.Application,
2744
*,
@@ -37,7 +54,7 @@ async def create_conversation(
3754

3855
_user_group_id = await get_user_primary_group_id(app, user_id=user_id)
3956

40-
return await _conversation_repository.create(
57+
created_conversation = await _conversation_repository.create(
4158
app,
4259
name=name,
4360
project_uuid=project_uuid,
@@ -46,6 +63,15 @@ async def create_conversation(
4663
product_name=product_name,
4764
)
4865

66+
await notify_conversation_created(
67+
app,
68+
recipients=await _get_recipients(app, project_uuid),
69+
project_id=project_uuid,
70+
conversation=created_conversation,
71+
)
72+
73+
return created_conversation
74+
4975

5076
async def get_conversation(
5177
app: web.Application,
@@ -61,27 +87,51 @@ async def get_conversation(
6187
async def update_conversation(
6288
app: web.Application,
6389
*,
90+
project_id: ProjectID,
6491
conversation_id: ConversationID,
6592
# Update attributes
6693
updates: ConversationPatchDB,
6794
) -> ConversationGetDB:
68-
return await _conversation_repository.update(
95+
updated_conversation = await _conversation_repository.update(
6996
app,
7097
conversation_id=conversation_id,
7198
updates=updates,
7299
)
73100

101+
await notify_conversation_updated(
102+
app,
103+
recipients=await _get_recipients(app, project_id),
104+
project_id=project_id,
105+
conversation=updated_conversation,
106+
)
107+
108+
return updated_conversation
109+
74110

75111
async def delete_conversation(
76112
app: web.Application,
77113
*,
114+
product_name: ProductName,
115+
user_id: UserID,
116+
project_id: ProjectID,
78117
conversation_id: ConversationID,
79118
) -> None:
80119
await _conversation_repository.delete(
81120
app,
82121
conversation_id=conversation_id,
83122
)
84123

124+
_user_group_id = await get_user_primary_group_id(app, user_id=user_id)
125+
126+
await notify_conversation_deleted(
127+
app,
128+
recipients=await _get_recipients(app, project_id),
129+
product_name=product_name,
130+
user_group_id=_user_group_id,
131+
project_id=project_id,
132+
conversation_id=conversation_id,
133+
)
134+
85135

86136
async def list_conversations_for_project(
87137
app: web.Application,

services/web/server/src/simcore_service_webserver/conversations/_socketio.py

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33

44
from aiohttp import web
55
from models_library.conversations import (
6+
ConversationGetDB,
67
ConversationID,
78
ConversationMessageGetDB,
89
ConversationMessageID,
910
ConversationMessageType,
11+
ConversationName,
12+
ConversationType,
1013
)
1114
from models_library.groups import GroupID
15+
from models_library.products import ProductName
1216
from models_library.projects import ProjectID
1317
from models_library.socketio import SocketMessageDict
1418
from models_library.users import UserID
@@ -20,6 +24,10 @@
2024

2125
_MAX_CONCURRENT_SENDS: Final[int] = 3
2226

27+
SOCKET_IO_CONVERSATION_CREATED_EVENT: Final[str] = "conversation:created"
28+
SOCKET_IO_CONVERSATION_DELETED_EVENT: Final[str] = "conversation:deleted"
29+
SOCKET_IO_CONVERSATION_UPDATED_EVENT: Final[str] = "conversation:updated"
30+
2331
SOCKET_IO_CONVERSATION_MESSAGE_CREATED_EVENT: Final[str] = (
2432
"conversation:message:created"
2533
)
@@ -31,7 +39,34 @@
3139
)
3240

3341

34-
class BaseConversationMessage(BaseModel):
42+
class BaseEvent(BaseModel):
43+
model_config = ConfigDict(
44+
populate_by_name=True,
45+
from_attributes=True,
46+
alias_generator=AliasGenerator(
47+
serialization_alias=to_camel,
48+
),
49+
)
50+
51+
52+
class BaseConversationEvent(BaseEvent):
53+
product_name: ProductName
54+
project_id: ProjectID | None
55+
user_group_id: GroupID
56+
conversation_id: ConversationID
57+
type: ConversationType
58+
59+
60+
class ConversationCreatedOrUpdatedEvent(BaseConversationEvent):
61+
name: ConversationName
62+
created: datetime.datetime
63+
modified: datetime.datetime
64+
65+
66+
class ConversationDeletedEvent(BaseConversationEvent): ...
67+
68+
69+
class BaseConversationMessageEvent(BaseEvent):
3570
conversation_id: ConversationID
3671
message_id: ConversationMessageID
3772
user_group_id: GroupID
@@ -46,13 +81,13 @@ class BaseConversationMessage(BaseModel):
4681
)
4782

4883

49-
class ConversationMessageCreatedOrUpdated(BaseConversationMessage):
84+
class ConversationMessageCreatedOrUpdatedEvent(BaseConversationMessageEvent):
5085
content: str
5186
created: datetime.datetime
5287
modified: datetime.datetime
5388

5489

55-
class ConversationMessageDeleted(BaseConversationMessage): ...
90+
class ConversationMessageDeletedEvent(BaseConversationMessageEvent): ...
5691

5792

5893
async def _send_message_to_recipients(
@@ -62,16 +97,79 @@ async def _send_message_to_recipients(
6297
):
6398
async for _ in limited_as_completed(
6499
(
65-
send_message_to_user(
66-
app, recipient, notification_message, ignore_queue=True
67-
)
100+
send_message_to_user(app, recipient, notification_message)
68101
for recipient in recipients
69102
),
70103
limit=_MAX_CONCURRENT_SENDS,
71104
):
72105
...
73106

74107

108+
async def notify_conversation_created(
109+
app: web.Application,
110+
*,
111+
recipients: set[UserID],
112+
project_id: ProjectID,
113+
conversation: ConversationGetDB,
114+
) -> None:
115+
notification_message = SocketMessageDict(
116+
event_type=SOCKET_IO_CONVERSATION_CREATED_EVENT,
117+
data={
118+
**ConversationCreatedOrUpdatedEvent(
119+
project_id=project_id,
120+
**conversation.model_dump(),
121+
).model_dump(mode="json", by_alias=True),
122+
},
123+
)
124+
125+
await _send_message_to_recipients(app, recipients, notification_message)
126+
127+
128+
async def notify_conversation_updated(
129+
app: web.Application,
130+
*,
131+
recipients: set[UserID],
132+
project_id: ProjectID,
133+
conversation: ConversationGetDB,
134+
) -> None:
135+
notification_message = SocketMessageDict(
136+
event_type=SOCKET_IO_CONVERSATION_UPDATED_EVENT,
137+
data={
138+
**ConversationCreatedOrUpdatedEvent(
139+
project_id=project_id,
140+
**conversation.model_dump(),
141+
).model_dump(mode="json", by_alias=True),
142+
},
143+
)
144+
145+
await _send_message_to_recipients(app, recipients, notification_message)
146+
147+
148+
async def notify_conversation_deleted(
149+
app: web.Application,
150+
*,
151+
recipients: set[UserID],
152+
product_name: ProductName,
153+
user_group_id: GroupID,
154+
project_id: ProjectID,
155+
conversation_id: ConversationID,
156+
) -> None:
157+
notification_message = SocketMessageDict(
158+
event_type=SOCKET_IO_CONVERSATION_DELETED_EVENT,
159+
data={
160+
**ConversationDeletedEvent(
161+
product_name=product_name,
162+
project_id=project_id,
163+
conversation_id=conversation_id,
164+
user_group_id=user_group_id,
165+
type=ConversationType.PROJECT_STATIC,
166+
).model_dump(mode="json", by_alias=True),
167+
},
168+
)
169+
170+
await _send_message_to_recipients(app, recipients, notification_message)
171+
172+
75173
async def notify_conversation_message_created(
76174
app: web.Application,
77175
*,
@@ -83,7 +181,7 @@ async def notify_conversation_message_created(
83181
event_type=SOCKET_IO_CONVERSATION_MESSAGE_CREATED_EVENT,
84182
data={
85183
"projectId": project_id,
86-
**ConversationMessageCreatedOrUpdated(
184+
**ConversationMessageCreatedOrUpdatedEvent(
87185
**conversation_message.model_dump()
88186
).model_dump(mode="json", by_alias=True),
89187
},
@@ -104,7 +202,7 @@ async def notify_conversation_message_updated(
104202
event_type=SOCKET_IO_CONVERSATION_MESSAGE_UPDATED_EVENT,
105203
data={
106204
"projectId": project_id,
107-
**ConversationMessageCreatedOrUpdated(
205+
**ConversationMessageCreatedOrUpdatedEvent(
108206
**conversation_message.model_dump()
109207
).model_dump(mode="json", by_alias=True),
110208
},
@@ -127,7 +225,7 @@ async def notify_conversation_message_deleted(
127225
event_type=SOCKET_IO_CONVERSATION_MESSAGE_DELETED_EVENT,
128226
data={
129227
"projectId": project_id,
130-
**ConversationMessageDeleted(
228+
**ConversationMessageDeletedEvent(
131229
conversation_id=conversation_id,
132230
message_id=message_id,
133231
user_group_id=user_group_id,

0 commit comments

Comments
 (0)