(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572package api
import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "sort" "strconv" "strings" "sync" "time"
"github.com/go-chi/chi/v5"
"margin.at/internal/db" internal_sync "margin.at/internal/sync" "margin.at/internal/xrpc")
type Handler struct { db *db.DB annotationService *AnnotationService refresher *TokenRefresher apiKeys *APIKeyHandler syncService *internal_sync.Service moderation *ModerationHandler}
func NewHandler(database *db.DB, annotationService *AnnotationService, refresher *TokenRefresher, syncService *internal_sync.Service) *Handler { return &Handler{ db: database, annotationService: annotationService, refresher: refresher, apiKeys: NewAPIKeyHandler(database, refresher), syncService: syncService, moderation: NewModerationHandler(database, refresher), }}
func (h *Handler) RegisterRoutes(r chi.Router) { r.Get("/health", h.Health)
r.Route("/api", func(r chi.Router) { r.Get("/annotations", h.GetAnnotations) r.Get("/annotations/feed", h.GetFeed) r.Get("/annotation", h.GetAnnotation) r.Get("/annotations/history", h.GetEditHistory) r.Put("/annotations", h.annotationService.UpdateAnnotation)
r.Get("/highlights", h.GetHighlights) r.Put("/highlights", h.annotationService.UpdateHighlight)
r.Get("/bookmarks", h.GetBookmarks) r.Post("/bookmarks", h.annotationService.CreateBookmark) r.Put("/bookmarks", h.annotationService.UpdateBookmark)
collectionService := NewCollectionService(h.db, h.refresher) r.Post("/collections", collectionService.CreateCollection) r.Get("/collections", collectionService.GetCollections) r.Put("/collections", collectionService.UpdateCollection) r.Delete("/collections", collectionService.DeleteCollection) r.Post("/collections/{collection}/items", collectionService.AddCollectionItem) r.Get("/collections/{collection}/items", collectionService.GetCollectionItems) r.Delete("/collections/items", collectionService.RemoveCollectionItem) r.Get("/collections/containing", collectionService.GetAnnotationCollections) r.Get("/collection", collectionService.GetCollection) r.Post("/sync", h.SyncAll)
r.Get("/targets", h.GetByTarget) r.Get("/discover", h.DiscoverForURL)
r.Get("/users/{did}/annotations", h.GetUserAnnotations) r.Get("/users/{did}/highlights", h.GetUserHighlights) r.Get("/users/{did}/bookmarks", h.GetUserBookmarks) r.Get("/users/{did}/targets", h.GetUserTargetItems)
r.Get("/replies", h.GetReplies) r.Get("/likes", h.GetLikeCount) r.Get("/url-metadata", h.GetURLMetadata) r.Get("/notifications", h.GetNotifications) r.Get("/notifications/count", h.GetUnreadNotificationCount) r.Post("/notifications/read", h.MarkNotificationsRead) r.Get("/avatar/{did}", h.HandleAvatarProxy)
r.Post("/keys", h.apiKeys.CreateKey) r.Get("/keys", h.apiKeys.ListKeys) r.Delete("/keys/{id}", h.apiKeys.DeleteKey)
r.Post("/quick/bookmark", h.apiKeys.QuickBookmark) r.Post("/quick/save", h.apiKeys.QuickSave)
r.Get("/preferences", h.GetPreferences) r.Put("/preferences", h.UpdatePreferences)
r.Post("/moderation/block", h.moderation.BlockUser) r.Delete("/moderation/block", h.moderation.UnblockUser) r.Get("/moderation/blocks", h.moderation.GetBlocks) r.Post("/moderation/mute", h.moderation.MuteUser) r.Delete("/moderation/mute", h.moderation.UnmuteUser) r.Get("/moderation/mutes", h.moderation.GetMutes) r.Get("/moderation/relationship", h.moderation.GetRelationship) r.Post("/moderation/report", h.moderation.CreateReport) r.Get("/moderation/admin/check", h.moderation.AdminCheckAccess) r.Get("/moderation/admin/reports", h.moderation.AdminGetReports) r.Get("/moderation/admin/report", h.moderation.AdminGetReport) r.Post("/moderation/admin/action", h.moderation.AdminTakeAction) r.Post("/moderation/admin/label", h.moderation.AdminCreateLabel) r.Delete("/moderation/admin/label", h.moderation.AdminDeleteLabel) r.Get("/moderation/admin/labels", h.moderation.AdminGetLabels) r.Get("/moderation/labeler", h.moderation.GetLabelerInfo) })}
func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok", "version": "1.0"})}
func (h *Handler) GetAnnotations(w http.ResponseWriter, r *http.Request) { source := r.URL.Query().Get("source") if source == "" { source = r.URL.Query().Get("url") }
limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0) motivation := r.URL.Query().Get("motivation") tag := r.URL.Query().Get("tag")
var annotations []db.Annotation var err error
if source != "" { urlHash := db.HashURL(source) annotations, err = h.db.GetAnnotationsByTargetHash(urlHash, limit, offset) } else if motivation != "" { annotations, err = h.db.GetAnnotationsByMotivation(motivation, limit, offset) } else if tag != "" { annotations, err = h.db.GetAnnotationsByTag(tag, limit, offset) } else { annotations, err = h.db.GetRecentAnnotations(limit, offset) }
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "AnnotationCollection", "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0) tag := r.URL.Query().Get("tag") creator := r.URL.Query().Get("creator") feedType := r.URL.Query().Get("type")
viewerDID := h.getViewerDID(r)
if viewerDID != "" && (feedType == "my-feed" || creator == viewerDID) { h.serveUserFeedFromPDS(w, r, viewerDID, tag, r.URL.Query().Get("motivation"), limit, offset) return }
var annotations []db.Annotation var highlights []db.Highlight var bookmarks []db.Bookmark var collectionItems []db.CollectionItem var err error
motivation := r.URL.Query().Get("motivation")
fetchLimit := limit + offset
if tag != "" { if creator != "" { if motivation == "" || motivation == "commenting" { switch feedType { case "margin": annotations, _ = h.db.GetMarginAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) case "semble": annotations, _ = h.db.GetSembleAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) default: annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) } } if motivation == "" || motivation == "highlighting" { switch feedType { case "margin": highlights, _ = h.db.GetMarginHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) case "semble": highlights, _ = h.db.GetSembleHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) default: highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) } } if motivation == "" || motivation == "bookmarking" { switch feedType { case "margin": bookmarks, _ = h.db.GetMarginBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) case "semble": bookmarks, _ = h.db.GetSembleBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) default: bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) } } collectionItems = []db.CollectionItem{} } else { if motivation == "" || motivation == "commenting" { switch feedType { case "margin": annotations, _ = h.db.GetMarginAnnotationsByTag(tag, fetchLimit, 0) case "semble": annotations, _ = h.db.GetSembleAnnotationsByTag(tag, fetchLimit, 0) default: annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0) } } if motivation == "" || motivation == "highlighting" { switch feedType { case "margin": highlights, _ = h.db.GetMarginHighlightsByTag(tag, fetchLimit, 0) case "semble": highlights, _ = h.db.GetSembleHighlightsByTag(tag, fetchLimit, 0) default: highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0) } } if motivation == "" || motivation == "bookmarking" { switch feedType { case "margin": bookmarks, _ = h.db.GetMarginBookmarksByTag(tag, fetchLimit, 0) case "semble": bookmarks, _ = h.db.GetSembleBookmarksByTag(tag, fetchLimit, 0) default: bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0) } } collectionItems = []db.CollectionItem{} } } else if creator != "" { if motivation == "" || motivation == "commenting" { switch feedType { case "margin": annotations, _ = h.db.GetMarginAnnotationsByAuthor(creator, fetchLimit, 0) case "semble": annotations, _ = h.db.GetSembleAnnotationsByAuthor(creator, fetchLimit, 0) default: annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0) } } if motivation == "" || motivation == "highlighting" { switch feedType { case "margin": highlights, _ = h.db.GetMarginHighlightsByAuthor(creator, fetchLimit, 0) case "semble": highlights, _ = h.db.GetSembleHighlightsByAuthor(creator, fetchLimit, 0) default: highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0) } } if motivation == "" || motivation == "bookmarking" { switch feedType { case "margin": bookmarks, _ = h.db.GetMarginBookmarksByAuthor(creator, fetchLimit, 0) case "semble": bookmarks, _ = h.db.GetSembleBookmarksByAuthor(creator, fetchLimit, 0) default: bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0) } } collectionItems = []db.CollectionItem{} } else { if motivation == "" || motivation == "commenting" { switch feedType { case "margin": annotations, _ = h.db.GetMarginAnnotations(fetchLimit, 0) case "semble": annotations, _ = h.db.GetSembleAnnotations(fetchLimit, 0) case "popular": annotations, _ = h.db.GetPopularAnnotations(fetchLimit, 0) case "shelved": annotations, _ = h.db.GetShelvedAnnotations(fetchLimit, 0) default: annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0) } } if motivation == "" || motivation == "highlighting" { switch feedType { case "margin": highlights, _ = h.db.GetMarginHighlights(fetchLimit, 0) case "semble": highlights, _ = h.db.GetSembleHighlights(fetchLimit, 0) case "popular": highlights, _ = h.db.GetPopularHighlights(fetchLimit, 0) case "shelved": highlights, _ = h.db.GetShelvedHighlights(fetchLimit, 0) default: highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0) } } if motivation == "" || motivation == "bookmarking" { switch feedType { case "margin": bookmarks, _ = h.db.GetMarginBookmarks(fetchLimit, 0) case "semble": bookmarks, _ = h.db.GetSembleBookmarks(fetchLimit, 0) case "popular": bookmarks, _ = h.db.GetPopularBookmarks(fetchLimit, 0) case "shelved": bookmarks, _ = h.db.GetShelvedBookmarks(fetchLimit, 0) default: bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0) } } if motivation == "" { switch feedType { case "popular": collectionItems, err = h.db.GetPopularCollectionItems(fetchLimit, 0) case "shelved": collectionItems, err = h.db.GetShelvedCollectionItems(fetchLimit, 0) default: collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0) } if err != nil { log.Printf("Error fetching collection items: %v\n", err) } } }
authAnnos, _ := hydrateAnnotations(h.db, annotations, viewerDID) authHighs, _ := hydrateHighlights(h.db, highlights, viewerDID) authBooks, _ := hydrateBookmarks(h.db, bookmarks, viewerDID)
if len(collectionItems) > 0 { var sembleURIs []string for _, item := range collectionItems { if strings.Contains(item.AnnotationURI, "network.cosmik.card") { sembleURIs = append(sembleURIs, item.AnnotationURI) } } if len(sembleURIs) > 0 { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() ensureSembleCardsIndexed(ctx, h.db, sembleURIs) } }
authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems, viewerDID)
collectionItemURIs := make(map[string]string) for _, ci := range authCollectionItems { var annotationURI string if ci.Annotation != nil { annotationURI = ci.Annotation.ID } else if ci.Highlight != nil { annotationURI = ci.Highlight.ID } else if ci.Bookmark != nil { annotationURI = ci.Bookmark.ID } if annotationURI != "" { collectionItemURIs[annotationURI] = ci.Author.DID } }
var feed []interface{} for _, a := range authAnnos { if addedBy, exists := collectionItemURIs[a.ID]; exists && addedBy == a.Author.DID { continue } feed = append(feed, a) } for _, h := range authHighs { if addedBy, exists := collectionItemURIs[h.ID]; exists && addedBy == h.Author.DID { continue } feed = append(feed, h) } for _, b := range authBooks { if addedBy, exists := collectionItemURIs[b.ID]; exists && addedBy == b.Author.DID { continue } feed = append(feed, b) } for _, ci := range authCollectionItems { feed = append(feed, ci) }
if feedType != "" && feedType != "all" && feedType != "my-feed" { var filtered []interface{} for _, item := range feed { isSemble := false var uri string switch v := item.(type) { case APIAnnotation: uri = v.ID case APIHighlight: uri = v.ID case APIBookmark: uri = v.ID case APICollectionItem: if v.Annotation != nil { uri = v.Annotation.ID } else if v.Highlight != nil { uri = v.Highlight.ID } else if v.Bookmark != nil { uri = v.Bookmark.ID } else { uri = v.ID } } if strings.Contains(uri, "network.cosmik") { isSemble = true }
switch feedType { case "semble": if isSemble { filtered = append(filtered, item) } case "margin": if !isSemble { filtered = append(filtered, item) } case "popular", "shelved": filtered = append(filtered, item) } } feed = filtered }
feed = h.filterFeedByModeration(feed, viewerDID)
switch feedType { case "popular": sortFeedByPopularity(feed) default: sortFeed(feed) }
log.Printf("[DEBUG] FeedType: %s, Total Items before slice: %d", feedType, len(feed)) if len(feed) > 0 { first := feed[0] switch v := first.(type) { case APIAnnotation: log.Printf("[DEBUG] First Item (Annotation): %s, Likes: %d, Replies: %d", v.ID, v.LikeCount, v.ReplyCount) case APIHighlight: log.Printf("[DEBUG] First Item (Highlight): %s, Likes: %d, Replies: %d", v.ID, v.LikeCount, v.ReplyCount) } }
if offset < len(feed) { feed = feed[offset:] } else { feed = []interface{}{} }
if len(feed) > limit { feed = feed[:limit] }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "Collection", "items": feed, "totalItems": len(feed), })}
func (h *Handler) serveUserFeedFromPDS(w http.ResponseWriter, r *http.Request, did, tag, motivation string, limit, offset int) { var wg sync.WaitGroup var rawAnnos, rawHighs, rawBooks []interface{} var errAnnos, errHighs, errBooks error
fetchLimit := limit + offset if fetchLimit < 50 { fetchLimit = 50 }
if motivation == "" || motivation == "commenting" { wg.Add(1) go func() { defer wg.Done() rawAnnos, errAnnos = h.FetchLatestUserRecords(r, did, xrpc.CollectionAnnotation, fetchLimit) }() } if motivation == "" || motivation == "highlighting" { wg.Add(1) go func() { defer wg.Done() rawHighs, errHighs = h.FetchLatestUserRecords(r, did, xrpc.CollectionHighlight, fetchLimit) }() } if motivation == "" || motivation == "bookmarking" { wg.Add(1) go func() { defer wg.Done() rawBooks, errBooks = h.FetchLatestUserRecords(r, did, xrpc.CollectionBookmark, fetchLimit) }() } wg.Wait()
if errAnnos != nil { log.Printf("PDS Fetch Error (Annos): %v", errAnnos) } if errHighs != nil { log.Printf("PDS Fetch Error (Highs): %v", errHighs) } if errBooks != nil { log.Printf("PDS Fetch Error (Books): %v", errBooks) }
var annotations []db.Annotation var highlights []db.Highlight var bookmarks []db.Bookmark
for _, r := range rawAnnos { if a, ok := r.(*db.Annotation); ok { if tag == "" || containsTag(a.TagsJSON, tag) { annotations = append(annotations, *a) } } } for _, r := range rawHighs { if h, ok := r.(*db.Highlight); ok { if tag == "" || containsTag(h.TagsJSON, tag) { highlights = append(highlights, *h) } } } for _, r := range rawBooks { if b, ok := r.(*db.Bookmark); ok { if tag == "" || containsTag(b.TagsJSON, tag) { bookmarks = append(bookmarks, *b) } } }
go func() { for _, a := range annotations { h.db.CreateAnnotation(&a) } for _, hi := range highlights { h.db.CreateHighlight(&hi) } for _, b := range bookmarks { h.db.CreateBookmark(&b) } }()
collectionItems := []db.CollectionItem{} if tag == "" && motivation == "" { items, err := h.db.GetCollectionItemsByAuthor(did) if err != nil { log.Printf("Error fetching collection items for user feed: %v", err) } else { collectionItems = items } }
if len(collectionItems) > 0 { var sembleURIs []string for _, item := range collectionItems { if strings.Contains(item.AnnotationURI, "network.cosmik.card") { sembleURIs = append(sembleURIs, item.AnnotationURI) } } if len(sembleURIs) > 0 { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() ensureSembleCardsIndexed(ctx, h.db, sembleURIs) } }
authAnnos, _ := hydrateAnnotations(h.db, annotations, did) authHighs, _ := hydrateHighlights(h.db, highlights, did) authBooks, _ := hydrateBookmarks(h.db, bookmarks, did) authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems, did)
var feed []interface{} for _, a := range authAnnos { feed = append(feed, a) } for _, h := range authHighs { feed = append(feed, h) } for _, b := range authBooks { feed = append(feed, b) } if motivation == "" { for _, ci := range authCollectionItems { feed = append(feed, ci) } }
sortFeed(feed)
if offset < len(feed) { feed = feed[offset:] } else { feed = []interface{}{} }
if len(feed) > limit { feed = feed[:limit] }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "Collection", "items": feed, "totalItems": len(feed), })
}
func containsTag(tagsJSON *string, tag string) bool { if tagsJSON == nil || *tagsJSON == "" { return false } var tags []string if err := json.Unmarshal([]byte(*tagsJSON), &tags); err != nil { return false } for _, t := range tags { if t == tag { return true } } return false}
func sortFeed(feed []interface{}) { sort.Slice(feed, func(i, j int) bool { t1 := getCreatedAt(feed[i]) t2 := getCreatedAt(feed[j]) return t1.After(t2) })}
func getCreatedAt(item interface{}) time.Time { switch v := item.(type) { case APIAnnotation: return v.CreatedAt case APIHighlight: return v.CreatedAt case APIBookmark: return v.CreatedAt case APICollectionItem: return v.CreatedAt default: return time.Time{} }}
func sortFeedByPopularity(feed []interface{}) { sort.Slice(feed, func(i, j int) bool { p1 := getPopularity(feed[i]) p2 := getPopularity(feed[j]) if p1 != p2 { return p1 > p2 } t1 := getCreatedAt(feed[i]) t2 := getCreatedAt(feed[j]) return t1.After(t2) })}
func getPopularity(item interface{}) int { switch v := item.(type) { case APIAnnotation: return v.LikeCount + v.ReplyCount case APIHighlight: return v.LikeCount + v.ReplyCount case APIBookmark: return v.LikeCount + v.ReplyCount case APICollectionItem: pop := 0 if v.Annotation != nil { pop += v.Annotation.LikeCount + v.Annotation.ReplyCount } if v.Highlight != nil { pop += v.Highlight.LikeCount + v.Highlight.ReplyCount } if v.Bookmark != nil { pop += v.Bookmark.LikeCount + v.Bookmark.ReplyCount } return pop default: return 0 }}
func (h *Handler) GetAnnotation(w http.ResponseWriter, r *http.Request) { uri := r.URL.Query().Get("uri") if uri == "" { http.Error(w, "uri query parameter required", http.StatusBadRequest) return }
serveResponse := func(data interface{}, context string) { w.Header().Set("Content-Type", "application/json") response := map[string]interface{}{ "@context": context, } jsonData, _ := json.Marshal(data) json.Unmarshal(jsonData, &response) json.NewEncoder(w).Encode(response) }
if annotation, err := h.db.GetAnnotationByURI(uri); err == nil { if annotation.CID == nil || *annotation.CID == "" { parts := parseATURI(uri) if len(parts) >= 3 { did := parts[0] collection := parts[1] rkey := parts[2]
session, err := h.refresher.GetSessionWithAutoRefresh(r) if err == nil { _ = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, _ string) error { record, getErr := client.GetRecord(r.Context(), did, collection, rkey) if getErr == nil { h.db.UpdateAnnotation(uri, *annotation.BodyValue, *annotation.TagsJSON, record.CID) cid := record.CID annotation.CID = &cid } return nil }) } } }
if enriched, _ := hydrateAnnotations(h.db, []db.Annotation{*annotation}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } }
if highlight, err := h.db.GetHighlightByURI(uri); err == nil { if highlight.CID == nil || *highlight.CID == "" { parts := parseATURI(uri) if len(parts) >= 3 { did := parts[0] collection := parts[1] rkey := parts[2]
session, err := h.refresher.GetSessionWithAutoRefresh(r) if err == nil { _ = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, _ string) error { record, getErr := client.GetRecord(r.Context(), did, collection, rkey) if getErr == nil { tagsJSON := "" if highlight.TagsJSON != nil { tagsJSON = *highlight.TagsJSON } color := "" if highlight.Color != nil { color = *highlight.Color } h.db.UpdateHighlight(uri, color, tagsJSON, record.CID) cid := record.CID highlight.CID = &cid } return nil }) } } }
if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } }
if strings.Contains(uri, "at.margin.annotation") { highlightURI := strings.Replace(uri, "at.margin.annotation", "at.margin.highlight", 1) if highlight, err := h.db.GetHighlightByURI(highlightURI); err == nil { if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } } }
if strings.Contains(uri, "at.margin.annotation") || strings.Contains(uri, "at.margin.bookmark") { if strings.HasPrefix(uri, "at://") { uriWithoutScheme := strings.TrimPrefix(uri, "at://") parts := strings.Split(uriWithoutScheme, "/") if len(parts) >= 3 { did := parts[0] rkey := parts[len(parts)-1]
sembleURI := fmt.Sprintf("at://%s/network.cosmik.card/%s", did, rkey)
if annotation, err := h.db.GetAnnotationByURI(sembleURI); err == nil { if enriched, _ := hydrateAnnotations(h.db, []db.Annotation{*annotation}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } }
if bookmark, err := h.db.GetBookmarkByURI(sembleURI); err == nil { if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } } } } }
if bookmark, err := h.db.GetBookmarkByURI(uri); err == nil { if bookmark.CID == nil || *bookmark.CID == "" { parts := parseATURI(uri) if len(parts) >= 3 { did := parts[0] collection := parts[1] rkey := parts[2]
session, err := h.refresher.GetSessionWithAutoRefresh(r) if err == nil { _ = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, _ string) error { record, getErr := client.GetRecord(r.Context(), did, collection, rkey) if getErr == nil { tagsJSON := "" if bookmark.TagsJSON != nil { tagsJSON = *bookmark.TagsJSON } title := "" if bookmark.Title != nil { title = *bookmark.Title } desc := "" if bookmark.Description != nil { desc = *bookmark.Description } h.db.UpdateBookmark(uri, title, desc, tagsJSON, record.CID) cid := record.CID bookmark.CID = &cid } return nil }) } } }
if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } }
if strings.Contains(uri, "at.margin.annotation") { bookmarkURI := strings.Replace(uri, "at.margin.annotation", "at.margin.bookmark", 1) if bookmark, err := h.db.GetBookmarkByURI(bookmarkURI); err == nil { if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 { serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") return } } }
http.Error(w, "Annotation, Highlight, or Bookmark not found", http.StatusNotFound)
}
func (h *Handler) GetByTarget(w http.ResponseWriter, r *http.Request) { source := r.URL.Query().Get("source") if source == "" { source = r.URL.Query().Get("url") } if source == "" { http.Error(w, "source or url parameter required", http.StatusBadRequest) return }
limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
urlHash := db.HashURL(source)
annotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, limit, offset) highlights, _ := h.db.GetHighlightsByTargetHash(urlHash, limit, offset) bookmarks, _ := h.db.GetBookmarksByTargetHash(urlHash, limit, offset)
enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r)) enrichedBookmarks, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "source": source, "sourceHash": urlHash, "annotations": enrichedAnnotations, "highlights": enrichedHighlights, "bookmarks": enrichedBookmarks, })}
func (h *Handler) DiscoverForURL(w http.ResponseWriter, r *http.Request) { source := r.URL.Query().Get("source") if source == "" { source = r.URL.Query().Get("url") } if source == "" { http.Error(w, "source or url parameter required", http.StatusBadRequest) return }
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel()
annotations, highlights, bookmarks, err := ConstellationClient.GetAllItemsForURL(ctx, source) if err != nil { log.Printf("Constellation discover error, falling back to local: %v", err) h.GetByTarget(w, r) return }
var annotationURIs, highlightURIs, bookmarkURIs []string seenURIs := make(map[string]bool)
for _, link := range annotations { if !seenURIs[link.URI] { annotationURIs = append(annotationURIs, link.URI) seenURIs[link.URI] = true } } for _, link := range highlights { if !seenURIs[link.URI] { highlightURIs = append(highlightURIs, link.URI) seenURIs[link.URI] = true } } for _, link := range bookmarks { if !seenURIs[link.URI] { bookmarkURIs = append(bookmarkURIs, link.URI) seenURIs[link.URI] = true } }
localAnnotations, _ := h.db.GetAnnotationsByURIs(annotationURIs) localHighlights, _ := h.db.GetHighlightsByURIs(highlightURIs) localBookmarks, _ := h.db.GetBookmarksByURIs(bookmarkURIs)
urlHash := db.HashURL(source) dbAnnotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, 100, 0) dbHighlights, _ := h.db.GetHighlightsByTargetHash(urlHash, 100, 0) dbBookmarks, _ := h.db.GetBookmarksByTargetHash(urlHash, 100, 0)
annoMap := make(map[string]db.Annotation) for _, a := range localAnnotations { annoMap[a.URI] = a } for _, a := range dbAnnotations { annoMap[a.URI] = a }
highMap := make(map[string]db.Highlight) for _, h := range localHighlights { highMap[h.URI] = h } for _, h := range dbHighlights { highMap[h.URI] = h }
bookMap := make(map[string]db.Bookmark) for _, b := range localBookmarks { bookMap[b.URI] = b } for _, b := range dbBookmarks { bookMap[b.URI] = b }
var mergedAnnotations []db.Annotation for _, a := range annoMap { mergedAnnotations = append(mergedAnnotations, a) } var mergedHighlights []db.Highlight for _, h := range highMap { mergedHighlights = append(mergedHighlights, h) } var mergedBookmarks []db.Bookmark for _, b := range bookMap { mergedBookmarks = append(mergedBookmarks, b) }
viewerDID := h.getViewerDID(r) enrichedAnnotations, _ := hydrateAnnotations(h.db, mergedAnnotations, viewerDID) enrichedHighlights, _ := hydrateHighlights(h.db, mergedHighlights, viewerDID) enrichedBookmarks, _ := hydrateBookmarks(h.db, mergedBookmarks, viewerDID)
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "source": source, "sourceHash": urlHash, "annotations": enrichedAnnotations, "highlights": enrichedHighlights, "bookmarks": enrichedBookmarks, "networkDiscovered": len(annotations) + len(highlights) + len(bookmarks), })}
func (h *Handler) GetHighlights(w http.ResponseWriter, r *http.Request) { did := r.URL.Query().Get("creator") tag := r.URL.Query().Get("tag") limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
var highlights []db.Highlight var err error
if did != "" { highlights, err = h.db.GetHighlightsByAuthor(did, limit, offset) } else if tag != "" { highlights, err = h.db.GetHighlightsByTag(tag, limit, offset) } else { highlights, err = h.db.GetRecentHighlights(limit, offset) }
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "HighlightCollection", "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetBookmarks(w http.ResponseWriter, r *http.Request) { did := r.URL.Query().Get("creator") limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
if did == "" { http.Error(w, "creator parameter required", http.StatusBadRequest) return }
bookmarks, err := h.db.GetBookmarksByAuthor(did, limit, offset) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "BookmarkCollection", "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetUserAnnotations(w http.ResponseWriter, r *http.Request) { did := chi.URLParam(r, "did") if decoded, err := url.QueryUnescape(did); err == nil { did = decoded } limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
var annotations []db.Annotation var err error
viewerDID := h.getViewerDID(r)
if offset == 0 && viewerDID != "" && did == viewerDID { go func() { if _, err := h.FetchLatestUserRecords(r, did, xrpc.CollectionAnnotation, limit); err != nil { log.Printf("Background sync error (annotations): %v", err) } }() }
annotations, err = h.db.GetAnnotationsByAuthor(did, limit, offset)
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "AnnotationCollection", "creator": did, "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetUserHighlights(w http.ResponseWriter, r *http.Request) { did := chi.URLParam(r, "did") if decoded, err := url.QueryUnescape(did); err == nil { did = decoded } limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
var highlights []db.Highlight var err error
viewerDID := h.getViewerDID(r)
if offset == 0 && viewerDID != "" && did == viewerDID { go func() { if _, err := h.FetchLatestUserRecords(r, did, xrpc.CollectionHighlight, limit); err != nil { log.Printf("Background sync error (highlights): %v", err) } }() }
highlights, err = h.db.GetHighlightsByAuthor(did, limit, offset)
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "HighlightCollection", "creator": did, "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetUserBookmarks(w http.ResponseWriter, r *http.Request) { did := chi.URLParam(r, "did") if decoded, err := url.QueryUnescape(did); err == nil { did = decoded } limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
var bookmarks []db.Bookmark var err error
viewerDID := h.getViewerDID(r)
if offset == 0 && viewerDID != "" && did == viewerDID { go func() { if _, err := h.FetchLatestUserRecords(r, did, xrpc.CollectionBookmark, limit); err != nil { log.Printf("Background sync error (bookmarks): %v", err) } }() }
bookmarks, err = h.db.GetBookmarksByAuthor(did, limit, offset)
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "BookmarkCollection", "creator": did, "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetUserTargetItems(w http.ResponseWriter, r *http.Request) { did := chi.URLParam(r, "did") if decoded, err := url.QueryUnescape(did); err == nil { did = decoded }
source := r.URL.Query().Get("source") if source == "" { source = r.URL.Query().Get("url") } if source == "" { http.Error(w, "source or url parameter required", http.StatusBadRequest) return }
limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
urlHash := db.HashURL(source)
annotations, _ := h.db.GetAnnotationsByAuthorAndTargetHash(did, urlHash, limit, offset) highlights, _ := h.db.GetHighlightsByAuthorAndTargetHash(did, urlHash, limit, offset)
enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "creator": did, "source": source, "sourceHash": urlHash, "annotations": enrichedAnnotations, "highlights": enrichedHighlights, })}
func (h *Handler) GetReplies(w http.ResponseWriter, r *http.Request) { uri := r.URL.Query().Get("uri") if uri == "" { http.Error(w, "uri query parameter required", http.StatusBadRequest) return }
replies, err := h.db.GetRepliesByRoot(uri) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
enriched, _ := hydrateReplies(h.db, replies)
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "@context": "http://www.w3.org/ns/anno.jsonld", "type": "ReplyCollection", "inReplyTo": uri, "items": enriched, "totalItems": len(enriched), })}
func (h *Handler) GetLikeCount(w http.ResponseWriter, r *http.Request) { uri := r.URL.Query().Get("uri") if uri == "" { http.Error(w, "uri query parameter required", http.StatusBadRequest) return }
count, err := h.db.GetLikeCount(uri) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
liked := false cookie, err := r.Cookie("margin_session") if err == nil && cookie != nil { session, err := h.refresher.GetSessionWithAutoRefresh(r) if err == nil { userLike, err := h.db.GetLikeByUserAndSubject(session.DID, uri) if err == nil && userLike != nil { liked = true } } }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "count": count, "liked": liked, })}
func (h *Handler) GetEditHistory(w http.ResponseWriter, r *http.Request) { uri := r.URL.Query().Get("uri") if uri == "" { http.Error(w, "uri query parameter required", http.StatusBadRequest) return }
history, err := h.db.GetEditHistory(uri) if err != nil { http.Error(w, "Failed to fetch edit history", http.StatusInternalServerError) return }
if history == nil { history = []db.EditHistory{} }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(history)}
func parseIntParam(r *http.Request, name string, defaultVal int) int { val := r.URL.Query().Get(name) if val == "" { return defaultVal } i, err := strconv.Atoi(val) if err != nil { return defaultVal } return i}
func (h *Handler) GetURLMetadata(w http.ResponseWriter, r *http.Request) { targetURL := r.URL.Query().Get("url") if targetURL == "" { http.Error(w, "url parameter required", http.StatusBadRequest) return }
client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(targetURL) if err != nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"title": "", "error": "failed to fetch"}) return } defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 500*1024)) if err != nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"title": ""}) return }
content := string(body)
extract := func(key string) string { attr := fmt.Sprintf("property=\"og:%s\"", key) if idx := strings.Index(content, attr); idx != -1 { rest := content[idx:] if contentIdx := strings.Index(rest, "content=\""); contentIdx != -1 { start := contentIdx + 9 if end := strings.Index(rest[start:], "\""); end != -1 { return rest[start : start+end] } } }
attr = fmt.Sprintf("name=\"%s\"", key) if idx := strings.Index(content, attr); idx != -1 { rest := content[idx:] if contentIdx := strings.Index(rest, "content=\""); contentIdx != -1 { start := contentIdx + 9 if end := strings.Index(rest[start:], "\""); end != -1 { return rest[start : start+end] } } } return "" }
title := extract("title") if title == "" { if idx := strings.Index(content, "<title>"); idx != -1 { start := idx + 7 if end := strings.Index(content[start:], "</title>"); end != -1 { title = content[start : start+end] } } }
description := extract("description") image := extract("image")
var favicon string findIcon := func(rel string) string { search := fmt.Sprintf("rel=\"%s\"", rel) if idx := strings.Index(content, search); idx != -1 { startTag := strings.LastIndex(content[:idx], "<link") if startTag != -1 { endTag := strings.Index(content[startTag:], ">") if endTag != -1 { tag := content[startTag : startTag+endTag] if hrefIdx := strings.Index(tag, "href=\""); hrefIdx != -1 { start := hrefIdx + 6 if end := strings.Index(tag[start:], "\""); end != -1 { return tag[start : start+end] } } } } } return "" }
favicon = findIcon("icon") if favicon == "" { favicon = findIcon("shortcut icon") } if favicon == "" { favicon = findIcon("apple-touch-icon") }
resolveURL := func(base, target string) string { if target == "" { return "" } if strings.HasPrefix(target, "http") { return target } if strings.HasPrefix(target, "//") { return "https:" + target } u, err := url.Parse(base) if err != nil { return target } t, err := url.Parse(target) if err != nil { return target } return u.ResolveReference(t).String() }
image = resolveURL(targetURL, image) favicon = resolveURL(targetURL, favicon)
if favicon == "" { u, err := url.Parse(targetURL) if err == nil { favicon = fmt.Sprintf("%s://%s/favicon.ico", u.Scheme, u.Host) } }
data := map[string]string{ "title": title, "description": description, "image": image, "icon": favicon, }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data)}
func (h *Handler) GetNotifications(w http.ResponseWriter, r *http.Request) { session, err := h.refresher.GetSessionWithAutoRefresh(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return }
limit := parseIntParam(r, "limit", 50) offset := parseIntParam(r, "offset", 0)
notifications, err := h.db.GetNotifications(session.DID, limit, offset) if err != nil { http.Error(w, "Failed to get notifications", http.StatusInternalServerError) return }
enriched, err := hydrateNotifications(h.db, notifications) if err != nil { log.Printf("Failed to hydrate notifications: %v\n", err) }
w.Header().Set("Content-Type", "application/json") if enriched != nil { json.NewEncoder(w).Encode(map[string]interface{}{"items": enriched}) } else { json.NewEncoder(w).Encode(map[string]interface{}{"items": notifications}) }}
func (h *Handler) GetUnreadNotificationCount(w http.ResponseWriter, r *http.Request) { session, err := h.refresher.GetSessionWithAutoRefresh(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return }
count, err := h.db.GetUnreadNotificationCount(session.DID) if err != nil { http.Error(w, "Failed to get count", http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]int{"count": count})}
func (h *Handler) MarkNotificationsRead(w http.ResponseWriter, r *http.Request) { session, err := h.refresher.GetSessionWithAutoRefresh(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return }
if err := h.db.MarkNotificationsRead(session.DID); err != nil { http.Error(w, "Failed to mark as read", http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"})}func (h *Handler) getViewerDID(r *http.Request) string { cookie, err := r.Cookie("margin_session") if err != nil { return "" } did, _, _, _, _, err := h.db.GetSession(cookie.Value) if err != nil { return "" } return did}
func getItemAuthorDID(item interface{}) string { switch v := item.(type) { case APIAnnotation: return v.Author.DID case APIHighlight: return v.Author.DID case APIBookmark: return v.Author.DID case APICollectionItem: return v.Author.DID default: return "" }}
func (h *Handler) filterFeedByModeration(feed []interface{}, viewerDID string) []interface{} { if viewerDID == "" { return feed }
hiddenDIDs, err := h.db.GetAllHiddenDIDs(viewerDID) if err != nil || len(hiddenDIDs) == 0 { return feed }
var filtered []interface{} for _, item := range feed { authorDID := getItemAuthorDID(item) if authorDID != "" && hiddenDIDs[authorDID] { continue } filtered = append(filtered, item) } return filtered}