package handler import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" db "github.com/multica-ai/multica/server/pkg/db/generated" "github.com/multica-ai/multica/server/pkg/protocol" ) // SubscriberResponse is the JSON shape returned for each issue subscriber. type SubscriberResponse struct { IssueID string `json:"issue_id"` UserType string `json:"user_type"` UserID string `json:"user_id"` Reason string `json:"reason"` CreatedAt string `json:"created_at"` } func subscriberToResponse(s db.IssueSubscriber) SubscriberResponse { return SubscriberResponse{ IssueID: uuidToString(s.IssueID), UserType: s.UserType, UserID: uuidToString(s.UserID), Reason: s.Reason, CreatedAt: timestampToString(s.CreatedAt), } } // ListIssueSubscribers returns all subscribers for an issue. func (h *Handler) ListIssueSubscribers(w http.ResponseWriter, r *http.Request) { issueID := chi.URLParam(r, "id") issue, ok := h.loadIssueForUser(w, r, issueID) if !ok { return } subscribers, err := h.Queries.ListIssueSubscribers(r.Context(), issue.ID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list subscribers") return } resp := make([]SubscriberResponse, len(subscribers)) for i, s := range subscribers { resp[i] = subscriberToResponse(s) } writeJSON(w, http.StatusOK, resp) } // SubscribeToIssue subscribes a user to an issue with reason "manual". // If request body contains user_id, subscribes that user; otherwise subscribes the caller. func (h *Handler) SubscribeToIssue(w http.ResponseWriter, r *http.Request) { issueID := chi.URLParam(r, "id") issue, ok := h.loadIssueForUser(w, r, issueID) if !ok { return } // Default to current user as member; allow specifying another user/agent targetUserID := requestUserID(r) targetUserType := "member" var req struct { UserID *string `json:"user_id"` UserType *string `json:"user_type"` } if r.Body != nil { json.NewDecoder(r.Body).Decode(&req) } if req.UserID != nil && *req.UserID != "" { targetUserID = *req.UserID } if req.UserType != nil && *req.UserType != "" { targetUserType = *req.UserType } err := h.Queries.AddIssueSubscriber(r.Context(), db.AddIssueSubscriberParams{ IssueID: issue.ID, UserType: targetUserType, UserID: parseUUID(targetUserID), Reason: "manual", }) if err != nil { writeError(w, http.StatusInternalServerError, "failed to subscribe") return } workspaceID := uuidToString(issue.WorkspaceID) callerID := requestUserID(r) subActorType, subActorID := h.resolveActor(r, callerID, workspaceID) h.publish(protocol.EventSubscriberAdded, workspaceID, subActorType, subActorID, map[string]any{ "issue_id": issueID, "user_type": targetUserType, "user_id": targetUserID, "reason": "manual", }) writeJSON(w, http.StatusOK, map[string]bool{"subscribed": true}) } // UnsubscribeFromIssue removes a user's subscription from an issue. // If request body contains user_id, unsubscribes that user; otherwise unsubscribes the caller. func (h *Handler) UnsubscribeFromIssue(w http.ResponseWriter, r *http.Request) { issueID := chi.URLParam(r, "id") issue, ok := h.loadIssueForUser(w, r, issueID) if !ok { return } targetUserID := requestUserID(r) targetUserType := "member" var req struct { UserID *string `json:"user_id"` UserType *string `json:"user_type"` } if r.Body != nil { json.NewDecoder(r.Body).Decode(&req) } if req.UserID != nil && *req.UserID != "" { targetUserID = *req.UserID } if req.UserType != nil && *req.UserType != "" { targetUserType = *req.UserType } err := h.Queries.RemoveIssueSubscriber(r.Context(), db.RemoveIssueSubscriberParams{ IssueID: issue.ID, UserType: targetUserType, UserID: parseUUID(targetUserID), }) if err != nil { writeError(w, http.StatusInternalServerError, "failed to unsubscribe") return } workspaceID := uuidToString(issue.WorkspaceID) callerID := requestUserID(r) unsubActorType, unsubActorID := h.resolveActor(r, callerID, workspaceID) h.publish(protocol.EventSubscriberRemoved, workspaceID, unsubActorType, unsubActorID, map[string]any{ "issue_id": issueID, "user_type": targetUserType, "user_id": targetUserID, }) writeJSON(w, http.StatusOK, map[string]bool{"subscribed": false}) }