tangled
alpha
login
or
join now
margin.at
/
margin
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
76
fork
atom
overview
issues
2
pulls
pipelines
backend improvements
scanash.com
1 week ago
9a4df148
3ec15276
+786
-118
17 changed files
expand all
collapse all
unified
split
.env.example
backend
cmd
server
main.go
internal
api
annotations.go
errors.go
handler.go
hydration.go
config
config.go
db
db.go
queries_annotations.go
queries_bookmarks.go
queries_highlights.go
oauth
client.go
handler.go
xrpc
records.go
utils.go
web
src
api
client.js
pages
Login.jsx
+8
-10
.env.example
···
1
1
-
# Environment Configuration
1
1
+
# Margin Server Configuration
2
2
3
3
# Server
4
4
PORT=8080
5
5
BASE_URL=https://example.com
6
6
7
7
-
# Database
7
7
+
# Database (SQLite file path or PostgreSQL connection string)
8
8
DATABASE_URL=margin.db
9
9
10
10
# Static Files (path to built frontend)
11
11
STATIC_DIR=../web/dist
12
12
13
13
-
# AT Protocol OAuth
14
14
-
OAUTH_CLIENT_ID=https://example.com/client-metadata.json
15
15
-
OAUTH_CALLBACK_URL=https://example.com/auth/callback
13
13
+
# OAuth private key for signing requests (auto-generated if missing)
16
14
OAUTH_KEY_PATH=./oauth_private_key.pem
17
15
18
18
-
# Production Example:
19
19
-
# PORT=443
20
20
-
# BASE_URL=https://margin.at
21
21
-
# OAUTH_CLIENT_ID=https://margin.at/client-metadata.json
22
22
-
# OAUTH_CALLBACK_URL=https://margin.at/auth/callback
16
16
+
17
17
+
# Optional: Override default ATProto network URLs (you probably don't need these)
18
18
+
# BSKY_PUBLIC_API=https://public.api.bsky.app
19
19
+
# PLC_DIRECTORY_URL=https://plc.directory
20
20
+
# BLOCK_RELAY_URL=wss://jetstream2.us-east.bsky.network/subscribe
+9
-6
backend/cmd/server/main.go
···
92
92
r.Put("/api/bookmarks", annotationSvc.UpdateBookmark)
93
93
r.Delete("/api/bookmarks", annotationSvc.DeleteBookmark)
94
94
95
95
-
r.Get("/auth/login", oauthHandler.HandleLogin)
96
96
-
r.Post("/auth/start", oauthHandler.HandleStart)
97
97
-
r.Post("/auth/signup", oauthHandler.HandleSignup)
98
98
-
r.Get("/auth/callback", oauthHandler.HandleCallback)
99
99
-
r.Post("/auth/logout", oauthHandler.HandleLogout)
100
100
-
r.Get("/auth/session", oauthHandler.HandleSession)
95
95
+
r.Route("/auth", func(r chi.Router) {
96
96
+
r.Use(middleware.Throttle(10))
97
97
+
r.Get("/login", oauthHandler.HandleLogin)
98
98
+
r.Post("/start", oauthHandler.HandleStart)
99
99
+
r.Post("/signup", oauthHandler.HandleSignup)
100
100
+
r.Get("/callback", oauthHandler.HandleCallback)
101
101
+
r.Post("/logout", oauthHandler.HandleLogout)
102
102
+
r.Get("/session", oauthHandler.HandleSession)
103
103
+
})
101
104
r.Get("/client-metadata.json", oauthHandler.HandleClientMetadata)
102
105
r.Get("/jwks.json", oauthHandler.HandleJWKS)
103
106
+18
backend/internal/api/annotations.go
···
391
391
return
392
392
}
393
393
394
394
+
if req.SubjectURI == "" || req.SubjectCID == "" {
395
395
+
http.Error(w, "subjectUri and subjectCid are required", http.StatusBadRequest)
396
396
+
return
397
397
+
}
398
398
+
394
399
existingLike, _ := s.db.GetLikeByUserAndSubject(session.DID, req.SubjectURI)
395
400
if existingLike != nil {
396
401
w.Header().Set("Content-Type", "application/json")
···
494
499
var req CreateReplyRequest
495
500
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
496
501
http.Error(w, "Invalid request body", http.StatusBadRequest)
502
502
+
return
503
503
+
}
504
504
+
505
505
+
if req.ParentURI == "" || req.ParentCID == "" {
506
506
+
http.Error(w, "parentUri and parentCid are required", http.StatusBadRequest)
507
507
+
return
508
508
+
}
509
509
+
if req.RootURI == "" || req.RootCID == "" {
510
510
+
http.Error(w, "rootUri and rootCid are required", http.StatusBadRequest)
511
511
+
return
512
512
+
}
513
513
+
if req.Text == "" {
514
514
+
http.Error(w, "text is required", http.StatusBadRequest)
497
515
return
498
516
}
499
517
+58
backend/internal/api/errors.go
···
1
1
+
package api
2
2
+
3
3
+
import (
4
4
+
"encoding/json"
5
5
+
"net/http"
6
6
+
)
7
7
+
8
8
+
type APIError struct {
9
9
+
Error string `json:"error"`
10
10
+
Code string `json:"code,omitempty"`
11
11
+
Details string `json:"details,omitempty"`
12
12
+
}
13
13
+
14
14
+
func WriteJSONError(w http.ResponseWriter, statusCode int, message string) {
15
15
+
w.Header().Set("Content-Type", "application/json")
16
16
+
w.WriteHeader(statusCode)
17
17
+
json.NewEncoder(w).Encode(APIError{Error: message})
18
18
+
}
19
19
+
20
20
+
func WriteJSONErrorWithCode(w http.ResponseWriter, statusCode int, message, code string) {
21
21
+
w.Header().Set("Content-Type", "application/json")
22
22
+
w.WriteHeader(statusCode)
23
23
+
json.NewEncoder(w).Encode(APIError{Error: message, Code: code})
24
24
+
}
25
25
+
26
26
+
func WriteJSON(w http.ResponseWriter, statusCode int, data interface{}) {
27
27
+
w.Header().Set("Content-Type", "application/json")
28
28
+
w.WriteHeader(statusCode)
29
29
+
json.NewEncoder(w).Encode(data)
30
30
+
}
31
31
+
32
32
+
func WriteSuccess(w http.ResponseWriter, data interface{}) {
33
33
+
WriteJSON(w, http.StatusOK, data)
34
34
+
}
35
35
+
36
36
+
func WriteBadRequest(w http.ResponseWriter, message string) {
37
37
+
WriteJSONError(w, http.StatusBadRequest, message)
38
38
+
}
39
39
+
40
40
+
func WriteUnauthorized(w http.ResponseWriter, message string) {
41
41
+
WriteJSONError(w, http.StatusUnauthorized, message)
42
42
+
}
43
43
+
44
44
+
func WriteForbidden(w http.ResponseWriter, message string) {
45
45
+
WriteJSONError(w, http.StatusForbidden, message)
46
46
+
}
47
47
+
48
48
+
func WriteNotFound(w http.ResponseWriter, message string) {
49
49
+
WriteJSONError(w, http.StatusNotFound, message)
50
50
+
}
51
51
+
52
52
+
func WriteConflict(w http.ResponseWriter, message string) {
53
53
+
WriteJSONError(w, http.StatusConflict, message)
54
54
+
}
55
55
+
56
56
+
func WriteInternalError(w http.ResponseWriter, message string) {
57
57
+
WriteJSONError(w, http.StatusInternalServerError, message)
58
58
+
}
+96
-12
backend/internal/api/handler.go
···
168
168
if tag != "" {
169
169
if creator != "" {
170
170
if motivation == "" || motivation == "commenting" {
171
171
-
annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0)
171
171
+
switch feedType {
172
172
+
case "margin":
173
173
+
annotations, _ = h.db.GetMarginAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0)
174
174
+
case "semble":
175
175
+
annotations, _ = h.db.GetSembleAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0)
176
176
+
default:
177
177
+
annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0)
178
178
+
}
172
179
}
173
180
if motivation == "" || motivation == "highlighting" {
174
174
-
highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0)
181
181
+
switch feedType {
182
182
+
case "margin":
183
183
+
highlights, _ = h.db.GetMarginHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0)
184
184
+
case "semble":
185
185
+
highlights, _ = h.db.GetSembleHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0)
186
186
+
default:
187
187
+
highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0)
188
188
+
}
175
189
}
176
190
if motivation == "" || motivation == "bookmarking" {
177
177
-
bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0)
191
191
+
switch feedType {
192
192
+
case "margin":
193
193
+
bookmarks, _ = h.db.GetMarginBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0)
194
194
+
case "semble":
195
195
+
bookmarks, _ = h.db.GetSembleBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0)
196
196
+
default:
197
197
+
bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0)
198
198
+
}
178
199
}
179
200
collectionItems = []db.CollectionItem{}
180
201
} else {
181
202
if motivation == "" || motivation == "commenting" {
182
182
-
annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0)
203
203
+
switch feedType {
204
204
+
case "margin":
205
205
+
annotations, _ = h.db.GetMarginAnnotationsByTag(tag, fetchLimit, 0)
206
206
+
case "semble":
207
207
+
annotations, _ = h.db.GetSembleAnnotationsByTag(tag, fetchLimit, 0)
208
208
+
default:
209
209
+
annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0)
210
210
+
}
183
211
}
184
212
if motivation == "" || motivation == "highlighting" {
185
185
-
highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0)
213
213
+
switch feedType {
214
214
+
case "margin":
215
215
+
highlights, _ = h.db.GetMarginHighlightsByTag(tag, fetchLimit, 0)
216
216
+
case "semble":
217
217
+
highlights, _ = h.db.GetSembleHighlightsByTag(tag, fetchLimit, 0)
218
218
+
default:
219
219
+
highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0)
220
220
+
}
186
221
}
187
222
if motivation == "" || motivation == "bookmarking" {
188
188
-
bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0)
223
223
+
switch feedType {
224
224
+
case "margin":
225
225
+
bookmarks, _ = h.db.GetMarginBookmarksByTag(tag, fetchLimit, 0)
226
226
+
case "semble":
227
227
+
bookmarks, _ = h.db.GetSembleBookmarksByTag(tag, fetchLimit, 0)
228
228
+
default:
229
229
+
bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0)
230
230
+
}
189
231
}
190
232
collectionItems = []db.CollectionItem{}
191
233
}
192
234
} else if creator != "" {
193
235
if motivation == "" || motivation == "commenting" {
194
194
-
annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0)
236
236
+
switch feedType {
237
237
+
case "margin":
238
238
+
annotations, _ = h.db.GetMarginAnnotationsByAuthor(creator, fetchLimit, 0)
239
239
+
case "semble":
240
240
+
annotations, _ = h.db.GetSembleAnnotationsByAuthor(creator, fetchLimit, 0)
241
241
+
default:
242
242
+
annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0)
243
243
+
}
195
244
}
196
245
if motivation == "" || motivation == "highlighting" {
197
197
-
highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0)
246
246
+
switch feedType {
247
247
+
case "margin":
248
248
+
highlights, _ = h.db.GetMarginHighlightsByAuthor(creator, fetchLimit, 0)
249
249
+
case "semble":
250
250
+
highlights, _ = h.db.GetSembleHighlightsByAuthor(creator, fetchLimit, 0)
251
251
+
default:
252
252
+
highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0)
253
253
+
}
198
254
}
199
255
if motivation == "" || motivation == "bookmarking" {
200
200
-
bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0)
256
256
+
switch feedType {
257
257
+
case "margin":
258
258
+
bookmarks, _ = h.db.GetMarginBookmarksByAuthor(creator, fetchLimit, 0)
259
259
+
case "semble":
260
260
+
bookmarks, _ = h.db.GetSembleBookmarksByAuthor(creator, fetchLimit, 0)
261
261
+
default:
262
262
+
bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0)
263
263
+
}
201
264
}
202
265
collectionItems = []db.CollectionItem{}
203
266
} else {
204
267
if motivation == "" || motivation == "commenting" {
205
205
-
annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0)
268
268
+
switch feedType {
269
269
+
case "margin":
270
270
+
annotations, _ = h.db.GetMarginAnnotations(fetchLimit, 0)
271
271
+
case "semble":
272
272
+
annotations, _ = h.db.GetSembleAnnotations(fetchLimit, 0)
273
273
+
default:
274
274
+
annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0)
275
275
+
}
206
276
}
207
277
if motivation == "" || motivation == "highlighting" {
208
208
-
highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0)
278
278
+
switch feedType {
279
279
+
case "margin":
280
280
+
highlights, _ = h.db.GetMarginHighlights(fetchLimit, 0)
281
281
+
case "semble":
282
282
+
highlights, _ = h.db.GetSembleHighlights(fetchLimit, 0)
283
283
+
default:
284
284
+
highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0)
285
285
+
}
209
286
}
210
287
if motivation == "" || motivation == "bookmarking" {
211
211
-
bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0)
288
288
+
switch feedType {
289
289
+
case "margin":
290
290
+
bookmarks, _ = h.db.GetMarginBookmarks(fetchLimit, 0)
291
291
+
case "semble":
292
292
+
bookmarks, _ = h.db.GetSembleBookmarks(fetchLimit, 0)
293
293
+
default:
294
294
+
bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0)
295
295
+
}
212
296
}
213
297
if motivation == "" {
214
298
collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0)
+2
-1
backend/internal/api/hydration.go
···
11
11
"sync"
12
12
"time"
13
13
14
14
+
"margin.at/internal/config"
14
15
"margin.at/internal/constellation"
15
16
"margin.at/internal/db"
16
17
)
···
526
527
q.Add("actors", did)
527
528
}
528
529
529
529
-
resp, err := http.Get("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?" + q.Encode())
530
530
+
resp, err := http.Get(config.Get().BskyGetProfilesURL() + "?" + q.Encode())
530
531
if err != nil {
531
532
log.Printf("Hydration fetch error: %v\n", err)
532
533
return nil, err
+47
backend/internal/config/config.go
···
1
1
+
package config
2
2
+
3
3
+
import (
4
4
+
"os"
5
5
+
"sync"
6
6
+
)
7
7
+
8
8
+
type Config struct {
9
9
+
BskyPublicAPI string
10
10
+
PLCDirectory string
11
11
+
BaseURL string
12
12
+
}
13
13
+
14
14
+
var (
15
15
+
instance *Config
16
16
+
once sync.Once
17
17
+
)
18
18
+
19
19
+
func Get() *Config {
20
20
+
once.Do(func() {
21
21
+
instance = &Config{
22
22
+
BskyPublicAPI: getEnvOrDefault("BSKY_PUBLIC_API", "https://public.api.bsky.app"),
23
23
+
PLCDirectory: getEnvOrDefault("PLC_DIRECTORY_URL", "https://plc.directory"),
24
24
+
BaseURL: os.Getenv("BASE_URL"),
25
25
+
}
26
26
+
})
27
27
+
return instance
28
28
+
}
29
29
+
30
30
+
func getEnvOrDefault(key, defaultValue string) string {
31
31
+
if value := os.Getenv(key); value != "" {
32
32
+
return value
33
33
+
}
34
34
+
return defaultValue
35
35
+
}
36
36
+
37
37
+
func (c *Config) BskyResolveHandleURL(handle string) string {
38
38
+
return c.BskyPublicAPI + "/xrpc/com.atproto.identity.resolveHandle?handle=" + handle
39
39
+
}
40
40
+
41
41
+
func (c *Config) BskyGetProfilesURL() string {
42
42
+
return c.BskyPublicAPI + "/xrpc/app.bsky.actor.getProfiles"
43
43
+
}
44
44
+
45
45
+
func (c *Config) PLCResolveURL(did string) string {
46
46
+
return c.PLCDirectory + "/" + did
47
47
+
}
+2
-2
backend/internal/db/db.go
···
150
150
151
151
db, err := sql.Open(driver, dsn)
152
152
if err != nil {
153
153
-
return nil, err
153
153
+
return nil, fmt.Errorf("failed to open database connection: %w", err)
154
154
}
155
155
156
156
if driver == "sqlite3" {
···
172
172
}
173
173
174
174
if err := db.Ping(); err != nil {
175
175
-
return nil, err
175
175
+
return nil, fmt.Errorf("failed to ping database: %w", err)
176
176
}
177
177
178
178
return &DB{DB: db, driver: driver}, nil
+132
backend/internal/db/queries_annotations.go
···
67
67
return scanAnnotations(rows)
68
68
}
69
69
70
70
+
func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
71
71
+
rows, err := db.Query(db.Rebind(`
72
72
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
73
73
+
FROM annotations
74
74
+
WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
75
75
+
ORDER BY created_at DESC
76
76
+
LIMIT ? OFFSET ?
77
77
+
`), authorDID, limit, offset)
78
78
+
if err != nil {
79
79
+
return nil, err
80
80
+
}
81
81
+
defer rows.Close()
82
82
+
83
83
+
return scanAnnotations(rows)
84
84
+
}
85
85
+
86
86
+
func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
87
87
+
rows, err := db.Query(db.Rebind(`
88
88
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
89
89
+
FROM annotations
90
90
+
WHERE author_did = ? AND uri LIKE '%network.cosmik%'
91
91
+
ORDER BY created_at DESC
92
92
+
LIMIT ? OFFSET ?
93
93
+
`), authorDID, limit, offset)
94
94
+
if err != nil {
95
95
+
return nil, err
96
96
+
}
97
97
+
defer rows.Close()
98
98
+
99
99
+
return scanAnnotations(rows)
100
100
+
}
101
101
+
70
102
func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) {
71
103
rows, err := db.Query(db.Rebind(`
72
104
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
···
98
130
return scanAnnotations(rows)
99
131
}
100
132
133
133
+
func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) {
134
134
+
rows, err := db.Query(db.Rebind(`
135
135
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
136
136
+
FROM annotations
137
137
+
WHERE uri NOT LIKE '%network.cosmik%'
138
138
+
ORDER BY created_at DESC
139
139
+
LIMIT ? OFFSET ?
140
140
+
`), limit, offset)
141
141
+
if err != nil {
142
142
+
return nil, err
143
143
+
}
144
144
+
defer rows.Close()
145
145
+
146
146
+
return scanAnnotations(rows)
147
147
+
}
148
148
+
149
149
+
func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) {
150
150
+
rows, err := db.Query(db.Rebind(`
151
151
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
152
152
+
FROM annotations
153
153
+
WHERE uri LIKE '%network.cosmik%'
154
154
+
ORDER BY created_at DESC
155
155
+
LIMIT ? OFFSET ?
156
156
+
`), limit, offset)
157
157
+
if err != nil {
158
158
+
return nil, err
159
159
+
}
160
160
+
defer rows.Close()
161
161
+
162
162
+
return scanAnnotations(rows)
163
163
+
}
164
164
+
101
165
func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
102
166
pattern := "%\"" + tag + "\"%"
103
167
rows, err := db.Query(db.Rebind(`
···
115
179
return scanAnnotations(rows)
116
180
}
117
181
182
182
+
func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
183
183
+
pattern := "%\"" + tag + "\"%"
184
184
+
rows, err := db.Query(db.Rebind(`
185
185
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
186
186
+
FROM annotations
187
187
+
WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
188
188
+
ORDER BY created_at DESC
189
189
+
LIMIT ? OFFSET ?
190
190
+
`), pattern, limit, offset)
191
191
+
if err != nil {
192
192
+
return nil, err
193
193
+
}
194
194
+
defer rows.Close()
195
195
+
196
196
+
return scanAnnotations(rows)
197
197
+
}
198
198
+
199
199
+
func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
200
200
+
pattern := "%\"" + tag + "\"%"
201
201
+
rows, err := db.Query(db.Rebind(`
202
202
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
203
203
+
FROM annotations
204
204
+
WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
205
205
+
ORDER BY created_at DESC
206
206
+
LIMIT ? OFFSET ?
207
207
+
`), pattern, limit, offset)
208
208
+
if err != nil {
209
209
+
return nil, err
210
210
+
}
211
211
+
defer rows.Close()
212
212
+
213
213
+
return scanAnnotations(rows)
214
214
+
}
215
215
+
118
216
func (db *DB) DeleteAnnotation(uri string) error {
119
217
_, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri)
120
218
return err
···
135
233
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
136
234
FROM annotations
137
235
WHERE author_did = ? AND tags_json LIKE ?
236
236
+
ORDER BY created_at DESC
237
237
+
LIMIT ? OFFSET ?
238
238
+
`), authorDID, pattern, limit, offset)
239
239
+
if err != nil {
240
240
+
return nil, err
241
241
+
}
242
242
+
defer rows.Close()
243
243
+
244
244
+
return scanAnnotations(rows)
245
245
+
}
246
246
+
247
247
+
func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
248
248
+
pattern := "%\"" + tag + "\"%"
249
249
+
rows, err := db.Query(db.Rebind(`
250
250
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
251
251
+
FROM annotations
252
252
+
WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
253
253
+
ORDER BY created_at DESC
254
254
+
LIMIT ? OFFSET ?
255
255
+
`), authorDID, pattern, limit, offset)
256
256
+
if err != nil {
257
257
+
return nil, err
258
258
+
}
259
259
+
defer rows.Close()
260
260
+
261
261
+
return scanAnnotations(rows)
262
262
+
}
263
263
+
264
264
+
func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
265
265
+
pattern := "%\"" + tag + "\"%"
266
266
+
rows, err := db.Query(db.Rebind(`
267
267
+
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
268
268
+
FROM annotations
269
269
+
WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
138
270
ORDER BY created_at DESC
139
271
LIMIT ? OFFSET ?
140
272
`), authorDID, pattern, limit, offset)
+196
backend/internal/db/queries_bookmarks.go
···
54
54
return bookmarks, nil
55
55
}
56
56
57
57
+
func (db *DB) GetMarginBookmarks(limit, offset int) ([]Bookmark, error) {
58
58
+
rows, err := db.Query(db.Rebind(`
59
59
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
60
60
+
FROM bookmarks
61
61
+
WHERE uri NOT LIKE '%network.cosmik%'
62
62
+
ORDER BY created_at DESC
63
63
+
LIMIT ? OFFSET ?
64
64
+
`), limit, offset)
65
65
+
if err != nil {
66
66
+
return nil, err
67
67
+
}
68
68
+
defer rows.Close()
69
69
+
70
70
+
var bookmarks []Bookmark
71
71
+
for rows.Next() {
72
72
+
var b Bookmark
73
73
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
74
74
+
return nil, err
75
75
+
}
76
76
+
bookmarks = append(bookmarks, b)
77
77
+
}
78
78
+
return bookmarks, nil
79
79
+
}
80
80
+
81
81
+
func (db *DB) GetSembleBookmarks(limit, offset int) ([]Bookmark, error) {
82
82
+
rows, err := db.Query(db.Rebind(`
83
83
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
84
84
+
FROM bookmarks
85
85
+
WHERE uri LIKE '%network.cosmik%'
86
86
+
ORDER BY created_at DESC
87
87
+
LIMIT ? OFFSET ?
88
88
+
`), limit, offset)
89
89
+
if err != nil {
90
90
+
return nil, err
91
91
+
}
92
92
+
defer rows.Close()
93
93
+
94
94
+
var bookmarks []Bookmark
95
95
+
for rows.Next() {
96
96
+
var b Bookmark
97
97
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
98
98
+
return nil, err
99
99
+
}
100
100
+
bookmarks = append(bookmarks, b)
101
101
+
}
102
102
+
return bookmarks, nil
103
103
+
}
104
104
+
57
105
func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
58
106
pattern := "%\"" + tag + "\"%"
59
107
rows, err := db.Query(db.Rebind(`
60
108
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
61
109
FROM bookmarks
62
110
WHERE tags_json LIKE ?
111
111
+
ORDER BY created_at DESC
112
112
+
LIMIT ? OFFSET ?
113
113
+
`), pattern, limit, offset)
114
114
+
if err != nil {
115
115
+
return nil, err
116
116
+
}
117
117
+
defer rows.Close()
118
118
+
119
119
+
var bookmarks []Bookmark
120
120
+
for rows.Next() {
121
121
+
var b Bookmark
122
122
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
123
123
+
return nil, err
124
124
+
}
125
125
+
bookmarks = append(bookmarks, b)
126
126
+
}
127
127
+
return bookmarks, nil
128
128
+
}
129
129
+
130
130
+
func (db *DB) GetMarginBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
131
131
+
pattern := "%\"" + tag + "\"%"
132
132
+
rows, err := db.Query(db.Rebind(`
133
133
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
134
134
+
FROM bookmarks
135
135
+
WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
136
136
+
ORDER BY created_at DESC
137
137
+
LIMIT ? OFFSET ?
138
138
+
`), pattern, limit, offset)
139
139
+
if err != nil {
140
140
+
return nil, err
141
141
+
}
142
142
+
defer rows.Close()
143
143
+
144
144
+
var bookmarks []Bookmark
145
145
+
for rows.Next() {
146
146
+
var b Bookmark
147
147
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
148
148
+
return nil, err
149
149
+
}
150
150
+
bookmarks = append(bookmarks, b)
151
151
+
}
152
152
+
return bookmarks, nil
153
153
+
}
154
154
+
155
155
+
func (db *DB) GetSembleBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
156
156
+
pattern := "%\"" + tag + "\"%"
157
157
+
rows, err := db.Query(db.Rebind(`
158
158
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
159
159
+
FROM bookmarks
160
160
+
WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
63
161
ORDER BY created_at DESC
64
162
LIMIT ? OFFSET ?
65
163
`), pattern, limit, offset)
···
104
202
return bookmarks, nil
105
203
}
106
204
205
205
+
func (db *DB) GetMarginBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
206
206
+
pattern := "%\"" + tag + "\"%"
207
207
+
rows, err := db.Query(db.Rebind(`
208
208
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
209
209
+
FROM bookmarks
210
210
+
WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
211
211
+
ORDER BY created_at DESC
212
212
+
LIMIT ? OFFSET ?
213
213
+
`), authorDID, pattern, limit, offset)
214
214
+
if err != nil {
215
215
+
return nil, err
216
216
+
}
217
217
+
defer rows.Close()
218
218
+
219
219
+
var bookmarks []Bookmark
220
220
+
for rows.Next() {
221
221
+
var b Bookmark
222
222
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
223
223
+
return nil, err
224
224
+
}
225
225
+
bookmarks = append(bookmarks, b)
226
226
+
}
227
227
+
return bookmarks, nil
228
228
+
}
229
229
+
230
230
+
func (db *DB) GetSembleBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
231
231
+
pattern := "%\"" + tag + "\"%"
232
232
+
rows, err := db.Query(db.Rebind(`
233
233
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
234
234
+
FROM bookmarks
235
235
+
WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
236
236
+
ORDER BY created_at DESC
237
237
+
LIMIT ? OFFSET ?
238
238
+
`), authorDID, pattern, limit, offset)
239
239
+
if err != nil {
240
240
+
return nil, err
241
241
+
}
242
242
+
defer rows.Close()
243
243
+
244
244
+
var bookmarks []Bookmark
245
245
+
for rows.Next() {
246
246
+
var b Bookmark
247
247
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
248
248
+
return nil, err
249
249
+
}
250
250
+
bookmarks = append(bookmarks, b)
251
251
+
}
252
252
+
return bookmarks, nil
253
253
+
}
254
254
+
107
255
func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
108
256
rows, err := db.Query(db.Rebind(`
109
257
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
110
258
FROM bookmarks
111
259
WHERE author_did = ?
260
260
+
ORDER BY created_at DESC
261
261
+
LIMIT ? OFFSET ?
262
262
+
`), authorDID, limit, offset)
263
263
+
if err != nil {
264
264
+
return nil, err
265
265
+
}
266
266
+
defer rows.Close()
267
267
+
268
268
+
var bookmarks []Bookmark
269
269
+
for rows.Next() {
270
270
+
var b Bookmark
271
271
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
272
272
+
return nil, err
273
273
+
}
274
274
+
bookmarks = append(bookmarks, b)
275
275
+
}
276
276
+
return bookmarks, nil
277
277
+
}
278
278
+
279
279
+
func (db *DB) GetMarginBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
280
280
+
rows, err := db.Query(db.Rebind(`
281
281
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
282
282
+
FROM bookmarks
283
283
+
WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
284
284
+
ORDER BY created_at DESC
285
285
+
LIMIT ? OFFSET ?
286
286
+
`), authorDID, limit, offset)
287
287
+
if err != nil {
288
288
+
return nil, err
289
289
+
}
290
290
+
defer rows.Close()
291
291
+
292
292
+
var bookmarks []Bookmark
293
293
+
for rows.Next() {
294
294
+
var b Bookmark
295
295
+
if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
296
296
+
return nil, err
297
297
+
}
298
298
+
bookmarks = append(bookmarks, b)
299
299
+
}
300
300
+
return bookmarks, nil
301
301
+
}
302
302
+
303
303
+
func (db *DB) GetSembleBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
304
304
+
rows, err := db.Query(db.Rebind(`
305
305
+
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
306
306
+
FROM bookmarks
307
307
+
WHERE author_did = ? AND uri LIKE '%network.cosmik%'
112
308
ORDER BY created_at DESC
113
309
LIMIT ? OFFSET ?
114
310
`), authorDID, limit, offset)
+196
backend/internal/db/queries_highlights.go
···
55
55
return highlights, nil
56
56
}
57
57
58
58
+
func (db *DB) GetMarginHighlights(limit, offset int) ([]Highlight, error) {
59
59
+
rows, err := db.Query(db.Rebind(`
60
60
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
61
61
+
FROM highlights
62
62
+
WHERE uri NOT LIKE '%network.cosmik%'
63
63
+
ORDER BY created_at DESC
64
64
+
LIMIT ? OFFSET ?
65
65
+
`), limit, offset)
66
66
+
if err != nil {
67
67
+
return nil, err
68
68
+
}
69
69
+
defer rows.Close()
70
70
+
71
71
+
var highlights []Highlight
72
72
+
for rows.Next() {
73
73
+
var h Highlight
74
74
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
75
75
+
return nil, err
76
76
+
}
77
77
+
highlights = append(highlights, h)
78
78
+
}
79
79
+
return highlights, nil
80
80
+
}
81
81
+
82
82
+
func (db *DB) GetSembleHighlights(limit, offset int) ([]Highlight, error) {
83
83
+
rows, err := db.Query(db.Rebind(`
84
84
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
85
85
+
FROM highlights
86
86
+
WHERE uri LIKE '%network.cosmik%'
87
87
+
ORDER BY created_at DESC
88
88
+
LIMIT ? OFFSET ?
89
89
+
`), limit, offset)
90
90
+
if err != nil {
91
91
+
return nil, err
92
92
+
}
93
93
+
defer rows.Close()
94
94
+
95
95
+
var highlights []Highlight
96
96
+
for rows.Next() {
97
97
+
var h Highlight
98
98
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
99
99
+
return nil, err
100
100
+
}
101
101
+
highlights = append(highlights, h)
102
102
+
}
103
103
+
return highlights, nil
104
104
+
}
105
105
+
58
106
func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
59
107
pattern := "%\"" + tag + "\"%"
60
108
rows, err := db.Query(db.Rebind(`
61
109
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
62
110
FROM highlights
63
111
WHERE tags_json LIKE ?
112
112
+
ORDER BY created_at DESC
113
113
+
LIMIT ? OFFSET ?
114
114
+
`), pattern, limit, offset)
115
115
+
if err != nil {
116
116
+
return nil, err
117
117
+
}
118
118
+
defer rows.Close()
119
119
+
120
120
+
var highlights []Highlight
121
121
+
for rows.Next() {
122
122
+
var h Highlight
123
123
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
124
124
+
return nil, err
125
125
+
}
126
126
+
highlights = append(highlights, h)
127
127
+
}
128
128
+
return highlights, nil
129
129
+
}
130
130
+
131
131
+
func (db *DB) GetMarginHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
132
132
+
pattern := "%\"" + tag + "\"%"
133
133
+
rows, err := db.Query(db.Rebind(`
134
134
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
135
135
+
FROM highlights
136
136
+
WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
137
137
+
ORDER BY created_at DESC
138
138
+
LIMIT ? OFFSET ?
139
139
+
`), pattern, limit, offset)
140
140
+
if err != nil {
141
141
+
return nil, err
142
142
+
}
143
143
+
defer rows.Close()
144
144
+
145
145
+
var highlights []Highlight
146
146
+
for rows.Next() {
147
147
+
var h Highlight
148
148
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
149
149
+
return nil, err
150
150
+
}
151
151
+
highlights = append(highlights, h)
152
152
+
}
153
153
+
return highlights, nil
154
154
+
}
155
155
+
156
156
+
func (db *DB) GetSembleHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
157
157
+
pattern := "%\"" + tag + "\"%"
158
158
+
rows, err := db.Query(db.Rebind(`
159
159
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
160
160
+
FROM highlights
161
161
+
WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
64
162
ORDER BY created_at DESC
65
163
LIMIT ? OFFSET ?
66
164
`), pattern, limit, offset)
···
105
203
return highlights, nil
106
204
}
107
205
206
206
+
func (db *DB) GetMarginHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
207
207
+
pattern := "%\"" + tag + "\"%"
208
208
+
rows, err := db.Query(db.Rebind(`
209
209
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
210
210
+
FROM highlights
211
211
+
WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
212
212
+
ORDER BY created_at DESC
213
213
+
LIMIT ? OFFSET ?
214
214
+
`), authorDID, pattern, limit, offset)
215
215
+
if err != nil {
216
216
+
return nil, err
217
217
+
}
218
218
+
defer rows.Close()
219
219
+
220
220
+
var highlights []Highlight
221
221
+
for rows.Next() {
222
222
+
var h Highlight
223
223
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
224
224
+
return nil, err
225
225
+
}
226
226
+
highlights = append(highlights, h)
227
227
+
}
228
228
+
return highlights, nil
229
229
+
}
230
230
+
231
231
+
func (db *DB) GetSembleHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
232
232
+
pattern := "%\"" + tag + "\"%"
233
233
+
rows, err := db.Query(db.Rebind(`
234
234
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
235
235
+
FROM highlights
236
236
+
WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
237
237
+
ORDER BY created_at DESC
238
238
+
LIMIT ? OFFSET ?
239
239
+
`), authorDID, pattern, limit, offset)
240
240
+
if err != nil {
241
241
+
return nil, err
242
242
+
}
243
243
+
defer rows.Close()
244
244
+
245
245
+
var highlights []Highlight
246
246
+
for rows.Next() {
247
247
+
var h Highlight
248
248
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
249
249
+
return nil, err
250
250
+
}
251
251
+
highlights = append(highlights, h)
252
252
+
}
253
253
+
return highlights, nil
254
254
+
}
255
255
+
108
256
func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) {
109
257
rows, err := db.Query(db.Rebind(`
110
258
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
···
134
282
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
135
283
FROM highlights
136
284
WHERE author_did = ?
285
285
+
ORDER BY created_at DESC
286
286
+
LIMIT ? OFFSET ?
287
287
+
`), authorDID, limit, offset)
288
288
+
if err != nil {
289
289
+
return nil, err
290
290
+
}
291
291
+
defer rows.Close()
292
292
+
293
293
+
var highlights []Highlight
294
294
+
for rows.Next() {
295
295
+
var h Highlight
296
296
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
297
297
+
return nil, err
298
298
+
}
299
299
+
highlights = append(highlights, h)
300
300
+
}
301
301
+
return highlights, nil
302
302
+
}
303
303
+
304
304
+
func (db *DB) GetMarginHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
305
305
+
rows, err := db.Query(db.Rebind(`
306
306
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
307
307
+
FROM highlights
308
308
+
WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
309
309
+
ORDER BY created_at DESC
310
310
+
LIMIT ? OFFSET ?
311
311
+
`), authorDID, limit, offset)
312
312
+
if err != nil {
313
313
+
return nil, err
314
314
+
}
315
315
+
defer rows.Close()
316
316
+
317
317
+
var highlights []Highlight
318
318
+
for rows.Next() {
319
319
+
var h Highlight
320
320
+
if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
321
321
+
return nil, err
322
322
+
}
323
323
+
highlights = append(highlights, h)
324
324
+
}
325
325
+
return highlights, nil
326
326
+
}
327
327
+
328
328
+
func (db *DB) GetSembleHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
329
329
+
rows, err := db.Query(db.Rebind(`
330
330
+
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
331
331
+
FROM highlights
332
332
+
WHERE author_did = ? AND uri LIKE '%network.cosmik%'
137
333
ORDER BY created_at DESC
138
334
LIMIT ? OFFSET ?
139
335
`), authorDID, limit, offset)
+3
-2
backend/internal/oauth/client.go
···
18
18
19
19
"github.com/go-jose/go-jose/v4"
20
20
"github.com/go-jose/go-jose/v4/jwt"
21
21
+
"margin.at/internal/config"
21
22
)
22
23
23
24
type Client struct {
···
86
87
}
87
88
88
89
func (c *Client) ResolveHandle(ctx context.Context, handle string) (string, error) {
89
89
-
did, err := c.resolveHandleAt(ctx, handle, "https://public.api.bsky.app")
90
90
+
did, err := c.resolveHandleAt(ctx, handle, config.Get().BskyPublicAPI)
90
91
if err == nil {
91
92
return did, nil
92
93
}
···
140
141
func (c *Client) ResolveDIDToPDS(ctx context.Context, did string) (string, error) {
141
142
var docURL string
142
143
if strings.HasPrefix(did, "did:plc:") {
143
143
-
docURL = fmt.Sprintf("https://plc.directory/%s", did)
144
144
+
docURL = config.Get().PLCResolveURL(did)
144
145
} else if strings.HasPrefix(did, "did:web:") {
145
146
domain := strings.TrimPrefix(did, "did:web:")
146
147
docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
+3
-15
backend/internal/oauth/handler.go
···
184
184
}
185
185
186
186
var req struct {
187
187
-
Handle string `json:"handle"`
188
188
-
InviteCode string `json:"invite_code"`
187
187
+
Handle string `json:"handle"`
189
188
}
190
189
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
191
190
http.Error(w, "Invalid request body", http.StatusBadRequest)
···
194
193
195
194
if req.Handle == "" {
196
195
http.Error(w, "Handle is required", http.StatusBadRequest)
197
197
-
return
198
198
-
}
199
199
-
200
200
-
requiredCode := os.Getenv("INVITE_CODE")
201
201
-
if requiredCode != "" && req.InviteCode != requiredCode {
202
202
-
w.Header().Set("Content-Type", "application/json")
203
203
-
w.WriteHeader(http.StatusForbidden)
204
204
-
json.NewEncoder(w).Encode(map[string]string{
205
205
-
"error": "Invite code required",
206
206
-
"code": "invite_required",
207
207
-
})
208
196
return
209
197
}
210
198
···
457
445
Path: "/",
458
446
HttpOnly: true,
459
447
Secure: true,
460
460
-
SameSite: http.SameSiteNoneMode,
448
448
+
SameSite: http.SameSiteLaxMode,
461
449
MaxAge: 86400 * 7,
462
450
})
463
451
···
536
524
func deleteFromPDS(pds, accessToken string, dpopKey *ecdsa.PrivateKey, collection, did, rkey string) {
537
525
538
526
client := xrpc.NewClient(pds, accessToken, dpopKey)
539
539
-
err := client.DeleteRecord(context.Background(), collection, did, rkey)
527
527
+
err := client.DeleteRecord(context.Background(), did, collection, rkey)
540
528
if err != nil {
541
529
log.Printf("Failed to delete orphaned reply from PDS: %v", err)
542
530
} else {
+6
backend/internal/xrpc/records.go
···
242
242
}
243
243
244
244
func (r *ReplyRecord) Validate() error {
245
245
+
if r.Parent.URI == "" || r.Parent.CID == "" {
246
246
+
return fmt.Errorf("parent uri and cid are required")
247
247
+
}
248
248
+
if r.Root.URI == "" || r.Root.CID == "" {
249
249
+
return fmt.Errorf("root uri and cid are required")
250
250
+
}
245
251
if r.Text == "" {
246
252
return fmt.Errorf("text is required")
247
253
}
+3
-2
backend/internal/xrpc/utils.go
···
10
10
"strings"
11
11
"time"
12
12
13
13
+
"margin.at/internal/config"
13
14
"margin.at/internal/slingshot"
14
15
)
15
16
···
100
101
func resolveDIDToPDSDirect(did string) (string, error) {
101
102
var docURL string
102
103
if strings.HasPrefix(did, "did:plc:") {
103
103
-
docURL = fmt.Sprintf("https://plc.directory/%s", did)
104
104
+
docURL = config.Get().PLCResolveURL(did)
104
105
} else if strings.HasPrefix(did, "did:web:") {
105
106
domain := strings.TrimPrefix(did, "did:web:")
106
107
docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
···
161
162
}
162
163
163
164
func resolveHandleDirect(handle string) (string, error) {
164
164
-
url := fmt.Sprintf("https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=%s", handle)
165
165
+
url := config.Get().BskyResolveHandleURL(handle)
165
166
client := &http.Client{
166
167
Timeout: 5 * time.Second,
167
168
}
+2
-30
web/src/api/client.js
···
470
470
return data.did;
471
471
}
472
472
473
473
-
export async function startLogin(handle, inviteCode) {
473
473
+
export async function startLogin(handle) {
474
474
return request(`${AUTH_BASE}/start`, {
475
475
method: "POST",
476
476
-
body: JSON.stringify({ handle, invite_code: inviteCode }),
476
476
+
body: JSON.stringify({ handle }),
477
477
});
478
478
}
479
479
···
502
502
return request(`${API_BASE}/keys/${id}`, { method: "DELETE" });
503
503
}
504
504
505
505
-
export async function describeServer(service) {
506
506
-
const res = await fetch(`${service}/xrpc/com.atproto.server.describeServer`);
507
507
-
if (!res.ok) throw new Error("Failed to describe server");
508
508
-
return res.json();
509
509
-
}
510
505
511
511
-
export async function createAccount(
512
512
-
service,
513
513
-
{ handle, email, password, inviteCode },
514
514
-
) {
515
515
-
const res = await fetch(`${service}/xrpc/com.atproto.server.createAccount`, {
516
516
-
method: "POST",
517
517
-
headers: {
518
518
-
"Content-Type": "application/json",
519
519
-
},
520
520
-
body: JSON.stringify({
521
521
-
handle,
522
522
-
email,
523
523
-
password,
524
524
-
inviteCode,
525
525
-
}),
526
526
-
});
527
527
-
528
528
-
const data = await res.json();
529
529
-
if (!res.ok) {
530
530
-
throw new Error(data.message || data.error || "Failed to create account");
531
531
-
}
532
532
-
return data;
533
533
-
}
+5
-38
web/src/pages/Login.jsx
···
9
9
const { isAuthenticated, user, logout } = useAuth();
10
10
const [showSignUp, setShowSignUp] = useState(false);
11
11
const [handle, setHandle] = useState("");
12
12
-
const [inviteCode, setInviteCode] = useState("");
13
13
-
const [showInviteInput, setShowInviteInput] = useState(false);
14
12
const [suggestions, setSuggestions] = useState([]);
15
13
const [showSuggestions, setShowSuggestions] = useState(false);
16
14
const [loading, setLoading] = useState(false);
17
15
const [error, setError] = useState(null);
18
16
const [selectedIndex, setSelectedIndex] = useState(-1);
19
17
const inputRef = useRef(null);
20
20
-
const inviteRef = useRef(null);
21
18
const suggestionsRef = useRef(null);
22
19
23
20
const [providerIndex, setProviderIndex] = useState(0);
···
143
140
const handleSubmit = async (e) => {
144
141
e.preventDefault();
145
142
if (!handle.trim()) return;
146
146
-
if (showInviteInput && !inviteCode.trim()) return;
147
143
148
144
setLoading(true);
149
145
setError(null);
150
146
151
147
try {
152
152
-
const result = await startLogin(handle.trim(), inviteCode.trim());
148
148
+
const result = await startLogin(handle.trim());
153
149
if (result.authorizationUrl) {
154
150
window.location.href = result.authorizationUrl;
155
151
}
156
152
} catch (err) {
157
153
console.error("Login error:", err);
158
158
-
if (
159
159
-
err.message &&
160
160
-
(err.message.includes("invite_required") ||
161
161
-
err.message.includes("Invite code required"))
162
162
-
) {
163
163
-
setShowInviteInput(true);
164
164
-
setError("Please enter an invite code to continue.");
165
165
-
setTimeout(() => inviteRef.current?.focus(), 100);
166
166
-
} else {
167
167
-
setError(err.message || "Failed to start login");
168
168
-
}
154
154
+
setError(err.message || "Failed to start login");
169
155
setLoading(false);
170
156
}
171
157
};
···
261
247
)}
262
248
</div>
263
249
264
264
-
{showInviteInput && (
265
265
-
<div
266
266
-
className="login-input-wrapper"
267
267
-
style={{ marginTop: "12px", animation: "fadeIn 0.3s ease" }}
268
268
-
>
269
269
-
<input
270
270
-
ref={inviteRef}
271
271
-
type="text"
272
272
-
className="login-input"
273
273
-
placeholder="Enter invite code"
274
274
-
value={inviteCode}
275
275
-
onChange={(e) => setInviteCode(e.target.value)}
276
276
-
autoComplete="off"
277
277
-
disabled={loading}
278
278
-
style={{ borderColor: "var(--accent)" }}
279
279
-
/>
280
280
-
</div>
281
281
-
)}
250
250
+
282
251
283
252
{error && <p className="login-error">{error}</p>}
284
253
···
286
255
type="submit"
287
256
className="btn btn-primary login-submit"
288
257
disabled={
289
289
-
loading || !handle.trim() || (showInviteInput && !inviteCode.trim())
258
258
+
loading || !handle.trim()
290
259
}
291
260
>
292
261
{loading
293
262
? "Connecting..."
294
294
-
: showInviteInput
295
295
-
? "Submit Code"
296
296
-
: "Continue"}
263
263
+
: "Continue"}
297
264
</button>
298
265
299
266
<p className="login-legal">