Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
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}