about summary refs log tree commit diff
path: root/bskyweb
diff options
context:
space:
mode:
authorbnewbold <bnewbold@robocracy.org>2023-12-18 23:52:39 +0400
committerGitHub <noreply@github.com>2023-12-18 23:52:39 +0400
commit3e3a72a366f2b05ed987af67736ec3f0e2f60272 (patch)
treec424336d6310fcca4d9319ea3a7325d12781c3b1 /bskyweb
parentedc6bdb4d6e052778022bee997137dbf392c85c9 (diff)
downloadvoidsky-3e3a72a366f2b05ed987af67736ec3f0e2f60272.tar.zst
basic public RSS feed for profiles (#2229)
* web: initial implementation of profile RSS feed

* re-work RSS feed to use DID in URL, not handle

Shouldn't have RSS feeds break when folks change handle.

* rss: tweak XML
Diffstat (limited to 'bskyweb')
-rw-r--r--bskyweb/cmd/bskyweb/rss.go99
-rw-r--r--bskyweb/cmd/bskyweb/server.go3
-rw-r--r--bskyweb/templates/profile.html1
3 files changed, 103 insertions, 0 deletions
diff --git a/bskyweb/cmd/bskyweb/rss.go b/bskyweb/cmd/bskyweb/rss.go
new file mode 100644
index 000000000..f7caf8fe7
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/rss.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	appbsky "github.com/bluesky-social/indigo/api/bsky"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+
+	"github.com/labstack/echo/v4"
+)
+
+// We don't actually populate the title for "posts".
+// Some background: https://book.micro.blog/rss-for-microblogs/
+type Item struct {
+	Title       string `xml:"title,omitempty"`
+	Link        string `xml:"link,omitempty"`
+	Description string `xml:"description,omitempty"`
+	PubDate     string `xml:"pubDate,omitempty"`
+	Author      string `xml:"author,omitempty"`
+	GUID        string `xml:"guid,omitempty"`
+}
+
+type rss struct {
+	Version     string `xml:"version,attr"`
+	Description string `xml:"channel>description,omitempty"`
+	Link        string `xml:"channel>link"`
+	Title       string `xml:"channel>title"`
+
+	Item []Item `xml:"channel>item"`
+}
+
+func (srv *Server) WebProfileRSS(c echo.Context) error {
+	ctx := c.Request().Context()
+
+	didParam := c.Param("did")
+	did, err := syntax.ParseDID(didParam)
+	if err != nil {
+		return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", didParam))
+	}
+
+	// check that public view is Ok
+	pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did.String())
+	if err != nil {
+		return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did))
+	}
+	for _, label := range pv.Labels {
+		if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
+			return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did))
+		}
+	}
+
+	af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "", 30)
+	if err != nil {
+		log.Warn("failed to fetch author feed", "did", did, "err", err)
+		return err
+	}
+
+	posts := []Item{}
+	for _, p := range af.Feed {
+		// only include author's own posts in RSS
+		if p.Post.Author.Did != pv.Did {
+			continue
+		}
+		aturi, err := syntax.ParseATURI(p.Post.Uri)
+		if err != nil {
+			return err
+		}
+		rec := p.Post.Record.Val.(*appbsky.FeedPost)
+		// only top-level posts in RSS (no replies)
+		if rec.Reply != nil {
+			continue
+		}
+		posts = append(posts, Item{
+			Link:        fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()),
+			Description: rec.Text,
+			PubDate:     rec.CreatedAt,
+			Author:      "@" + pv.Handle,
+			GUID:        aturi.String(),
+		})
+	}
+
+	title := "@" + pv.Handle
+	if pv.DisplayName != nil {
+		title = title + " - " + *pv.DisplayName
+	}
+	desc := ""
+	if pv.Description != nil {
+		desc = *pv.Description
+	}
+	feed := &rss{
+		Version:     "2.0",
+		Description: desc,
+		Link:        fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle),
+		Title:       title,
+		Item:        posts,
+	}
+	return c.XML(http.StatusOK, feed)
+}
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 7760860f7..5d9a481fe 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -209,6 +209,9 @@ func serve(cctx *cli.Context) error {
 	e.GET("/profile/:handle/feed/:rkey", server.WebGeneric)
 	e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
 
+	// profile RSS feed (DID not handle)
+	e.GET("/profile/:did/rss", server.WebProfileRSS)
+
 	// post endpoints; only first populates info
 	e.GET("/profile/:handle/post/:rkey", server.WebPost)
 	e.GET("/profile/:handle/post/:rkey/liked-by", server.WebGeneric)
diff --git a/bskyweb/templates/profile.html b/bskyweb/templates/profile.html
index d324a265e..71c100327 100644
--- a/bskyweb/templates/profile.html
+++ b/bskyweb/templates/profile.html
@@ -34,6 +34,7 @@
   {% endif %}
   <meta name="twitter:label1" content="Account DID">
   <meta name="twitter:value1" content="{{ profileView.Did }}">
+  <link rel="alternate" type="application/rss+xml" href="/profile/{{ profileView.Did }}/rss">
 {% endif -%}
 {%- endblock %}