Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at ui-refactor 150 lines 3.6 kB view raw
1package api 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strings" 9 "time" 10 11 "github.com/go-chi/chi/v5" 12 13 "margin.at/internal/db" 14 "margin.at/internal/xrpc" 15) 16 17type UpdateProfileRequest struct { 18 Bio string `json:"bio"` 19 Website string `json:"website"` 20 Links []string `json:"links"` 21} 22 23func (h *Handler) UpdateProfile(w http.ResponseWriter, r *http.Request) { 24 session, err := h.refresher.GetSessionWithAutoRefresh(r) 25 if err != nil { 26 http.Error(w, err.Error(), http.StatusUnauthorized) 27 return 28 } 29 30 var req UpdateProfileRequest 31 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 32 http.Error(w, "Invalid request body", http.StatusBadRequest) 33 return 34 } 35 36 record := &xrpc.MarginProfileRecord{ 37 Type: xrpc.CollectionProfile, 38 Bio: req.Bio, 39 Website: req.Website, 40 Links: req.Links, 41 CreatedAt: time.Now().UTC().Format(time.RFC3339), 42 } 43 44 if err := record.Validate(); err != nil { 45 http.Error(w, err.Error(), http.StatusBadRequest) 46 return 47 } 48 49 err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 50 _, err := client.PutRecord(r.Context(), did, xrpc.CollectionProfile, "self", record) 51 return err 52 }) 53 54 if err != nil { 55 http.Error(w, "Failed to update profile: "+err.Error(), http.StatusInternalServerError) 56 return 57 } 58 59 linksJSON, _ := json.Marshal(req.Links) 60 profile := &db.Profile{ 61 URI: fmt.Sprintf("at://%s/%s/self", session.DID, xrpc.CollectionProfile), 62 AuthorDID: session.DID, 63 Bio: &req.Bio, 64 Website: &req.Website, 65 LinksJSON: stringPtr(string(linksJSON)), 66 CreatedAt: time.Now(), 67 IndexedAt: time.Now(), 68 } 69 h.db.UpsertProfile(profile) 70 71 w.Header().Set("Content-Type", "application/json") 72 w.WriteHeader(http.StatusOK) 73 json.NewEncoder(w).Encode(req) 74} 75 76func stringPtr(s string) *string { 77 return &s 78} 79 80func (h *Handler) GetProfile(w http.ResponseWriter, r *http.Request) { 81 did := chi.URLParam(r, "did") 82 if decoded, err := url.QueryUnescape(did); err == nil { 83 did = decoded 84 } 85 86 if did == "" { 87 http.Error(w, "DID required", http.StatusBadRequest) 88 return 89 } 90 91 if !strings.HasPrefix(did, "did:") { 92 var resolvedDID string 93 err := h.db.QueryRow("SELECT did FROM sessions WHERE handle = $1 LIMIT 1", did).Scan(&resolvedDID) 94 if err == nil { 95 did = resolvedDID 96 } else { 97 resolvedDID, err = xrpc.ResolveHandle(did) 98 if err == nil { 99 did = resolvedDID 100 } 101 } 102 } 103 104 profile, err := h.db.GetProfile(did) 105 if err != nil { 106 http.Error(w, "Failed to fetch profile", http.StatusInternalServerError) 107 return 108 } 109 110 if profile == nil { 111 w.Header().Set("Content-Type", "application/json") 112 if did != "" && strings.HasPrefix(did, "did:") { 113 json.NewEncoder(w).Encode(map[string]string{"did": did}) 114 } else { 115 w.Write([]byte("{}")) 116 } 117 return 118 } 119 120 resp := struct { 121 URI string `json:"uri"` 122 DID string `json:"did"` 123 Bio string `json:"bio"` 124 Website string `json:"website"` 125 Links []string `json:"links"` 126 CreatedAt string `json:"createdAt"` 127 IndexedAt string `json:"indexedAt"` 128 }{ 129 URI: profile.URI, 130 DID: profile.AuthorDID, 131 CreatedAt: profile.CreatedAt.Format(time.RFC3339), 132 IndexedAt: profile.IndexedAt.Format(time.RFC3339), 133 } 134 135 if profile.Bio != nil { 136 resp.Bio = *profile.Bio 137 } 138 if profile.Website != nil { 139 resp.Website = *profile.Website 140 } 141 if profile.LinksJSON != nil && *profile.LinksJSON != "" { 142 _ = json.Unmarshal([]byte(*profile.LinksJSON), &resp.Links) 143 } 144 if resp.Links == nil { 145 resp.Links = []string{} 146 } 147 148 w.Header().Set("Content-Type", "application/json") 149 json.NewEncoder(w).Encode(resp) 150}