A SimpleX Chat bot that answers arithmetic (closes #1)
check / check (push) Successful in 54s

Remove the template's HTTP service, database and fx wiring. Add exact
arithmetic on go/parser and go/constant, a client that runs simplex-chat
as a child process and drives its WebSocket API, and the bot, which keeps
an auto-accepting address and replies to each message. The image adds the
checksum-pinned simplex-chat v7.0.2 on Ubuntu 22.04.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 22:01:51 +00:00
parent f8ce8cef83
commit 16649e0f2f
66 changed files with 2119 additions and 5059 deletions
+204
View File
@@ -0,0 +1,204 @@
package simplex
import (
"encoding/json"
"fmt"
"strconv"
)
// Response and event types this package and the bot act on. The chat
// client sends many more; every other type is ignored, as its API
// documentation requires of clients.
const (
TypeActiveUser = "activeUser"
TypeUserContactLink = "userContactLink"
TypeUserContactLinkCreated = "userContactLinkCreated"
TypeUserContactLinkUpdated = "userContactLinkUpdated"
TypeNewChatItems = "newChatItems"
TypeContactConnected = "contactConnected"
TypeChatCmdError = "chatCmdError"
)
// Event is one message from the chat client: a response to a command,
// or an event it sends unprompted. The protocol is a discriminated
// union on "type"; the rest of the record is decoded on demand, into a
// struct declaring only the fields the caller reads, so a record whose
// other fields changed shape between releases still decodes.
type Event struct {
Type string
raw json.RawMessage
}
// Decode unmarshals the whole record into v.
func (e Event) Decode(v any) error {
err := json.Unmarshal(e.raw, v)
if err != nil {
return fmt.Errorf("decoding %s: %w", e.Type, err)
}
return nil
}
// Wire types, reduced to the fields this program uses. The field names
// are the chat client's, hence camelCase in the tags.
//
//nolint:tagliatelle // the chat client's wire format, not ours to name.
type (
// User is the chat client's local user profile: the bot itself.
User struct {
UserID int64 `json:"userId"`
Profile struct {
DisplayName string `json:"displayName"`
} `json:"profile"`
}
// ConnLink is a SimpleX link. The short form is what people share;
// the full form is what older clients understand.
ConnLink struct {
FullLink string `json:"connFullLink"`
ShortLink string `json:"connShortLink,omitempty"`
}
// Contact is a person connected to the bot.
Contact struct {
ContactID int64 `json:"contactId"`
}
// NewChatItems is the record of a newChatItems event: messages
// received, or sent from this profile elsewhere.
NewChatItems struct {
ChatItems []AChatItem `json:"chatItems"`
}
// ContactConnected is the record of a contactConnected event.
ContactConnected struct {
Contact Contact `json:"contact"`
}
// AChatItem is one message 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"`
}
// MsgContent is a message body. Only "text" is sent or read here.
MsgContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
// AddressSettings configures the bot's long-term address.
AddressSettings struct {
BusinessAddress bool `json:"businessAddress"`
AutoAccept *AutoAccept `json:"autoAccept,omitempty"`
AutoReply *MsgContent `json:"autoReply,omitempty"`
}
// AutoAccept makes the chat client accept every contact request
// to the address itself, when present in AddressSettings.
AutoAccept struct {
AcceptIncognito bool `json:"acceptIncognito"`
}
composedMessage struct {
QuotedItemID int64 `json:"quotedItemId,omitempty"`
MsgContent MsgContent `json:"msgContent"`
Mentions map[string]int64 `json:"mentions"`
}
envelope struct {
CorrID string `json:"corrId,omitempty"`
Resp json.RawMessage `json:"resp"`
}
command struct {
CorrID string `json:"corrId"`
Cmd string `json:"cmd"`
}
cmdError struct {
ChatError struct {
Type string `json:"type"`
ErrorType *tagged `json:"errorType,omitempty"`
StoreError *tagged `json:"storeError,omitempty"`
AgentError *tagged `json:"agentError,omitempty"`
} `json:"chatError"`
}
tagged struct {
Type string `json:"type"`
}
)
// Message is a text message a contact sent to the bot.
type Message struct {
ContactID int64
ItemID int64
Text string
}
// Message returns the text message a contact sent in a direct chat, and
// false for anything else: group messages, files, the bot's own
// messages, and the event items the client records in a chat.
func (a AChatItem) Message() (Message, bool) {
item := a.ChatItem
if a.ChatInfo.Type != "direct" || a.ChatInfo.Contact == nil ||
item.ChatDir.Type != "directRcv" || item.Content.Type != "rcvMsgContent" ||
item.Content.MsgContent == nil || item.Content.MsgContent.Type != "text" {
return Message{}, false
}
return Message{
ContactID: a.ChatInfo.Contact.ContactID,
ItemID: item.Meta.ItemID,
Text: item.Content.MsgContent.Text,
}, true
}
// Command strings. Their syntax is documented per command in the
// simplex-chat repository, bots/api/COMMANDS.md.
const cmdShowActiveUser = "/user"
func cmdShowAddress(userID int64) string {
return "/_show_address " + strconv.FormatInt(userID, 10)
}
func cmdCreateAddress(userID int64) string {
return "/_address " + strconv.FormatInt(userID, 10)
}
func cmdSetAddressSettings(userID int64, s AddressSettings) (string, error) {
b, err := json.Marshal(s)
if err != nil {
return "", fmt.Errorf("encoding address settings: %w", err)
}
return "/_address_settings " + strconv.FormatInt(userID, 10) + " " + string(b), nil
}
func cmdSendText(contactID, quotedItemID int64, text string) (string, error) {
b, err := json.Marshal([]composedMessage{{
QuotedItemID: quotedItemID,
MsgContent: MsgContent{Type: "text", Text: text},
Mentions: map[string]int64{},
}})
if err != nil {
return "", fmt.Errorf("encoding message: %w", err)
}
return "/_send @" + strconv.FormatInt(contactID, 10) + " json " + string(b), nil
}