From fb11d8099e44f0e02f71d46b807dcab9c890eac8 Mon Sep 17 00:00:00 2001 From: Guillermo Serrahima Date: Fri, 20 Feb 2026 16:27:29 +0100 Subject: added profile info --- backend/fetchers/app.bsky.go | 11 +++- backend/fetchers/profiles.go | 106 ++++++++++++++++++++++++++++++++++++ backend/general/profiles.go | 29 ++++++++++ backend/main.go | 2 +- backend/notes/app.bsky.feed.post.go | 20 ++++++- frontend/src/main.ts | 24 ++++++-- 6 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 backend/fetchers/profiles.go create mode 100644 backend/general/profiles.go diff --git a/backend/fetchers/app.bsky.go b/backend/fetchers/app.bsky.go index 2955cde..b787e00 100644 --- a/backend/fetchers/app.bsky.go +++ b/backend/fetchers/app.bsky.go @@ -12,7 +12,7 @@ type ListRecordsResponse struct { Records []notes.AppBskyFeedPostRecord `json:"records"` } -func FetchBlueskyPosts(did string, pdsURL string) ([]notes.AppBskyFeedPostNote, error) { +func FetchAppBskyFeedPosts(did string, pdsURL string) ([]notes.AppBskyFeedPostNote, error) { url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=app.bsky.feed.post&limit=50", pdsURL, did) resp, err := http.Get(url) @@ -32,9 +32,16 @@ func FetchBlueskyPosts(did string, pdsURL string) ([]notes.AppBskyFeedPostNote, return nil, err } + profile, err := FetchProfile(did, pdsURL) + if err != nil { + fmt.Printf("Profile fetch failed for %s: %v\n", did, err) + } else { + fmt.Printf("Fetched profile: %+v\n", profile) + } + var posts []notes.AppBskyFeedPostNote for _, record := range response.Records { - bp, err := notes.AppBskyFeedPostFromRecord(record) + bp, err := notes.AppBskyFeedPostFromRecord(record, profile) if err != nil { fmt.Printf("skipping malformed record %s: %v\n", record.URI, err) continue diff --git a/backend/fetchers/profiles.go b/backend/fetchers/profiles.go new file mode 100644 index 0000000..c25dd97 --- /dev/null +++ b/backend/fetchers/profiles.go @@ -0,0 +1,106 @@ +package fetchers + +import ( + "encoding/json" + "fmt" + "io" + "llostre/backend/general" + "net/http" + "strings" +) + +func FetchHandle(did string) (string, error) { + didDocURL := fmt.Sprintf("https://plc.directory/%s", did) + + resp, err := http.Get(didDocURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("DID resolution failed: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var didDoc struct { + AlsoKnownAs []string `json:"alsoKnownAs"` + } + + err = json.Unmarshal(body, &didDoc) + if err != nil { + return "", err + } + + if len(didDoc.AlsoKnownAs) == 0 { + return "", fmt.Errorf("no handle found") + } + + handle := strings.TrimPrefix(didDoc.AlsoKnownAs[0], "at://") + return handle, nil +} + +func FetchProfile(did string, pdsURL string) (*general.Profile, error) { + recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pdsURL, did) + + fmt.Printf("Fetching profile from: %s\n", recordURL) + resp, err := http.Get(recordURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + fmt.Printf("Response status: %d\n", resp.StatusCode) + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("profile fetch failed: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + handle, err := FetchHandle(did) + if err != nil { + handle = "handle.invalid" + } + + var response general.ProfileRecordResponse + + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + fmt.Printf("Unmarshaled value: %+v\n", response.Value) + + var avatarURL, bannerURL string + if response.Value.Avatar != nil { + avatarURL = fmt.Sprintf("https://cdn.bsky.app/img/avatar/plain/%s/%s@jpeg", did, response.Value.Avatar.Ref.Link) + } + if response.Value.Banner != nil { + bannerURL = fmt.Sprintf("https://cdn.bsky.app/img/banner/plain/%s/%s@jpeg", did, response.Value.Banner.Ref.Link) + } + + return &general.Profile{ + Handle: handle, + DisplayName: response.Value.DisplayName, + AvatarURL: avatarURL, + BannerURL: bannerURL, + Pronouns: response.Value.Pronouns, + }, nil +} + +func FetchProfiles(dids []string, pdsURL string) map[string]general.Profile { + profiles := make(map[string]general.Profile) + for _, did := range dids { + profile, err := FetchProfile(did, pdsURL) + if err == nil { + profiles[did] = *profile + } + } + return profiles +} diff --git a/backend/general/profiles.go b/backend/general/profiles.go new file mode 100644 index 0000000..be74364 --- /dev/null +++ b/backend/general/profiles.go @@ -0,0 +1,29 @@ +package general + +type Profile struct { + Handle string + DisplayName string + AvatarURL string + BannerURL string + Pronouns string +} + +type ProfileRecord struct { + DisplayName string `json:"displayName"` + Avatar *struct { + Ref struct { + Link string `json:"$link"` + } `json:"ref"` + } `json:"avatar,omitempty"` + Banner *struct { + Ref struct { + Link string `json:"$link"` + } `json:"ref"` + } `json:"banner,omitempty"` + Pronouns string `json:"pronouns"` +} + +type ProfileRecordResponse struct { + URI string `json:"uri"` + Value ProfileRecord `json:"value"` +} diff --git a/backend/main.go b/backend/main.go index 24a2973..1ca18df 100644 --- a/backend/main.go +++ b/backend/main.go @@ -14,7 +14,7 @@ func postsHandler(w http.ResponseWriter, r *http.Request) { did := "did:plc:hyyqbqqhpxt2bnn7s2e2iuxd" pdsURL := "https://bsky.vylion.com" - posts, err := fetchers.FetchBlueskyPosts(did, pdsURL) + posts, err := fetchers.FetchAppBskyFeedPosts(did, pdsURL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/backend/notes/app.bsky.feed.post.go b/backend/notes/app.bsky.feed.post.go index 32e7854..17ff5ee 100644 --- a/backend/notes/app.bsky.feed.post.go +++ b/backend/notes/app.bsky.feed.post.go @@ -2,6 +2,7 @@ package notes import ( "fmt" + "llostre/backend/general" "strings" "time" ) @@ -10,6 +11,8 @@ type AppBskyFeedPostNote 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"` @@ -68,7 +71,7 @@ func (bp *AppBskyFeedPostNote) GetReplyTo() *string { return bp.ReplyTo } -func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord) (*AppBskyFeedPostNote, error) { +func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord, profile *general.Profile) (*AppBskyFeedPostNote, error) { if record.URI == "" { return nil, fmt.Errorf("missing required field: uri") } @@ -115,10 +118,23 @@ func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord) (*AppBskyFeedPostNo } } + var handle, displayName, avatarURL string + if profile != nil { + handle = profile.Handle + displayName = profile.DisplayName + avatarURL = profile.AvatarURL + } else { + handle = "handle.invalid" + displayName = "" + avatarURL = "" + } + return &AppBskyFeedPostNote{ URI: record.URI, AuthorDID: did, - AuthorHandle: "author_handle", // wip + AuthorHandle: handle, + AuthorName: displayName, + AvatarURL: avatarURL, CreatedAt: createdAt, Text: record.Value.Text, ReplyTo: replyTo, diff --git a/frontend/src/main.ts b/frontend/src/main.ts index f53632c..dbf791b 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -53,6 +53,22 @@ function renderTextWithFacets(text: string, facets: any[]): string { return result; } +function renderAuthor(note: any): string { + const hasDisplayName = note.authorName && note.authorName !== ''; + const hasValidHandle = note.authorHandle !== 'handle.invalid'; + const pronouns = note.pronouns ? `@${note.authorHandle}`; + } else if (hasDisplayName && !hasValidHandle) { + return `
${note.authorName}${pronouns}
⚠️ InvalidHandle
`; + } else if (!hasDisplayName && hasValidHandle) { + return `
@${note.authorHandle}${pronouns}
`; + } else { + return `
⚠️ InvalidHandle${pronouns}
`; + } +} + async function main() { const app = document.querySelector('#app'); if (!app) { @@ -80,11 +96,11 @@ async function main() { const postHTML = `
- avatar + avatar
-
@${post.authorHandle}
-
${postText || '
'}
-
${new Date(post.createdAt).toLocaleString()}
+ ${renderAuthor(post)} +
${postText}
+
${new Date(post.createdAt).toLocaleString()}
`; -- cgit v1.2.3