Files
simplexcalc/internal/api/chats.go
T
clawbot 2ce8da766a
check / check (push) Successful in 1m19s
HTTP API: 404 for a chat id the list of chats leaves out (closes #5)
The chat client keeps contact records that GET /api/v1/chats does not
list, such as the bot's own profile (1 on a new profile) and a contact
it creates itself (2), and reads or sends in them when asked. The
message endpoints now look the id up, through chatID in
internal/api/chats.go, in the same list the chats endpoint answers
with, and answer 404 for any id not in it. The webhook endpoints can
call chatID too.

Model: opus-5-5
2026-09-29 04:32:28 +00:00

98 lines
2.5 KiB
Go

package api
import (
"cmp"
"context"
"net/http"
"slices"
"strconv"
"github.com/go-chi/chi/v5"
)
// noSuchChat answers a request naming a chat the bot does not have.
const noSuchChat = "no such chat"
// chat is how the API shows a chat. The bot talks to people only one to
// one, so its chats are its contacts, and a chat's id is its contact's.
type chat struct {
ID int64 `json:"id"`
DisplayName string `json:"display_name"`
ContactDeleted bool `json:"contact_deleted"`
}
// chats returns the bot's chats, ordered by id. GET /api/v1/chats
// answers with this list, and chatID accepts only an id in it.
func (h *handlers) chats(ctx context.Context) ([]chat, error) {
contacts, err := h.client.Contacts(ctx, h.userID)
if err != nil {
return nil, err
}
chats := make([]chat, 0, len(contacts))
for _, c := range contacts {
chats = append(chats, chat{
ID: c.ContactID,
DisplayName: c.Profile.DisplayName,
ContactDeleted: c.Deleted(),
})
}
slices.SortFunc(chats, func(a, b chat) int { return cmp.Compare(a.ID, b.ID) })
return chats, nil
}
// handleChats lists the bot's chats.
func (h *handlers) handleChats() http.HandlerFunc {
type response struct {
Chats []chat `json:"chats"`
}
return func(w http.ResponseWriter, r *http.Request) {
chats, err := h.chats(r.Context())
if err != nil {
h.log.Error("listing the chats", "error", err)
h.respondError(w, http.StatusInternalServerError,
"the chats could not be read")
return
}
h.respond(w, http.StatusOK, response{Chats: chats})
}
}
// chatID returns the chat id in the request's path, if it is one of the
// bot's chats. Otherwise it answers the request itself, 404, or 500 if
// the chats could not be read, and returns false.
//
// The id is looked up rather than passed to the chat client, which also
// keeps contact records that are not chats, such as the bot's own
// profile, and would read or send in them.
func (h *handlers) chatID(w http.ResponseWriter, r *http.Request) (int64, bool) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
h.respondError(w, http.StatusNotFound, noSuchChat)
return 0, false
}
chats, err := h.chats(r.Context())
if err != nil {
h.log.Error("looking up a chat", "error", err)
h.respondError(w, http.StatusInternalServerError,
"the chats could not be read")
return 0, false
}
if !slices.ContainsFunc(chats, func(c chat) bool { return c.ID == id }) {
h.respondError(w, http.StatusNotFound, noSuchChat)
return 0, false
}
return id, true
}