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 }