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