Files
so-many-crows/nestapi.go
T

185 lines
4.9 KiB
Go

package main
import (
"fmt"
"net/http"
"slices"
"strings"
"time"
"unicode/utf8"
)
var crowPostable = map[string]bool{
"say": true,
"request": true,
"claim": true,
"deliver": true,
"refer": true,
}
func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) {
nestID := r.PathValue("nest_id")
if _, found := s.nests.Get(nestID); !found {
httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", nestID))
return
}
all := s.msgs.All(nestID)
out := make([]map[string]any, 0, len(all))
for _, m := range all {
out = append(out, map[string]any{
"msg_id": m.MsgID(),
"kind": m.Kind,
"author": m.Author,
"text": m.Text,
"to": m.To,
"refers_to": m.RefersTo,
"tagged_by": m.TaggedBy,
"ts": m.Timestamp,
})
}
writeJSON(w, http.StatusOK, out)
}
func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
nestID := r.PathValue("nest_id")
nest, found := s.nests.Get(nestID)
if !found {
httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", nestID))
return
}
var body struct {
Author string `json:"author"`
Text string `json:"text"`
Kind string `json:"kind"`
To string `json:"to"`
RefersTo string `json:"refers_to"`
}
if !decode(w, r, &body) {
return
}
body.Text = strings.TrimSpace(body.Text)
body.To = strings.TrimSpace(body.To)
// Guardrails (EVENTS.md: Nest messages → Guardrails)
if n := utf8.RuneCountInString(body.Text); n < 1 || n > s.p.G.MaxItemChars {
httpError(w, http.StatusBadRequest, fmt.Sprintf("A message is 1 to %d characters.", s.p.G.MaxItemChars))
return
}
if body.Author == "owner" {
// owner can always post
} else if !slices.Contains(nest.Crows, body.Author) {
httpError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a member of %s.", body.Author, nestID))
return
}
if body.Author != "owner" {
if ok, msg, wait := s.p.Ledger.Admit(body.Author, time.Now(), s.p.G); !ok {
cooldown := int(wait.Seconds())
writeJSON(w, http.StatusTooManyRequests, map[string]any{"message": msg, "cooldown_s": cooldown})
return
}
}
if body.Kind != "" && body.Author != "owner" && !crowPostable[body.Kind] {
httpError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a kind crows may post.", body.Kind))
return
}
// Tagging (A1, EVENTS.md): Jev first, header fallback, say as last resort.
kind, taggedBy := s.tagKind(body.Kind, body.Text)
msg := Message{
NestID: nestID,
Kind: kind,
Author: body.Author,
Text: body.Text,
To: body.To,
RefersTo: body.RefersTo,
TaggedBy: taggedBy,
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
record, err := s.msgs.Append(msg)
if err != nil {
httpError(w, http.StatusInternalServerError, "could not save the message: "+err.Error())
return
}
s.emitNestMessage(record)
// Event-driven trigger: if kind is request and To is a crow, respond.
if kind == "request" && record.To != "" && record.To != "owner" {
go s.crowResponds(nest, record)
}
writeJSON(w, http.StatusCreated, map[string]any{
"msg_id": record.MsgID(),
"kind": record.Kind,
"author": record.Author,
"text": record.Text,
"to": record.To,
"refers_to": record.RefersTo,
"tagged_by": record.TaggedBy,
"ts": record.Timestamp,
})
}
// tagKind runs the A1 rule: Jev first; Jev unsure -> the poster's header;
// neither -> say. tagged_by records which source won, and a Jev override of a
// header logs as a disagreement (fuel for calibration).
func (s *Server) tagKind(header, text string) (string, string) {
kind, _, ok := s.p.Flock.ClassifyKind(s.ctx, text)
if ok {
if crowPostable[header] && header != kind {
return kind, "disagreement"
}
return kind, "jev"
}
if crowPostable[header] {
return header, "header"
}
return "say", "default"
}
func (s *Server) emitNestMessage(m Message) {
s.hub.PublishNest(m.NestID, "nest.message", map[string]any{
"nest_id": m.NestID,
"msg_id": m.MsgID(),
"kind": m.Kind,
"author": m.Author,
"text": m.Text,
"to": m.To,
"refers_to": m.RefersTo,
"tagged_by": m.TaggedBy,
})
}
func (s *Server) crowResponds(nest Nest, req Message) {
crows := s.p.Flock.pick(nest, 1)
if len(crows) == 0 {
return
}
crow := crows[0] // B4 (@-addressing) makes this the addressed crow
system := systemFor(crow, crowSystem, true)
prompt := "NEST: " + nest.Name + "\nREQUEST FROM: " + req.Author + "\nTEXT: " + req.Text
budget := &Budget{name: "nest-message", limit: s.p.G.TokenBudgetPerItem}
answer, err := s.p.Flock.ask(s.ctx, nil, crow, system, prompt, budget)
if err != nil {
return
}
reply := Message{
NestID: nest.ID,
Kind: "say",
Author: crow,
Text: strings.TrimSpace(answer),
To: req.Author,
RefersTo: req.MsgID(),
TaggedBy: "harness",
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
record, err := s.msgs.Append(reply)
if err != nil {
return
}
s.emitNestMessage(record)
}