|
| 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