diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index fa14eb412..f2f8e8790 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -200,7 +200,7 @@ const docTemplate = `{ "200": { "description": "List of spaces", "schema": { - "$ref": "#/definitions/api.PaginatedResponse-api_Space" + "$ref": "#/definitions/pagination.PaginatedResponse-space_Space" } }, "403": { @@ -249,7 +249,7 @@ const docTemplate = `{ "200": { "description": "Space created successfully", "schema": { - "$ref": "#/definitions/api.CreateSpaceResponse" + "$ref": "#/definitions/space.CreateSpaceResponse" } }, "403": { @@ -305,7 +305,7 @@ const docTemplate = `{ "200": { "description": "List of members", "schema": { - "$ref": "#/definitions/api.PaginatedResponse-api_Member" + "$ref": "#/definitions/pagination.PaginatedResponse-space_Member" } }, "403": { @@ -708,286 +708,9 @@ const docTemplate = `{ } } } - }, - "/v1/spaces/{space_id}/chat/messages": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Retrieve last chat messages", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "The number of items to skip before starting to collect the result set", - "name": "offset", - "in": "query" - }, - { - "type": "integer", - "default": 100, - "description": "The number of items to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of chat messages", - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/api.ChatMessage" - } - } - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Add a new chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "description": "Chat message", - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - } - ], - "responses": { - "201": { - "description": "Created chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "400": { - "description": "Invalid input", - "schema": { - "$ref": "#/definitions/api.ValidationError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - } - }, - "/v1/spaces/{space_id}/chat/messages/{message_id}": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Retrieve a specific chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Update an existing chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - }, - { - "description": "Chat message", - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - } - ], - "responses": { - "200": { - "description": "Updated chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "400": { - "description": "Invalid input", - "schema": { - "$ref": "#/definitions/api.ValidationError" - } - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Delete a chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "Message deleted successfully" - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - } } }, "definitions": { - "api.Attachment": { - "type": "object", - "properties": { - "target": { - "description": "Identifier for the attachment object", - "type": "string" - }, - "type": { - "description": "Type of attachment", - "type": "string" - } - } - }, "api.AuthDisplayCodeResponse": { "type": "object", "properties": { @@ -1039,72 +762,6 @@ const docTemplate = `{ } } }, - "api.ChatMessage": { - "type": "object", - "properties": { - "attachments": { - "description": "Attachments slice", - "type": "array", - "items": { - "$ref": "#/definitions/api.Attachment" - } - }, - "chat_message": { - "type": "string" - }, - "created_at": { - "type": "integer" - }, - "creator": { - "description": "Identifier for the message creator", - "type": "string" - }, - "id": { - "description": "Unique message identifier", - "type": "string" - }, - "message": { - "description": "Message content", - "allOf": [ - { - "$ref": "#/definitions/api.MessageContent" - } - ] - }, - "modified_at": { - "type": "integer" - }, - "order_id": { - "description": "Used for subscriptions", - "type": "string" - }, - "reactions": { - "description": "Reactions to the message", - "allOf": [ - { - "$ref": "#/definitions/api.Reactions" - } - ] - }, - "reply_to_message_id": { - "description": "Identifier for the message being replied to", - "type": "string" - } - } - }, - "api.CreateSpaceResponse": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "Space Name" - }, - "space_id": { - "type": "string", - "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" - } - } - }, "api.Detail": { "type": "object", "properties": { @@ -1149,71 +806,6 @@ const docTemplate = `{ } } }, - "api.IdentityList": { - "type": "object", - "properties": { - "ids": { - "description": "List of user IDs", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "api.Member": { - "type": "object", - "properties": { - "global_name": { - "type": "string", - "example": "john.any" - }, - "icon": { - "type": "string", - "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" - }, - "id": { - "type": "string", - "example": "_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ" - }, - "identity": { - "type": "string", - "example": "AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ" - }, - "name": { - "type": "string", - "example": "John Doe" - }, - "role": { - "type": "string", - "example": "Owner" - }, - "type": { - "type": "string", - "example": "member" - } - } - }, - "api.MessageContent": { - "type": "object", - "properties": { - "marks": { - "description": "List of marks applied to the text", - "type": "array", - "items": { - "type": "string" - } - }, - "style": { - "description": "The style/type of the message part", - "type": "string" - }, - "text": { - "description": "The text content of the message part", - "type": "string" - } - } - }, "api.NotFoundError": { "type": "object", "properties": { @@ -1317,71 +909,6 @@ const docTemplate = `{ } } }, - "api.PaginatedResponse-api_Member": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api.Member" - } - }, - "pagination": { - "$ref": "#/definitions/api.PaginationMeta" - } - } - }, - "api.PaginatedResponse-api_Space": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api.Space" - } - }, - "pagination": { - "$ref": "#/definitions/api.PaginationMeta" - } - } - }, - "api.PaginationMeta": { - "type": "object", - "properties": { - "has_more": { - "description": "whether there are more items available", - "type": "boolean", - "example": true - }, - "limit": { - "description": "the current limit", - "type": "integer", - "example": 100 - }, - "offset": { - "description": "the current offset", - "type": "integer", - "example": 0 - }, - "total": { - "description": "the total number of items available on that endpoint", - "type": "integer", - "example": 1024 - } - } - }, - "api.Reactions": { - "type": "object", - "properties": { - "reactions": { - "description": "Map of emoji to list of user IDs", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/api.IdentityList" - } - } - } - }, "api.ServerError": { "type": "object", "properties": { @@ -1395,75 +922,6 @@ const docTemplate = `{ } } }, - "api.Space": { - "type": "object", - "properties": { - "account_space_id": { - "type": "string", - "example": "bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1" - }, - "archive_object_id": { - "type": "string", - "example": "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri" - }, - "device_id": { - "type": "string", - "example": "12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF" - }, - "home_object_id": { - "type": "string", - "example": "bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya" - }, - "icon": { - "type": "string", - "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" - }, - "id": { - "type": "string", - "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" - }, - "marketplace_workspace_id": { - "type": "string", - "example": "_anytype_marketplace" - }, - "name": { - "type": "string", - "example": "Space Name" - }, - "network_id": { - "type": "string", - "example": "N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU" - }, - "profile_object_id": { - "type": "string", - "example": "bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4" - }, - "space_view_id": { - "type": "string", - "example": "bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy" - }, - "tech_space_id": { - "type": "string", - "example": "bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1" - }, - "timezone": { - "type": "string", - "example": "" - }, - "type": { - "type": "string", - "example": "space" - }, - "widgets_id": { - "type": "string", - "example": "bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva" - }, - "workspace_object_id": { - "type": "string", - "example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y" - } - } - }, "api.Text": { "type": "object", "properties": { @@ -1509,6 +967,181 @@ const docTemplate = `{ } } } + }, + "pagination.PaginatedResponse-space_Member": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/space.Member" + } + }, + "pagination": { + "$ref": "#/definitions/pagination.PaginationMeta" + } + } + }, + "pagination.PaginatedResponse-space_Space": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/space.Space" + } + }, + "pagination": { + "$ref": "#/definitions/pagination.PaginationMeta" + } + } + }, + "pagination.PaginationMeta": { + "type": "object", + "properties": { + "has_more": { + "description": "whether there are more items available", + "type": "boolean", + "example": true + }, + "limit": { + "description": "the current limit", + "type": "integer", + "example": 100 + }, + "offset": { + "description": "the current offset", + "type": "integer", + "example": 0 + }, + "total": { + "description": "the total number of items available on that endpoint", + "type": "integer", + "example": 1024 + } + } + }, + "space.CreateSpaceResponse": { + "type": "object", + "properties": { + "space": { + "$ref": "#/definitions/space.Space" + } + } + }, + "space.Member": { + "type": "object", + "properties": { + "global_name": { + "type": "string", + "example": "john.any" + }, + "icon": { + "type": "string", + "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" + }, + "id": { + "type": "string", + "example": "_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ" + }, + "identity": { + "type": "string", + "example": "AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "role": { + "type": "string", + "example": "Owner" + }, + "type": { + "type": "string", + "example": "member" + } + } + }, + "space.Space": { + "type": "object", + "properties": { + "account_space_id": { + "type": "string", + "example": "bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1" + }, + "analytics_id": { + "type": "string", + "example": "" + }, + "archive_object_id": { + "type": "string", + "example": "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri" + }, + "device_id": { + "type": "string", + "example": "12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF" + }, + "gateway_url": { + "type": "string", + "example": "" + }, + "home_object_id": { + "type": "string", + "example": "bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya" + }, + "icon": { + "type": "string", + "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" + }, + "id": { + "type": "string", + "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" + }, + "local_storage_path": { + "type": "string", + "example": "" + }, + "marketplace_workspace_id": { + "type": "string", + "example": "_anytype_marketplace" + }, + "name": { + "type": "string", + "example": "Space Name" + }, + "network_id": { + "type": "string", + "example": "N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU" + }, + "profile_object_id": { + "type": "string", + "example": "bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4" + }, + "space_view_id": { + "type": "string", + "example": "bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy" + }, + "tech_space_id": { + "type": "string", + "example": "bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1" + }, + "timezone": { + "type": "string", + "example": "" + }, + "type": { + "type": "string", + "example": "space" + }, + "widgets_id": { + "type": "string", + "example": "bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva" + }, + "workspace_object_id": { + "type": "string", + "example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y" + } + } } }, "securityDefinitions": { diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 1cddd64a3..8285da535 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -194,7 +194,7 @@ "200": { "description": "List of spaces", "schema": { - "$ref": "#/definitions/api.PaginatedResponse-api_Space" + "$ref": "#/definitions/pagination.PaginatedResponse-space_Space" } }, "403": { @@ -243,7 +243,7 @@ "200": { "description": "Space created successfully", "schema": { - "$ref": "#/definitions/api.CreateSpaceResponse" + "$ref": "#/definitions/space.CreateSpaceResponse" } }, "403": { @@ -299,7 +299,7 @@ "200": { "description": "List of members", "schema": { - "$ref": "#/definitions/api.PaginatedResponse-api_Member" + "$ref": "#/definitions/pagination.PaginatedResponse-space_Member" } }, "403": { @@ -702,286 +702,9 @@ } } } - }, - "/v1/spaces/{space_id}/chat/messages": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Retrieve last chat messages", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "The number of items to skip before starting to collect the result set", - "name": "offset", - "in": "query" - }, - { - "type": "integer", - "default": 100, - "description": "The number of items to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of chat messages", - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/api.ChatMessage" - } - } - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Add a new chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "description": "Chat message", - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - } - ], - "responses": { - "201": { - "description": "Created chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "400": { - "description": "Invalid input", - "schema": { - "$ref": "#/definitions/api.ValidationError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - } - }, - "/v1/spaces/{space_id}/chat/messages/{message_id}": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Retrieve a specific chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Update an existing chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - }, - { - "description": "Chat message", - "name": "message", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - } - ], - "responses": { - "200": { - "description": "Updated chat message", - "schema": { - "$ref": "#/definitions/api.ChatMessage" - } - }, - "400": { - "description": "Invalid input", - "schema": { - "$ref": "#/definitions/api.ValidationError" - } - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - }, - "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "chat" - ], - "summary": "Delete a chat message", - "parameters": [ - { - "type": "string", - "description": "The ID of the space", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "message_id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "Message deleted successfully" - }, - "404": { - "description": "Message not found", - "schema": { - "$ref": "#/definitions/api.NotFoundError" - } - }, - "502": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api.ServerError" - } - } - } - } } }, "definitions": { - "api.Attachment": { - "type": "object", - "properties": { - "target": { - "description": "Identifier for the attachment object", - "type": "string" - }, - "type": { - "description": "Type of attachment", - "type": "string" - } - } - }, "api.AuthDisplayCodeResponse": { "type": "object", "properties": { @@ -1033,72 +756,6 @@ } } }, - "api.ChatMessage": { - "type": "object", - "properties": { - "attachments": { - "description": "Attachments slice", - "type": "array", - "items": { - "$ref": "#/definitions/api.Attachment" - } - }, - "chat_message": { - "type": "string" - }, - "created_at": { - "type": "integer" - }, - "creator": { - "description": "Identifier for the message creator", - "type": "string" - }, - "id": { - "description": "Unique message identifier", - "type": "string" - }, - "message": { - "description": "Message content", - "allOf": [ - { - "$ref": "#/definitions/api.MessageContent" - } - ] - }, - "modified_at": { - "type": "integer" - }, - "order_id": { - "description": "Used for subscriptions", - "type": "string" - }, - "reactions": { - "description": "Reactions to the message", - "allOf": [ - { - "$ref": "#/definitions/api.Reactions" - } - ] - }, - "reply_to_message_id": { - "description": "Identifier for the message being replied to", - "type": "string" - } - } - }, - "api.CreateSpaceResponse": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "Space Name" - }, - "space_id": { - "type": "string", - "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" - } - } - }, "api.Detail": { "type": "object", "properties": { @@ -1143,71 +800,6 @@ } } }, - "api.IdentityList": { - "type": "object", - "properties": { - "ids": { - "description": "List of user IDs", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "api.Member": { - "type": "object", - "properties": { - "global_name": { - "type": "string", - "example": "john.any" - }, - "icon": { - "type": "string", - "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" - }, - "id": { - "type": "string", - "example": "_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ" - }, - "identity": { - "type": "string", - "example": "AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ" - }, - "name": { - "type": "string", - "example": "John Doe" - }, - "role": { - "type": "string", - "example": "Owner" - }, - "type": { - "type": "string", - "example": "member" - } - } - }, - "api.MessageContent": { - "type": "object", - "properties": { - "marks": { - "description": "List of marks applied to the text", - "type": "array", - "items": { - "type": "string" - } - }, - "style": { - "description": "The style/type of the message part", - "type": "string" - }, - "text": { - "description": "The text content of the message part", - "type": "string" - } - } - }, "api.NotFoundError": { "type": "object", "properties": { @@ -1311,71 +903,6 @@ } } }, - "api.PaginatedResponse-api_Member": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api.Member" - } - }, - "pagination": { - "$ref": "#/definitions/api.PaginationMeta" - } - } - }, - "api.PaginatedResponse-api_Space": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api.Space" - } - }, - "pagination": { - "$ref": "#/definitions/api.PaginationMeta" - } - } - }, - "api.PaginationMeta": { - "type": "object", - "properties": { - "has_more": { - "description": "whether there are more items available", - "type": "boolean", - "example": true - }, - "limit": { - "description": "the current limit", - "type": "integer", - "example": 100 - }, - "offset": { - "description": "the current offset", - "type": "integer", - "example": 0 - }, - "total": { - "description": "the total number of items available on that endpoint", - "type": "integer", - "example": 1024 - } - } - }, - "api.Reactions": { - "type": "object", - "properties": { - "reactions": { - "description": "Map of emoji to list of user IDs", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/api.IdentityList" - } - } - } - }, "api.ServerError": { "type": "object", "properties": { @@ -1389,75 +916,6 @@ } } }, - "api.Space": { - "type": "object", - "properties": { - "account_space_id": { - "type": "string", - "example": "bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1" - }, - "archive_object_id": { - "type": "string", - "example": "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri" - }, - "device_id": { - "type": "string", - "example": "12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF" - }, - "home_object_id": { - "type": "string", - "example": "bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya" - }, - "icon": { - "type": "string", - "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" - }, - "id": { - "type": "string", - "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" - }, - "marketplace_workspace_id": { - "type": "string", - "example": "_anytype_marketplace" - }, - "name": { - "type": "string", - "example": "Space Name" - }, - "network_id": { - "type": "string", - "example": "N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU" - }, - "profile_object_id": { - "type": "string", - "example": "bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4" - }, - "space_view_id": { - "type": "string", - "example": "bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy" - }, - "tech_space_id": { - "type": "string", - "example": "bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1" - }, - "timezone": { - "type": "string", - "example": "" - }, - "type": { - "type": "string", - "example": "space" - }, - "widgets_id": { - "type": "string", - "example": "bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva" - }, - "workspace_object_id": { - "type": "string", - "example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y" - } - } - }, "api.Text": { "type": "object", "properties": { @@ -1503,6 +961,181 @@ } } } + }, + "pagination.PaginatedResponse-space_Member": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/space.Member" + } + }, + "pagination": { + "$ref": "#/definitions/pagination.PaginationMeta" + } + } + }, + "pagination.PaginatedResponse-space_Space": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/space.Space" + } + }, + "pagination": { + "$ref": "#/definitions/pagination.PaginationMeta" + } + } + }, + "pagination.PaginationMeta": { + "type": "object", + "properties": { + "has_more": { + "description": "whether there are more items available", + "type": "boolean", + "example": true + }, + "limit": { + "description": "the current limit", + "type": "integer", + "example": 100 + }, + "offset": { + "description": "the current offset", + "type": "integer", + "example": 0 + }, + "total": { + "description": "the total number of items available on that endpoint", + "type": "integer", + "example": 1024 + } + } + }, + "space.CreateSpaceResponse": { + "type": "object", + "properties": { + "space": { + "$ref": "#/definitions/space.Space" + } + } + }, + "space.Member": { + "type": "object", + "properties": { + "global_name": { + "type": "string", + "example": "john.any" + }, + "icon": { + "type": "string", + "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" + }, + "id": { + "type": "string", + "example": "_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ" + }, + "identity": { + "type": "string", + "example": "AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "role": { + "type": "string", + "example": "Owner" + }, + "type": { + "type": "string", + "example": "member" + } + } + }, + "space.Space": { + "type": "object", + "properties": { + "account_space_id": { + "type": "string", + "example": "bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1" + }, + "analytics_id": { + "type": "string", + "example": "" + }, + "archive_object_id": { + "type": "string", + "example": "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri" + }, + "device_id": { + "type": "string", + "example": "12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF" + }, + "gateway_url": { + "type": "string", + "example": "" + }, + "home_object_id": { + "type": "string", + "example": "bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya" + }, + "icon": { + "type": "string", + "example": "http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100" + }, + "id": { + "type": "string", + "example": "bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1" + }, + "local_storage_path": { + "type": "string", + "example": "" + }, + "marketplace_workspace_id": { + "type": "string", + "example": "_anytype_marketplace" + }, + "name": { + "type": "string", + "example": "Space Name" + }, + "network_id": { + "type": "string", + "example": "N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU" + }, + "profile_object_id": { + "type": "string", + "example": "bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4" + }, + "space_view_id": { + "type": "string", + "example": "bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy" + }, + "tech_space_id": { + "type": "string", + "example": "bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1" + }, + "timezone": { + "type": "string", + "example": "" + }, + "type": { + "type": "string", + "example": "space" + }, + "widgets_id": { + "type": "string", + "example": "bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva" + }, + "workspace_object_id": { + "type": "string", + "example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y" + } + } } }, "securityDefinitions": { diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 08959f27e..a9f287cae 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -1,14 +1,5 @@ basePath: /v1 definitions: - api.Attachment: - properties: - target: - description: Identifier for the attachment object - type: string - type: - description: Type of attachment - type: string - type: object api.AuthDisplayCodeResponse: properties: challenge_id: @@ -43,49 +34,6 @@ definitions: vertical_align: type: string type: object - api.ChatMessage: - properties: - attachments: - description: Attachments slice - items: - $ref: '#/definitions/api.Attachment' - type: array - chat_message: - type: string - created_at: - type: integer - creator: - description: Identifier for the message creator - type: string - id: - description: Unique message identifier - type: string - message: - allOf: - - $ref: '#/definitions/api.MessageContent' - description: Message content - modified_at: - type: integer - order_id: - description: Used for subscriptions - type: string - reactions: - allOf: - - $ref: '#/definitions/api.Reactions' - description: Reactions to the message - reply_to_message_id: - description: Identifier for the message being replied to - type: string - type: object - api.CreateSpaceResponse: - properties: - name: - example: Space Name - type: string - space_id: - example: bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1 - type: string - type: object api.Detail: properties: details: @@ -115,52 +63,6 @@ definitions: type: type: string type: object - api.IdentityList: - properties: - ids: - description: List of user IDs - items: - type: string - type: array - type: object - api.Member: - properties: - global_name: - example: john.any - type: string - icon: - example: http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100 - type: string - id: - example: _participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ - type: string - identity: - example: AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ - type: string - name: - example: John Doe - type: string - role: - example: Owner - type: string - type: - example: member - type: string - type: object - api.MessageContent: - properties: - marks: - description: List of marks applied to the text - items: - type: string - type: array - style: - description: The style/type of the message part - type: string - text: - description: The text content of the message part - type: string - type: object api.NotFoundError: properties: error: @@ -233,25 +135,62 @@ definitions: example: ot-page type: string type: object - api.PaginatedResponse-api_Member: + api.ServerError: + properties: + error: + properties: + message: + type: string + type: object + type: object + api.Text: + properties: + checked: + type: boolean + color: + type: string + icon: + type: string + style: + type: string + text: + type: string + type: object + api.UnauthorizedError: + properties: + error: + properties: + message: + type: string + type: object + type: object + api.ValidationError: + properties: + error: + properties: + message: + type: string + type: object + type: object + pagination.PaginatedResponse-space_Member: properties: data: items: - $ref: '#/definitions/api.Member' + $ref: '#/definitions/space.Member' type: array pagination: - $ref: '#/definitions/api.PaginationMeta' + $ref: '#/definitions/pagination.PaginationMeta' type: object - api.PaginatedResponse-api_Space: + pagination.PaginatedResponse-space_Space: properties: data: items: - $ref: '#/definitions/api.Space' + $ref: '#/definitions/space.Space' type: array pagination: - $ref: '#/definitions/api.PaginationMeta' + $ref: '#/definitions/pagination.PaginationMeta' type: object - api.PaginationMeta: + pagination.PaginationMeta: properties: has_more: description: whether there are more items available @@ -270,33 +209,52 @@ definitions: example: 1024 type: integer type: object - api.Reactions: + space.CreateSpaceResponse: properties: - reactions: - additionalProperties: - $ref: '#/definitions/api.IdentityList' - description: Map of emoji to list of user IDs - type: object + space: + $ref: '#/definitions/space.Space' type: object - api.ServerError: + space.Member: properties: - error: - properties: - message: - type: string - type: object + global_name: + example: john.any + type: string + icon: + example: http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100 + type: string + id: + example: _participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ + type: string + identity: + example: AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ + type: string + name: + example: John Doe + type: string + role: + example: Owner + type: string + type: + example: member + type: string type: object - api.Space: + space.Space: properties: account_space_id: example: bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1 type: string + analytics_id: + example: "" + type: string archive_object_id: example: bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri type: string device_id: example: 12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF type: string + gateway_url: + example: "" + type: string home_object_id: example: bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya type: string @@ -306,6 +264,9 @@ definitions: id: example: bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1 type: string + local_storage_path: + example: "" + type: string marketplace_workspace_id: example: _anytype_marketplace type: string @@ -337,35 +298,6 @@ definitions: example: bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y type: string type: object - api.Text: - properties: - checked: - type: boolean - color: - type: string - icon: - type: string - style: - type: string - text: - type: string - type: object - api.UnauthorizedError: - properties: - error: - properties: - message: - type: string - type: object - type: object - api.ValidationError: - properties: - error: - properties: - message: - type: string - type: object - type: object externalDocs: description: OpenAPI url: https://swagger.io/resources/open-api/ @@ -501,7 +433,7 @@ paths: "200": description: List of spaces schema: - $ref: '#/definitions/api.PaginatedResponse-api_Space' + $ref: '#/definitions/pagination.PaginatedResponse-space_Space' "403": description: Unauthorized schema: @@ -533,7 +465,7 @@ paths: "200": description: Space created successfully schema: - $ref: '#/definitions/api.CreateSpaceResponse' + $ref: '#/definitions/space.CreateSpaceResponse' "403": description: Unauthorized schema: @@ -571,7 +503,7 @@ paths: "200": description: List of members schema: - $ref: '#/definitions/api.PaginatedResponse-api_Member' + $ref: '#/definitions/pagination.PaginatedResponse-space_Member' "403": description: Unauthorized schema: @@ -842,182 +774,6 @@ paths: summary: Update an existing object in a specific space tags: - space_objects - /v1/spaces/{space_id}/chat/messages: - get: - consumes: - - application/json - parameters: - - description: The ID of the space - in: path - name: space_id - required: true - type: string - - description: The number of items to skip before starting to collect the result - set - in: query - name: offset - type: integer - - default: 100 - description: The number of items to return - in: query - name: limit - type: integer - produces: - - application/json - responses: - "200": - description: List of chat messages - schema: - additionalProperties: - items: - $ref: '#/definitions/api.ChatMessage' - type: array - type: object - "502": - description: Internal server error - schema: - $ref: '#/definitions/api.ServerError' - summary: Retrieve last chat messages - tags: - - chat - post: - consumes: - - application/json - parameters: - - description: The ID of the space - in: path - name: space_id - required: true - type: string - - description: Chat message - in: body - name: message - required: true - schema: - $ref: '#/definitions/api.ChatMessage' - produces: - - application/json - responses: - "201": - description: Created chat message - schema: - $ref: '#/definitions/api.ChatMessage' - "400": - description: Invalid input - schema: - $ref: '#/definitions/api.ValidationError' - "502": - description: Internal server error - schema: - $ref: '#/definitions/api.ServerError' - summary: Add a new chat message - tags: - - chat - /v1/spaces/{space_id}/chat/messages/{message_id}: - delete: - consumes: - - application/json - parameters: - - description: The ID of the space - in: path - name: space_id - required: true - type: string - - description: Message ID - in: path - name: message_id - required: true - type: string - produces: - - application/json - responses: - "204": - description: Message deleted successfully - "404": - description: Message not found - schema: - $ref: '#/definitions/api.NotFoundError' - "502": - description: Internal server error - schema: - $ref: '#/definitions/api.ServerError' - summary: Delete a chat message - tags: - - chat - get: - consumes: - - application/json - parameters: - - description: The ID of the space - in: path - name: space_id - required: true - type: string - - description: Message ID - in: path - name: message_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Chat message - schema: - $ref: '#/definitions/api.ChatMessage' - "404": - description: Message not found - schema: - $ref: '#/definitions/api.NotFoundError' - "502": - description: Internal server error - schema: - $ref: '#/definitions/api.ServerError' - summary: Retrieve a specific chat message - tags: - - chat - put: - consumes: - - application/json - parameters: - - description: The ID of the space - in: path - name: space_id - required: true - type: string - - description: Message ID - in: path - name: message_id - required: true - type: string - - description: Chat message - in: body - name: message - required: true - schema: - $ref: '#/definitions/api.ChatMessage' - produces: - - application/json - responses: - "200": - description: Updated chat message - schema: - $ref: '#/definitions/api.ChatMessage' - "400": - description: Invalid input - schema: - $ref: '#/definitions/api.ValidationError' - "404": - description: Message not found - schema: - $ref: '#/definitions/api.NotFoundError' - "502": - description: Internal server error - schema: - $ref: '#/definitions/api.ServerError' - summary: Update an existing chat message - tags: - - chat securityDefinitions: BasicAuth: type: basic diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 3ca7dc70c..a5e132519 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -1,24 +1,20 @@ package api import ( - "crypto/rand" - "math/big" "net/http" "sort" "github.com/gin-gonic/gin" "github.com/gogo/protobuf/types" + "github.com/anyproto/anytype-heart/cmd/api/pagination" + "github.com/anyproto/anytype-heart/cmd/api/utils" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pkg/lib/bundle" "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/util/pbtypes" ) -type CreateSpaceRequest struct { - Name string `json:"name"` -} - type CreateObjectRequest struct { Name string `json:"name"` Icon string `json:"icon"` @@ -86,205 +82,6 @@ func (a *ApiServer) authTokenHandler(c *gin.Context) { }) } -// getSpacesHandler retrieves a list of spaces -// -// @Summary Retrieve a list of spaces -// @Tags spaces -// @Accept json -// @Produce json -// @Param offset query int false "The number of items to skip before starting to collect the result set" -// @Param limit query int false "The number of items to return" default(100) -// @Success 200 {object} PaginatedResponse[Space] "List of spaces" -// @Failure 403 {object} UnauthorizedError "Unauthorized" -// @Failure 404 {object} NotFoundError "Resource not found" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /spaces [get] -func (a *ApiServer) getSpacesHandler(c *gin.Context) { - offset := c.GetInt("offset") - limit := c.GetInt("limit") - - // Call ObjectSearch for all objects of type spaceView - resp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{ - SpaceId: a.accountInfo.TechSpaceId, - Filters: []*model.BlockContentDataviewFilter{ - { - RelationKey: bundle.RelationKeyLayout.String(), - Condition: model.BlockContentDataviewFilter_Equal, - Value: pbtypes.Int64(int64(model.ObjectType_spaceView)), - }, - { - RelationKey: bundle.RelationKeySpaceLocalStatus.String(), - Condition: model.BlockContentDataviewFilter_Equal, - Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), - }, - { - RelationKey: bundle.RelationKeySpaceRemoteStatus.String(), - Condition: model.BlockContentDataviewFilter_Equal, - Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), - }, - }, - Sorts: []*model.BlockContentDataviewSort{ - { - RelationKey: "spaceOrder", - Type: model.BlockContentDataviewSort_Asc, - NoCollate: true, - EmptyPlacement: model.BlockContentDataviewSort_End, - }, - }, - Keys: []string{"targetSpaceId", "name", "iconEmoji", "iconImage"}, - }) - - if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of spaces."}) - return - } - - if len(resp.Records) == 0 { - c.JSON(http.StatusNotFound, gin.H{"message": "No spaces found."}) - return - } - - paginatedSpaces, hasMore := paginate(resp.Records, offset, limit) - spaces := make([]Space, 0, len(paginatedSpaces)) - - for _, record := range paginatedSpaces { - workspace, statusCode, errorMessage := a.getWorkspaceInfo(record.Fields["targetSpaceId"].GetStringValue()) - if statusCode != http.StatusOK { - c.JSON(statusCode, gin.H{"message": errorMessage}) - return - } - - workspace.Name = record.Fields["name"].GetStringValue() - workspace.Icon = a.getIconFromEmojiOrImage(record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) - - spaces = append(spaces, workspace) - } - - respondWithPagination(c, http.StatusOK, spaces, len(resp.Records), offset, limit, hasMore) -} - -// createSpaceHandler creates a new space -// -// @Summary Create a new Space -// @Tags spaces -// @Accept json -// @Produce json -// @Param name body string true "Space Name" -// @Success 200 {object} CreateSpaceResponse "Space created successfully" -// @Failure 403 {object} UnauthorizedError "Unauthorized" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /spaces [post] -func (a *ApiServer) createSpaceHandler(c *gin.Context) { - nameRequest := CreateSpaceRequest{} - if err := c.BindJSON(&nameRequest); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid JSON"}) - return - } - name := nameRequest.Name - iconOption, err := rand.Int(rand.Reader, big.NewInt(13)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to generate random icon."}) - return - } - - // Create new workspace with a random icon and import default use case - resp := a.mw.WorkspaceCreate(c.Request.Context(), &pb.RpcWorkspaceCreateRequest{ - Details: &types.Struct{ - Fields: map[string]*types.Value{ - "iconOption": {Kind: &types.Value_NumberValue{NumberValue: float64(iconOption.Int64())}}, - "name": {Kind: &types.Value_StringValue{StringValue: name}}, - "spaceDashboardId": {Kind: &types.Value_StringValue{ - StringValue: "lastOpened", - }}, - }, - }, - UseCase: pb.RpcObjectImportUseCaseRequest_GET_STARTED, - WithChat: true, - }) - - if resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create a new space."}) - return - } - - c.JSON(http.StatusOK, CreateSpaceResponse{SpaceId: resp.SpaceId, Name: name}) -} - -// getMembersHandler retrieves a list of members for the specified space -// -// @Summary Retrieve a list of members for the specified Space -// @Tags spaces -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param offset query int false "The number of items to skip before starting to collect the result set" -// @Param limit query int false "The number of items to return" default(100) -// @Success 200 {object} PaginatedResponse[Member] "List of members" -// @Failure 403 {object} UnauthorizedError "Unauthorized" -// @Failure 404 {object} NotFoundError "Resource not found" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /spaces/{space_id}/members [get] -func (a *ApiServer) getMembersHandler(c *gin.Context) { - spaceId := c.Param("space_id") - offset := c.GetInt("offset") - limit := c.GetInt("limit") - - // Call ObjectSearch for all objects of type participant - resp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{ - SpaceId: spaceId, - Filters: []*model.BlockContentDataviewFilter{ - { - RelationKey: bundle.RelationKeyLayout.String(), - Condition: model.BlockContentDataviewFilter_Equal, - Value: pbtypes.Int64(int64(model.ObjectType_participant)), - }, - { - RelationKey: bundle.RelationKeyParticipantStatus.String(), - Condition: model.BlockContentDataviewFilter_Equal, - Value: pbtypes.Int64(int64(model.ParticipantStatus_Active)), - }, - }, - Sorts: []*model.BlockContentDataviewSort{ - { - RelationKey: "name", - Type: model.BlockContentDataviewSort_Asc, - }, - }, - Keys: []string{"id", "name", "iconEmoji", "iconImage", "identity", "globalName", "participantPermissions"}, - }) - - if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of members."}) - return - } - - if len(resp.Records) == 0 { - c.JSON(http.StatusNotFound, gin.H{"message": "No members found."}) - return - } - - paginatedMembers, hasMore := paginate(resp.Records, offset, limit) - members := make([]Member, 0, len(paginatedMembers)) - - for _, record := range paginatedMembers { - icon := a.getIconFromEmojiOrImage(record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) - - member := Member{ - Type: "space_member", - Id: record.Fields["id"].GetStringValue(), - Name: record.Fields["name"].GetStringValue(), - Icon: icon, - Identity: record.Fields["identity"].GetStringValue(), - GlobalName: record.Fields["globalName"].GetStringValue(), - Role: model.ParticipantPermissions_name[int32(record.Fields["participantPermissions"].GetNumberValue())], - } - - members = append(members, member) - } - - respondWithPagination(c, http.StatusOK, members, len(resp.Records), offset, limit, hasMore) -} - // getObjectsHandler retrieves objects in a specific space // // @Summary Retrieve objects in a specific space @@ -342,11 +139,11 @@ func (a *ApiServer) getObjectsHandler(c *gin.Context) { return } - paginatedObjects, hasMore := paginate(resp.Records, offset, limit) + paginatedObjects, hasMore := pagination.Paginate(resp.Records, offset, limit) objects := make([]Object, 0, len(paginatedObjects)) for _, record := range paginatedObjects { - icon := a.getIconFromEmojiOrImage(record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) + icon := utils.GetIconFromEmojiOrImage(a.accountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) objectTypeName, statusCode, errorMessage := a.resolveTypeToName(spaceId, record.Fields["type"].GetStringValue()) if statusCode != http.StatusOK { c.JSON(statusCode, gin.H{"message": errorMessage}) @@ -374,7 +171,7 @@ func (a *ApiServer) getObjectsHandler(c *gin.Context) { objects = append(objects, object) } - respondWithPagination(c, http.StatusOK, objects, len(resp.Records), offset, limit, hasMore) + pagination.RespondWithPagination(c, http.StatusOK, objects, len(resp.Records), offset, limit, hasMore) } // getObjectHandler retrieves a specific object in a space @@ -556,7 +353,7 @@ func (a *ApiServer) getObjectTypesHandler(c *gin.Context) { return } - paginatedTypes, hasMore := paginate(resp.Records, offset, limit) + paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit) objectTypes := make([]ObjectType, 0, len(paginatedTypes)) for _, record := range paginatedTypes { @@ -569,7 +366,7 @@ func (a *ApiServer) getObjectTypesHandler(c *gin.Context) { }) } - respondWithPagination(c, http.StatusOK, objectTypes, len(resp.Records), offset, limit, hasMore) + pagination.RespondWithPagination(c, http.StatusOK, objectTypes, len(resp.Records), offset, limit, hasMore) } // getObjectTypeTemplatesHandler retrieves a list of templates for a specific object type in a space @@ -649,7 +446,7 @@ func (a *ApiServer) getObjectTypeTemplatesHandler(c *gin.Context) { } // Finally, open each template and populate the response - paginatedTemplates, hasMore := paginate(templateIds, offset, limit) + paginatedTemplates, hasMore := pagination.Paginate(templateIds, offset, limit) templates := make([]ObjectTemplate, 0, len(paginatedTemplates)) for _, templateId := range paginatedTemplates { @@ -671,7 +468,7 @@ func (a *ApiServer) getObjectTypeTemplatesHandler(c *gin.Context) { }) } - respondWithPagination(c, http.StatusOK, templates, len(templateIds), offset, limit, hasMore) + pagination.RespondWithPagination(c, http.StatusOK, templates, len(templateIds), offset, limit, hasMore) } // searchHandler searches and retrieves objects across all the spaces @@ -796,7 +593,7 @@ func (a *ApiServer) searchHandler(c *gin.Context) { } for _, record := range objectResp.Records { - icon := a.getIconFromEmojiOrImage(record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) + icon := utils.GetIconFromEmojiOrImage(a.accountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) objectTypeName, statusCode, errorMessage := a.resolveTypeToName(spaceId, record.Fields["type"].GetStringValue()) if statusCode != http.StatusOK { c.JSON(statusCode, gin.H{"message": errorMessage}) @@ -828,190 +625,7 @@ func (a *ApiServer) searchHandler(c *gin.Context) { }) // TODO: solve global pagination vs per space pagination - paginatedResults, hasMore := paginate(searchResults, offset, limit) + paginatedResults, hasMore := pagination.Paginate(searchResults, offset, limit) - respondWithPagination(c, http.StatusOK, paginatedResults, len(searchResults), offset, limit, hasMore) -} - -// getChatMessagesHandler retrieves last chat messages -// -// @Summary Retrieve last chat messages -// @Tags chat -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param offset query int false "The number of items to skip before starting to collect the result set" -// @Param limit query int false "The number of items to return" default(100) -// @Success 200 {object} map[string][]ChatMessage "List of chat messages" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /v1/spaces/{space_id}/chat/messages [get] -func (a *ApiServer) getChatMessagesHandler(c *gin.Context) { - spaceId := c.Param("space_id") - // TODO: implement offset - // offset := c.GetInt("offset") - limit := c.GetInt("limit") - - chatId, statusCode, errorMessage := a.getChatIdForSpace(spaceId) - if statusCode != http.StatusOK { - c.JSON(statusCode, gin.H{"message": errorMessage}) - return - } - - lastMessages := a.mw.ChatSubscribeLastMessages(c.Request.Context(), &pb.RpcChatSubscribeLastMessagesRequest{ - ChatObjectId: chatId, - Limit: int32(limit), - }) - - if lastMessages.Error.Code != pb.RpcChatSubscribeLastMessagesResponseError_NULL { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve last messages."}) - } - - messages := make([]ChatMessage, 0, len(lastMessages.Messages)) - for _, message := range lastMessages.Messages { - - attachments := make([]Attachment, 0, len(message.Attachments)) - for _, attachment := range message.Attachments { - target := attachment.Target - if attachment.Type != model.ChatMessageAttachment_LINK { - target = a.getGatewayURLForMedia(attachment.Target, false) - } - attachments = append(attachments, Attachment{ - Target: target, - Type: model.ChatMessageAttachmentAttachmentType_name[int32(attachment.Type)], - }) - } - - messages = append(messages, ChatMessage{ - Type: "chat_message", - Id: message.Id, - Creator: message.Creator, - CreatedAt: message.CreatedAt, - ReplyToMessageId: message.ReplyToMessageId, - Message: MessageContent{ - Text: message.Message.Text, - // TODO: params - // Style: nil, - // Marks: nil, - }, - Attachments: attachments, - Reactions: Reactions{ - ReactionsMap: func() map[string]IdentityList { - reactionsMap := make(map[string]IdentityList) - for emoji, ids := range message.Reactions.Reactions { - reactionsMap[emoji] = IdentityList{Ids: ids.Ids} - } - return reactionsMap - }(), - }, - }) - } - - c.JSON(http.StatusOK, gin.H{"chatId": chatId, "messages": messages}) -} - -// getChatMessageHandler retrieves a specific chat message by message_id -// -// @Summary Retrieve a specific chat message -// @Tags chat -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param message_id path string true "Message ID" -// @Success 200 {object} ChatMessage "Chat message" -// @Failure 404 {object} NotFoundError "Message not found" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /v1/spaces/{space_id}/chat/messages/{message_id} [get] -func (a *ApiServer) getChatMessageHandler(c *gin.Context) { - // TODO: Implement logic to retrieve a specific chat message by message_id - - c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet"}) -} - -// addChatMessageHandler adds a new chat message to chat -// -// @Summary Add a new chat message -// @Tags chat -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param message body ChatMessage true "Chat message" -// @Success 201 {object} ChatMessage "Created chat message" -// @Failure 400 {object} ValidationError "Invalid input" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /v1/spaces/{space_id}/chat/messages [post] -func (a *ApiServer) addChatMessageHandler(c *gin.Context) { - spaceId := c.Param("space_id") - - request := AddMessageRequest{} - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid JSON"}) - return - } - - chatId, statusCode, errorMessage := a.getChatIdForSpace(spaceId) - if statusCode != http.StatusOK { - c.JSON(statusCode, gin.H{"message": errorMessage}) - return - } - - resp := a.mw.ChatAddMessage(c.Request.Context(), &pb.RpcChatAddMessageRequest{ - ChatObjectId: chatId, - Message: &model.ChatMessage{ - Id: "", - OrderId: "", - Creator: "", - CreatedAt: 0, - ModifiedAt: 0, - ReplyToMessageId: "", - Message: &model.ChatMessageMessageContent{ - Text: request.Text, - // TODO: param - // Style: request.Style, - }, - }, - }) - - if resp.Error.Code != pb.RpcChatAddMessageResponseError_NULL { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create message."}) - } - - c.JSON(http.StatusOK, gin.H{"messageId": resp.MessageId}) -} - -// updateChatMessageHandler updates an existing chat message by message_id -// -// @Summary Update an existing chat message -// @Tags chat -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param message_id path string true "Message ID" -// @Param message body ChatMessage true "Chat message" -// @Success 200 {object} ChatMessage "Updated chat message" -// @Failure 400 {object} ValidationError "Invalid input" -// @Failure 404 {object} NotFoundError "Message not found" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /v1/spaces/{space_id}/chat/messages/{message_id} [put] -func (a *ApiServer) updateChatMessageHandler(c *gin.Context) { - // TODO: Implement logic to update an existing chat message by message_id - - c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet"}) -} - -// deleteChatMessageHandler deletes a chat message by message_id -// -// @Summary Delete a chat message -// @Tags chat -// @Accept json -// @Produce json -// @Param space_id path string true "The ID of the space" -// @Param message_id path string true "Message ID" -// @Success 204 "Message deleted successfully" -// @Failure 404 {object} NotFoundError "Message not found" -// @Failure 502 {object} ServerError "Internal server error" -// @Router /v1/spaces/{space_id}/chat/messages/{message_id} [delete] -func (a *ApiServer) deleteChatMessageHandler(c *gin.Context) { - // TODO: Implement logic to delete a chat message by message_id - - c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet"}) + pagination.RespondWithPagination(c, http.StatusOK, paginatedResults, len(searchResults), offset, limit, hasMore) } diff --git a/cmd/api/helper.go b/cmd/api/helper.go index 8bd9839ca..1c4db84d8 100644 --- a/cmd/api/helper.go +++ b/cmd/api/helper.go @@ -2,27 +2,16 @@ package api import ( "context" - "fmt" "net/http" "strings" - "github.com/gin-gonic/gin" - + "github.com/anyproto/anytype-heart/cmd/api/utils" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pkg/lib/bundle" "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/util/pbtypes" ) -// getGatewayURLForMedia returns the URL of file gateway for the media object with the given ID -func (a *ApiServer) getGatewayURLForMedia(objectId string, isIcon bool) string { - widthParam := "" - if isIcon { - widthParam = "?width=100" - } - return fmt.Sprintf("%s/image/%s%s", a.accountInfo.GatewayUrl, objectId, widthParam) -} - // resolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type" func (a *ApiServer) resolveTypeToName(spaceId string, typeId string) (typeName string, statusCode int, errorMessage string) { // Can't look up preinstalled types based on relation key, therefore need to use unique key @@ -54,71 +43,6 @@ func (a *ApiServer) resolveTypeToName(spaceId string, typeId string) (typeName s return resp.Records[0].Fields["name"].GetStringValue(), http.StatusOK, "" } -// getChatIdForSpace returns the chat ID for the space with the given ID -func (a *ApiServer) getChatIdForSpace(spaceId string) (chatId string, statusCode int, errorMessage string) { - workspace, statusCode, errorMessage := a.getWorkspaceInfo(spaceId) - if statusCode != http.StatusOK { - return "", statusCode, errorMessage - } - - resp := a.mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{ - SpaceId: spaceId, - ObjectId: workspace.WorkspaceObjectId, - }) - - if resp.Error.Code != pb.RpcObjectShowResponseError_NULL { - return "", http.StatusInternalServerError, "Failed to open workspace object." - } - - if !resp.ObjectView.Details[0].Details.Fields["hasChat"].GetBoolValue() { - return "", http.StatusNotFound, "Chat not found." - } - - return resp.ObjectView.Details[0].Details.Fields["chatId"].GetStringValue(), http.StatusOK, "" -} - -// getWorkspaceInfo returns the workspace info for the space with the given ID -func (a *ApiServer) getWorkspaceInfo(spaceId string) (space Space, statusCode int, errorMessage string) { - workspaceResponse := a.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{ - SpaceId: spaceId, - WithChat: true, - }) - - if workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL { - return Space{}, http.StatusInternalServerError, "Failed to open workspace." - } - - return Space{ - Type: "space", - Id: spaceId, - HomeObjectId: workspaceResponse.Info.HomeObjectId, - ArchiveObjectId: workspaceResponse.Info.ArchiveObjectId, - ProfileObjectId: workspaceResponse.Info.ProfileObjectId, - MarketplaceWorkspaceId: workspaceResponse.Info.MarketplaceWorkspaceId, - WorkspaceObjectId: workspaceResponse.Info.WorkspaceObjectId, - DeviceId: workspaceResponse.Info.DeviceId, - AccountSpaceId: workspaceResponse.Info.AccountSpaceId, - WidgetsId: workspaceResponse.Info.WidgetsId, - SpaceViewId: workspaceResponse.Info.SpaceViewId, - TechSpaceId: workspaceResponse.Info.TechSpaceId, - Timezone: workspaceResponse.Info.TimeZone, - NetworkId: workspaceResponse.Info.NetworkId, - }, http.StatusOK, "" -} - -// getIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url -func (a *ApiServer) getIconFromEmojiOrImage(iconEmoji string, iconImage string) string { - if iconEmoji != "" { - return iconEmoji - } - - if iconImage != "" { - return a.getGatewayURLForMedia(iconImage, true) - } - - return "" -} - // getBlocks returns the blocks of the object func (a *ApiServer) getBlocks(resp *pb.RpcObjectShowResponse) []Block { blocks := []Block{} @@ -134,7 +58,7 @@ func (a *ApiServer) getBlocks(resp *pb.RpcObjectShowResponse) []Block { Style: model.BlockContentTextStyle_name[int32(content.Text.Style)], Checked: content.Text.Checked, Color: content.Text.Color, - Icon: a.getIconFromEmojiOrImage(content.Text.IconEmoji, content.Text.IconImage), + Icon: utils.GetIconFromEmojiOrImage(a.accountInfo, content.Text.IconEmoji, content.Text.IconImage), } case *model.BlockContentOfFile: file = &File{ @@ -241,34 +165,3 @@ func (a *ApiServer) getTags(resp *pb.RpcObjectShowResponse) []Tag { } return tags } - -// respondWithPagination returns a json response with the paginated data and corresponding metadata -func respondWithPagination[T any](c *gin.Context, statusCode int, data []T, total, offset, limit int, hasMore bool) { - c.JSON(statusCode, PaginatedResponse[T]{ - Data: data, - Pagination: PaginationMeta{ - Total: total, - Offset: offset, - Limit: limit, - HasMore: hasMore, - }, - }) -} - -// paginate paginates the given records based on the offset and limit -func paginate[T any](records []T, offset, limit int) ([]T, bool) { - total := len(records) - start := offset - end := offset + limit - - if start > total { - start = total - } - if end > total { - end = total - } - - paginated := records[start:end] - hasMore := end < total - return paginated, hasMore -} diff --git a/cmd/api/main.go b/cmd/api/main.go index e0e26baa9..b443cc27c 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -13,6 +13,7 @@ import ( "github.com/webstradev/gin-pagination/v2/pkg/pagination" _ "github.com/anyproto/anytype-heart/cmd/api/docs" + "github.com/anyproto/anytype-heart/cmd/api/space" "github.com/anyproto/anytype-heart/core" "github.com/anyproto/anytype-heart/pb/service" "github.com/anyproto/anytype-heart/pkg/lib/pb/model" @@ -29,8 +30,8 @@ type ApiServer struct { router *gin.Engine server *http.Server - // init after app start - accountInfo *model.AccountInfo + accountInfo *model.AccountInfo + spaceService *space.SpaceService } // TODO: User represents an authenticated user with permissions @@ -41,9 +42,10 @@ type User struct { func newApiServer(mw service.ClientCommandsServer, mwInternal core.MiddlewareInternal) *ApiServer { a := &ApiServer{ - mw: mw, - mwInternal: mwInternal, - router: gin.Default(), + mw: mw, + mwInternal: mwInternal, + router: gin.Default(), + spaceService: space.NewService(mw), } a.server = &http.Server{ @@ -100,8 +102,8 @@ func RunApiServer(ctx context.Context, mw service.ClientCommandsServer, mwIntern // readOnly.Use(a.AuthMiddleware()) // readOnly.Use(a.PermissionMiddleware("read-only")) { - readOnly.GET("/spaces", paginator, a.getSpacesHandler) - readOnly.GET("/spaces/:space_id/members", paginator, a.getMembersHandler) + readOnly.GET("/spaces", paginator, space.GetSpacesHandler(a.spaceService)) + readOnly.GET("/spaces/:space_id/members", paginator, space.GetMembersHandler(a.spaceService)) readOnly.GET("/spaces/:space_id/objects", paginator, a.getObjectsHandler) readOnly.GET("/spaces/:space_id/objects/:object_id", a.getObjectHandler) readOnly.GET("/spaces/:space_id/objectTypes", paginator, a.getObjectTypesHandler) @@ -114,23 +116,11 @@ func RunApiServer(ctx context.Context, mw service.ClientCommandsServer, mwIntern // readWrite.Use(a.AuthMiddleware()) // readWrite.Use(a.PermissionMiddleware("read-write")) { - readWrite.POST("/spaces", a.createSpaceHandler) + // readWrite.POST("/spaces", a.createSpaceHandler) readWrite.POST("/spaces/:space_id/objects", a.createObjectHandler) readWrite.PUT("/spaces/:space_id/objects/:object_id", a.updateObjectHandler) } - // Chat routes - chat := a.router.Group("/v1/spaces/:space_id/chat") - // chat.Use(a.AuthMiddleware()) - // chat.Use(a.PermissionMiddleware("read-write")) - { - chat.GET("/messages", paginator, a.getChatMessagesHandler) - chat.GET("/messages/:message_id", a.getChatMessageHandler) - chat.POST("/messages", a.addChatMessageHandler) - chat.PUT("/messages/:message_id", a.updateChatMessageHandler) - chat.DELETE("/messages/:message_id", a.deleteChatMessageHandler) - } - // Start the HTTP server go func() { if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index b4fa0aa7f..882d659a4 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -27,6 +27,7 @@ func (a *ApiServer) initAccountInfo() gin.HandlerFunc { } a.accountInfo = accInfo + a.spaceService.AccountInfo = accInfo c.Next() } } diff --git a/cmd/api/pagination/model.go b/cmd/api/pagination/model.go new file mode 100644 index 000000000..06539ce63 --- /dev/null +++ b/cmd/api/pagination/model.go @@ -0,0 +1,13 @@ +package pagination + +type PaginationMeta struct { + Total int `json:"total" example:"1024"` // the total number of items available on that endpoint + Offset int `json:"offset" example:"0"` // the current offset + Limit int `json:"limit" example:"100"` // the current limit + HasMore bool `json:"has_more" example:"true"` // whether there are more items available +} + +type PaginatedResponse[T any] struct { + Data []T `json:"data"` + Pagination PaginationMeta `json:"pagination"` +} diff --git a/cmd/api/pagination/pagination.go b/cmd/api/pagination/pagination.go new file mode 100644 index 000000000..e648f470e --- /dev/null +++ b/cmd/api/pagination/pagination.go @@ -0,0 +1,39 @@ +package pagination + +import "github.com/gin-gonic/gin" + +type Service[T any] interface { + RespondWithPagination(c *gin.Context, statusCode int, data []T, total, offset, limit int, hasMore bool) + Paginate(records []T, offset, limit int) ([]T, bool) +} + +// RespondWithPagination returns a json response with the paginated data and corresponding metadata +func RespondWithPagination[T any](c *gin.Context, statusCode int, data []T, total, offset, limit int, hasMore bool) { + c.JSON(statusCode, PaginatedResponse[T]{ + Data: data, + Pagination: PaginationMeta{ + Total: total, + Offset: offset, + Limit: limit, + HasMore: hasMore, + }, + }) +} + +// Paginate paginates the given records based on the offset and limit +func Paginate[T any](records []T, offset, limit int) ([]T, bool) { + total := len(records) + start := offset + end := offset + limit + + if start > total { + start = total + } + if end > total { + end = total + } + + paginated := records[start:end] + hasMore := end < total + return paginated, hasMore +} diff --git a/cmd/api/schemas.go b/cmd/api/schemas.go index 33461598c..5ec8247f4 100644 --- a/cmd/api/schemas.go +++ b/cmd/api/schemas.go @@ -1,17 +1,5 @@ package api -type PaginationMeta struct { - Total int `json:"total" example:"1024"` // the total number of items available on that endpoint - Offset int `json:"offset" example:"0"` // the current offset - Limit int `json:"limit" example:"100"` // the current limit - HasMore bool `json:"has_more" example:"true"` // whether there are more items available -} - -type PaginatedResponse[T any] struct { - Data []T `json:"data"` - Pagination PaginationMeta `json:"pagination"` -} - type AuthDisplayCodeResponse struct { ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"` } @@ -21,40 +9,6 @@ type AuthTokenResponse struct { AppKey string `json:"app_key" example:""` } -type CreateSpaceResponse struct { - SpaceId string `json:"space_id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"` - Name string `json:"name" example:"Space Name"` -} - -type Space struct { - Type string `json:"type" example:"space"` - Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"` - Name string `json:"name" example:"Space Name"` - Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"` - HomeObjectId string `json:"home_object_id" example:"bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya"` - ArchiveObjectId string `json:"archive_object_id" example:"bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"` - ProfileObjectId string `json:"profile_object_id" example:"bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4"` - MarketplaceWorkspaceId string `json:"marketplace_workspace_id" example:"_anytype_marketplace"` - WorkspaceObjectId string `json:"workspace_object_id" example:"bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"` - DeviceId string `json:"device_id" example:"12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF"` - AccountSpaceId string `json:"account_space_id" example:"bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1"` - WidgetsId string `json:"widgets_id" example:"bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva"` - SpaceViewId string `json:"space_view_id" example:"bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy"` - TechSpaceId string `json:"tech_space_id" example:"bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1"` - Timezone string `json:"timezone" example:""` - NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"` -} - -type Member struct { - Type string `json:"type" example:"member"` - Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"` - Name string `json:"name" example:"John Doe"` - Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"` - Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"` - GlobalName string `json:"global_name" example:"john.any"` - Role string `json:"role" enum:"Reader,Writer,Owner,NoPermission" example:"Owner"` -} - type Object struct { Type string `json:"type" example:"object"` Id string `json:"id" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"` @@ -123,38 +77,6 @@ type ObjectTemplate struct { Icon string `json:"icon" example:"📄"` } -type ChatMessage struct { - Type string `json:"chat_message"` - Id string `json:"id"` // Unique message identifier - OrderId string `json:"order_id"` // Used for subscriptions - Creator string `json:"creator"` // Identifier for the message creator - CreatedAt int64 `json:"created_at"` - ModifiedAt int64 `json:"modified_at"` - ReplyToMessageId string `json:"reply_to_message_id"` // Identifier for the message being replied to - Message MessageContent `json:"message"` // Message content - Attachments []Attachment `json:"attachments"` // Attachments slice - Reactions Reactions `json:"reactions"` // Reactions to the message -} - -type MessageContent struct { - Text string `json:"text"` // The text content of the message part - Style string `json:"style"` // The style/type of the message part - Marks []string `json:"marks"` // List of marks applied to the text -} - -type Attachment struct { - Target string `json:"target"` // Identifier for the attachment object - Type string `json:"type"` // Type of attachment -} - -type Reactions struct { - ReactionsMap map[string]IdentityList `json:"reactions"` // Map of emoji to list of user IDs -} - -type IdentityList struct { - Ids []string `json:"ids"` // List of user IDs -} - type ServerError struct { Error struct { Message string `json:"message"` diff --git a/cmd/api/space/handler.go b/cmd/api/space/handler.go new file mode 100644 index 000000000..dd9ebb0bb --- /dev/null +++ b/cmd/api/space/handler.go @@ -0,0 +1,124 @@ +package space + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/anyproto/anytype-heart/cmd/api/pagination" +) + +// GetSpacesHandler retrieves a list of spaces +// +// @Summary Retrieve a list of spaces +// @Tags spaces +// @Accept json +// @Produce json +// @Param offset query int false "The number of items to skip before starting to collect the result set" +// @Param limit query int false "The number of items to return" default(100) +// @Success 200 {object} pagination.PaginatedResponse[Space] "List of spaces" +// @Failure 403 {object} api.UnauthorizedError "Unauthorized" +// @Failure 404 {object} api.NotFoundError "Resource not found" +// @Failure 502 {object} api.ServerError "Internal server error" +// @Router /spaces [get] +func GetSpacesHandler(s *SpaceService) gin.HandlerFunc { + return func(c *gin.Context) { + offset := c.GetInt("offset") + limit := c.GetInt("limit") + + spaces, total, hasMore, err := s.ListSpaces(c.Request.Context(), offset, limit) + if err != nil { + switch { + case errors.Is(err, ErrNoSpacesFound): + c.JSON(http.StatusNotFound, gin.H{"message": "No spaces found."}) + return + case errors.Is(err, ErrFailedListSpaces): + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of spaces."}) + return + case errors.Is(err, ErrFailedOpenWorkspace): + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to open workspace."}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + } + + pagination.RespondWithPagination(c, http.StatusOK, spaces, total, offset, limit, hasMore) + } +} + +// CreateSpaceHandler creates a new space +// +// @Summary Create a new Space +// @Tags spaces +// @Accept json +// @Produce json +// @Param name body string true "Space Name" +// @Success 200 {object} CreateSpaceResponse "Space created successfully" +// @Failure 403 {object} api.UnauthorizedError "Unauthorized" +// @Failure 502 {object} api.ServerError "Internal server error" +// @Router /spaces [post] +func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc { + return func(c *gin.Context) { + nameRequest := CreateSpaceRequest{} + if err := c.BindJSON(&nameRequest); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid JSON"}) + return + } + name := nameRequest.Name + + space, err := s.CreateSpace(c.Request.Context(), name) + if err != nil { + switch { + case errors.Is(err, ErrFailedCreateSpace): + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create space."}) + return + default: + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + } + + c.JSON(http.StatusOK, CreateSpaceResponse{Space: space}) + } +} + +// GetMembersHandler retrieves a list of members for the specified space +// +// @Summary Retrieve a list of members for the specified Space +// @Tags spaces +// @Accept json +// @Produce json +// @Param space_id path string true "The ID of the space" +// @Param offset query int false "The number of items to skip before starting to collect the result set" +// @Param limit query int false "The number of items to return" default(100) +// @Success 200 {object} pagination.PaginatedResponse[Member] "List of members" +// @Failure 403 {object} api.UnauthorizedError "Unauthorized" +// @Failure 404 {object} api.NotFoundError "Resource not found" +// @Failure 502 {object} api.ServerError "Internal server error" +// @Router /spaces/{space_id}/members [get] +func GetMembersHandler(s *SpaceService) gin.HandlerFunc { + return func(c *gin.Context) { + spaceId := c.Param("space_id") + offset := c.GetInt("offset") + limit := c.GetInt("limit") + + members, total, hasMore, err := s.ListMembers(c.Request.Context(), spaceId, offset, limit) + if err != nil { + switch { + case errors.Is(err, ErrNoMembersFound): + c.JSON(http.StatusNotFound, gin.H{"message": "No members found."}) + return + case errors.Is(err, ErrFailedListMembers): + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of members."}) + return + default: + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + } + + pagination.RespondWithPagination(c, http.StatusOK, members, total, offset, limit, hasMore) + } +} diff --git a/cmd/api/space/model.go b/cmd/api/space/model.go new file mode 100644 index 000000000..78ef51e45 --- /dev/null +++ b/cmd/api/space/model.go @@ -0,0 +1,41 @@ +package space + +type CreateSpaceRequest struct { + Name string `json:"name"` +} + +type CreateSpaceResponse struct { + Space Space `json:"space"` +} + +type Space struct { + Type string `json:"type" example:"space"` + Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"` + Name string `json:"name" example:"Space Name"` + Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"` + HomeObjectId string `json:"home_object_id" example:"bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya"` + ArchiveObjectId string `json:"archive_object_id" example:"bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"` + ProfileObjectId string `json:"profile_object_id" example:"bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4"` + MarketplaceWorkspaceId string `json:"marketplace_workspace_id" example:"_anytype_marketplace"` + WorkspaceObjectId string `json:"workspace_object_id" example:"bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"` + DeviceId string `json:"device_id" example:"12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF"` + AccountSpaceId string `json:"account_space_id" example:"bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1"` + WidgetsId string `json:"widgets_id" example:"bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva"` + SpaceViewId string `json:"space_view_id" example:"bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy"` + TechSpaceId string `json:"tech_space_id" example:"bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1"` + GatewayUrl string `json:"gateway_url" example:"http://127.0.0.1:31006"` + LocalStoragePath string `json:"local_storage_path" example:"/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"` + Timezone string `json:"timezone" example:""` + AnalyticsId string `json:"analytics_id" example:"624aecdd-4797-4611-9d61-a2ae5f53cf1c"` + NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"` +} + +type Member struct { + Type string `json:"type" example:"member"` + Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"` + Name string `json:"name" example:"John Doe"` + Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"` + Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"` + GlobalName string `json:"global_name" example:"john.any"` + Role string `json:"role" enum:"Reader,Writer,Owner,NoPermission" example:"Owner"` +} diff --git a/cmd/api/space/service.go b/cmd/api/space/service.go new file mode 100644 index 000000000..9ba824149 --- /dev/null +++ b/cmd/api/space/service.go @@ -0,0 +1,214 @@ +package space + +import ( + "context" + "crypto/rand" + "errors" + "math/big" + + "github.com/gogo/protobuf/types" + + "github.com/anyproto/anytype-heart/cmd/api/pagination" + "github.com/anyproto/anytype-heart/cmd/api/utils" + "github.com/anyproto/anytype-heart/pb" + "github.com/anyproto/anytype-heart/pb/service" + "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/util/pbtypes" +) + +var ( + ErrNoSpacesFound = errors.New("no spaces found") + ErrFailedListSpaces = errors.New("failed to retrieve list of spaces") + ErrFailedOpenWorkspace = errors.New("failed to open workspace") + ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon") + ErrFailedCreateSpace = errors.New("failed to create space") + ErrNoMembersFound = errors.New("no members found") + ErrFailedListMembers = errors.New("failed to retrieve list of members") +) + +type Service interface { + ListSpaces(ctx context.Context) ([]Space, error) + CreateSpace(ctx context.Context, name string) (Space, error) +} + +type SpaceService struct { + mw service.ClientCommandsServer + AccountInfo *model.AccountInfo +} + +func NewService(mw service.ClientCommandsServer) *SpaceService { + return &SpaceService{mw: mw} +} + +func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (spaces []Space, total int, hasMore bool, err error) { + resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{ + SpaceId: s.AccountInfo.TechSpaceId, + Filters: []*model.BlockContentDataviewFilter{ + { + RelationKey: bundle.RelationKeyLayout.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.ObjectType_spaceView)), + }, + { + RelationKey: bundle.RelationKeySpaceLocalStatus.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), + }, + { + RelationKey: bundle.RelationKeySpaceRemoteStatus.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), + }, + }, + Sorts: []*model.BlockContentDataviewSort{ + { + RelationKey: "spaceOrder", + Type: model.BlockContentDataviewSort_Asc, + NoCollate: true, + EmptyPlacement: model.BlockContentDataviewSort_End, + }, + }, + Keys: []string{"targetSpaceId", "name", "iconEmoji", "iconImage"}, + }) + + if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL { + return nil, 0, false, ErrFailedListSpaces + } + + if len(resp.Records) == 0 { + return nil, 0, false, ErrNoSpacesFound + } + + total = len(resp.Records) + paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit) + spaces = make([]Space, 0, len(paginatedRecords)) + + for _, record := range paginatedRecords { + workspace, err := s.getWorkspaceInfo(record.Fields["targetSpaceId"].GetStringValue()) + if err != nil { + return nil, 0, false, err + } + + // TODO: name and icon are only returned here; fix that + workspace.Name = record.Fields["name"].GetStringValue() + workspace.Icon = utils.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) + + spaces = append(spaces, workspace) + } + + return spaces, total, hasMore, nil +} + +func (s *SpaceService) CreateSpace(ctx context.Context, name string) (Space, error) { + iconOption, err := rand.Int(rand.Reader, big.NewInt(13)) + if err != nil { + return Space{}, ErrFailedGenerateRandomIcon + } + + // Create new workspace with a random icon and import default use case + resp := s.mw.WorkspaceCreate(ctx, &pb.RpcWorkspaceCreateRequest{ + Details: &types.Struct{ + Fields: map[string]*types.Value{ + "iconOption": pbtypes.Float64(float64(iconOption.Int64())), + "name": pbtypes.String(name), + "spaceDashboardId": pbtypes.String("lastOpened"), + }, + }, + UseCase: pb.RpcObjectImportUseCaseRequest_GET_STARTED, + WithChat: true, + }) + + if resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL { + return Space{}, ErrFailedCreateSpace + } + + return s.getWorkspaceInfo(resp.SpaceId) +} + +func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset int, limit int) (members []Member, total int, hasMore bool, err error) { + resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{ + SpaceId: spaceId, + Filters: []*model.BlockContentDataviewFilter{ + { + RelationKey: bundle.RelationKeyLayout.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.ObjectType_participant)), + }, + { + RelationKey: bundle.RelationKeyParticipantStatus.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.ParticipantStatus_Active)), + }, + }, + Sorts: []*model.BlockContentDataviewSort{ + { + RelationKey: "name", + Type: model.BlockContentDataviewSort_Asc, + }, + }, + Keys: []string{"id", "name", "iconEmoji", "iconImage", "identity", "globalName", "participantPermissions"}, + }) + + if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL { + return nil, 0, false, ErrFailedListMembers + } + + if len(resp.Records) == 0 { + return nil, 0, false, ErrNoMembersFound + } + + total = len(resp.Records) + paginatedMembers, hasMore := pagination.Paginate(resp.Records, offset, limit) + members = make([]Member, 0, len(paginatedMembers)) + + for _, record := range paginatedMembers { + icon := utils.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue()) + + member := Member{ + Type: "space_member", + Id: record.Fields["id"].GetStringValue(), + Name: record.Fields["name"].GetStringValue(), + Icon: icon, + Identity: record.Fields["identity"].GetStringValue(), + GlobalName: record.Fields["globalName"].GetStringValue(), + Role: model.ParticipantPermissions_name[int32(record.Fields["participantPermissions"].GetNumberValue())], + } + + members = append(members, member) + } + + return members, total, hasMore, nil +} + +// getWorkspaceInfo returns the workspace info for the space with the given ID +func (s *SpaceService) getWorkspaceInfo(spaceId string) (space Space, err error) { + workspaceResponse := s.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{ + SpaceId: spaceId, + WithChat: true, + }) + + if workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL { + return Space{}, ErrFailedOpenWorkspace + } + + return Space{ + Type: "space", + Id: spaceId, + HomeObjectId: workspaceResponse.Info.HomeObjectId, + ArchiveObjectId: workspaceResponse.Info.ArchiveObjectId, + ProfileObjectId: workspaceResponse.Info.ProfileObjectId, + MarketplaceWorkspaceId: workspaceResponse.Info.MarketplaceWorkspaceId, + WorkspaceObjectId: workspaceResponse.Info.WorkspaceObjectId, + DeviceId: workspaceResponse.Info.DeviceId, + AccountSpaceId: workspaceResponse.Info.AccountSpaceId, + WidgetsId: workspaceResponse.Info.WidgetsId, + SpaceViewId: workspaceResponse.Info.SpaceViewId, + TechSpaceId: workspaceResponse.Info.TechSpaceId, + GatewayUrl: workspaceResponse.Info.GatewayUrl, + LocalStoragePath: workspaceResponse.Info.LocalStoragePath, + Timezone: workspaceResponse.Info.TimeZone, + AnalyticsId: workspaceResponse.Info.AnalyticsId, + NetworkId: workspaceResponse.Info.NetworkId, + }, nil +} diff --git a/cmd/api/space/service_test.go b/cmd/api/space/service_test.go new file mode 100644 index 000000000..0ab45fdad --- /dev/null +++ b/cmd/api/space/service_test.go @@ -0,0 +1,312 @@ +package space + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/gogo/protobuf/types" + + "github.com/anyproto/anytype-heart/pb" + "github.com/anyproto/anytype-heart/pb/service/mock_service" + "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/util/pbtypes" +) + +const ( + offset = 0 + limit = 100 + techSpaceId = "tech-space-id" + gatewayUrl = "http://localhost:31006" + iconImage = "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri" +) + +type fixture struct { + *SpaceService + mwMock *mock_service.MockClientCommandsServer +} + +func newFixture(t *testing.T) *fixture { + mw := mock_service.NewMockClientCommandsServer(t) + spaceService := &SpaceService{mw: mw, AccountInfo: &model.AccountInfo{TechSpaceId: techSpaceId, GatewayUrl: gatewayUrl}} + + return &fixture{ + SpaceService: spaceService, + mwMock: mw, + } +} + +func TestSpaceService_ListSpaces(t *testing.T) { + t.Run("successful retrieval of spaces", func(t *testing.T) { + // given + fx := newFixture(t) + + fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{ + SpaceId: techSpaceId, + Filters: []*model.BlockContentDataviewFilter{ + { + RelationKey: bundle.RelationKeyLayout.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.ObjectType_spaceView)), + }, + { + RelationKey: bundle.RelationKeySpaceLocalStatus.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), + }, + { + RelationKey: bundle.RelationKeySpaceRemoteStatus.String(), + Condition: model.BlockContentDataviewFilter_Equal, + Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)), + }, + }, + Sorts: []*model.BlockContentDataviewSort{ + { + RelationKey: "spaceOrder", + Type: model.BlockContentDataviewSort_Asc, + NoCollate: true, + EmptyPlacement: model.BlockContentDataviewSort_End, + }, + }, + Keys: []string{"targetSpaceId", "name", "iconEmoji", "iconImage"}, + }).Return(&pb.RpcObjectSearchResponse{ + Records: []*types.Struct{ + { + Fields: map[string]*types.Value{ + "name": pbtypes.String("Another Workspace"), + "targetSpaceId": pbtypes.String("another-space-id"), + "iconEmoji": pbtypes.String(""), + "iconImage": pbtypes.String(iconImage), + }, + }, + { + Fields: map[string]*types.Value{ + "name": pbtypes.String("My Workspace"), + "targetSpaceId": pbtypes.String("my-space-id"), + "iconEmoji": pbtypes.String("🚀"), + "iconImage": pbtypes.String(""), + }, + }, + }, + Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL}, + }).Once() + + fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{ + Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL}, + Info: &model.AccountInfo{ + HomeObjectId: "home-object-id", + ArchiveObjectId: "archive-object-id", + ProfileObjectId: "profile-object-id", + MarketplaceWorkspaceId: "marketplace-workspace-id", + WorkspaceObjectId: "workspace-object-id", + DeviceId: "device-id", + AccountSpaceId: "account-space-id", + WidgetsId: "widgets-id", + SpaceViewId: "space-view-id", + TechSpaceId: "tech-space-id", + GatewayUrl: "gateway-url", + LocalStoragePath: "local-storage-path", + TimeZone: "time-zone", + AnalyticsId: "analytics-id", + NetworkId: "network-id", + }, + }, nil).Twice() + + // when + spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit) + + // then + require.NoError(t, err) + require.Len(t, spaces, 2) + require.Equal(t, "Another Workspace", spaces[0].Name) + require.Equal(t, "another-space-id", spaces[0].Id) + require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), spaces[0].Icon, "Icon URL does not match") + require.Equal(t, "My Workspace", spaces[1].Name) + require.Equal(t, "my-space-id", spaces[1].Id) + require.Equal(t, "🚀", spaces[1].Icon) + require.Equal(t, 2, total) + require.False(t, hasMore) + }) + + t.Run("no spaces found", func(t *testing.T) { + // given + fx := newFixture(t) + + fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything). + Return(&pb.RpcObjectSearchResponse{ + Records: []*types.Struct{}, + Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL}, + }).Once() + + // when + spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit) + + // then + require.ErrorIs(t, err, ErrNoSpacesFound) + require.Len(t, spaces, 0) + require.Equal(t, 0, total) + require.False(t, hasMore) + }) + + t.Run("failed workspace open", func(t *testing.T) { + // given + fx := newFixture(t) + + fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything). + Return(&pb.RpcObjectSearchResponse{ + Records: []*types.Struct{ + { + Fields: map[string]*types.Value{ + "name": pbtypes.String("My Workspace"), + "targetSpaceId": pbtypes.String("my-space-id"), + "iconEmoji": pbtypes.String("🚀"), + "iconImage": pbtypes.String(""), + }, + }, + }, + Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL}, + }).Once() + + fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything). + Return(&pb.RpcWorkspaceOpenResponse{ + Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_UNKNOWN_ERROR}, + }, nil).Once() + + // when + spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit) + + // then + require.ErrorIs(t, err, ErrFailedOpenWorkspace) + require.Len(t, spaces, 0) + require.Equal(t, 0, total) + require.False(t, hasMore) + }) +} + +func TestSpaceService_CreateSpace(t *testing.T) { + t.Run("successful create space", func(t *testing.T) { + // given + fx := newFixture(t) + fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything). + Return(&pb.RpcWorkspaceCreateResponse{ + Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_NULL}, + SpaceId: "new-space-id", + }).Once() + + fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{ + Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL}, + Info: &model.AccountInfo{ + HomeObjectId: "home-object-id", + ArchiveObjectId: "archive-object-id", + ProfileObjectId: "profile-object-id", + MarketplaceWorkspaceId: "marketplace-workspace-id", + WorkspaceObjectId: "workspace-object-id", + DeviceId: "device-id", + AccountSpaceId: "account-space-id", + WidgetsId: "widgets-id", + SpaceViewId: "space-view-id", + TechSpaceId: "tech-space-id", + GatewayUrl: "gateway-url", + LocalStoragePath: "local-storage-path", + TimeZone: "time-zone", + AnalyticsId: "analytics-id", + NetworkId: "network-id", + }, + }, nil).Once() + + // when + space, err := fx.CreateSpace(nil, "New Space") + + // then + require.NoError(t, err) + require.Equal(t, "new-space-id", space.Id) + }) + + t.Run("failed workspace creation", func(t *testing.T) { + // given + fx := newFixture(t) + fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything). + Return(&pb.RpcWorkspaceCreateResponse{ + Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_UNKNOWN_ERROR}, + }).Once() + + // when + space, err := fx.CreateSpace(nil, "New Space") + + // then + require.ErrorIs(t, err, ErrFailedCreateSpace) + require.Equal(t, Space{}, space) + }) +} + +func TestSpaceService_ListMembers(t *testing.T) { + t.Run("successfully get members", func(t *testing.T) { + // given + fx := newFixture(t) + + fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything). + Return(&pb.RpcObjectSearchResponse{ + Records: []*types.Struct{ + { + Fields: map[string]*types.Value{ + "id": pbtypes.String("member-1"), + "name": pbtypes.String("John Doe"), + "iconEmoji": pbtypes.String("👤"), + "identity": pbtypes.String("AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"), + "globalName": pbtypes.String("john.any"), + }, + }, + { + Fields: map[string]*types.Value{ + "id": pbtypes.String("member-2"), + "name": pbtypes.String("Jane Doe"), + "iconImage": pbtypes.String(iconImage), + "identity": pbtypes.String("AAjLbEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMD4"), + "globalName": pbtypes.String("jane.any"), + }, + }, + }, + Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL}, + }).Once() + + // when + members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit) + + // then + require.NoError(t, err) + require.Len(t, members, 2) + require.Equal(t, "member-1", members[0].Id) + require.Equal(t, "John Doe", members[0].Name) + require.Equal(t, "👤", members[0].Icon) + require.Equal(t, "john.any", members[0].GlobalName) + require.Equal(t, "member-2", members[1].Id) + require.Equal(t, "Jane Doe", members[1].Name) + require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), members[1].Icon, "Icon URL does not match") + require.Equal(t, "jane.any", members[1].GlobalName) + require.Equal(t, 2, total) + require.False(t, hasMore) + }) + + t.Run("no members found", func(t *testing.T) { + // given + fx := newFixture(t) + + fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything). + Return(&pb.RpcObjectSearchResponse{ + Records: []*types.Struct{}, + Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL}, + }).Once() + + // when + members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit) + + // then + require.ErrorIs(t, err, ErrNoMembersFound) + require.Len(t, members, 0) + require.Equal(t, 0, total) + require.False(t, hasMore) + }) +} diff --git a/cmd/api/utils/utils.go b/cmd/api/utils/utils.go new file mode 100644 index 000000000..bd24f7e11 --- /dev/null +++ b/cmd/api/utils/utils.go @@ -0,0 +1,29 @@ +package utils + +import ( + "fmt" + + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" +) + +// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url +func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string { + if iconEmoji != "" { + return iconEmoji + } + + if iconImage != "" { + return GetGatewayURLForMedia(accountInfo, iconImage, true) + } + + return "" +} + +// GetGatewayURLForMedia returns the URL of file gateway for the media object with the given ID +func GetGatewayURLForMedia(accountInfo *model.AccountInfo, objectId string, isIcon bool) string { + widthParam := "" + if isIcon { + widthParam = "?width=100" + } + return fmt.Sprintf("%s/image/%s%s", accountInfo.GatewayUrl, objectId, widthParam) +}