A community based topic aggregation platform built on atproto
at main 200 lines 5.4 kB view raw
1package userblock 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "strconv" 10 "time" 11 12 "Coves/internal/api/middleware" 13 "Coves/internal/core/userblocks" 14) 15 16// BlockHandler handles user-to-user blocking operations 17type BlockHandler struct { 18 service userblocks.Service 19} 20 21// NewBlockHandler creates a new user block handler 22func NewBlockHandler(service userblocks.Service) *BlockHandler { 23 if service == nil { 24 panic("userblock: NewBlockHandler requires a non-nil service") 25 } 26 return &BlockHandler{ 27 service: service, 28 } 29} 30 31// blockRequest is the input for block/unblock operations. 32type blockRequest struct { 33 Subject string `json:"subject"` 34} 35 36// blockResponse is the output for a successful block operation. 37type blockResponse struct { 38 Block blockRecord `json:"block"` 39} 40 41type blockRecord struct { 42 RecordURI string `json:"recordUri"` 43 RecordCID string `json:"recordCid"` 44} 45 46// unblockResponse is the output for a successful unblock operation. 47type unblockResponse struct { 48 Success bool `json:"success"` 49} 50 51// blockedUsersResponse is the output for the get-blocked-users endpoint. 52type blockedUsersResponse struct { 53 Blocks []blockedUserEntry `json:"blocks"` 54} 55 56type blockedUserEntry struct { 57 BlockedDID string `json:"blockedDid"` 58 RecordURI string `json:"recordUri"` 59 RecordCID string `json:"recordCid"` 60 BlockedAt time.Time `json:"blockedAt"` 61} 62 63// HandleBlock blocks a user 64// POST /xrpc/social.coves.actor.blockUser 65// 66// Request body: { "subject": "did-or-handle" } 67// The subject can be a DID (did:plc:xxx) or a handle. 68func (h *BlockHandler) HandleBlock(w http.ResponseWriter, r *http.Request) { 69 r.Body = http.MaxBytesReader(w, r.Body, 10*1024) 70 71 var req blockRequest 72 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 73 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 74 return 75 } 76 77 if req.Subject == "" { 78 writeError(w, http.StatusBadRequest, "InvalidRequest", "subject is required") 79 return 80 } 81 82 session := middleware.GetOAuthSession(r) 83 if session == nil { 84 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 85 return 86 } 87 88 result, err := h.service.BlockUser(r.Context(), session, req.Subject) 89 if err != nil { 90 handleServiceError(w, err) 91 return 92 } 93 94 writeJSON(w, http.StatusOK, blockResponse{ 95 Block: blockRecord{ 96 RecordURI: result.RecordURI, 97 RecordCID: result.RecordCID, 98 }, 99 }) 100} 101 102// HandleUnblock unblocks a user 103// POST /xrpc/social.coves.actor.unblockUser 104// 105// Request body: { "subject": "did-or-handle" } 106func (h *BlockHandler) HandleUnblock(w http.ResponseWriter, r *http.Request) { 107 r.Body = http.MaxBytesReader(w, r.Body, 10*1024) 108 109 var req blockRequest 110 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 111 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 112 return 113 } 114 115 if req.Subject == "" { 116 writeError(w, http.StatusBadRequest, "InvalidRequest", "subject is required") 117 return 118 } 119 120 session := middleware.GetOAuthSession(r) 121 if session == nil { 122 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 123 return 124 } 125 126 err := h.service.UnblockUser(r.Context(), session, req.Subject) 127 if err != nil { 128 handleServiceError(w, err) 129 return 130 } 131 132 writeJSON(w, http.StatusOK, unblockResponse{Success: true}) 133} 134 135// HandleGetBlocked returns the list of users blocked by the authenticated user 136// GET /xrpc/social.coves.actor.getBlockedUsers?limit=50&offset=0 137func (h *BlockHandler) HandleGetBlocked(w http.ResponseWriter, r *http.Request) { 138 session := middleware.GetOAuthSession(r) 139 if session == nil { 140 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 141 return 142 } 143 144 limit := 50 145 offset := 0 146 147 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 148 parsed, err := strconv.Atoi(limitStr) 149 if err != nil { 150 writeError(w, http.StatusBadRequest, "InvalidRequest", fmt.Sprintf("invalid limit parameter: %q", limitStr)) 151 return 152 } 153 limit = parsed 154 } 155 156 if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" { 157 parsed, err := strconv.Atoi(offsetStr) 158 if err != nil { 159 writeError(w, http.StatusBadRequest, "InvalidRequest", fmt.Sprintf("invalid offset parameter: %q", offsetStr)) 160 return 161 } 162 offset = parsed 163 } 164 165 userDID := session.AccountDID.String() 166 167 blocks, err := h.service.GetBlockedUsers(r.Context(), userDID, limit, offset) 168 if err != nil { 169 handleServiceError(w, err) 170 return 171 } 172 173 entries := make([]blockedUserEntry, 0, len(blocks)) 174 for _, b := range blocks { 175 entries = append(entries, blockedUserEntry{ 176 BlockedDID: b.BlockedDID, 177 RecordURI: b.RecordURI, 178 RecordCID: b.RecordCID, 179 BlockedAt: b.BlockedAt, 180 }) 181 } 182 183 writeJSON(w, http.StatusOK, blockedUsersResponse{Blocks: entries}) 184} 185 186// writeJSON encodes the response to a buffer first, then writes headers and body. 187// This avoids sending a 200 status with a broken/empty body if encoding fails. 188func writeJSON(w http.ResponseWriter, status int, v any) { 189 var buf bytes.Buffer 190 if err := json.NewEncoder(&buf).Encode(v); err != nil { 191 slog.Error("Failed to encode response", "error", err) 192 writeError(w, http.StatusInternalServerError, "InternalServerError", "An internal error occurred") 193 return 194 } 195 w.Header().Set("Content-Type", "application/json") 196 w.WriteHeader(status) 197 if _, err := w.Write(buf.Bytes()); err != nil { 198 slog.Error("Failed to write response", "error", err) 199 } 200}