Skip to content

Commit

Permalink
progress on embedr
Browse files Browse the repository at this point in the history
  • Loading branch information
bnewbold committed Apr 12, 2024
1 parent 1430538 commit 666247a
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 86 deletions.
223 changes: 223 additions & 0 deletions bskyweb/cmd/embedr/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package main

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"

"github.com/labstack/echo/v4"
)

var ErrPostNotFound = errors.New("post not found")
var ErrPostNotPublic = errors.New("post is not publicly accessible")

func (srv *Server) getBlueskyPost(ctx context.Context, did syntax.DID, rkey syntax.RecordKey) (*appbsky.FeedDefs_PostView, error) {

// requires two fetches: first fetch profile
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did.String())
if err != nil {
log.Warnf("failed to fetch profile for: %s\t%v", did, err)
// TODO: detect 404, specifically?
return nil, ErrPostNotFound
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return nil, ErrPostNotPublic
}
}

// then fetch the post thread (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
// TODO: detect 404, specifically?
return nil, ErrPostNotFound
}

if tpv.Thread.FeedDefs_BlockedPost != nil {
return nil, ErrPostNotPublic
} else if tpv.Thread.FeedDefs_ThreadViewPost.Post == nil {
return nil, ErrPostNotFound
}
return tpv.Thread.FeedDefs_ThreadViewPost.Post, nil
}

func (srv *Server) WebHome(c echo.Context) error {
return c.Render(http.StatusOK, "home.html", nil)
}

type OEmbedResponse struct {
Type string `json:"type"`
Version string `json:"version"`
AuthorName string `json:"author_name,omitempty"`
AuthorURL string `json:"author_url,omitempty"`
ProviderName string `json:"provider_url,omitempty"`
CacheAge int `json:"cache_age,omitempty"`
Width int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
HTML string `json:"html,omitempty"`
}

func (srv *Server) parseBlueskyURL(ctx context.Context, raw string) (*syntax.ATURI, error) {

if raw == "" {
return nil, fmt.Errorf("empty url")
}

// first try simple AT-URI
uri, err := syntax.ParseATURI(raw)
if nil == err {
return &uri, nil
}

// then try bsky.app post URL
u, err := url.Parse(raw)
if err != nil {
return nil, err
}
if u.Hostname() != "bsky.app" {
return nil, fmt.Errorf("only bsky.app URLs currently supported")
}
pathParts := strings.Split(u.Path, "/") // NOTE: pathParts[0] will be empty string
if len(pathParts) != 5 || pathParts[1] != "profile" || pathParts[3] != "post" {
return nil, fmt.Errorf("only bsky.app post URLs currently supported")
}
atid, err := syntax.ParseAtIdentifier(pathParts[2])
if err != nil {
return nil, err
}
rkey, err := syntax.ParseRecordKey(pathParts[4])
if err != nil {
return nil, err
}
var did syntax.DID
if atid.IsHandle() {
ident, err := srv.dir.Lookup(ctx, *atid)
if err != nil {
return nil, err
}
did = ident.DID
} else {
did, err = atid.AsDID()
if err != nil {
return nil, err
}
}

// TODO: don't really need to re-parse here, if we had test coverage
aturi, err := syntax.ParseATURI(fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey))
if err != nil {
return nil, err
} else {
return &aturi, nil
}
}

func (srv *Server) postEmbedHTML(post *appbsky.FeedDefs_PostView) string {
aturi, err := syntax.ParseATURI(post.Uri)
if err != nil {
log.Error("bad AT-URI in reponse", "aturi", aturi)
}
// TODO: could add language, maybe other fiels here?
// XXX: should actually use html/template for this render
return fmt.Sprintf(
"<iframe src=\"%s\" class=\"bluesky-post-embed\" data-aturi=\"%s\" data-record-cid=\"%s\"></iframe><script src=\"%s\" async=\"async\"></script>",
fmt.Sprintf("https://embed.bsky.app/embed/%s/app.bsky.feed.post/%s", post.Author.Did, aturi.RecordKey()),
post.Uri,
post.Cid,
"https://embed.bsky.app/iframe-resize.js",
)
}

func (srv *Server) WebOEmbed(c echo.Context) error {
formatParam := c.QueryParam("format")
if formatParam != "" && formatParam != "json" {
return c.String(http.StatusNotImplemented, "Unsupported oEmbed format: "+formatParam)
}

width := 550
maxWidthParam := c.QueryParam("maxwidth")
if maxWidthParam != "" {
maxWidthInt, err := strconv.Atoi(maxWidthParam)
if err != nil || maxWidthInt < 220 || maxWidthInt > 550 {
return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer between 220 and 550)")
}
width = maxWidthInt
}
// NOTE: maxheight ignored

aturi, err := srv.parseBlueskyURL(c.Request().Context(), c.QueryParam("url"))
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Expected 'url' to be bsky.app URL or AT-URI: %v", err))
}
if aturi.Collection() != syntax.NSID("app.bsky.feed.post") {
return c.String(http.StatusNotImplemented, "Only posts (app.bsky.feed.post records) can be embedded currently")
}
did, err := aturi.Authority().AsDID()
if err != nil {
return err
}

post, err := srv.getBlueskyPost(c.Request().Context(), did, aturi.RecordKey())
if err == ErrPostNotFound {
return c.String(http.StatusNotFound, fmt.Sprintf("%v", err))
} else if err == ErrPostNotPublic {
return c.String(http.StatusForbidden, fmt.Sprintf("%v", err))
} else if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}

