package message import ( "database/sql" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-chi/chi/v5" ) type FeatureRequestHandler struct { db *sql.DB } func NewFeatureRequestHandler(db *sql.DB) *FeatureRequestHandler { return &FeatureRequestHandler{db: db} } func (h *FeatureRequestHandler) RegisterRoutes(r chi.Router) { r.Get("/", h.List) r.Post("/", h.Create) r.Post("/{id}/vote", h.Vote) r.Delete("/{id}/vote", h.Unvote) r.Patch("/{id}/status", h.UpdateStatus) } type featureRequestResponse struct { ID string `json:"id"` ServerID string `json:"server_id"` ChannelID string `json:"channel_id"` AuthorID string `json:"author_id"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` VoteCount int `json:"vote_count"` Voters []string `json:"voters"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } func (h *FeatureRequestHandler) List(w http.ResponseWriter, r *http.Request) { serverID := chi.URLParam(r, "serverID") if serverID == "" { http.Error(w, `{"error":"serverID required"}`, http.StatusBadRequest) return } rows, err := h.db.QueryContext(r.Context(), ` SELECT fr.id, fr.server_id, fr.channel_id, fr.author_id, fr.title, fr.description, fr.status, fr.created_at, fr.updated_at, COALESCE((SELECT count(*) FROM feature_request_votes frv WHERE frv.feature_request_id = fr.id), 0) as vote_count FROM feature_requests fr WHERE fr.server_id = $1 ORDER BY vote_count DESC, fr.created_at DESC `, serverID) if err != nil { http.Error(w, `{"error":"query failed"}`, http.StatusInternalServerError) return } defer rows.Close() var results []featureRequestResponse for rows.Next() { var fr featureRequestResponse if err := rows.Scan(&fr.ID, &fr.ServerID, &fr.ChannelID, &fr.AuthorID, &fr.Title, &fr.Description, &fr.Status, &fr.CreatedAt, &fr.UpdatedAt, &fr.VoteCount); err != nil { continue } // Fetch voters voterRows, _ := h.db.QueryContext(r.Context(), ` SELECT user_id FROM feature_request_votes WHERE feature_request_id = $1 `, fr.ID) if voterRows != nil { for voterRows.Next() { var vid string if voterRows.Scan(&vid) == nil { fr.Voters = append(fr.Voters, vid) } } voterRows.Close() } if fr.Voters == nil { fr.Voters = []string{} } results = append(results, fr) } if results == nil { results = []featureRequestResponse{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(results) } type createFeatureRequest struct { Title string `json:"title"` Description string `json:"description"` ChannelID string `json:"channel_id"` } func (h *FeatureRequestHandler) Create(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") if serverID == "" { http.Error(w, `{"error":"serverID required"}`, http.StatusBadRequest) return } var req createFeatureRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Title == "" { http.Error(w, `{"error":"title required"}`, http.StatusBadRequest) return } if req.ChannelID == "" { // Default to the first text channel in the server h.db.QueryRowContext(r.Context(), ` SELECT id FROM channels WHERE server_id = $1 AND type = 'text' ORDER BY position LIMIT 1 `, serverID).Scan(&req.ChannelID) } var fr featureRequestResponse err := h.db.QueryRowContext(r.Context(), ` INSERT INTO feature_requests (server_id, channel_id, author_id, title, description) VALUES ($1, $2, $3, $4, $5) RETURNING id, server_id, channel_id, author_id, title, description, status, created_at, updated_at `, serverID, req.ChannelID, userID, req.Title, req.Description).Scan( &fr.ID, &fr.ServerID, &fr.ChannelID, &fr.AuthorID, &fr.Title, &fr.Description, &fr.Status, &fr.CreatedAt, &fr.UpdatedAt, ) if err != nil { http.Error(w, `{"error":"failed to create feature request"}`, http.StatusInternalServerError) return } fr.VoteCount = 0 fr.Voters = []string{} w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(fr) } func (h *FeatureRequestHandler) Vote(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } id := chi.URLParam(r, "id") _, err := h.db.ExecContext(r.Context(), ` INSERT INTO feature_request_votes (feature_request_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING `, id, userID) if err != nil { http.Error(w, `{"error":"vote failed"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"ok":true}`)) } func (h *FeatureRequestHandler) Unvote(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } id := chi.URLParam(r, "id") h.db.ExecContext(r.Context(), ` DELETE FROM feature_request_votes WHERE feature_request_id = $1 AND user_id = $2 `, id, userID) w.WriteHeader(http.StatusOK) w.Write([]byte(`{"ok":true}`)) } func (h *FeatureRequestHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req struct { Status string `json:"status"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } validStatuses := map[string]bool{"open": true, "planned": true, "done": true, "rejected": true} if !validStatuses[req.Status] { http.Error(w, `{"error":"invalid status"}`, http.StatusBadRequest) return } _, err := h.db.ExecContext(r.Context(), ` UPDATE feature_requests SET status = $1, updated_at = NOW() WHERE id = $2 `, req.Status, id) if err != nil { http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"ok":true}`)) }