Skip to content

Commit f119400

Browse files
Splits message endpoints to dedicated controller
Moves REST endpoints for conversation messages into a separate controller for improved modularity and maintainability. Refactors logic to better support support-type conversations, removing unnecessary project_id handling and redundant code. Enhances separation between conversation and message operations.
1 parent 3faff0f commit f119400

File tree

3 files changed

+310
-183
lines changed

3 files changed

+310
-183
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import logging
2+
3+
from aiohttp import web
4+
from models_library.api_schemas_webserver.conversations import (
5+
ConversationMessagePatch,
6+
ConversationMessageRestGet,
7+
)
8+
from models_library.conversations import (
9+
ConversationID,
10+
ConversationMessageID,
11+
ConversationMessagePatchDB,
12+
ConversationMessageType,
13+
ConversationType,
14+
)
15+
from models_library.rest_pagination import (
16+
Page,
17+
PageQueryParameters,
18+
)
19+
from models_library.rest_pagination_utils import paginate_data
20+
from pydantic import BaseModel, ConfigDict, field_validator
21+
from servicelib.aiohttp import status
22+
from servicelib.aiohttp.requests_validation import (
23+
parse_request_body_as,
24+
parse_request_path_parameters_as,
25+
parse_request_query_parameters_as,
26+
)
27+
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
28+
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
29+
30+
from ..._meta import API_VTAG as VTAG
31+
from ...login.decorators import login_required
32+
from ...models import AuthenticatedRequestContext
33+
from ...users import users_service
34+
from ...utils_aiohttp import envelope_json_response
35+
from .. import _conversation_message_service, _conversation_service
36+
37+
_logger = logging.getLogger(__name__)
38+
39+
routes = web.RouteTableDef()
40+
41+
42+
class _ConversationPathParams(BaseModel):
43+
conversation_id: ConversationID
44+
model_config = ConfigDict(extra="forbid")
45+
46+
47+
class _ConversationMessagePathParams(_ConversationPathParams):
48+
message_id: ConversationMessageID
49+
model_config = ConfigDict(extra="forbid")
50+
51+
52+
class _GetConversationsQueryParams(BaseModel):
53+
type: ConversationType
54+
model_config = ConfigDict(extra="forbid")
55+
56+
@field_validator("type")
57+
@classmethod
58+
def validate_type(cls, value):
59+
if value is not None and value != ConversationType.SUPPORT:
60+
raise ValueError("Only support conversations are allowed")
61+
return value
62+
63+
64+
class _ListConversationsQueryParams(PageQueryParameters, _GetConversationsQueryParams):
65+
66+
model_config = ConfigDict(extra="forbid")
67+
68+
69+
class _ConversationMessageCreateBodyParams(BaseModel):
70+
content: str
71+
type: ConversationMessageType
72+
model_config = ConfigDict(extra="forbid")
73+
74+
75+
@routes.post(
76+
f"/{VTAG}/conversations/{{conversation_id}}/messages",
77+
name="create_conversation_message",
78+
)
79+
@login_required
80+
async def create_conversation_message(request: web.Request):
81+
"""Create a new message in a conversation"""
82+
try:
83+
req_ctx = AuthenticatedRequestContext.model_validate(request)
84+
path_params = parse_request_path_parameters_as(_ConversationPathParams, request)
85+
body_params = await parse_request_body_as(
86+
_ConversationMessageCreateBodyParams, request
87+
)
88+
89+
user_primary_gid = await users_service.get_user_primary_group_id(
90+
request.app, user_id=req_ctx.user_id
91+
)
92+
conversation = await _conversation_service.get_conversation_for_user(
93+
app=request.app,
94+
conversation_id=path_params.conversation_id,
95+
user_group_id=user_primary_gid,
96+
)
97+
if conversation.type != ConversationType.SUPPORT:
98+
raise web.HTTPBadRequest(
99+
reason="Only support conversations are allowed for this endpoint"
100+
)
101+
102+
message = await _conversation_message_service.create_message(
103+
app=request.app,
104+
user_id=req_ctx.user_id,
105+
project_id=None, # Support conversations don't use project_id
106+
conversation_id=path_params.conversation_id,
107+
content=body_params.content,
108+
type_=body_params.type,
109+
)
110+
111+
data = ConversationMessageRestGet.from_domain_model(message)
112+
return envelope_json_response(data, web.HTTPCreated)
113+
114+
except Exception as exc:
115+
_logger.exception("Failed to create conversation message")
116+
raise web.HTTPInternalServerError(
117+
reason="Failed to create conversation message"
118+
) from exc
119+
120+
121+
@routes.get(
122+
f"/{VTAG}/conversations/{{conversation_id}}/messages",
123+
name="list_conversation_messages",
124+
)
125+
@login_required
126+
async def list_conversation_messages(request: web.Request):
127+
"""List messages in a conversation"""
128+
try:
129+
req_ctx = AuthenticatedRequestContext.model_validate(request)
130+
path_params = parse_request_path_parameters_as(_ConversationPathParams, request)
131+
query_params = parse_request_query_parameters_as(
132+
_ListConversationsQueryParams, request
133+
)
134+
135+
user_primary_gid = await users_service.get_user_primary_group_id(
136+
request.app, user_id=req_ctx.user_id
137+
)
138+
conversation = await _conversation_service.get_conversation_for_user(
139+
app=request.app,
140+
conversation_id=path_params.conversation_id,
141+
user_group_id=user_primary_gid,
142+
)
143+
if conversation.type != ConversationType.SUPPORT:
144+
raise web.HTTPBadRequest(
145+
reason="Only support conversations are allowed for this endpoint"
146+
)
147+
148+
total, messages = (
149+
await _conversation_message_service.list_messages_for_conversation(
150+
app=request.app,
151+
conversation_id=path_params.conversation_id,
152+
offset=query_params.offset,
153+
limit=query_params.limit,
154+
)
155+
)
156+
157+
page = Page[ConversationMessageRestGet].model_validate(
158+
paginate_data(
159+
chunk=[
160+
ConversationMessageRestGet.from_domain_model(message)
161+
for message in messages
162+
],
163+
request_url=request.url,
164+
total=total,
165+
limit=query_params.limit,
166+
offset=query_params.offset,
167+
)
168+
)
169+
return web.Response(
170+
text=page.model_dump_json(**RESPONSE_MODEL_POLICY),
171+
content_type=MIMETYPE_APPLICATION_JSON,
172+
)
173+
174+
except Exception as exc:
175+
_logger.exception("Failed to list conversation messages")
176+
raise web.HTTPInternalServerError(
177+
reason="Failed to list conversation messages"
178+
) from exc
179+
180+
181+
@routes.get(
182+
f"/{VTAG}/conversations/{{conversation_id}}/messages/{{message_id}}",
183+
name="get_conversation_message",
184+
)
185+
@login_required
186+
async def get_conversation_message(request: web.Request):
187+
"""Get a specific message in a conversation"""
188+
try:
189+
req_ctx = AuthenticatedRequestContext.model_validate(request)
190+
path_params = parse_request_path_parameters_as(
191+
_ConversationMessagePathParams, request
192+
)
193+
194+
user_primary_gid = await users_service.get_user_primary_group_id(
195+
request.app, user_id=req_ctx.user_id
196+
)
197+
conversation = await _conversation_service.get_conversation_for_user(
198+
app=request.app,
199+
conversation_id=path_params.conversation_id,
200+
user_group_id=user_primary_gid,
201+
)
202+
if conversation.type != ConversationType.SUPPORT:
203+
raise web.HTTPBadRequest(
204+
reason="Only support conversations are allowed for this endpoint"
205+
)
206+
207+
message = await _conversation_message_service.get_message(
208+
app=request.app,
209+
conversation_id=path_params.conversation_id,
210+
message_id=path_params.message_id,
211+
)
212+
213+
data = ConversationMessageRestGet.from_domain_model(message)
214+
return envelope_json_response(data)
215+
216+
except Exception as exc:
217+
_logger.exception("Failed to get conversation message")
218+
raise web.HTTPNotFound(reason="Message not found") from exc
219+
220+
221+
@routes.put(
222+
f"/{VTAG}/conversations/{{conversation_id}}/messages/{{message_id}}",
223+
name="update_conversation_message",
224+
)
225+
@login_required
226+
async def update_conversation_message(request: web.Request):
227+
"""Update a message in a conversation"""
228+
try:
229+
req_ctx = AuthenticatedRequestContext.model_validate(request)
230+
path_params = parse_request_path_parameters_as(
231+
_ConversationMessagePathParams, request
232+
)
233+
body_params = await parse_request_body_as(ConversationMessagePatch, request)
234+
235+
user_primary_gid = await users_service.get_user_primary_group_id(
236+
request.app, user_id=req_ctx.user_id
237+
)
238+
conversation = await _conversation_service.get_conversation_for_user(
239+
app=request.app,
240+
conversation_id=path_params.conversation_id,
241+
user_group_id=user_primary_gid,
242+
)
243+
if conversation.type != ConversationType.SUPPORT:
244+
raise web.HTTPBadRequest(
245+
reason="Only support conversations are allowed for this endpoint"
246+
)
247+
248+
message = await _conversation_message_service.update_message(
249+
app=request.app,
250+
project_id=None, # Support conversations don't use project_id
251+
conversation_id=path_params.conversation_id,
252+
message_id=path_params.message_id,
253+
updates=ConversationMessagePatchDB(content=body_params.content),
254+
)
255+
256+
data = ConversationMessageRestGet.from_domain_model(message)
257+
return envelope_json_response(data)
258+
259+
except Exception as exc:
260+
_logger.exception("Failed to update conversation message")
261+
raise web.HTTPNotFound(reason="Message not found") from exc
262+
263+
264+
@routes.delete(
265+
f"/{VTAG}/conversations/{{conversation_id}}/messages/{{message_id}}",
266+
name="delete_conversation_message",
267+
)
268+
@login_required
269+
async def delete_conversation_message(request: web.Request):
270+
"""Delete a message in a conversation"""
271+
try:
272+
req_ctx = AuthenticatedRequestContext.model_validate(request)
273+
path_params = parse_request_path_parameters_as(
274+
_ConversationMessagePathParams, request
275+
)
276+
277+
user_primary_gid = await users_service.get_user_primary_group_id(
278+
request.app, user_id=req_ctx.user_id
279+
)
280+
conversation = await _conversation_service.get_conversation_for_user(
281+
app=request.app,
282+
conversation_id=path_params.conversation_id,
283+
user_group_id=user_primary_gid,
284+
)
285+
if conversation.type != ConversationType.SUPPORT:
286+
raise web.HTTPBadRequest(
287+
reason="Only support conversations are allowed for this endpoint"
288+
)
289+
290+
await _conversation_message_service.delete_message(
291+
app=request.app,
292+
user_id=req_ctx.user_id,
293+
project_id=None, # Support conversations don't use project_id
294+
conversation_id=path_params.conversation_id,
295+
message_id=path_params.message_id,
296+
)
297+
298+
return web.json_response(status=status.HTTP_204_NO_CONTENT)
299+
300+
except Exception as exc:
301+
_logger.exception("Failed to delete conversation message")
302+
raise web.HTTPNotFound(reason="Message not found") from exc

0 commit comments

Comments
 (0)