data := OEmbedResponse{
Type: "rich",
Version: "1.0",
AuthorName: "@" + post.Author.Handle,
AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle),
ProviderName: "Bluesky Social",
CacheAge: 86400,
Width: width,
Height: nil,
HTML: srv.postEmbedHTML(post),
}
if post.Author.DisplayName != nil {
data.AuthorName = fmt.Sprintf("%s (@%s)", *post.Author.DisplayName, post.Author.Handle)
}
return c.JSON(http.StatusOK, data)
}

func (srv *Server) WebPostEmbed(c echo.Context) error {
ctx := c.Request().Context()

// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.QueryParam("rkey")
rkey, err := syntax.ParseRecordKey(rkeyParam)
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid RecordKey: %v", err))
}
didParam := c.QueryParam("did")
did, err := syntax.ParseDID(didParam)
if err != nil {
return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid DID: %v", err))
}

postView, err := srv.getBlueskyPost(ctx, did, rkey)
if err == ErrPostNotFound {
return c.String(http.StatusNotFound, fmt.Sprintf("%v", err))
} else if err == ErrPostNotPublic {
return c.String(http.StatusForbidden, fmt.Sprintf("%v", err))
} else if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}

data := map[string]interface{}{
"post": postView,
}
return c.Render(http.StatusOK, "postEmbed.html", data)
}
90 changes: 5 additions & 85 deletions bskyweb/cmd/embedr/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import (
"syscall"
"time"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/util/cliutil"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/social-app/bskyweb"
Expand All @@ -30,6 +29,7 @@ type Server struct {
echo *echo.Echo
httpd *http.Server
xrpcc *xrpc.Client
dir identity.Directory
}

func serve(cctx *cli.Context) error {
Expand Down Expand Up @@ -71,6 +71,7 @@ func serve(cctx *cli.Context) error {
server := &Server{
echo: e,
xrpcc: xrpcc,
dir: identity.DefaultDirectory(),
}

// Create the HTTP server.
Expand Down Expand Up @@ -140,7 +141,7 @@ func serve(cctx *cli.Context) error {
log.Debugf("serving static file from the local file system")
return http.FS(os.DirFS("static"))
}
fsys, err := fs.Sub(bskyweb.StaticFS, "static")
fsys, err := fs.Sub(bskyweb.EmbedrStaticFS, "static")
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -172,6 +173,7 @@ func serve(cctx *cli.Context) error {

// actual routes
e.GET("/", server.WebHome)
e.GET("/iframe-resize.js", echo.WrapHandler(staticHandler))
e.GET("/embed.js", echo.WrapHandler(staticHandler))
e.GET("/oembed", server.WebOEmbed)
e.GET("/embed/:did/app.bsky.feed.post/:rkey", server.WebPostEmbed)
Expand Down Expand Up @@ -232,85 +234,3 @@ func (srv *Server) errorHandler(err error, c echo.Context) {
}
c.Render(code, "error.html", data)
}

func (srv *Server) WebHome(c echo.Context) error {
data := map[string]interface{}{}
return c.Render(http.StatusOK, "home.html", data)
}

func (srv *Server) WebOEmbed(c echo.Context) error {
data := map[string]interface{}{}
return c.Render(http.StatusOK, "oembed.html", data)
}

func (srv *Server) WebPostEmbed(c echo.Context) error {
ctx := c.Request().Context()
data := map[string]interface{}{}

// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.Param("rkey")
rkey, err := syntax.ParseRecordKey(rkeyParam)
if err != nil {
return c.Render(http.StatusOK, "postEmbed.html", data)
}
didParam := c.Param("did")
did, err := syntax.ParseDID(didParam)
if err != nil {
return c.Render(http.StatusOK, "postEmbed.html", data)
}

// requires two fetches: first fetch profile (!)
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did.String())
if err != nil {
log.Warnf("failed to fetch profile for: %s\t%v", did, err)
return c.Render(http.StatusOK, "postEmbed.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}

if !unauthedViewingOkay {
return c.Render(http.StatusOK, "postEmbed.html", data)
}
data["did"] = did.String()

// then fetch the post thread (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
return c.Render(http.StatusOK, "postEmbed.html", data)
}
req := c.Request()
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
if postView.Embed != nil {
if postView.Embed.EmbedImages_View != nil {
var thumbUrls []string
for i := range postView.Embed.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
} else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil {
var thumbUrls []string
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
}
}

if postView.Record != nil {
postRecord, ok := postView.Record.Val.(*appbsky.FeedPost)
if ok {
_ = postRecord
data["postText"] = "" // XXX
}
}

return c.Render(http.StatusOK, "postEmbed.html", data)
}
1 change: 1 addition & 0 deletions bskyweb/embedr-static/iframe-resize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* script to resize embed ifame would go here? */
9 changes: 8 additions & 1 deletion bskyweb/embedr-templates/home.html
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
embed homepage: could redirect to bsky.app, or show a "create embed" widget
<html>
<head>
</head>
<body>
<h1>embed.bsky.app homepage</h1>
<p>could redirect to bsky.app? or show a "create embed" widget?
</body>
</html>
3 changes: 3 additions & 0 deletions bskyweb/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ import "embed"

//go:embed static/*
var StaticFS embed.FS

//go:embed embedr-static/*
var EmbedrStaticFS embed.FS

0 comments on commit 666247a

Please sign in to comment.