1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-09 09:35:00 +09:00

GO-4459: Refactor into space, pagination, utils, remove chat

This commit is contained in:
Jannis Metrikat 2024-12-30 15:19:04 +01:00
parent c379686478
commit c0f69df4b9
No known key found for this signature in database
GPG key ID: B223CAC5AAF85615
15 changed files with 1235 additions and 2021 deletions

View file

@ -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": {

View file

@ -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": {

View file

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

View file

@ -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)
}

View file

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

View file

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

View file

@ -27,6 +27,7 @@ func (a *ApiServer) initAccountInfo() gin.HandlerFunc {
}
a.accountInfo = accInfo
a.spaceService.AccountInfo = accInfo
c.Next()
}
}

View file

@ -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"`
}

View file

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

View file

@ -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"`

124
cmd/api/space/handler.go Normal file
View file

@ -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)
}
}

41
cmd/api/space/model.go Normal file
View file

@ -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"`
}

214
cmd/api/space/service.go Normal file
View file

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

View file

@ -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)
})
}

29
cmd/api/utils/utils.go Normal file
View file

@ -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)
}