Add Chat REST API endpoints#2915
Conversation
|
Nice work on this! I've been working on chat support from the CLI side (anyproto/anytype-cli#25) and noticed you took the REST API approach. After comparing both, I think yours is architecturally cleaner and more consistent with how the CLI is designed to work. One feature we implemented that might be worth adding here: real-time subscriptions. Our CLI has a chat subscribe command that uses gRPC streaming to get live events (new messages, edits, reactions, etc.) as they happen. This is essential for building chat bots or bridges that need to react to messages without polling. Would you be open to adding a subscription endpoint? Two options that would fit the REST API pattern: WebSocket: Or SSE (simpler): Either would wrap the existing ChatSubscribe gRPC stream internally. Happy to contribute the implementation if you'd like — just let me know which approach you'd prefer. |
happy to take a look into it! is that subscribe command up somewhere I could take a look at it? |
|
Awesome! Here's our implementation: CLI subscribe command: anyproto/anytype-cli#25 Key files:
The flow:
For the REST API, we were thinking SSE would fit well: Happy to collaborate on the implementation! |
9624401 to
b01b7f4
Compare
I'm not seeing any of that subscribe stuff in anyproto/anytype-cli#25, is there a commit not pushed up? |
b01b7f4 to
0ab42a2
Compare
|
@jsandai had a go at it, seems to work! |
|
Hey Chris! This REST API is fantastic — exactly what's needed for programmatic chat integration. I've been testing it against a self-hosted network and everything works great: messages, reactions, SSE subscriptions. For chat bot use cases (like OpenClaw integration), two additional endpoints would be really valuable: 1. Mark as ReadEndpoint: Bots need to mark messages as read after processing them, otherwise unread counters accumulate. The gRPC method {
"type": "messages",
"before_order_id": "!!Xz",
"last_state_id": "6980b7db..."
}2. File UploadEndpoint: Bots often need to send images, documents, or generated files. Currently attachments reference object IDs, but there's no way to upload via REST. The gRPC Response would return the Both have existing gRPC foundations, so implementation should be straightforward. Happy to help with either if useful! |
|
@themightychris Thanks - this is awesome, and much needed! I agree with the need for subscribing to events - which you've added. Also we need a way to mark messages as read as @jsandai mentioned. Potential variants: specific messages (array of message ids), or by adjusting the watermark (before order_id) |
685867b to
84f6645
Compare
Add REST API layer for chat functionality, exposing the existing gRPC
chat operations to the public API:
- GET /v1/spaces/{space_id}/chats/{chat_id}/messages - List messages
- GET /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Get message
- POST /v1/spaces/{space_id}/chats/{chat_id}/messages - Create message
- PATCH /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Update message
- DELETE /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Delete message
- POST /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/reactions - Toggle reaction
- POST /v1/spaces/{space_id}/chats/{chat_id}/search - Search messages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds Server-Sent Events (SSE) endpoint to the REST API for streaming chat updates:
- GET /v1/spaces/{space_id}/chats/{chat_id}/subscribe
- Streams real-time events: message add/update/delete, reactions, state changes
- Extends EventService with callback registration for REST API integration
- Includes SSE session management and event dispatching
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
84f6645 to
b208f94
Compare
Adds POST /v1/spaces/{space_id}/chats/{chat_id}/read endpoint to mark
messages as read, wrapping the existing ChatReadMessages gRPC method.
Request body:
- type: "messages" or "mentions" (which counter to update)
- after_order_id: optional start of range
- before_order_id: required end of range
- last_state_id: from ChatState to prevent race conditions
Returns 204 No Content on success.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds POST /v1/spaces/{space_id}/files endpoint to upload files via
multipart/form-data, wrapping the existing FileUpload gRPC method.
Request (multipart/form-data):
- file: The file to upload (required)
- type: Optional file type (file, image, video, audio, pdf)
Response:
- object: "file"
- object_id: The object ID to use as attachments in chat messages
The returned object_id can be used in chat message attachments:
{"attachments": [{"target": "<object_id>", "type": "file"}]}
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
b208f94 to
c019245
Compare
We successfully built a complete OpenClaw chat channel using only the REST API - no gRPC needed! Bidirectional messaging, file attachments, and real-time SSE all working great. ✅ API Completeness - Better Than Expected!
The API is nearly feature-complete! Only relations and message reactions are missing. 🔧 Code Fixes RequiredWe had to make 3 small fixes to get stable operation: 1. Nil pointer crash in any-sync (Critical)File: // Add before accessing c.client
if c.client == nil {
return nil, fmt.Errorf("coordinator client not initialized")
}2. chat_id not exposed in space responseFile: type SpaceResponse struct {
// ... existing fields ...
ChatID string `json:"chat_id,omitempty"` // ADD THIS
}File: Without this, clients can't discover which chat to subscribe to. 3. Nil guard in space serviceFile: if info == nil {
return nil, fmt.Errorf("space info not found")
}Total: ~15 lines of code across 3 files. 📝 SuggestionsSSE Event Types - DocumentationThe event types are: (Not Read Receipts - Consider SimplifyingCurrent API requires 3 fields: {
"type": "messages",
"before_order_id": "...",
"last_state_id": "..."
}Suggestion: Make Missing Endpoints
🎉 OverallExcellent work! The REST API is solid and well-designed. We were able to build a fully functional chat integration with real-time events, file uploads, and message management. Happy to submit PRs for the fixes if helpful! |
|
@jsandai I have a reactions endpoint in here already and it appeared to work in my tests, did it not work for you or did you miss that? Can you elaborate on relations and the use case for that? I'm not familiar yet I should be able to incorporate all this on my next pass |
Reactions: You're right, I see it now — ToggleReactionHandler at /messages/{id}/reactions. I missed it in testing, my bad! Confirmed working! Tested POST /chats/{id}/messages/{id}/reactions with {"emoji":"🦉"} — returned {"added":true} and the reaction appears on the message. Nice work! Relations: The use case is creating objects with structured metadata. Example: I create "Memory" objects with relations like kind (select), topic (text), importance (select). Without relation support, I can create the object but can't populate those fields via API — they stay empty. Would love to be able to pass relation values when creating/updating objects. |
|
@jsandai can you give me an example curl call of what you're picturing with relations? |
|
Sure! Here's what I'm envisioning: Creating an object with relation values: curl -X POST "http://localhost:31012/v1/spaces/{space_id}/objects" \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"type_key": "memory",
"name": "Project Decision",
"body": "Decided to use X approach...",
"properties": [
{"key": "memory_kind", "select": "decision"},
{"key": "topic", "text": "architecture"},
{"key": "importance", "select": "high"}
]
}'Updating relation values on existing object: curl -X PATCH "http://localhost:31012/v1/spaces/{space_id}/objects/{object_id}" \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"properties": [
{"key": "importance", "select": "critical"}
]
}'Currently I can create the object but the |
|
@jsandai correct if I'm wrong but that seems unrelated to chat, no? do you see a reason that would make sense to address in this PR, or might it be better in a separate one? |
You're right, relations/properties support isn't chat-specific, it's a general object API enhancement. Makes more sense as a separate PR. For this PR, everything we need for chat is covered: messages, SSE subscriptions, reactions, read receipts, and file uploads all work great. |
| } | ||
|
|
||
| // ProtoMessageToApiMessage converts a protobuf ChatMessage to an API ChatMessage. | ||
| func (s *Service) ProtoMessageToApiMessage(msg *model.ChatMessage) apimodel.ChatMessage { |
There was a problem hiding this comment.
All converting methods should be functions to reduce number of dependencies
| var sseLog = logging.Logger("api-sse") | ||
|
|
||
| // SSESession represents an active SSE connection | ||
| type SSESession struct { |
There was a problem hiding this comment.
The name should reflect that it's related to chats. SSESession is too generic. Same with other entities here
| select { | ||
| case session.Events <- event: | ||
| // Event sent successfully | ||
| default: |
There was a problem hiding this comment.
Maybe use some small timeout here instead of default? 100ms would be enough I think. It'll reduce possibility to lose some messages. But probably buffered channel will do the job well, I'm just little bit scared about default. It's up to you
| return | ||
| } | ||
|
|
||
| for _, msg := range event.Messages { |
There was a problem hiding this comment.
you can use event.GetMessages(), it has nil check inside. Also you don't need to check if length is zero
|
|
||
| func (_c *MockSender_Broadcast_Call) RunAndReturn(run func(*pb.Event)) *MockSender_Broadcast_Call { | ||
| _c.Call.Return(run) | ||
| _c.Run(run) |
There was a problem hiding this comment.
Please use make test-deps for updating mocks
| // Also call registered callbacks | ||
| es.callbackMu.RLock() | ||
| for _, cb := range es.callbacks { | ||
| go cb(event) |
There was a problem hiding this comment.
Consider using a separate goroutine for an observer with buffered channel + throttling. And maybe add a filter function for observers to accept only desired events.
| m.mu.Lock() | ||
| defer m.mu.Unlock() | ||
| m.sessions[session.SubId] = session | ||
| sseLog.Infof("SSE session registered: subId=%s chatId=%s", session.SubId, session.ChatId) |
There was a problem hiding this comment.
Looks like debug-level logs. Are those logs necessary?
| // Event sent successfully | ||
| default: | ||
| // Channel full, log warning | ||
| sseLog.Warnf("SSE event channel full for subId=%s, dropping event", subId) |
There was a problem hiding this comment.
Use log fields for subId like
sseLog.Warn("SSE event channel is full, dropping event", zap.String("subId", subId))
You need to Desugar() logger first
| "github.com/anyproto/anytype-heart/pkg/lib/pb/model" | ||
| ) | ||
|
|
||
| var sseLog = logging.Logger("api-sse") |
There was a problem hiding this comment.
var sseLog = logging.Logger("api.chat-sse").Desugar()
| // @Param Anytype-Version header string true "The version of the API to use" default(2025-11-08) | ||
| // @Param space_id path string true "The ID of the space" | ||
| // @Param chat_id path string true "The ID of the chat object" | ||
| // @Param offset query int false "The number of items to skip (for offset-based pagination)" default(0) |
|
Hello! Thank you for your contribution. I wrote some comments, but I need some more time to complete code review |
| } | ||
|
|
||
| // ChatMessage represents a chat message in the API response | ||
| type ChatMessage struct { |
There was a problem hiding this comment.
Please update example values according to this real world example:
{
"id": "bafyreigznj5dz7rym36hi3lazbpqsqv2k3nhd7luvv3qwz2b4a77vburoq",
"orderId": "!#'m",
"creator": "AAXFe78EtNAUNS8vFruj3rj4KZ47eVfEYmndEzumFs3nxwNu",
"createdAt": 1770741464,
"modifiedAt": 1770741464,
"stateId": "698b5ed84ac3df5d5b4046b2",
"replyToMessageId": "bafyreic26d2ubft2pdtoua773jhcrbns5zgfdyxd2lw6oqtav6ilmyg2xe",
"message": {
"text": "example 2",
"style": 0
},
"read": true,
"mentionRead": true,
"hasMention": true,
"synced": false,
"reactionRead": false,
"hasOthersReactions": false
}
| Content MessageContent `json:"content"` // The content of the message | ||
| Attachments []MessageAttachment `json:"attachments"` // The attachments of the message | ||
| Reactions map[string][]string `json:"reactions"` // Map of emoji to list of user IDs who reacted | ||
| Read bool `json:"read" example:"true"` // Whether the message has been read |
There was a problem hiding this comment.
Probably it's worth adding MentionRead+HasMention flags as well. I imagine someone can use that in their applications
| } | ||
|
|
||
| // ChatState represents the state of a chat | ||
| type ChatState struct { |
There was a problem hiding this comment.
Example values:
{
"messages": {
"oldestOrderId": "!#(v",
"counter": 1
},
"mentions": {
"oldestOrderId": "",
"counter": 0
},
"lastStateId": "698b5fc44ac3df5d5b4046bb",
"order": 3
}
|
|
||
| // SearchMessageResult represents a single search result | ||
| type SearchMessageResult struct { | ||
| ChatId string `json:"chat_id" example:"chat_abc123"` // The chat ID containing the message |
There was a problem hiding this comment.
You can use any CID as example values for object, chat, file and message IDs. For example: bafyreihnwyhjxaxm4jbgb3u7fky46lflk3wmknxzqfusu7cbmx6zcpolbu
Please check and update other examples in models in this file
| // @Failure 500 {object} util.ServerError "Internal server error" | ||
| // @Security bearerauth | ||
| // @Router /v1/spaces/{space_id}/files [post] | ||
| func UploadFileHandler(s *service.Service) gin.HandlerFunc { |
There was a problem hiding this comment.
I appreciate your efforts for enhancing our API, though I think file uploading shouldn't be in the scope of this PR
Also there is an existing PR with the same feature #2843
I'm planning to merge this one after the author will fix my comments. Feel free to join the discussion if you have any thoughts or concerns about their implementation.
There was a problem hiding this comment.
makes sense, I'll drop that from this PR and address the rest of your feedback sometime this week. It was a core use case for me and others to be able to post files to chat, but if #2843 is on track that'll cover it
|
Hello! Just checking in on the status of this PR — any updates on when the chat REST API endpoints might be merged? We're planning our iOS integration roadmap and would love to align with the expected timeline. Happy to help with testing or feedback if needed! Thanks for the great work! |
Summary
Adds REST API endpoints for chat functionality and file uploads, exposing the existing gRPC layer to the public API.
New Endpoints
/v1/spaces/{space_id}/chats/{chat_id}/messages/v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/v1/spaces/{space_id}/chats/{chat_id}/messages/v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/reactions/v1/spaces/{space_id}/chats/{chat_id}/search/v1/spaces/{space_id}/chats/{chat_id}/read/v1/spaces/{space_id}/chats/{chat_id}/subscribe/v1/spaces/{space_id}/filesFeatures
before_order_id,after_order_id)last_state_idobject_idfor use in attachmentsFiles Changed
core/api/model/chat.go: Chat message modelscore/api/model/file.go: File upload response modelcore/api/model/sse.go: SSE event modelscore/api/service/chat.go: Chat service (messages, reactions, search, read, subscriptions)core/api/service/file.go: File upload servicecore/api/service/sse.go: SSE session managementcore/api/handler/chat.go: Chat HTTP handlerscore/api/handler/file.go: File upload handlercore/api/server/router.go: Route registrationcore/api/core/core.go: ExtendedClientCommandsinterfaceUsage Examples
Test Plan
🤖 Generated with Claude Code