Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 283 lines 7.0 kB view raw
1package api 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "strings" 8 "time" 9 10 "margin.at/internal/db" 11 "margin.at/internal/xrpc" 12) 13 14var pdsClient = &http.Client{Timeout: 10 * time.Second} 15 16func (h *Handler) FetchLatestUserRecords(r *http.Request, did string, collection string, limit int) ([]interface{}, error) { 17 session, err := h.refresher.GetSessionWithAutoRefresh(r) 18 if err != nil { 19 return nil, err 20 } 21 22 var results []interface{} 23 24 err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, _ string) error { 25 url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=%d", client.PDS, did, collection, limit) 26 27 req, _ := http.NewRequestWithContext(r.Context(), "GET", url, nil) 28 req.Header.Set("Authorization", "Bearer "+client.AccessToken) 29 30 resp, err := pdsClient.Do(req) 31 if err != nil { 32 return fmt.Errorf("failed to fetch %s: %w", collection, err) 33 } 34 defer resp.Body.Close() 35 36 if resp.StatusCode != 200 { 37 return fmt.Errorf("XRPC error %d", resp.StatusCode) 38 } 39 40 var output struct { 41 Records []struct { 42 URI string `json:"uri"` 43 CID string `json:"cid"` 44 Value json.RawMessage `json:"value"` 45 } `json:"records"` 46 Cursor string `json:"cursor"` 47 } 48 49 if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { 50 return err 51 } 52 53 for _, rec := range output.Records { 54 parsed, err := parseRecord(did, collection, rec.URI, rec.CID, rec.Value) 55 if err == nil && parsed != nil { 56 switch v := parsed.(type) { 57 case *db.Annotation: 58 h.db.CreateAnnotation(v) 59 case *db.Highlight: 60 h.db.CreateHighlight(v) 61 case *db.Bookmark: 62 h.db.CreateBookmark(v) 63 case *db.APIKey: 64 h.db.CreateAPIKey(v) 65 case *db.Preferences: 66 } 67 results = append(results, parsed) 68 } 69 } 70 return nil 71 }) 72 73 if err != nil { 74 return nil, err 75 } 76 77 return results, nil 78} 79 80func parseRecord(did, collection, uri, cid string, value json.RawMessage) (interface{}, error) { 81 cidPtr := &cid 82 83 switch collection { 84 case xrpc.CollectionAnnotation: 85 var record xrpc.AnnotationRecord 86 if err := json.Unmarshal(value, &record); err != nil { 87 return nil, err 88 } 89 90 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 91 92 targetSource := record.Target.Source 93 94 var targetHash string 95 if targetSource != "" { 96 targetHash = db.HashURL(targetSource) 97 } 98 99 motivation := record.Motivation 100 if motivation == "" { 101 motivation = "commenting" 102 } 103 104 var bodyValuePtr, bodyFormatPtr, bodyURIPtr *string 105 if record.Body != nil { 106 if record.Body.Value != "" { 107 val := record.Body.Value 108 bodyValuePtr = &val 109 } 110 if record.Body.Format != "" { 111 fmt := record.Body.Format 112 bodyFormatPtr = &fmt 113 } 114 } 115 116 var targetTitlePtr, selectorJSONPtr, tagsJSONPtr *string 117 if record.Target.Title != "" { 118 t := record.Target.Title 119 targetTitlePtr = &t 120 } 121 if len(record.Target.Selector) > 0 { 122 selectorStr := string(record.Target.Selector) 123 selectorJSONPtr = &selectorStr 124 } 125 if len(record.Tags) > 0 { 126 tagsBytes, _ := json.Marshal(record.Tags) 127 tagsStr := string(tagsBytes) 128 tagsJSONPtr = &tagsStr 129 } 130 131 return &db.Annotation{ 132 URI: uri, 133 AuthorDID: did, 134 Motivation: motivation, 135 BodyValue: bodyValuePtr, 136 BodyFormat: bodyFormatPtr, 137 BodyURI: bodyURIPtr, 138 TargetSource: targetSource, 139 TargetHash: targetHash, 140 TargetTitle: targetTitlePtr, 141 SelectorJSON: selectorJSONPtr, 142 TagsJSON: tagsJSONPtr, 143 CreatedAt: createdAt, 144 IndexedAt: time.Now(), 145 CID: cidPtr, 146 }, nil 147 148 case xrpc.CollectionHighlight: 149 var record xrpc.HighlightRecord 150 if err := json.Unmarshal(value, &record); err != nil { 151 return nil, err 152 } 153 154 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 155 if createdAt.IsZero() { 156 createdAt = time.Now() 157 } 158 159 var targetHash string 160 if record.Target.Source != "" { 161 targetHash = db.HashURL(record.Target.Source) 162 } 163 164 var titlePtr, selectorJSONPtr, colorPtr, tagsJSONPtr *string 165 if record.Target.Title != "" { 166 t := record.Target.Title 167 titlePtr = &t 168 } 169 if len(record.Target.Selector) > 0 { 170 selectorStr := string(record.Target.Selector) 171 selectorJSONPtr = &selectorStr 172 } 173 if record.Color != "" { 174 c := record.Color 175 colorPtr = &c 176 } 177 if len(record.Tags) > 0 { 178 tagsBytes, _ := json.Marshal(record.Tags) 179 tagsStr := string(tagsBytes) 180 tagsJSONPtr = &tagsStr 181 } 182 return &db.Highlight{ 183 URI: uri, 184 AuthorDID: did, 185 TargetSource: record.Target.Source, 186 TargetHash: targetHash, 187 TargetTitle: titlePtr, 188 SelectorJSON: selectorJSONPtr, 189 Color: colorPtr, 190 TagsJSON: tagsJSONPtr, 191 CreatedAt: createdAt, 192 IndexedAt: time.Now(), 193 CID: cidPtr, 194 }, nil 195 case xrpc.CollectionAPIKey: 196 var record xrpc.APIKeyRecord 197 if err := json.Unmarshal(value, &record); err != nil { 198 return nil, fmt.Errorf("failed to unmarshal api key record: %v", err) 199 } 200 201 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 202 203 apiKey := &db.APIKey{ 204 ID: strings.Split(uri, "/")[len(strings.Split(uri, "/"))-1], 205 OwnerDID: did, 206 Name: record.Name, 207 KeyHash: record.KeyHash, 208 CreatedAt: createdAt, 209 URI: uri, 210 CID: cidPtr, 211 IndexedAt: time.Now(), 212 } 213 214 return apiKey, nil 215 216 case xrpc.CollectionBookmark: 217 var record xrpc.BookmarkRecord 218 if err := json.Unmarshal(value, &record); err != nil { 219 return nil, err 220 } 221 222 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 223 224 var sourceHash string 225 if record.Source != "" { 226 sourceHash = db.HashURL(record.Source) 227 } 228 229 var titlePtr, descPtr, tagsJSONPtr *string 230 if record.Title != "" { 231 t := record.Title 232 titlePtr = &t 233 } 234 if record.Description != "" { 235 d := record.Description 236 descPtr = &d 237 } 238 if len(record.Tags) > 0 { 239 tagsBytes, _ := json.Marshal(record.Tags) 240 tagsStr := string(tagsBytes) 241 tagsJSONPtr = &tagsStr 242 } 243 244 return &db.Bookmark{ 245 URI: uri, 246 AuthorDID: did, 247 Source: record.Source, 248 SourceHash: sourceHash, 249 Title: titlePtr, 250 Description: descPtr, 251 TagsJSON: tagsJSONPtr, 252 CreatedAt: createdAt, 253 IndexedAt: time.Now(), 254 CID: cidPtr, 255 }, nil 256 257 case xrpc.CollectionPreferences: 258 var record xrpc.PreferencesRecord 259 if err := json.Unmarshal(value, &record); err != nil { 260 return nil, err 261 } 262 263 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) 264 265 var skippedHostnamesJSONPtr *string 266 if len(record.ExternalLinkSkippedHostnames) > 0 { 267 b, _ := json.Marshal(record.ExternalLinkSkippedHostnames) 268 s := string(b) 269 skippedHostnamesJSONPtr = &s 270 } 271 272 return &db.Preferences{ 273 URI: uri, 274 AuthorDID: did, 275 ExternalLinkSkippedHostnames: skippedHostnamesJSONPtr, 276 CreatedAt: createdAt, 277 IndexedAt: time.Now(), 278 CID: cidPtr, 279 }, nil 280 } 281 282 return nil, nil 283}