package api import ( "encoding/json" "errors" "io" "net/http" "net/url" "strconv" "time" "sneak.berlin/go/simplexcalc/internal/simplex" ) const ( // defaultCount and maxCount bound how many of a chat's last items a // request for its messages reads. defaultCount = 20 maxCount = 100 ) // message is how the API shows a message, wherever it shows one. type message struct { ID int64 `json:"id"` Direction string `json:"direction"` Type string `json:"type"` Text string `json:"text"` Time time.Time `json:"time"` } // newMessage returns the message a chat item holds, and false for an // item that holds none: the chat client also records events in a chat, // such as the contact connecting. func newMessage(item simplex.ChatItem) (message, bool) { var direction string switch item.Content.Type { case "rcvMsgContent": direction = "received" case "sndMsgContent": direction = "sent" default: return message{}, false } content := item.Content.MsgContent if content == nil { return message{}, false } return message{ ID: item.Meta.ItemID, Direction: direction, Type: content.Type, Text: content.Text, Time: item.Meta.ItemTs, }, true } // handleMessages answers with the messages among a chat's last count // items, oldest first. func (h *handlers) handleMessages() http.HandlerFunc { type response struct { Messages []message `json:"messages"` } return func(w http.ResponseWriter, r *http.Request) { contactID, ok := h.chatID(w, r) if !ok { return } count, ok := messageCount(r) if !ok { h.respondError(w, http.StatusBadRequest, "count must be a whole number from 1 to "+strconv.Itoa(maxCount)) return } items, err := h.client.ChatItems(r.Context(), contactID, count) if err != nil { h.respondChatError(w, err, "reading a chat's messages", "the messages could not be read") return } messages := make([]message, 0, len(items)) for _, item := range items { if m, ok := newMessage(item); ok { messages = append(messages, m) } } h.respond(w, http.StatusOK, response{Messages: messages}) } } // handleSend sends the text in the request's body to a chat, and answers // with the message sent. func (h *handlers) handleSend() http.HandlerFunc { type request struct { Text string `json:"text"` } type response struct { Message message `json:"message"` } return func(w http.ResponseWriter, r *http.Request) { contactID, ok := h.chatID(w, r) if !ok { return } body, err := io.ReadAll(r.Body) var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { h.respondError(w, http.StatusRequestEntityTooLarge, "the body is too large") return } var req request if err != nil || json.Unmarshal(body, &req) != nil { h.respondError(w, http.StatusBadRequest, `the body must be JSON such as {"text":"hello"}`) return } if req.Text == "" { h.respondError(w, http.StatusBadRequest, "text is empty") return } item, err := h.client.SendMessage(r.Context(), contactID, req.Text) if err != nil { h.respondChatError(w, err, "sending a message", "the message could not be sent") return } sent, ok := newMessage(item) if !ok { h.log.Error("sending a message: the answer holds no message", "content_type", item.Content.Type) h.respondError(w, http.StatusInternalServerError, "the chat client's answer could not be read") return } h.respond(w, http.StatusCreated, response{Message: sent}) } } // respondChatError answers a request the chat client did not serve: 404 // for a contact removed after chatID found it, 409 for one who cannot // receive messages, 413 for a text too long to send, and 500 with // sentence for anything else, which is logged as what. func (h *handlers) respondChatError( w http.ResponseWriter, err error, what, sentence string, ) { switch { case errors.Is(err, simplex.ErrNoContact): h.respondError(w, http.StatusNotFound, noSuchChat) case errors.Is(err, simplex.ErrContactNotReady): h.respondError(w, http.StatusConflict, "the contact cannot receive messages") case errors.Is(err, simplex.ErrMessageTooLarge): h.respondError(w, http.StatusRequestEntityTooLarge, "the text is too long") default: h.log.Error(what, "error", err) h.respondError(w, http.StatusInternalServerError, sentence) } } // messageCount returns the request's count, defaultCount if it has none, // and false if it is anything but a whole number from 1 to maxCount. The // query is parsed here because r.URL.Query drops a pair it cannot // decode, which would turn count=1% into the default. func messageCount(r *http.Request) (int, bool) { query, err := url.ParseQuery(r.URL.RawQuery) if err != nil { return 0, false } if !query.Has("count") { return defaultCount, true } count, err := strconv.Atoi(query.Get("count")) return count, err == nil && count >= 1 && count <= maxCount }