aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-02-20 16:27:29 +0100
committerGuillermo Serrahima <dev@vylion.com>2026-02-20 16:27:29 +0100
commitfb11d8099e44f0e02f71d46b807dcab9c890eac8 (patch)
tree0bcbb65ff9179e3835012f950363e7e328e862b0
parent4a2a8c34e87c5dd25f6d01a1aafaa10799aa907f (diff)
downloadllostre-fb11d8099e44f0e02f71d46b807dcab9c890eac8.tar.xz
llostre-fb11d8099e44f0e02f71d46b807dcab9c890eac8.zip
added profile info
-rw-r--r--backend/fetchers/app.bsky.go11
-rw-r--r--backend/fetchers/profiles.go106
-rw-r--r--backend/general/profiles.go29
-rw-r--r--backend/main.go2
-rw-r--r--backend/notes/app.bsky.feed.post.go20
-rw-r--r--frontend/src/main.ts24
6 files changed, 183 insertions, 9 deletions
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 ? `<span class="muted> · ${note.pronouns}</span>` : '';
+
+ if (hasDisplayName && hasValidHandle) {
+ return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">@${note.authorHandle}</div>`;
+ } else if (hasDisplayName && !hasValidHandle) {
+ return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">⚠️ InvalidHandle</div>`;
+ } else if (!hasDisplayName && hasValidHandle) {
+ return `<div><strong>@${note.authorHandle}</strong>${pronouns}</div>`;
+ } else {
+ return `<div><strong>⚠️ InvalidHandle</strong>${pronouns}</div>`;
+ }
+}
+
async function main() {
const app = document.querySelector('#app');
if (!app) {
@@ -80,11 +96,11 @@ async function main() {
const postHTML = `
<div class="note">
- <img class="note-avatar" src="https://cdn.bsky.app/img/avatar/plain/did:plc:hyyqbqqhpxt2bnn7s2e2iuxd/bafkreidoiynom7v2y5rpsm467l3d2cnr7ozvlfusrchjey3k2jtyquufq4@jpeg" alt="avatar">
+ <img class="note-avatar" src="${post.avatarURL}" alt="avatar">
<div class="note-content">
- <div style="font-weight: bold">@${post.authorHandle}</div>
- <div class="note-text">${postText || '<br>'}</div>
- <div style="font-size: 12px; color: #888">${new Date(post.createdAt).toLocaleString()}</div>
+ ${renderAuthor(post)}
+ <div class="note-text">${postText}</div>
+ <div class="muted">${new Date(post.createdAt).toLocaleString()}</div>
</div>
</div>
`;