Changeset 0.23.2 (#155)
This commit is contained in:
311
server/handlers/persona_groups.go
Normal file
311
server/handlers/persona_groups.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package handlers
|
||||
|
||||
// persona_groups.go — Persona group (roster template) CRUD (v0.23.2)
|
||||
//
|
||||
// Persona groups are saved collections of personas used as templates
|
||||
// for creating group chats. Each member has an is_leader flag.
|
||||
//
|
||||
// Routes:
|
||||
// GET /api/v1/persona-groups
|
||||
// POST /api/v1/persona-groups
|
||||
// GET /api/v1/persona-groups/:id
|
||||
// PUT /api/v1/persona-groups/:id
|
||||
// DELETE /api/v1/persona-groups/:id
|
||||
// POST /api/v1/persona-groups/:id/members
|
||||
// DELETE /api/v1/persona-groups/:id/members/:memberId
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type PersonaGroupHandler struct{}
|
||||
|
||||
func NewPersonaGroupHandler() *PersonaGroupHandler { return &PersonaGroupHandler{} }
|
||||
|
||||
// ── List ────────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) List(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(`
|
||||
SELECT id, name, description, owner_id, scope, team_id, created_at, updated_at
|
||||
FROM persona_groups
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name
|
||||
`), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list groups"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := []models.PersonaGroup{}
|
||||
for rows.Next() {
|
||||
var g models.PersonaGroup
|
||||
if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.OwnerID,
|
||||
&g.Scope, &g.TeamID, &g.CreatedAt, &g.UpdatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
// Load members for each group
|
||||
for i := range groups {
|
||||
groups[i].Members = h.loadMembers(c, groups[i].ID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"groups": groups})
|
||||
}
|
||||
|
||||
// ── Get ─────────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) Get(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
id := c.Param("id")
|
||||
|
||||
var g models.PersonaGroup
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT id, name, description, owner_id, scope, team_id, created_at, updated_at
|
||||
FROM persona_groups WHERE id = $1 AND owner_id = $2
|
||||
`), id, userID).Scan(&g.ID, &g.Name, &g.Description, &g.OwnerID,
|
||||
&g.Scope, &g.TeamID, &g.CreatedAt, &g.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get group"})
|
||||
return
|
||||
}
|
||||
|
||||
g.Members = h.loadMembers(c, g.ID)
|
||||
c.JSON(http.StatusOK, g)
|
||||
}
|
||||
|
||||
// ── Create ──────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) Create(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=100"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var g models.PersonaGroup
|
||||
if database.IsSQLite() {
|
||||
id := store.NewID()
|
||||
_, err := database.DB.ExecContext(c.Request.Context(), `
|
||||
INSERT INTO persona_groups (id, name, description, owner_id, scope)
|
||||
VALUES (?, ?, ?, ?, 'personal')
|
||||
`, id, strings.TrimSpace(req.Name), req.Description, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create group"})
|
||||
return
|
||||
}
|
||||
database.DB.QueryRowContext(c.Request.Context(), `
|
||||
SELECT id, name, description, owner_id, scope, team_id, created_at, updated_at
|
||||
FROM persona_groups WHERE id = ?
|
||||
`, id).Scan(&g.ID, &g.Name, &g.Description, &g.OwnerID,
|
||||
&g.Scope, &g.TeamID, &g.CreatedAt, &g.UpdatedAt)
|
||||
} else {
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), `
|
||||
INSERT INTO persona_groups (name, description, owner_id, scope)
|
||||
VALUES ($1, $2, $3, 'personal')
|
||||
RETURNING id, name, description, owner_id, scope, team_id, created_at, updated_at
|
||||
`, strings.TrimSpace(req.Name), req.Description, userID).Scan(
|
||||
&g.ID, &g.Name, &g.Description, &g.OwnerID,
|
||||
&g.Scope, &g.TeamID, &g.CreatedAt, &g.UpdatedAt)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create group"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.Members = []models.PersonaGroupMember{}
|
||||
c.JSON(http.StatusCreated, g)
|
||||
}
|
||||
|
||||
// ── Update ──────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) Update(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
var ownerID string
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT owner_id FROM persona_groups WHERE id = $1
|
||||
`), id).Scan(&ownerID)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
|
||||
return
|
||||
}
|
||||
if ownerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not your group"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
UPDATE persona_groups SET name = $1, updated_at = NOW() WHERE id = $2
|
||||
`), strings.TrimSpace(*req.Name), id)
|
||||
}
|
||||
if req.Description != nil {
|
||||
database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
UPDATE persona_groups SET description = $1, updated_at = NOW() WHERE id = $2
|
||||
`), *req.Description, id)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ── Delete ──────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) Delete(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
id := c.Param("id")
|
||||
|
||||
result, err := database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
DELETE FROM persona_groups WHERE id = $1 AND owner_id = $2
|
||||
`), id, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ── Add Member ──────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) AddMember(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
groupID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
PersonaID string `json:"persona_id" binding:"required"`
|
||||
IsLeader bool `json:"is_leader"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
var ownerID string
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT owner_id FROM persona_groups WHERE id = $1
|
||||
`), groupID).Scan(&ownerID)
|
||||
if err != nil || ownerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not your group"})
|
||||
return
|
||||
}
|
||||
|
||||
// If setting as leader, clear existing leader
|
||||
if req.IsLeader {
|
||||
database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
UPDATE persona_group_members SET is_leader = false WHERE group_id = $1
|
||||
`), groupID)
|
||||
}
|
||||
|
||||
if database.IsSQLite() {
|
||||
id := store.NewID()
|
||||
_, err = database.DB.ExecContext(c.Request.Context(), `
|
||||
INSERT INTO persona_group_members (id, group_id, persona_id, is_leader)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (group_id, persona_id) DO UPDATE SET is_leader = excluded.is_leader
|
||||
`, id, groupID, req.PersonaID, req.IsLeader)
|
||||
} else {
|
||||
_, err = database.DB.ExecContext(c.Request.Context(), `
|
||||
INSERT INTO persona_group_members (group_id, persona_id, is_leader)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (group_id, persona_id) DO UPDATE SET is_leader = EXCLUDED.is_leader
|
||||
`, groupID, req.PersonaID, req.IsLeader)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add member"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ── Remove Member ───────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) RemoveMember(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
groupID := c.Param("id")
|
||||
memberID := c.Param("memberId")
|
||||
|
||||
// Verify ownership
|
||||
var ownerID string
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT owner_id FROM persona_groups WHERE id = $1
|
||||
`), groupID).Scan(&ownerID)
|
||||
if err != nil || ownerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not your group"})
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.ExecContext(c.Request.Context(), database.Q(`
|
||||
DELETE FROM persona_group_members WHERE id = $1 AND group_id = $2
|
||||
`), memberID, groupID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────
|
||||
|
||||
func (h *PersonaGroupHandler) loadMembers(c *gin.Context, groupID string) []models.PersonaGroupMember {
|
||||
rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(`
|
||||
SELECT pgm.id, pgm.group_id, pgm.persona_id, pgm.is_leader, pgm.sort_order,
|
||||
COALESCE(p.name, '') AS persona_name,
|
||||
COALESCE(p.handle, '') AS persona_handle,
|
||||
COALESCE(p.avatar, '') AS persona_avatar
|
||||
FROM persona_group_members pgm
|
||||
LEFT JOIN personas p ON p.id = pgm.persona_id
|
||||
WHERE pgm.group_id = $1
|
||||
ORDER BY pgm.is_leader DESC, pgm.sort_order, pgm.id
|
||||
`), groupID)
|
||||
if err != nil {
|
||||
return []models.PersonaGroupMember{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
members := []models.PersonaGroupMember{}
|
||||
for rows.Next() {
|
||||
var m models.PersonaGroupMember
|
||||
if err := rows.Scan(&m.ID, &m.GroupID, &m.PersonaID, &m.IsLeader, &m.SortOrder,
|
||||
&m.PersonaName, &m.PersonaHandle, &m.PersonaAvatar); err != nil {
|
||||
continue
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
return members
|
||||
}
|
||||
Reference in New Issue
Block a user