package handlers import ( "context" "database/sql" "encoding/json" "errors" "fmt" "log" "net/http" "atcr.io/pkg/appview/db" "atcr.io/pkg/appview/middleware" "atcr.io/pkg/atproto" "atcr.io/pkg/auth/oauth" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/gorilla/mux" ) // StarRepositoryHandler handles starring a repository type StarRepositoryHandler struct { DB *sql.DB Directory identity.Directory Refresher *oauth.Refresher } func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get authenticated user from middleware user := middleware.GetUser(r) if user == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Extract parameters vars := mux.Vars(r) handle := vars["handle"] repository := vars["repository"] // Resolve owner's handle to DID ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) if err != nil { log.Printf("StarRepository: Failed to resolve handle %s: %v", handle, err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) return } // Get OAuth session for the authenticated user log.Printf("StarRepository: Getting OAuth session for user DID %s", user.DID) session, err := h.Refresher.GetSession(r.Context(), user.DID) if err != nil { log.Printf("StarRepository: Failed to get OAuth session for %s: %v", user.DID, err) http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized) return } // Get user's PDS client (use indigo's API client which handles DPoP automatically) apiClient := session.APIClient() pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient) // Create star record starRecord := atproto.NewStarRecord(ownerDID, repository) rkey := atproto.StarRecordKey(ownerDID, repository) // Write star record to user's PDS _, err = pdsClient.PutRecord(r.Context(), atproto.StarCollection, rkey, starRecord) if err != nil { log.Printf("StarRepository: Failed to create star record: %v", err) http.Error(w, fmt.Sprintf("Failed to create star: %v", err), http.StatusInternalServerError) return } // Return success w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]bool{"starred": true}) } // UnstarRepositoryHandler handles unstarring a repository type UnstarRepositoryHandler struct { DB *sql.DB Directory identity.Directory Refresher *oauth.Refresher } func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get authenticated user from middleware user := middleware.GetUser(r) if user == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Extract parameters vars := mux.Vars(r) handle := vars["handle"] repository := vars["repository"] // Resolve owner's handle to DID ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) if err != nil { log.Printf("UnstarRepository: Failed to resolve handle %s: %v", handle, err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) return } // Get OAuth session for the authenticated user log.Printf("UnstarRepository: Getting OAuth session for user DID %s", user.DID) session, err := h.Refresher.GetSession(r.Context(), user.DID) if err != nil { log.Printf("UnstarRepository: Failed to get OAuth session for %s: %v", user.DID, err) http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized) return } // Get user's PDS client (use indigo's API client which handles DPoP automatically) apiClient := session.APIClient() pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient) // Delete star record from user's PDS rkey := atproto.StarRecordKey(ownerDID, repository) log.Printf("UnstarRepository: Deleting star record for %s/%s (rkey: %s)", handle, repository, rkey) err = pdsClient.DeleteRecord(r.Context(), atproto.StarCollection, rkey) if err != nil { // If record doesn't exist, still return success (idempotent) if !errors.Is(err, atproto.ErrRecordNotFound) { log.Printf("UnstarRepository: Failed to delete star record: %v", err) http.Error(w, fmt.Sprintf("Failed to delete star: %v", err), http.StatusInternalServerError) return } log.Printf("UnstarRepository: Star record not found (already unstarred)") } // Return success w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"starred": false}) } // CheckStarHandler checks if current user has starred a repository type CheckStarHandler struct { DB *sql.DB Directory identity.Directory Refresher *oauth.Refresher } func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get authenticated user from middleware user := middleware.GetUser(r) if user == nil { // Not authenticated - return not starred w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"starred": false}) return } // Extract parameters vars := mux.Vars(r) handle := vars["handle"] repository := vars["repository"] // Resolve owner's handle to DID ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) if err != nil { log.Printf("CheckStar: Failed to resolve handle %s: %v", handle, err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) return } // Get OAuth session for the authenticated user session, err := h.Refresher.GetSession(r.Context(), user.DID) if err != nil { log.Printf("CheckStar: Failed to get OAuth session for %s: %v", user.DID, err) // No OAuth session - return not starred w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"starred": false}) return } // Get user's PDS client (use indigo's API client which handles DPoP automatically) apiClient := session.APIClient() pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient) // Check if star record exists rkey := atproto.StarRecordKey(ownerDID, repository) _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey) starred := err == nil // Return result w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"starred": starred}) } // GetStatsHandler returns repository statistics type GetStatsHandler struct { DB *sql.DB Directory identity.Directory } func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Extract parameters vars := mux.Vars(r) handle := vars["handle"] repository := vars["repository"] // Resolve owner's handle to DID ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) if err != nil { http.Error(w, "Failed to resolve handle", http.StatusBadRequest) return } // Get repository stats from database stats, err := db.GetRepositoryStats(h.DB, ownerDID, repository) if err != nil { http.Error(w, "Failed to fetch stats", http.StatusInternalServerError) return } // Return stats as JSON w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) } // resolveIdentityToDID is a helper function that resolves a handle or DID to a DID func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) { // Parse as AT identifier (handle or DID) atID, err := syntax.ParseAtIdentifier(identityStr) if err != nil { return "", err } // Resolve to DID via directory ident, err := directory.Lookup(ctx, *atID) if err != nil { return "", err } return ident.DID.String(), nil }