Skip to content

Commit 3faff0f

Browse files
Handles support conversations without project ID
Allows support conversations and messages to be created, updated, and deleted without requiring a project ID. Prevents unnecessary notifications for support conversations by skipping events when no project ID is present. Improves access control checks and context handling for support-type conversations. Enhances code clarity and robustness for support workflows.
1 parent b202cf8 commit 3faff0f

File tree

6 files changed

+115
-85
lines changed

6 files changed

+115
-85
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ConversationRestGet(OutputSchema):
2828
type: ConversationType
2929
created: datetime
3030
modified: datetime
31+
extra_context: dict[str, str]
3132

3233
@classmethod
3334
def from_domain_model(cls, domain: ConversationGetDB) -> Self:
@@ -40,6 +41,7 @@ def from_domain_model(cls, domain: ConversationGetDB) -> Self:
4041
type=domain.type,
4142
created=domain.created,
4243
modified=domain.modified,
44+
extra_context=domain.extra_context,
4345
)
4446

4547

services/web/server/src/simcore_service_webserver/conversations/_controller/conversations_rest.py

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class _GetConversationsQueryParams(BaseModel):
6565
type: ConversationType
6666
model_config = ConfigDict(extra="forbid")
6767

68-
@field_validator('type')
68+
@field_validator("type")
6969
@classmethod
7070
def validate_type(cls, value):
7171
if value is not None and value != ConversationType.SUPPORT:
@@ -101,7 +101,9 @@ async def create_conversation(request: web.Request):
101101
"""Create a new conversation (supports only type='support')"""
102102
try:
103103
req_ctx = AuthenticatedRequestContext.model_validate(request)
104-
body_params = await parse_request_body_as(_ConversationsCreateBodyParams, request)
104+
body_params = await parse_request_body_as(
105+
_ConversationsCreateBodyParams, request
106+
)
105107

106108
# Ensure only support conversations are allowed
107109
if body_params.type != ConversationType.SUPPORT:
@@ -126,7 +128,9 @@ async def create_conversation(request: web.Request):
126128
raise
127129
except Exception as exc:
128130
_logger.exception("Failed to create conversation")
129-
raise web.HTTPInternalServerError(reason="Failed to create conversation") from exc
131+
raise web.HTTPInternalServerError(
132+
reason="Failed to create conversation"
133+
) from exc
130134

131135

