717 lines
16 KiB
Go
717 lines
16 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"g.fc.run/misc/byre/internal/agents"
|
|
"g.fc.run/misc/byre/internal/api"
|
|
"g.fc.run/misc/byre/internal/git"
|
|
"g.fc.run/misc/byre/internal/store"
|
|
"g.fc.run/misc/byre/internal/todo"
|
|
)
|
|
|
|
type Server struct {
|
|
store *store.Store
|
|
agents *agents.Manager
|
|
hub *hub
|
|
terminals *terminals
|
|
startedAt time.Time
|
|
spa fs.FS
|
|
|
|
usersMu sync.Mutex
|
|
users map[string]string
|
|
}
|
|
|
|
type ctxKey struct{}
|
|
|
|
func New(dataDir string, portFirst int, spa fs.FS) (*Server, error) {
|
|
err := os.MkdirAll(dataDir, 0o755)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &Server{
|
|
hub: newHub(),
|
|
terminals: newTerminals(),
|
|
startedAt: time.Now().UTC(),
|
|
spa: spa,
|
|
users: map[string]string{},
|
|
}
|
|
|
|
s.store, err = store.Open(filepath.Join(dataDir, "byre.db"), portFirst, s.publish)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.agents = agents.New(s.store, filepath.Join(dataDir, "worktrees"), filepath.Join(dataDir, "leads"), filepath.Join(dataDir, "codex"))
|
|
err = s.agents.Recover(context.Background())
|
|
if err != nil {
|
|
s.store.Close()
|
|
return nil, fmt.Errorf("recover sessions: %w", err)
|
|
}
|
|
go s.agents.Watch()
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Server) publish(ev api.Event) {
|
|
s.hub.publish(ev)
|
|
s.agents.Observe(ev)
|
|
if (ev.Kind == "task.merged" || ev.Kind == "task.discarded") && ev.TaskId != nil {
|
|
s.terminals.close(*ev.TaskId)
|
|
go s.agents.Unexpose(context.Background(), *ev.TaskId)
|
|
}
|
|
}
|
|
|
|
func (s *Server) Close() error {
|
|
s.terminals.closeAll()
|
|
s.agents.Close()
|
|
return s.store.Close()
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
api.HandlerWithOptions(s, api.StdHTTPServerOptions{
|
|
BaseRouter: mux,
|
|
Middlewares: []api.MiddlewareFunc{s.identify},
|
|
ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
},
|
|
})
|
|
mux.Handle("/", http.FileServerFS(s.spa))
|
|
return mux
|
|
}
|
|
|
|
func (s *Server) identify(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
writeError(w, http.StatusForbidden, err)
|
|
return
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil || !ip.IsLoopback() {
|
|
writeError(w, http.StatusForbidden, fmt.Errorf("request from %s did not arrive via tailscale serve", host))
|
|
return
|
|
}
|
|
|
|
login := r.Header.Get("Tailscale-User-Login")
|
|
name := r.Header.Get("Tailscale-User-Name")
|
|
if login == "" || name == "" {
|
|
writeError(w, http.StatusUnauthorized, errors.New("missing Tailscale-User-Login or Tailscale-User-Name"))
|
|
return
|
|
}
|
|
|
|
err = s.ensureUser(r.Context(), login, name)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, login)))
|
|
})
|
|
}
|
|
|
|
func (s *Server) ensureUser(ctx context.Context, login, name string) error {
|
|
s.usersMu.Lock()
|
|
defer s.usersMu.Unlock()
|
|
|
|
if s.users[login] == name {
|
|
return nil
|
|
}
|
|
|
|
_, err := s.store.EnsureUser(ctx, login, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.users[login] = name
|
|
return nil
|
|
}
|
|
|
|
func actor(r *http.Request) string {
|
|
return r.Context().Value(ctxKey{}).(string)
|
|
}
|
|
|
|
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, u)
|
|
}
|
|
|
|
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
|
|
req := api.UserUpdate{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.GitName) == "" || !strings.Contains(req.GitEmail, "@") {
|
|
writeError(w, http.StatusBadRequest, errors.New("git_name and a valid git_email are required"))
|
|
return
|
|
}
|
|
|
|
u, err := s.store.UpdateUserGit(r.Context(), actor(r), req.GitName, req.GitEmail)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, u)
|
|
}
|
|
|
|
func (s *Server) ListProjects(w http.ResponseWriter, r *http.Request) {
|
|
projects, err := s.store.ListProjects(r.Context())
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, projects)
|
|
}
|
|
|
|
func (s *Server) CreateProject(w http.ResponseWriter, r *http.Request) {
|
|
req := api.ProjectCreate{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
writeError(w, http.StatusBadRequest, errors.New("name is required"))
|
|
return
|
|
}
|
|
|
|
info, err := git.Inspect(req.RepoPath)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
p := api.Project{
|
|
Id: store.NewID(),
|
|
Name: req.Name,
|
|
RepoPath: info.Root,
|
|
DefaultBranch: info.DefaultBranch,
|
|
Remote: info.Remote,
|
|
CreatedBy: actor(r),
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
err = s.store.CreateProject(r.Context(), p)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, p)
|
|
}
|
|
|
|
func (s *Server) GetProject(w http.ResponseWriter, r *http.Request, projectID string) {
|
|
p, err := s.store.GetProject(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, p)
|
|
}
|
|
|
|
func (s *Server) ListTasks(w http.ResponseWriter, r *http.Request, projectID string) {
|
|
_, err := s.store.GetProject(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
tasks, err := s.store.ListTasks(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, tasks)
|
|
}
|
|
|
|
func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID string) {
|
|
req := api.TaskCreate{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Title) == "" {
|
|
writeError(w, http.StatusBadRequest, errors.New("title is required"))
|
|
return
|
|
}
|
|
|
|
p, err := s.store.GetProject(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
t, err := s.agents.CreateTask(r.Context(), p, req.Title, actor(r), p.DefaultBranch, actor(r))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, t)
|
|
}
|
|
|
|
func (s *Server) GetTodo(w http.ResponseWriter, r *http.Request, projectID string) {
|
|
p, err := s.store.GetProject(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
src, found, err := git.ReadFile(p.RepoPath, p.DefaultBranch, todo.File)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, api.Todo{Exists: found, Items: todo.Parse(src)})
|
|
}
|
|
|
|
func (s *Server) StartTodoItem(w http.ResponseWriter, r *http.Request, projectID string) {
|
|
req := api.TodoStart{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Title) == "" {
|
|
writeError(w, http.StatusBadRequest, errors.New("title is required"))
|
|
return
|
|
}
|
|
|
|
p, err := s.store.GetProject(r.Context(), projectID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
model := ""
|
|
if req.Model != nil {
|
|
model = *req.Model
|
|
}
|
|
|
|
t, err := s.agents.StartTodo(r.Context(), p, u, req.Title, model, u.Login)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, t)
|
|
}
|
|
|
|
func (s *Server) GetTask(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
t, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func (s *Server) GetUnmerged(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
t, p, ok := s.activeTask(w, r, taskID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
u, err := git.UnmergedChanges(t.WorktreePath, p.DefaultBranch)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, api.Unmerged{
|
|
Commits: u.Commits,
|
|
Files: u.Files,
|
|
Insertions: u.Insertions,
|
|
Deletions: u.Deletions,
|
|
Uncommitted: u.Uncommitted,
|
|
})
|
|
}
|
|
|
|
func (s *Server) GetCoAuthors(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
_, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
coAuthors, err := s.agents.CoAuthors(r.Context(), taskID, actor(r))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, coAuthors)
|
|
}
|
|
|
|
func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
req := api.MergeTaskJSONRequestBody{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
t, p, ok := s.activeTask(w, r, taskID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
t, err = s.agents.Merge(r.Context(), t, p, u, req.MergeTitle)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, t)
|
|
}
|
|
|
|
func (s *Server) MergeIn(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
t, p, ok := s.activeTask(w, r, taskID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
sess, err := s.agents.MergeIn(r.Context(), t, u, p.DefaultBranch)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, sess)
|
|
}
|
|
|
|
func (s *Server) DiscardTask(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
t, p, ok := s.activeTask(w, r, taskID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
t, err := s.agents.Discard(r.Context(), t, p, actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func (s *Server) activeTask(w http.ResponseWriter, r *http.Request, taskID string) (api.Task, api.Project, bool) {
|
|
t, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return t, api.Project{}, false
|
|
}
|
|
|
|
if t.Status != api.TaskStatusActive {
|
|
writeError(w, http.StatusConflict, fmt.Errorf("task %s is %s", t.Id, t.Status))
|
|
return t, api.Project{}, false
|
|
}
|
|
|
|
p, err := s.store.GetProject(r.Context(), t.ProjectId)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return t, p, false
|
|
}
|
|
return t, p, true
|
|
}
|
|
|
|
func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) {
|
|
c, err := s.store.Counts(r.Context())
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
usage, err := s.agents.Usage(r.Context())
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
health, err := s.agents.Health(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
dbBytes := int64(0)
|
|
for _, suffix := range []string{"", "-wal", "-shm"} {
|
|
fi, err := os.Stat(s.store.Path() + suffix)
|
|
if err == nil {
|
|
dbBytes += fi.Size()
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, api.Status{
|
|
StartedAt: s.startedAt,
|
|
DbBytes: dbBytes,
|
|
LastEventSeq: c.LastEventSeq,
|
|
Projects: c.Projects,
|
|
ActiveTasks: c.ActiveTasks,
|
|
SseClients: s.hub.count(),
|
|
LiveAgents: s.agents.LiveCount(),
|
|
RunningTurns: usage.Running,
|
|
RateLimits: usage.RateLimits,
|
|
|
|
AgentProcesses: health.Processes,
|
|
Zombies: health.Zombies,
|
|
StuckAfterSeconds: int(agents.StuckAfter / time.Second),
|
|
ProtocolErrors: s.agents.ProtocolErrors(),
|
|
})
|
|
}
|
|
|
|
func (s *Server) ListTaskEvents(w http.ResponseWriter, r *http.Request, taskID string, params api.ListTaskEventsParams) {
|
|
_, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
after := int64(0)
|
|
if params.After != nil {
|
|
after = *params.After
|
|
}
|
|
limit := 500
|
|
if params.Limit != nil {
|
|
limit = *params.Limit
|
|
}
|
|
|
|
events, err := s.store.TaskEvents(r.Context(), taskID, after, limit)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, events)
|
|
}
|
|
|
|
func (s *Server) ListSnapshots(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
_, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
snaps, err := s.store.ListSnapshots(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, snaps)
|
|
}
|
|
|
|
func (s *Server) ListPreviews(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
_, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
previews, err := s.store.ListPreviews(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, previews)
|
|
}
|
|
|
|
func (s *Server) ForkSnapshot(w http.ResponseWriter, r *http.Request, taskID string, n int) {
|
|
t, p, ok := s.activeTask(w, r, taskID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
fork, err := s.agents.Fork(r.Context(), t, p, n, u)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, fork)
|
|
}
|
|
|
|
func (s *Server) ListSessions(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
_, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
sessions, err := s.store.ListSessions(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, sessions)
|
|
}
|
|
|
|
func (s *Server) CreateSession(w http.ResponseWriter, r *http.Request, taskID string) {
|
|
req := api.SessionCreate{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Prompt) == "" {
|
|
writeError(w, http.StatusBadRequest, errors.New("prompt is required"))
|
|
return
|
|
}
|
|
|
|
t, err := s.store.GetTask(r.Context(), taskID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
u, err := s.store.GetUser(r.Context(), actor(r))
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
|
|
model := ""
|
|
if req.Model != nil {
|
|
model = *req.Model
|
|
}
|
|
|
|
images, ok := s.images(w, r, req.Images)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sess, err := s.agents.Create(r.Context(), t, u, req.Prompt, model, images...)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, sess)
|
|
}
|
|
|
|
func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
|
sess, err := s.store.GetSession(r.Context(), sessionID)
|
|
if err != nil {
|
|
writeStoreError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, sess)
|
|
}
|
|
|
|
func (s *Server) SendMessage(w http.ResponseWriter, r *http.Request, sessionID string) {
|
|
req := api.MessageCreate{}
|
|
if !readJSON(w, r, &req) {
|
|
return
|
|
}
|
|
|
|
images, ok := s.images(w, r, req.Images)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Text) == "" && len(images) == 0 {
|
|
writeError(w, http.StatusBadRequest, errors.New("text or images are required"))
|
|
return
|
|
}
|
|
|
|
model := ""
|
|
if req.Model != nil {
|
|
model = *req.Model
|
|
}
|
|
|
|
sess, err := s.agents.Message(r.Context(), sessionID, actor(r), req.Text, model, images...)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, sess)
|
|
}
|
|
|
|
func (s *Server) CompactSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
|
sess, err := s.agents.Compact(r.Context(), sessionID)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, sess)
|
|
}
|
|
|
|
func (s *Server) InterruptSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
|
sess, err := s.agents.Interrupt(r.Context(), sessionID)
|
|
if err != nil {
|
|
writeAgentError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, sess)
|
|
}
|
|
|
|
func writeAgentError(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, store.ErrNotFound):
|
|
writeError(w, http.StatusNotFound, err)
|
|
case errors.Is(err, agents.ErrConflict):
|
|
writeError(w, http.StatusConflict, err)
|
|
case errors.Is(err, agents.ErrInvalid), errors.Is(err, agents.ErrNoGitIdentity):
|
|
writeError(w, http.StatusBadRequest, err)
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
}
|
|
}
|
|
|
|
func readJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(v)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Errorf("decode request: %w", err))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
err := json.NewEncoder(w).Encode(v)
|
|
if err != nil {
|
|
slog.Warn("write response", "err", err)
|
|
}
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, err error) {
|
|
writeJSON(w, status, api.Error{Message: err.Error()})
|
|
}
|
|
|
|
func writeStoreError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
}
|