Files
synapbus/internal/api/channels_handler.go
T
Algis DumbrisandClaude Opus 4.6 b9d402c135 feat: add offset-based pagination and advanced filtering to service layer
Add Offset, After, Before date filters to ReadOptions and SearchOptions.
Add Channel name filter to SearchOptions. Introduce PaginatedMessages,
PaginatedChannels, and PaginatedTasks types with total counts. Update
ReadInbox, SearchMessages, and GetChannelMessages to return paginated
results. Add CountInboxMessages, CountSearchMessages, CountChannelMessages,
and CountTasks store methods. Update all callers in MCP tools, REST API
handlers, and search service. Add comprehensive tests for offset
pagination, date filtering, channel name filtering, and combined
filters with pagination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 07:59:33 +02:00

285 lines
8.3 KiB
Go

package api
import (
"encoding/json"
"log/slog"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/synapbus/synapbus/internal/agents"
"github.com/synapbus/synapbus/internal/channels"
"github.com/synapbus/synapbus/internal/messaging"
)
// ChannelsHandler handles REST API requests for channels.
type ChannelsHandler struct {
channelService *channels.Service
agentService *agents.AgentService
msgService *messaging.MessagingService
logger *slog.Logger
}
// NewChannelsHandler creates a new channels handler.
func NewChannelsHandler(channelService *channels.Service, agentService *agents.AgentService, msgService *messaging.MessagingService) *ChannelsHandler {
return &ChannelsHandler{
channelService: channelService,
agentService: agentService,
msgService: msgService,
logger: slog.Default().With("component", "api.channels"),
}
}
// ListChannels handles GET /api/channels.
func (h *ChannelsHandler) ListChannels(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
// Get the first owned agent to list channels visible to it
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
if err != nil {
h.logger.Error("list agents failed", "error", err)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to list agents"))
return
}
var allChannels []*channels.ChannelWithCount
seen := make(map[int64]bool)
for _, agent := range ownedAgents {
chs, err := h.channelService.ListChannels(r.Context(), agent.Name)
if err != nil {
continue
}
for _, ch := range chs {
if !seen[ch.ID] {
seen[ch.ID] = true
allChannels = append(allChannels, ch)
}
}
}
if allChannels == nil {
allChannels = []*channels.ChannelWithCount{}
}
writeJSON(w, http.StatusOK, map[string]any{"channels": allChannels})
}
// GetChannel handles GET /api/channels/{name}.
func (h *ChannelsHandler) GetChannel(w http.ResponseWriter, r *http.Request) {
_, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
name := chi.URLParam(r, "name")
ch, err := h.channelService.GetChannelByName(r.Context(), name)
if err != nil {
writeJSON(w, http.StatusNotFound, errorBody("not_found", "Channel not found"))
return
}
members, err := h.channelService.GetMembers(r.Context(), ch.ID)
if err != nil {
members = []*channels.Membership{}
}
writeJSON(w, http.StatusOK, map[string]any{
"channel": ch,
"members": members,
})
}
// CreateChannel handles POST /api/channels.
func (h *ChannelsHandler) CreateChannel(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Topic string `json:"topic"`
Type string `json:"type"`
IsPrivate bool `json:"is_private"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body"))
return
}
if req.Name == "" {
writeJSON(w, http.StatusBadRequest, errorBody("validation_error", "Channel name is required"))
return
}
// Use the first owned agent as the creator
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
if err != nil || len(ownedAgents) == 0 {
writeJSON(w, http.StatusBadRequest, errorBody("no_agents", "No agents registered. Register an agent first."))
return
}
createReq := channels.CreateChannelRequest{
Name: req.Name,
Description: req.Description,
Topic: req.Topic,
Type: req.Type,
IsPrivate: req.IsPrivate,
CreatedBy: ownedAgents[0].Name,
}
ch, err := h.channelService.CreateChannel(r.Context(), createReq)
if err != nil {
h.logger.Error("create channel failed", "error", err)
writeJSON(w, http.StatusBadRequest, errorBody("create_failed", err.Error()))
return
}
writeJSON(w, http.StatusCreated, ch)
}
// JoinChannel handles POST /api/channels/{name}/join.
func (h *ChannelsHandler) JoinChannel(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
name := chi.URLParam(r, "name")
ch, err := h.channelService.GetChannelByName(r.Context(), name)
if err != nil {
writeJSON(w, http.StatusNotFound, errorBody("not_found", "Channel not found"))
return
}
var req struct {
Agent string `json:"agent"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
// If no agent specified, use the first owned agent
agentName := req.Agent
if agentName == "" {
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
if err != nil || len(ownedAgents) == 0 {
writeJSON(w, http.StatusBadRequest, errorBody("no_agents", "No agents registered"))
return
}
agentName = ownedAgents[0].Name
}
// Verify the agent belongs to this user
agent, err := h.agentService.GetAgent(r.Context(), agentName)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_agent", "Agent not found"))
return
}
if agent.OwnerID != ownerID {
writeJSON(w, http.StatusForbidden, errorBody("forbidden", "You do not own this agent"))
return
}
if err := h.channelService.JoinChannel(r.Context(), ch.ID, agentName); err != nil {
h.logger.Error("join channel failed", "error", err)
writeJSON(w, http.StatusBadRequest, errorBody("join_failed", err.Error()))
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "joined"})
}
// ChannelMessages handles GET /api/channels/{name}/messages.
func (h *ChannelsHandler) ChannelMessages(w http.ResponseWriter, r *http.Request) {
_, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
name := chi.URLParam(r, "name")
ch, err := h.channelService.GetChannelByName(r.Context(), name)
if err != nil {
writeJSON(w, http.StatusNotFound, errorBody("not_found", "Channel not found"))
return
}
limit := 100
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
paginated, err := h.msgService.GetChannelMessages(r.Context(), ch.ID, limit, 0)
if err != nil {
h.logger.Error("get channel messages failed", "error", err)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to get messages"))
return
}
writeJSON(w, http.StatusOK, map[string]any{
"messages": paginated.Messages,
"total": paginated.Total,
})
}
// LeaveChannel handles POST /api/channels/{name}/leave.
func (h *ChannelsHandler) LeaveChannel(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
name := chi.URLParam(r, "name")
ch, err := h.channelService.GetChannelByName(r.Context(), name)
if err != nil {
writeJSON(w, http.StatusNotFound, errorBody("not_found", "Channel not found"))
return
}
var req struct {
Agent string `json:"agent"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
agentName := req.Agent
if agentName == "" {
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
if err != nil || len(ownedAgents) == 0 {
writeJSON(w, http.StatusBadRequest, errorBody("no_agents", "No agents registered"))
return
}
agentName = ownedAgents[0].Name
}
agent, err := h.agentService.GetAgent(r.Context(), agentName)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_agent", "Agent not found"))
return
}
if agent.OwnerID != ownerID {
writeJSON(w, http.StatusForbidden, errorBody("forbidden", "You do not own this agent"))
return
}
if err := h.channelService.LeaveChannel(r.Context(), ch.ID, agentName); err != nil {
h.logger.Error("leave channel failed", "error", err)
writeJSON(w, http.StatusBadRequest, errorBody("leave_failed", err.Error()))
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "left"})
}