132136
@routes.get(
@@ -169,7 +173,9 @@ async def list_conversations(request: web.Request):
169173

170174
except Exception as exc:
171175
_logger.exception("Failed to list conversations")
172-
raise web.HTTPInternalServerError(reason="Failed to list conversations") from exc
176+
raise web.HTTPInternalServerError(
177+
reason="Failed to list conversations"
178+
) from exc
173179

174180

175181
@routes.get(
@@ -180,16 +186,17 @@ async def list_conversations(request: web.Request):
180186
async def get_conversation(request: web.Request):
181187
"""Get a specific conversation"""
182188
try:
189+
req_ctx = AuthenticatedRequestContext.model_validate(request)
183190
path_params = parse_request_path_parameters_as(_ConversationPathParams, request)
184191
query_params = parse_request_query_parameters_as(
185192
_GetConversationsQueryParams, request
186193
)
187-
188-
# I need to check whether I have access to that conversation?
194+
assert query_params.type == ConversationType.SUPPORT # nosec
189195

190196
conversation = await get_support_conversation_for_user(
191197
app=request.app,
192-
user_id=
198+
user_id=req_ctx.user_id,
199+
product_name=req_ctx.product_name,
193200
conversation_id=path_params.conversation_id,
194201
)
195202

@@ -209,17 +216,25 @@ async def get_conversation(request: web.Request):
209216
async def update_conversation(request: web.Request):
210217
"""Update a conversation"""
211218
try:
219+
req_ctx = AuthenticatedRequestContext.model_validate(request)
212220
path_params = parse_request_path_parameters_as(_ConversationPathParams, request)
213221
body_params = await parse_request_body_as(ConversationPatch, request)
222+
query_params = parse_request_query_parameters_as(
223+
_GetConversationsQueryParams, request
224+
)
214225

215-
# For support conversations, we need a dummy project_id since the service requires it
216-
# but for support conversations it won't be used
217-
from uuid import uuid4
218-
dummy_project_id = uuid4() # This won't be used for support conversations
226+
assert query_params.type == ConversationType.SUPPORT # nosec
227+
228+
await get_support_conversation_for_user(
229+
app=request.app,
230+
user_id=req_ctx.user_id,
231+
product_name=req_ctx.product_name,
232+
conversation_id=path_params.conversation_id,
233+
)
219234

220235
conversation = await conversations_service.update_conversation(
221236
app=request.app,
222-
project_id=dummy_project_id, # Support conversations don't use project_id
237+
project_id=None, # Support conversations don't use project_id
223238
conversation_id=path_params.conversation_id,
224239
updates=ConversationPatchDB(name=body_params.name),
225240
)
@@ -242,16 +257,23 @@ async def delete_conversation(request: web.Request):
242257
try:
243258
req_ctx = AuthenticatedRequestContext.model_validate(request)
244259
path_params = parse_request_path_parameters_as(_ConversationPathParams, request)
260+
query_params = parse_request_query_parameters_as(
261+
_GetConversationsQueryParams, request
262+
)
263+
assert query_params.type == ConversationType.SUPPORT # nosec
245264

246-
# For support conversations, we need a dummy project_id since the service requires it
247-
from uuid import uuid4
248-
dummy_project_id = uuid4() # This won't be used for support conversations
265+
await get_support_conversation_for_user(
266+
app=request.app,
267+
user_id=req_ctx.user_id,
268+
product_name=req_ctx.product_name,
269+
conversation_id=path_params.conversation_id,
270+
)
249271

250272
await conversations_service.delete_conversation(
251273
app=request.app,
252274
product_name=req_ctx.product_name,
253275
user_id=req_ctx.user_id,
254-
project_id=dummy_project_id, # Support conversations don't use project_id
276+
project_id=None, # Support conversations don't use project_id
255277
conversation_id=path_params.conversation_id,
256278
)
257279

@@ -281,6 +303,7 @@ async def create_conversation_message(request: web.Request):
281303

282304
# For support conversations, we need a dummy project_id since the service requires it
283305
from uuid import uuid4
306+
284307
dummy_project_id = uuid4() # This won't be used for support conversations
285308

286309
message = await conversations_service.create_message(
@@ -297,7 +320,9 @@ async def create_conversation_message(request: web.Request):
297320

298321
except Exception as exc:
299322
_logger.exception("Failed to create conversation message")
300-
raise web.HTTPInternalServerError(reason="Failed to create conversation message") from exc
323+
raise web.HTTPInternalServerError(
324+
reason="Failed to create conversation message"
325+
) from exc
301326

302327

303328
@routes.get(
@@ -339,7 +364,9 @@ async def list_conversation_messages(request: web.Request):
339364

340365
except Exception as exc:
341366
_logger.exception("Failed to list conversation messages")
342-
raise web.HTTPInternalServerError(reason="Failed to list conversation messages") from exc
367+
raise web.HTTPInternalServerError(
368+
reason="Failed to list conversation messages"
369+
) from exc
343370

344371

345372
@routes.get(
@@ -350,7 +377,9 @@ async def list_conversation_messages(request: web.Request):
350377
async def get_conversation_message(request: web.Request):
351378
"""Get a specific message in a conversation"""
352379
try:
353-
path_params = parse_request_path_parameters_as(_ConversationMessagePathParams, request)
380+
path_params = parse_request_path_parameters_as(
381+
_ConversationMessagePathParams, request
382+
)
354383

355384
message = await conversations_service.get_message(
356385
app=request.app,
@@ -374,16 +403,14 @@ async def get_conversation_message(request: web.Request):
374403
async def update_conversation_message(request: web.Request):
375404
"""Update a message in a conversation"""
376405
try:
377-
path_params = parse_request_path_parameters_as(_ConversationMessagePathParams, request)
406+
path_params = parse_request_path_parameters_as(
407+
_ConversationMessagePathParams, request
408+
)
378409
body_params = await parse_request_body_as(ConversationMessagePatch, request)
379410

380-
# For support conversations, we need a dummy project_id since the service requires it
381-
from uuid import uuid4
382-
dummy_project_id = uuid4() # This won't be used for support conversations
383-
384411
message = await conversations_service.update_message(
385412
app=request.app,
386-
project_id=dummy_project_id, # Support conversations don't use project_id
413+
project_id=None, # Support conversations don't use project_id
387414
conversation_id=path_params.conversation_id,
388415
message_id=path_params.message_id,
389416
updates=ConversationMessagePatchDB(content=body_params.content),
@@ -406,16 +433,14 @@ async def delete_conversation_message(request: web.Request):
406433
"""Delete a message in a conversation"""
407434
try:
408435
req_ctx = AuthenticatedRequestContext.model_validate(request)
409-
path_params = parse_request_path_parameters_as(_ConversationMessagePathParams, request)
410-
411-
# For support conversations, we need a dummy project_id since the service requires it
412-
from uuid import uuid4
413-
dummy_project_id = uuid4() # This won't be used for support conversations
436+
path_params = parse_request_path_parameters_as(
437+
_ConversationMessagePathParams, request
438+
)
414439

415440
await conversations_service.delete_message(
416441
app=request.app,
417442
user_id=req_ctx.user_id,
418-
project_id=dummy_project_id, # Support conversations don't use project_id
443+
project_id=None, # Support conversations don't use project_id
419444
conversation_id=path_params.conversation_id,
420445
message_id=path_params.message_id,
421446
)

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def get_message(
7373
async def update_message(
7474
app: web.Application,
7575
*,
76-
project_id: ProjectID,
76+
project_id: ProjectID | None,
7777
conversation_id: ConversationID,
7878
message_id: ConversationMessageID,
7979
# Update attributes
@@ -86,12 +86,13 @@ async def update_message(
8686
updates=updates,
8787
)
8888

89-
await notify_conversation_message_updated(
90-
app,
91-
recipients=await _get_recipients(app, project_id),
92-
project_id=project_id,
93-
conversation_message=updated_message,
94-
)
89+
if project_id:
90+
await notify_conversation_message_updated(
91+
app,
92+
recipients=await _get_recipients(app, project_id),
93+
project_id=project_id,
94+
conversation_message=updated_message,
95+
)
9596

9697
return updated_message
9798

@@ -100,7 +101,7 @@ async def delete_message(
100101
app: web.Application,
101102
*,
102103
user_id: UserID,
103-
project_id: ProjectID,
104+
project_id: ProjectID | None,
104105
conversation_id: ConversationID,
105106
message_id: ConversationMessageID,
106107
) -> None:
@@ -112,14 +113,15 @@ async def delete_message(
112113

113114
_user_group_id = await users_service.get_user_primary_group_id(app, user_id=user_id)
114115

115-
await notify_conversation_message_deleted(
116-
app,
117-
recipients=await _get_recipients(app, project_id),
118-
user_group_id=_user_group_id,
119-
project_id=project_id,
120-
conversation_id=conversation_id,
121-
message_id=message_id,
122-
)
116+
if project_id:
117+
await notify_conversation_message_deleted(
118+
app,
119+
recipients=await _get_recipients(app, project_id),
120+
user_group_id=_user_group_id,
121+
project_id=project_id,
122+
conversation_id=conversation_id,
123+
message_id=message_id,
124+
)
123125

124126

125127
async def list_messages_for_conversation(

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

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ async def create_conversation(
5353
type_: ConversationType,
5454
extra_context: dict[str, Any],
5555
) -> ConversationGetDB:
56-
if project_uuid is None:
57-
raise NotImplementedError
58-
5956
_user_group_id = await users_service.get_user_primary_group_id(app, user_id=user_id)
6057

6158
created_conversation = await _conversation_repository.create(
@@ -68,12 +65,13 @@ async def create_conversation(
6865
extra_context=extra_context,
6966
)
7067

71-
await notify_conversation_created(
72-
app,
73-
recipients=await _get_recipients(app, project_uuid),
74-
project_id=project_uuid,
75-
conversation=created_conversation,
76-
)
68+
if project_uuid:
69+
await notify_conversation_created(
70+
app,
71+
recipients=await _get_recipients(app, project_uuid),
72+
project_id=project_uuid,
73+
conversation=created_conversation,
74+
)
7775

7876
return created_conversation
7977

@@ -110,7 +108,7 @@ async def get_conversation_for_user(
110108
async def update_conversation(
111109
app: web.Application,
112110
*,
113-
project_id: ProjectID,
111+
project_id: ProjectID | None,
114112
conversation_id: ConversationID,
115113
# Update attributes
116114
updates: ConversationPatchDB,
@@ -121,12 +119,13 @@ async def update_conversation(
121119
updates=updates,
122120
)
123121

124-
await notify_conversation_updated(
125-
app,
126-
recipients=await _get_recipients(app, project_id),
127-
project_id=project_id,
128-
conversation=updated_conversation,
129-
)
122+
if project_id:
123+
await notify_conversation_updated(
124+
app,
125+
recipients=await _get_recipients(app, project_id),
126+
project_id=project_id,
127+
conversation=updated_conversation,
128+
)
130129

131130
return updated_conversation
132131

@@ -136,7 +135,7 @@ async def delete_conversation(
136135
*,
137136
product_name: ProductName,
138137
user_id: UserID,
139-
project_id: ProjectID,
138+
project_id: ProjectID | None,
140139
conversation_id: ConversationID,
141140
) -> None:
142141
await _conversation_repository.delete(
@@ -146,14 +145,15 @@ async def delete_conversation(
146145

147146
_user_group_id = await users_service.get_user_primary_group_id(app, user_id=user_id)
148147

149-
await notify_conversation_deleted(
150-
app,
151-
recipients=await _get_recipients(app, project_id),
152-
product_name=product_name,
153-
user_group_id=_user_group_id,
154-
project_id=project_id,
155-
conversation_id=conversation_id,
156-
)
148+
if project_id:
149+
await notify_conversation_deleted(
150+
app,
151+
recipients=await _get_recipients(app, project_id),
152+
product_name=product_name,
153+
user_group_id=_user_group_id,
154+
project_id=project_id,
155+
conversation_id=conversation_id,
156+
)
157157

158158

159159
async def list_project_conversations(
@@ -180,7 +180,7 @@ async def get_support_conversation_for_user(
180180
product_name: ProductName,
181181
conversation_id: ConversationID,
182182
):
183-
# Check if user is part of support group (in that case list all support conversations)
183+
# Check if user is part of support group (in that case he has access to all support conversations)
184184
product = products_service.get_product(app, product_name=product_name)
185185
_support_standard_group_id = product.support_standard_group_id
186186
if _support_standard_group_id is not None:

0 commit comments

Comments
 (0)