Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 215 lines 6.3 kB view raw
1package main 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strconv" 10 "strings" 11 12 appbsky "github.com/bluesky-social/indigo/api/bsky" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 15 "github.com/labstack/echo/v4" 16) 17 18var ErrPostNotFound = errors.New("post not found") 19var ErrPostNotPublic = errors.New("post is not publicly accessible") 20 21func (srv *Server) getBlueskyPost(ctx context.Context, did syntax.DID, rkey syntax.RecordKey) (*appbsky.FeedDefs_PostView, error) { 22 23 // fetch the post post (with extra context) 24 uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) 25 tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri) 26 if err != nil { 27 log.Warnf("failed to fetch post: %s\t%v", uri, err) 28 // TODO: detect 404, specifically? 29 return nil, ErrPostNotFound 30 } 31 32 if tpv.Thread.FeedDefs_BlockedPost != nil { 33 return nil, ErrPostNotPublic 34 } else if tpv.Thread.FeedDefs_ThreadViewPost.Post == nil { 35 return nil, ErrPostNotFound 36 } 37 38 postView := tpv.Thread.FeedDefs_ThreadViewPost.Post 39 for _, label := range postView.Author.Labels { 40 if label.Src == postView.Author.Did && label.Val == "!no-unauthenticated" { 41 return nil, ErrPostNotPublic 42 } 43 } 44 return postView, nil 45} 46 47func (srv *Server) WebHome(c echo.Context) error { 48 return c.Render(http.StatusOK, "home.html", nil) 49} 50 51type OEmbedResponse struct { 52 Type string `json:"type"` 53 Version string `json:"version"` 54 AuthorName string `json:"author_name,omitempty"` 55 AuthorURL string `json:"author_url,omitempty"` 56 ProviderName string `json:"provider_name,omitempty"` 57 ProviderURL string `json:"provider_url,omitempty"` 58 CacheAge int `json:"cache_age,omitempty"` 59 Width *int `json:"width"` 60 Height *int `json:"height"` 61 HTML string `json:"html,omitempty"` 62} 63 64func (srv *Server) parseBlueskyURL(ctx context.Context, raw string) (*syntax.ATURI, error) { 65 66 if raw == "" { 67 return nil, fmt.Errorf("empty url") 68 } 69 70 // first try simple AT-URI 71 uri, err := syntax.ParseATURI(raw) 72 if nil == err { 73 return &uri, nil 74 } 75 76 // then try bsky.app post URL 77 u, err := url.Parse(raw) 78 if err != nil { 79 return nil, err 80 } 81 if u.Hostname() != "bsky.app" { 82 return nil, fmt.Errorf("only bsky.app URLs currently supported") 83 } 84 pathParts := strings.Split(u.Path, "/") // NOTE: pathParts[0] will be empty string 85 if len(pathParts) != 5 || pathParts[1] != "profile" || pathParts[3] != "post" { 86 return nil, fmt.Errorf("only bsky.app post URLs currently supported") 87 } 88 atid, err := syntax.ParseAtIdentifier(pathParts[2]) 89 if err != nil { 90 return nil, err 91 } 92 rkey, err := syntax.ParseRecordKey(pathParts[4]) 93 if err != nil { 94 return nil, err 95 } 96 var did syntax.DID 97 if atid.IsHandle() { 98 ident, err := srv.dir.Lookup(ctx, *atid) 99 if err != nil { 100 return nil, err 101 } 102 did = ident.DID 103 } else { 104 did, err = atid.AsDID() 105 if err != nil { 106 return nil, err 107 } 108 } 109 110 // TODO: don't really need to re-parse here, if we had test coverage 111 aturi, err := syntax.ParseATURI(fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)) 112 if err != nil { 113 return nil, err 114 } else { 115 return &aturi, nil 116 } 117} 118 119func (srv *Server) WebOEmbed(c echo.Context) error { 120 formatParam := c.QueryParam("format") 121 if formatParam != "" && formatParam != "json" { 122 return c.String(http.StatusNotImplemented, "Unsupported oEmbed format: "+formatParam) 123 } 124 125 // TODO: do we actually do something with width? 126 width := 600 127 maxWidthParam := c.QueryParam("maxwidth") 128 if maxWidthParam != "" { 129 maxWidthInt, err := strconv.Atoi(maxWidthParam) 130 if err != nil { 131 return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer)") 132 } 133 if maxWidthInt < 220 { 134 width = 220 135 } else if maxWidthInt > 600 { 136 width = 600 137 } else { 138 width = maxWidthInt 139 } 140 } 141 // NOTE: maxheight ignored 142 143 aturi, err := srv.parseBlueskyURL(c.Request().Context(), c.QueryParam("url")) 144 if err != nil { 145 return c.String(http.StatusBadRequest, fmt.Sprintf("Expected 'url' to be bsky.app URL or AT-URI: %v", err)) 146 } 147 if aturi.Collection() != syntax.NSID("app.bsky.feed.post") { 148 return c.String(http.StatusNotImplemented, "Only posts (app.bsky.feed.post records) can be embedded currently") 149 } 150 did, err := aturi.Authority().AsDID() 151 if err != nil { 152 return err 153 } 154 155 post, err := srv.getBlueskyPost(c.Request().Context(), did, aturi.RecordKey()) 156 if err == ErrPostNotFound { 157 return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) 158 } else if err == ErrPostNotPublic { 159 return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) 160 } else if err != nil { 161 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 162 } 163 164 html, err := srv.postEmbedHTML(post) 165 if err != nil { 166 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 167 } 168 data := OEmbedResponse{ 169 Type: "rich", 170 Version: "1.0", 171 AuthorName: "@" + post.Author.Handle, 172 AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle), 173 ProviderName: "Bluesky Social", 174 ProviderURL: "https://bsky.app", 175 CacheAge: 86400, 176 Width: &width, 177 Height: nil, 178 HTML: html, 179 } 180 if post.Author.DisplayName != nil { 181 data.AuthorName = fmt.Sprintf("%s (@%s)", *post.Author.DisplayName, post.Author.Handle) 182 } 183 return c.JSON(http.StatusOK, data) 184} 185 186func (srv *Server) WebPostEmbed(c echo.Context) error { 187 188 // sanity check arguments. don't 4xx, just let app handle if not expected format 189 rkeyParam := c.Param("rkey") 190 rkey, err := syntax.ParseRecordKey(rkeyParam) 191 if err != nil { 192 return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid RecordKey: %v", err)) 193 } 194 didParam := c.Param("did") 195 did, err := syntax.ParseDID(didParam) 196 if err != nil { 197 return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid DID: %v", err)) 198 } 199 _ = rkey 200 _ = did 201 202 // NOTE: this request was't really necessary; the JS will do the same fetch 203 /* 204 postView, err := srv.getBlueskyPost(ctx, did, rkey) 205 if err == ErrPostNotFound { 206 return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) 207 } else if err == ErrPostNotPublic { 208 return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) 209 } else if err != nil { 210 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 211 } 212 */ 213 214 return c.Render(http.StatusOK, "postEmbed.html", nil) 215}