aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-03-20 00:54:07 +0100
committerGuillermo Serrahima <dev@vylion.com>2026-03-20 00:54:07 +0100
commit1d947b771995399b677c040136ef2cf88b33d4f2 (patch)
tree3cdd673f6c27c9a5a481627895d1fee932b85580
parent59545c874d8635c39ccdbecf84f953686dd5c03c (diff)
downloadllostre-1d947b771995399b677c040136ef2cf88b33d4f2.tar.xz
llostre-1d947b771995399b677c040136ef2cf88b33d4f2.zip
readded auth that I removed in my hubris + check for no-unauthorized labels
-rw-r--r--backend/fetchers/app.bsky.actor.go11
-rw-r--r--backend/fetchers/app.bsky.feed.go31
-rw-r--r--backend/general/records.go67
-rw-r--r--backend/main.go54
-rw-r--r--backend/notes/app.bsky.actor.go17
-rw-r--r--backend/notes/app.bsky.feed.go19
-rw-r--r--backend/notes/author.go1
-rw-r--r--backend/notes/note.go20
8 files changed, 159 insertions, 61 deletions
diff --git a/backend/fetchers/app.bsky.actor.go b/backend/fetchers/app.bsky.actor.go
index e07f5be..0752223 100644
--- a/backend/fetchers/app.bsky.actor.go
+++ b/backend/fetchers/app.bsky.actor.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "llostre/backend/auth"
"llostre/backend/general"
"llostre/backend/notes"
"log/slog"
@@ -130,13 +131,13 @@ func FetchHandleAndPDS(did string) (string, string, error) {
return handle, pds, nil
}
-func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL string, cache *Cache) (*notes.Author, error) {
+func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Author, error) {
// construct profile record URI
uri := fmt.Sprintf("at://%s/app.bsky.actor.profile/self", did)
// try slingshot first
if slingshotURL != "" {
- rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL)
+ rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session)
if err != nil {
logger.Warn("slingshot profile fetch failed", "did", did, "error", err)
} else {
@@ -145,7 +146,7 @@ func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL str
}
// fallback to PDS
- rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL)
+ rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session)
if err != nil {
return nil, err
}
@@ -153,10 +154,10 @@ func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL str
return parseAppBskyActorProfile(did, rawRecord, cache)
}
-func FetchAppBskyActorProfiles(logger *slog.Logger, dids []string, pdsURL, slingshotURL string, cache *Cache) map[string]notes.Author {
+func FetchAppBskyActorProfiles(logger *slog.Logger, dids []string, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) map[string]notes.Author {
authors := make(map[string]notes.Author)
for _, did := range dids {
- author, err := FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, cache)
+ author, err := FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache)
if err == nil {
authors[did] = *author
}
diff --git a/backend/fetchers/app.bsky.feed.go b/backend/fetchers/app.bsky.feed.go
index 10c08e7..2a38d5a 100644
--- a/backend/fetchers/app.bsky.feed.go
+++ b/backend/fetchers/app.bsky.feed.go
@@ -3,17 +3,18 @@ package fetchers
import (
"encoding/json"
"fmt"
+ "llostre/backend/auth"
"llostre/backend/general"
"llostre/backend/notes"
"log/slog"
)
-func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, cache *Cache) ([]notes.Note, error) {
+func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) ([]notes.Note, error) {
// get author of the note
author, found := cache.GetProfile(did)
var err error
if !found {
- author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, cache)
+ author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache)
}
if err != nil {
return nil, err
@@ -21,7 +22,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string
// try slingshot first if given
if slingshotURL != "" {
- records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL)
+ records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL, session)
if err != nil {
logger.Error("Fetching from slingshot failed")
} else {
@@ -29,7 +30,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string
}
}
- records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL)
+ records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL, session)
if err != nil {
return nil, err
}
@@ -37,12 +38,22 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string
return parseAppBskyFeedPostCollection(logger, records, author, cache)
}
-func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, cache *Cache) (*notes.Note, error) {
- // check cache
+func GetAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) {
+ // check cache, return if it was cached without auth or if it was cached with auth and we have auth
if cached, found := cache.Get(uri); found {
- return cached, nil
+ if !cached.AuthorNoUnauth {
+ return cached, nil
+ }
+
+ if general.IsValidSession(session) {
+ return cached, nil
+ }
}
+ return FetchAppBskyFeedPostByURI(logger, uri, slingshotURL, session, cache)
+}
+
+func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) {
repo, _, _, err := general.ParseURI(uri)
// need to resolve DID to PDS
@@ -54,7 +65,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca
// get author of the note
author, found := cache.GetProfile(repo)
if !found {
- author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, cache)
+ author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, session, cache)
}
if err != nil {
return nil, err
@@ -62,7 +73,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca
// try slingshot first if given
if slingshotURL != "" {
- rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL)
+ rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session)
if err != nil {
logger.Error("fetching from slingshot failed", "error", err)
} else {
@@ -70,7 +81,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca
}
}
- rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL)
+ rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session)
if err != nil {
logger.Error("Fetching from PDS failed")
return nil, err
diff --git a/backend/general/records.go b/backend/general/records.go
index d623862..ff37785 100644
--- a/backend/general/records.go
+++ b/backend/general/records.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "llostre/backend/auth"
"log/slog"
"net/http"
)
@@ -13,12 +14,27 @@ type RawRecord struct {
Value json.RawMessage `json:"value"`
}
-func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL string) ([]RawRecord, error) {
+func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL string, session *auth.OAuthState) ([]RawRecord, error) {
url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s", baseURL, did, collection)
logger.Debug("fetching raw record collection", "did", did, "endpoint", baseURL, "collection", collection)
- resp, err := http.Get(url)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // only add auth if provided
+ if session != nil {
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
@@ -39,7 +55,7 @@ func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL stri
return result.Records, nil
}
-func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord, error) {
+func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string, session *auth.OAuthState) (*RawRecord, error) {
repo, collection, rkey, err := ParseURI(uri)
if err != nil {
return nil, err
@@ -50,7 +66,22 @@ func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord,
logger.Debug("fetching raw record by URI", "uri", uri, "endpoint", baseURL)
- resp, err := http.Get(url)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // only add auth if provided
+ if session != nil {
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
@@ -68,3 +99,31 @@ func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord,
return &result, nil
}
+
+func IsValidSession(session *auth.OAuthState) bool {
+ if session == nil {
+ return false
+ }
+
+ url := fmt.Sprintf("%s/xrpc/com.atproto.server.getSession", session.PDS)
+
+ dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return false
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return false
+ }
+ req.Header.Set("Authorization", "Bearer "+session.AccessToken)
+ req.Header.Set("DPoP", dpopProof)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+
+ return resp.StatusCode == 200
+}
diff --git a/backend/main.go b/backend/main.go
index 3c10887..234d57d 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -247,6 +247,34 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
+func refreshTokens(session *auth.OAuthState, cookieValue string) error {
+ // Check for access token refresh
+ needsRefresh := time.Now().After(session.ExpiresAt)
+ pds := session.PDS
+ refreshToken := session.RefreshToken
+ keypair := session.KeyPair
+ sessionsMutex.RUnlock()
+
+ if needsRefresh {
+ newTokens, err := auth.RefreshAccessToken(pds, refreshToken, backendURL+"/callback", keypair)
+ if err != nil {
+ sessionsMutex.Lock()
+ delete(sessions, cookieValue)
+ sessionsMutex.Unlock()
+ logger.Error("no authorization servers found for token refresh")
+ return fmt.Errorf("no authorization servers found")
+ }
+
+ sessionsMutex.Lock()
+ session.AccessToken = newTokens.AccessToken
+ session.RefreshToken = newTokens.RefreshToken
+ session.ExpiresAt = time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
+ sessionsMutex.Unlock()
+ }
+
+ return nil
+}
+
func postsHandler(w http.ResponseWriter, r *http.Request) {
logger.Debug("posts handler start")
@@ -272,32 +300,10 @@ func postsHandler(w http.ResponseWriter, r *http.Request) {
logger.Debug("session public key", "key", session.KeyPair.PublicKey)
}
- // Check for access token refresh
- needsRefresh := time.Now().After(session.ExpiresAt)
- pds := session.PDS
- refreshToken := session.RefreshToken
- keypair := session.KeyPair
- sessionsMutex.RUnlock()
-
- if needsRefresh {
- newTokens, err := auth.RefreshAccessToken(pds, refreshToken, backendURL+"/callback", keypair)
- if err != nil {
- sessionsMutex.Lock()
- delete(sessions, cookie.Value)
- sessionsMutex.Unlock()
- http.Error(w, "session expired, please login again", http.StatusUnauthorized)
- return
- }
-
- sessionsMutex.Lock()
- session.AccessToken = newTokens.AccessToken
- session.RefreshToken = newTokens.RefreshToken
- session.ExpiresAt = time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
- sessionsMutex.Unlock()
- }
+ refreshTokens(session, cookie.Value)
// Fetch app.bsky.feed.post records
- posts, err := fetchers.FetchAppBskyFeedPosts(logger, session.DID, session.PDS, slingshotURL, cache)
+ posts, err := fetchers.FetchAppBskyFeedPosts(logger, session.DID, session.PDS, slingshotURL, session, cache)
if err != nil {
logger.Error("fetching app.bsky.feed.posts failed", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/backend/notes/app.bsky.actor.go b/backend/notes/app.bsky.actor.go
index dc5d536..797a8e8 100644
--- a/backend/notes/app.bsky.actor.go
+++ b/backend/notes/app.bsky.actor.go
@@ -17,6 +17,12 @@ type AppBskyActorProfileRecord struct {
} `json:"ref"`
} `json:"banner,omitempty"`
Pronouns string `json:"pronouns"`
+ Labels *struct {
+ Type string `json:"$type"`
+ Values []struct {
+ Val string `json:"val"`
+ } `json:"values"`
+ } `json:"labels,omitempty"`
}
func AuthorFromAppBskyActorProfileRecord(record AppBskyActorProfileRecord, did, handle string) (*Author, error) {
@@ -28,12 +34,23 @@ func AuthorFromAppBskyActorProfileRecord(record AppBskyActorProfileRecord, did,
bannerURL = fmt.Sprintf("https://cdn.bsky.app/img/banner/plain/%s/%s@jpeg", did, record.Banner.Ref.Link)
}
+ // check for !no-unauthenticated label
+ noUnauth := false
+ if record.Labels != nil && record.Labels.Type == "com.atproto.label.defs#selfLabels" {
+ for _, label := range record.Labels.Values {
+ if label.Val == "!no-unauthenticated" {
+ noUnauth = true
+ }
+ }
+ }
+
author := &Author{
Handle: handle,
DisplayName: record.DisplayName,
AvatarURL: avatarURL,
BannerURL: bannerURL,
Pronouns: record.Pronouns,
+ NoUnauth: noUnauth,
}
return author, nil
diff --git a/backend/notes/app.bsky.feed.go b/backend/notes/app.bsky.feed.go
index b5cfd03..961f8d7 100644
--- a/backend/notes/app.bsky.feed.go
+++ b/backend/notes/app.bsky.feed.go
@@ -89,14 +89,15 @@ func NoteFromAppBskyFeedPostRecord(record AppBskyFeedPostRecord, author *Author)
did, _, _, err := general.ParseURI(record.URI)
return &Note{
- URI: record.URI,
- AuthorDID: did,
- AuthorHandle: handle,
- AuthorName: displayName,
- AvatarURL: avatarURL,
- CreatedAt: createdAt,
- Text: record.Value.Text,
- ReplyTo: replyTo,
- Facets: facets,
+ URI: record.URI,
+ AuthorDID: did,
+ AuthorHandle: handle,
+ AuthorName: displayName,
+ AuthorNoUnauth: author.NoUnauth,
+ AvatarURL: avatarURL,
+ CreatedAt: createdAt,
+ Text: record.Value.Text,
+ ReplyTo: replyTo,
+ Facets: facets,
}, nil
}
diff --git a/backend/notes/author.go b/backend/notes/author.go
index 02afabf..644a10c 100644
--- a/backend/notes/author.go
+++ b/backend/notes/author.go
@@ -6,4 +6,5 @@ type Author struct {
AvatarURL string
BannerURL string
Pronouns string
+ NoUnauth bool
}
diff --git a/backend/notes/note.go b/backend/notes/note.go
index 7c8de36..3b32aa1 100644
--- a/backend/notes/note.go
+++ b/backend/notes/note.go
@@ -5,15 +5,17 @@ import (
)
type Note struct {
- URI string `json:"uri"`
- AuthorDID string `json:"authorDid"`
- AuthorHandle string `json:"authorHandle"`
- AuthorName string `json:"authorName"`
- AvatarURL string `json:"avatarURL"`
- Text string `json:"text"`
- CreatedAt time.Time `json:"createdAt"`
- ReplyTo *string `json:"replyTo,omitempty"`
- Facets []Facet `json:"facets,omitempty"`
+ URI string `json:"uri"`
+ AuthorDID string `json:"authorDid"`
+ AuthorHandle string `json:"authorHandle"`
+ AuthorName string `json:"authorName"`
+ AuthorNoUnauth bool `json:"authorNoUnauth"` // author has !no-unauthenticated label
+ AvatarURL string `json:"avatarURL"`
+ Text string `json:"text"`
+ CreatedAt time.Time `json:"createdAt"`
+ ReplyTo *string `json:"replyTo,omitempty"`
+ Facets []Facet `json:"facets,omitempty"`
+ Parents []string `json:"parents,omitempty"`
}
type Facet struct {