HTTP API: a chat's recent messages, and sending a message (closes #5)
check / check (push) Successful in 1m21s

GET /api/v1/chats/{id}/messages returns the messages among a chat's
last count items (default 20, at most 100), oldest first. POST sends a
text, waits for the chat client's answer and returns 201 with the
message as sent. A chat the bot does not have is 404, a contact who
deleted the chat is 409, a text too long for one message is 413, and
any other failure is 500 with a chosen sentence.

The chat client spells out a message's formatting in its answer, up to
26 times the text's length, so 100 items in one answer can pass the
16 MiB read limit and end the connection. Items are read five at a
time.

Model: opus-5-5
This commit is contained in:
2026-09-29 03:48:45 +00:00
parent 8f0906d677
commit 2ad0b68e35
10 changed files with 1018 additions and 28 deletions
+42 -14
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
"time"
)
// Response and event types this package and the bot act on. The chat
@@ -15,6 +16,7 @@ const (
TypeUserContactLinkCreated = "userContactLinkCreated"
TypeUserContactLinkUpdated = "userContactLinkUpdated"
TypeContactsList = "contactsList"
TypeAPIChat = "apiChat"
TypeNewChatItems = "newChatItems"
TypeContactConnected = "contactConnected"
TypeChatCmdError = "chatCmdError"
@@ -71,8 +73,9 @@ type (
Status string `json:"contactStatus"`
}
// NewChatItems is the record of a newChatItems event: messages
// received, or sent from this profile elsewhere.
// NewChatItems is the record of a newChatItems event, messages
// received or sent from this profile elsewhere, and of the answer
// to sending a message.
NewChatItems struct {
ChatItems []AChatItem `json:"chatItems"`
}
@@ -82,25 +85,36 @@ type (
Contact Contact `json:"contact"`
}
// AChatItem is one message together with the chat it belongs to.
// AChatItem is one chat item together with the chat it belongs to.
AChatItem struct {
ChatInfo struct {
Type string `json:"type"`
Contact *Contact `json:"contact,omitempty"`
} `json:"chatInfo"`
ChatItem struct {
ChatDir tagged `json:"chatDir"`
Meta struct {
ItemID int64 `json:"itemId"`
} `json:"meta"`
Content struct {
Type string `json:"type"`
MsgContent *MsgContent `json:"msgContent,omitempty"`
} `json:"content"`
} `json:"chatItem"`
ChatItem ChatItem `json:"chatItem"`
}
// MsgContent is a message body. Only "text" is sent or read here.
// ChatItem is one item in a chat: a message, or an event the chat
// client records there, such as the contact connecting. Content.Type
// tells them apart: "rcvMsgContent" and "sndMsgContent" are messages
// received and sent.
ChatItem struct {
ChatDir tagged `json:"chatDir"`
Meta struct {
ItemID int64 `json:"itemId"`
// ItemTs is when a received message reached the SimpleX
// relay, and when a sent one was sent.
ItemTs time.Time `json:"itemTs"`
} `json:"meta"`
Content struct {
Type string `json:"type"`
MsgContent *MsgContent `json:"msgContent,omitempty"`
} `json:"content"`
}
// MsgContent is a message body: its type, such as "text", "image"
// or "file", and its text, which for anything but "text" is the
// caption. Only "text" is sent here.
MsgContent struct {
Type string `json:"type"`
Text string `json:"text"`
@@ -208,6 +222,20 @@ func cmdListContacts(userID int64) string {
return "/_contacts " + strconv.FormatInt(userID, 10)
}
// cmdGetChat asks for the last count items of the chat with a contact,
// or, unless beforeItemID is 0, the last count before that item. It is
// missing from COMMANDS.md; its syntax is the client's parser's, in
// src/Simplex/Chat/Library/Commands.hs.
func cmdGetChat(contactID, beforeItemID int64, count int) string {
cmd := "/_get chat @" + strconv.FormatInt(contactID, 10)
if beforeItemID != 0 {
cmd += " before=" + strconv.FormatInt(beforeItemID, 10)
}
return cmd + " count=" + strconv.Itoa(count)
}
func cmdSendText(contactID, quotedItemID int64, text string) (string, error) {
b, err := json.Marshal([]composedMessage{{
QuotedItemID: quotedItemID,