Something went wrong. Try again.
The social RSS reader. Built on AT Protocol. glean.at
app glean atproto atmosphere rss feed social
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225package server
import ( "context" "database/sql" "fmt" "net/http" "strconv" "strings" "time"
"github.com/go-chi/chi/v5" "golang.org/x/sync/errgroup"
"pkg.rbrt.fr/glean/internal/atproto" "pkg.rbrt.fr/glean/internal/db")
func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { user := currentUser(r) ctx := r.Context()
limit := 20
likedPageNum, err := strconv.Atoi(r.URL.Query().Get("liked_page")) if err != nil || likedPageNum < 1 { likedPageNum = 1 } annotPageNum, err := strconv.Atoi(r.URL.Query().Get("annot_page")) if err != nil || annotPageNum < 1 { annotPageNum = 1 }
likedPage := Pagination{Page: likedPageNum, PageSize: limit} annotPage := Pagination{Page: annotPageNum, PageSize: limit}
var ( articles []*db.Article annotations []*db.Annotation )
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error { var err error articles, err = s.dbs.Articles.ListLikedArticles(gCtx, user.DID, limit+1, likedPage.Offset(), false) if err != nil { s.logger.Warn("failed to list liked articles", "error", err, "did", user.DID) return nil } likedHasMore := len(articles) > limit if likedHasMore { articles = articles[:limit] } likedPage = likedPage.Paginate(len(articles)) if likedHasMore { likedPage.HasNext = true likedPage.NextPage = likedPage.Page + 1 } return nil })
g.Go(func() error { var err error annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", "", user.DID, limit+1, annotPage.Offset()) if err != nil { s.logger.Warn("failed to list annotations", "error", err, "did", user.DID) return nil } resolveAnnotationHandles(gCtx, annotations) annotHasMore := len(annotations) > limit if annotHasMore { annotations = annotations[:limit] } annotPage = annotPage.Paginate(len(annotations)) if annotHasMore { annotPage.HasNext = true annotPage.NextPage = annotPage.Page + 1 } return nil })
_ = g.Wait()
items := make([]Article, len(articles)) for i, a := range articles { a.NavSuffix = buildNavSuffix("", true, "") items[i] = toArticle(a) }
writeJSON(w, http.StatusOK, libraryResponse{ User: toUser(user), Articles: items, Annotations: toAnnotations(annotations), LikedPage: likedPage, AnnotPage: annotPage, })}
func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) { user := currentUser(r) ctx := r.Context() a := &db.Annotation{ AuthorDID: user.DID, FeedURL: r.FormValue("feed_url"), ArticleURL: r.FormValue("article_url"), Quote: sql.NullString{String: r.FormValue("quote"), Valid: r.FormValue("quote") != ""}, Note: sql.NullString{String: r.FormValue("note"), Valid: r.FormValue("note") != ""}, Tags: sql.NullString{String: r.FormValue("tags"), Valid: r.FormValue("tags") != ""}, CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, } if rv := r.FormValue("rating"); rv != "" { var n int64 if _, err := fmt.Sscanf(rv, "%d", &n); err == nil { a.Rating = sql.NullInt64{Int64: n, Valid: true} } }
if client := s.pdsClientForUser(r); client != nil { var tags []string if a.Tags.Valid && a.Tags.String != "" { tags = strings.Split(a.Tags.String, ",") } record := atproto.AnnotationRecord{ CreatedAt: time.Now().Format(time.RFC3339), FeedURL: a.FeedURL, ArticleURL: a.ArticleURL, Quote: a.Quote.String, Note: a.Note.String, Tags: atproto.StringList(tags), Rating: int(a.Rating.Int64), } uri, cid, err := client.CreateRecord(ctx, user.DID, atproto.CollectionAnnotation, record) if err != nil { s.writePDSError(w, r, err, "failed to write annotation to PDS") return } a.URI = uri a.CID = sql.NullString{String: cid, Valid: true}
marginRec := atproto.NewMarginNoteRecord( a.ArticleURL, a.Quote.String, a.Note.String, tags, time.Now().Format(time.RFC3339), ) if _, _, err := client.CreateRecord(ctx, user.DID, atproto.CollectionMarginNote, marginRec); err != nil { s.logger.Error("failed to write margin note to PDS", "error", err) } } else { a.URI = fmt.Sprintf("glean:annotation:%d", time.Now().UnixNano()) }
if err := s.dbs.Articles.CreateAnnotation(ctx, a); err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return }
a.AuthorHandle = atproto.ResolveProfile(r.Context(), user.DID).Handle writeJSON(w, http.StatusOK, annotationResponse{Annotation: toAnnotation(a)})}
func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) { user := currentUser(r) ctx := r.Context() id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeAPIError(w, http.StatusBadRequest, "invalid id") return }
annotation, err := s.dbs.Articles.GetAnnotation(ctx, id) if err != nil { writeAPIError(w, http.StatusNotFound, "annotation not found") return }
if annotation.AuthorDID != user.DID { writeAPIError(w, http.StatusForbidden, "forbidden") return }
if annotation.URI != "" { if client := s.pdsClientForUser(r); client != nil { parsed, ok := atproto.ParseRecordURI(annotation.URI) if ok { if delErr := client.DeleteRecord(ctx, user.DID, parsed.Collection, parsed.RKey); delErr != nil { s.writePDSError(w, r, delErr, "failed to delete annotation from PDS") return } }
// The annotation is mirrored to at.margin.note on creation; delete the // mirror too, otherwise the next sync resurrects it as a margin note. if mirrorErr := atproto.DeleteMirroredMarginNotes(ctx, client, user.DID, annotation.ArticleURL, annotation.Quote.String, annotation.Note.String); mirrorErr != nil { s.logger.Error("failed to delete mirrored margin note", "error", mirrorErr) } } }
// Delete by content so any duplicate row created by the mirror (different URI, // identical content) is removed alongside the canonical annotation. if err := s.dbs.Articles.DeleteAnnotationsByContent(ctx, user.DID, annotation.ArticleURL, annotation.Quote.String, annotation.Note.String); err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return }
w.WriteHeader(http.StatusNoContent)}
func resolveAnnotationHandles(ctx context.Context, annotations []*db.Annotation) { g, gCtx := errgroup.WithContext(ctx) g.SetLimit(5) for _, a := range annotations { g.Go(func() error { if a.AuthorDID != "" { a.AuthorHandle = atproto.ResolveProfile(gCtx, a.AuthorDID).Handle } return nil }) } _ = g.Wait()}