diff --git a/openapi/chat/v1/components/parameters/ChatIdPath.yaml b/openapi/chat/v1/components/parameters/ChatIdPath.yaml new file mode 100644 index 0000000..5874a12 --- /dev/null +++ b/openapi/chat/v1/components/parameters/ChatIdPath.yaml @@ -0,0 +1,6 @@ +name: chatId +in: path +required: true +schema: + type: string + format: uuid diff --git a/openapi/chat/v1/components/responses/ProblemResponse.yaml b/openapi/chat/v1/components/responses/ProblemResponse.yaml new file mode 100644 index 0000000..0a98c5a --- /dev/null +++ b/openapi/chat/v1/components/responses/ProblemResponse.yaml @@ -0,0 +1,5 @@ +description: RFC 7807 problem response +content: + application/problem+json: + schema: + $ref: '../schemas/Problem.yaml' diff --git a/openapi/chat/v1/components/schemas/Chat.yaml b/openapi/chat/v1/components/schemas/Chat.yaml new file mode 100644 index 0000000..01a47de --- /dev/null +++ b/openapi/chat/v1/components/schemas/Chat.yaml @@ -0,0 +1,9 @@ +type: object +properties: + id: { type: string, format: uuid } + participants: + type: array + items: { $ref: './ChatParticipant.yaml' } + createdAt: { type: string, format: date-time } + updatedAt: { type: string, format: date-time } +required: [id, participants, createdAt] diff --git a/openapi/chat/v1/components/schemas/ChatCreateRequest.yaml b/openapi/chat/v1/components/schemas/ChatCreateRequest.yaml new file mode 100644 index 0000000..d5b7f96 --- /dev/null +++ b/openapi/chat/v1/components/schemas/ChatCreateRequest.yaml @@ -0,0 +1,11 @@ +type: object +description: >- + Create a new chat. The authenticated user is added as a participant + automatically. Provide the identity IDs of other participants. +properties: + participantIds: + type: array + items: { type: string, format: uuid } + minItems: 1 + description: Identity IDs of other participants. +required: [participantIds] diff --git a/openapi/chat/v1/components/schemas/ChatMessage.yaml b/openapi/chat/v1/components/schemas/ChatMessage.yaml new file mode 100644 index 0000000..00fa5b1 --- /dev/null +++ b/openapi/chat/v1/components/schemas/ChatMessage.yaml @@ -0,0 +1,11 @@ +type: object +properties: + id: { type: string, format: uuid } + chatId: { type: string, format: uuid } + senderId: { type: string, format: uuid } + body: { type: string } + fileIds: + type: array + items: { type: string, format: uuid } + createdAt: { type: string, format: date-time } +required: [id, chatId, senderId, body, fileIds, createdAt] diff --git a/openapi/chat/v1/components/schemas/ChatMessageCreateRequest.yaml b/openapi/chat/v1/components/schemas/ChatMessageCreateRequest.yaml new file mode 100644 index 0000000..c23e257 --- /dev/null +++ b/openapi/chat/v1/components/schemas/ChatMessageCreateRequest.yaml @@ -0,0 +1,12 @@ +type: object +description: >- + Send a message in a chat. The sender is the authenticated user. + At least one of body or fileIds must be provided. +properties: + body: + type: string + description: Text content. May be empty when fileIds is non-empty. + fileIds: + type: array + items: { type: string, format: uuid } + description: File references (UUIDs). May be empty when body is non-empty. diff --git a/openapi/chat/v1/components/schemas/ChatParticipant.yaml b/openapi/chat/v1/components/schemas/ChatParticipant.yaml new file mode 100644 index 0000000..05b2c5f --- /dev/null +++ b/openapi/chat/v1/components/schemas/ChatParticipant.yaml @@ -0,0 +1,5 @@ +type: object +properties: + id: { type: string, format: uuid } + joinedAt: { type: string, format: date-time } +required: [id, joinedAt] diff --git a/openapi/chat/v1/components/schemas/MarkAsReadRequest.yaml b/openapi/chat/v1/components/schemas/MarkAsReadRequest.yaml new file mode 100644 index 0000000..3d3ed4b --- /dev/null +++ b/openapi/chat/v1/components/schemas/MarkAsReadRequest.yaml @@ -0,0 +1,9 @@ +type: object +description: Mark messages as read for the authenticated user. +properties: + messageIds: + type: array + items: { type: string, format: uuid } + minItems: 1 + description: Message UUIDs to acknowledge as read. Must belong to the specified chat. +required: [messageIds] diff --git a/openapi/chat/v1/components/schemas/MarkAsReadResponse.yaml b/openapi/chat/v1/components/schemas/MarkAsReadResponse.yaml new file mode 100644 index 0000000..d419f42 --- /dev/null +++ b/openapi/chat/v1/components/schemas/MarkAsReadResponse.yaml @@ -0,0 +1,9 @@ +type: object +properties: + readCount: + type: integer + minimum: 0 + description: >- + Number of messages newly marked as read. Already-read and unknown + IDs are silently ignored for idempotency. +required: [readCount] diff --git a/openapi/chat/v1/components/schemas/PaginatedChatMessages.yaml b/openapi/chat/v1/components/schemas/PaginatedChatMessages.yaml new file mode 100644 index 0000000..b2d01b2 --- /dev/null +++ b/openapi/chat/v1/components/schemas/PaginatedChatMessages.yaml @@ -0,0 +1,13 @@ +type: object +properties: + items: + type: array + items: { $ref: './ChatMessage.yaml' } + nextPageToken: + type: string + description: Opaque cursor for the next page. Absent when there are no more results. + unreadCount: + type: integer + minimum: 0 + description: Number of unread messages for the authenticated user in this chat. +required: [items, unreadCount] diff --git a/openapi/chat/v1/components/schemas/PaginatedChats.yaml b/openapi/chat/v1/components/schemas/PaginatedChats.yaml new file mode 100644 index 0000000..fa08f3e --- /dev/null +++ b/openapi/chat/v1/components/schemas/PaginatedChats.yaml @@ -0,0 +1,9 @@ +type: object +properties: + items: + type: array + items: { $ref: './Chat.yaml' } + nextPageToken: + type: string + description: Opaque cursor for the next page. Absent when there are no more results. +required: [items] diff --git a/openapi/chat/v1/components/schemas/Problem.yaml b/openapi/chat/v1/components/schemas/Problem.yaml new file mode 100644 index 0000000..ef475b1 --- /dev/null +++ b/openapi/chat/v1/components/schemas/Problem.yaml @@ -0,0 +1,8 @@ +type: object +properties: + type: { type: string, format: uri } + title: { type: string } + status: { type: integer } + detail: { type: string } + instance: { type: string, format: uri } +required: [title, status] diff --git a/openapi/chat/v1/openapi.yaml b/openapi/chat/v1/openapi.yaml new file mode 100644 index 0000000..8e5e708 --- /dev/null +++ b/openapi/chat/v1/openapi.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.3 +info: + title: Chat API + version: 0.1.0 +servers: + - url: https://api.example.com +tags: + - name: Chats + - name: Messages +paths: + /chats: + $ref: './paths/chats.yaml' + /chats/{chatId}/messages: + $ref: './paths/chat-messages.yaml' + /chats/{chatId}/read: + $ref: './paths/chat-read.yaml' +components: + parameters: + ChatIdPath: + $ref: './components/parameters/ChatIdPath.yaml' + responses: + ProblemResponse: + $ref: './components/responses/ProblemResponse.yaml' + schemas: + Problem: + $ref: './components/schemas/Problem.yaml' + Chat: + $ref: './components/schemas/Chat.yaml' + ChatParticipant: + $ref: './components/schemas/ChatParticipant.yaml' + ChatCreateRequest: + $ref: './components/schemas/ChatCreateRequest.yaml' + PaginatedChats: + $ref: './components/schemas/PaginatedChats.yaml' + ChatMessage: + $ref: './components/schemas/ChatMessage.yaml' + ChatMessageCreateRequest: + $ref: './components/schemas/ChatMessageCreateRequest.yaml' + PaginatedChatMessages: + $ref: './components/schemas/PaginatedChatMessages.yaml' + MarkAsReadRequest: + $ref: './components/schemas/MarkAsReadRequest.yaml' + MarkAsReadResponse: + $ref: './components/schemas/MarkAsReadResponse.yaml' diff --git a/openapi/chat/v1/paths/chat-messages.yaml b/openapi/chat/v1/paths/chat-messages.yaml new file mode 100644 index 0000000..d8153e7 --- /dev/null +++ b/openapi/chat/v1/paths/chat-messages.yaml @@ -0,0 +1,56 @@ +get: + operationId: getChatMessages + tags: [Messages] + summary: List messages + description: >- + List messages in a chat with cursor-based pagination. + Includes unread count for the authenticated user. + Read-only — does not change acknowledgment state. + parameters: + - $ref: '../components/parameters/ChatIdPath.yaml' + - in: query + name: pageSize + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Maximum number of messages to return. + - in: query + name: pageToken + schema: + type: string + description: Opaque cursor from a previous response's nextPageToken. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../components/schemas/PaginatedChatMessages.yaml' + default: + $ref: '../components/responses/ProblemResponse.yaml' +post: + operationId: sendChatMessage + tags: [Messages] + summary: Send message + description: >- + Send a message in a chat. The sender is the authenticated user. + At least one of body or fileIds must be provided. + parameters: + - $ref: '../components/parameters/ChatIdPath.yaml' + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/ChatMessageCreateRequest.yaml' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '../components/schemas/ChatMessage.yaml' + default: + $ref: '../components/responses/ProblemResponse.yaml' diff --git a/openapi/chat/v1/paths/chat-read.yaml b/openapi/chat/v1/paths/chat-read.yaml new file mode 100644 index 0000000..fe1a108 --- /dev/null +++ b/openapi/chat/v1/paths/chat-read.yaml @@ -0,0 +1,25 @@ +post: + operationId: markChatAsRead + tags: [Messages] + summary: Mark messages as read + description: >- + Mark messages as read for the authenticated user. Delegates to + Threads AckMessages. Already-read and unknown IDs are silently + ignored for idempotency. + parameters: + - $ref: '../components/parameters/ChatIdPath.yaml' + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/MarkAsReadRequest.yaml' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../components/schemas/MarkAsReadResponse.yaml' + default: + $ref: '../components/responses/ProblemResponse.yaml' diff --git a/openapi/chat/v1/paths/chats.yaml b/openapi/chat/v1/paths/chats.yaml new file mode 100644 index 0000000..d197a56 --- /dev/null +++ b/openapi/chat/v1/paths/chats.yaml @@ -0,0 +1,51 @@ +get: + operationId: getChats + tags: [Chats] + summary: List chats + description: >- + List chats for the authenticated user with cursor-based pagination. + parameters: + - in: query + name: pageSize + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Maximum number of chats to return. + - in: query + name: pageToken + schema: + type: string + description: Opaque cursor from a previous response's nextPageToken. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../components/schemas/PaginatedChats.yaml' + default: + $ref: '../components/responses/ProblemResponse.yaml' +post: + operationId: createChat + tags: [Chats] + summary: Create chat + description: >- + Create a new chat thread. The authenticated user is automatically + added as a participant. + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/ChatCreateRequest.yaml' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '../components/schemas/Chat.yaml' + default: + $ref: '../components/responses/ProblemResponse.yaml' diff --git a/proto/agynio/api/chat/v1/chat.proto b/proto/agynio/api/chat/v1/chat.proto new file mode 100644 index 0000000..aa98e85 --- /dev/null +++ b/proto/agynio/api/chat/v1/chat.proto @@ -0,0 +1,147 @@ +syntax = "proto3"; + +package agynio.api.chat.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/agynio/api/gen/agynio/api/chat/v1;chatv1"; + +// ChatService implements the built-in web and mobile app chat experience +// on top of Threads. It manages chat creation, message retrieval with +// unread counts, sending messages, and read-receipt tracking. +// +// Identity is resolved from gRPC metadata (identity_id, identity_type, +// tenant_id). Requests do not carry explicit user identifiers — the +// server extracts the authenticated identity from context. +service ChatService { + // Create a new chat thread between participants. + // The authenticated identity is automatically added as a participant. + // Delegates to Threads.CreateThread. + rpc CreateChat(CreateChatRequest) returns (CreateChatResponse); + + // List chats for the authenticated user with pagination. + // Delegates to Threads.GetThreads using the authenticated identity_id. + rpc GetChats(GetChatsRequest) returns (GetChatsResponse); + + // List messages in a chat with pagination. + // Returns an unread_count derived from Threads.GetUnackedMessages. + // Read-only — does not change acknowledgment state. + rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); + + // Send a message in a chat. + // The sender_id is the authenticated identity_id (set by the server). + // Delegates to Threads.SendMessage. + rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); + + // Mark messages as read for the authenticated user. + // Delegates to Threads.AckMessages using the authenticated identity_id. + rpc MarkAsRead(MarkAsReadRequest) returns (MarkAsReadResponse); +} + +// =========================================================================== +// Entities +// =========================================================================== + +// A chat thread between participants. +message Chat { + string id = 1; // UUID — maps to Threads thread_id + repeated ChatParticipant participants = 2; + google.protobuf.Timestamp created_at = 3; + google.protobuf.Timestamp updated_at = 4; +} + +// A participant in a chat. +message ChatParticipant { + string id = 1; // UUID — identity_id of the participant + google.protobuf.Timestamp joined_at = 2; +} + +// A message within a chat. +message ChatMessage { + string id = 1; // UUID + string chat_id = 2; // UUID — parent chat + string sender_id = 3; // UUID — identity_id of the sender + string body = 4; // text content + repeated string file_ids = 5; // referenced file UUIDs (may be empty) + google.protobuf.Timestamp created_at = 6; +} + +// =========================================================================== +// CreateChat +// =========================================================================== + +message CreateChatRequest { + // Identity IDs of other participants. The authenticated user is added + // automatically by the server. At least one participant is required. + repeated string participant_ids = 1; +} + +message CreateChatResponse { + Chat chat = 1; +} + +// =========================================================================== +// GetChats +// =========================================================================== + +message GetChatsRequest { + int32 page_size = 1; + string page_token = 2; +} + +message GetChatsResponse { + repeated Chat chats = 1; + string next_page_token = 2; +} + +// =========================================================================== +// GetMessages +// =========================================================================== + +message GetMessagesRequest { + string chat_id = 1; // UUID + int32 page_size = 2; + string page_token = 3; +} + +message GetMessagesResponse { + repeated ChatMessage messages = 1; + string next_page_token = 2; + // Number of unacknowledged messages for the authenticated user in this + // chat. Derived from Threads.GetUnackedMessages. + int32 unread_count = 3; +} + +// =========================================================================== +// SendMessage +// =========================================================================== + +message SendMessageRequest { + string chat_id = 1; // UUID + // Text content. May be empty when file_ids is non-empty. + // At least one of body or file_ids must be provided. + string body = 2; + // File references (UUIDs). May be empty when body is non-empty. + // At least one of body or file_ids must be provided. + repeated string file_ids = 3; +} + +message SendMessageResponse { + ChatMessage message = 1; +} + +// =========================================================================== +// MarkAsRead +// =========================================================================== + +message MarkAsReadRequest { + string chat_id = 1; // UUID + // Message UUIDs to acknowledge as read. Must belong to the specified chat. + repeated string message_ids = 2; +} + +message MarkAsReadResponse { + // Number of messages newly marked as read. Already-read and unknown + // IDs are silently ignored for idempotency. + int32 read_count = 1; +}