mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at utm-source 213 lines 6.2 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_url,omitempty"` 57 CacheAge int `json:"cache_age,omitempty"` 58 Width *int `json:"width"` 59 Height *int `json:"height"` 60 HTML string `json:"html,omitempty"` 61} 62 63func (srv *Server) parseBlueskyURL(ctx context.Context, raw string) (*syntax.ATURI, error) { 64 65 if raw == "" { 66 return nil, fmt.Errorf("empty url") 67 } 68 69 // first try simple AT-URI 70 uri, err := syntax.ParseATURI(raw) 71 if nil == err { 72 return &uri, nil 73 } 74 75 // then try bsky.app post URL 76 u, err := url.Parse(raw) 77 if err != nil { 78 return nil, err 79 } 80 if u.Hostname() != "bsky.app" { 81 return nil, fmt.Errorf("only bsky.app URLs currently supported") 82 } 83 pathParts := strings.Split(u.Path, "/") // NOTE: pathParts[0] will be empty string 84 if len(pathParts) != 5 || pathParts[1] != "profile" || pathParts[3] != "post" { 85 return nil, fmt.Errorf("only bsky.app post URLs currently supported") 86 } 87 atid, err := syntax.ParseAtIdentifier(pathParts[2]) 88 if err != nil { 89 return nil, err 90 } 91 rkey, err := syntax.ParseRecordKey(pathParts[4]) 92 if err != nil { 93 return nil, err 94 } 95 var did syntax.DID 96 if atid.IsHandle() { 97 ident, err := srv.dir.Lookup(ctx, *atid) 98 if err != nil { 99 return nil, err 100 } 101 did = ident.DID 102 } else { 103 did, err = atid.AsDID() 104 if err != nil { 105 return nil, err 106 } 107 } 108 109 // TODO: don't really need to re-parse here, if we had test coverage 110 aturi, err := syntax.ParseATURI(fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)) 111 if err != nil { 112 return nil, err 113 } else { 114 return &aturi, nil 115 } 116} 117 118func (srv *Server) WebOEmbed(c echo.Context) error { 119 formatParam := c.QueryParam("format") 120 if formatParam != "" && formatParam != "json" { 121 return c.String(http.StatusNotImplemented, "Unsupported oEmbed format: "+formatParam) 122 } 123 124 // TODO: do we actually do something with width? 125 width := 600 126 maxWidthParam := c.QueryParam("maxwidth") 127 if maxWidthParam != "" { 128 maxWidthInt, err := strconv.Atoi(maxWidthParam) 129 if err != nil { 130 return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer)") 131 } 132 if maxWidthInt < 220 { 133 width = 220 134 } else if maxWidthInt > 600 { 135 width = 600 136 } else { 137 width = maxWidthInt 138 } 139 } 140 // NOTE: maxheight ignored 141 142 aturi, err := srv.parseBlueskyURL(c.Request().Context(), c.QueryParam("url")) 143 if err != nil { 144 return c.String(http.StatusBadRequest, fmt.Sprintf("Expected 'url' to be bsky.app URL or AT-URI: %v", err)) 145 } 146 if aturi.Collection() != syntax.NSID("app.bsky.feed.post") { 147 return c.String(http.StatusNotImplemented, "Only posts (app.bsky.feed.post records) can be embedded currently") 148 } 149 did, err := aturi.Authority().AsDID() 150 if err != nil { 151 return err 152 } 153 154 post, err := srv.getBlueskyPost(c.Request().Context(), did, aturi.RecordKey()) 155 if err == ErrPostNotFound { 156 return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) 157 } else if err == ErrPostNotPublic { 158 return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) 159 } else if err != nil { 160 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 161 } 162 163 html, err := srv.postEmbedHTML(post) 164 if err != nil { 165 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 166 } 167 data := OEmbedResponse{ 168 Type: "rich", 169 Version: "1.0", 170 AuthorName: "@" + post.Author.Handle, 171 AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle), 172 ProviderName: "Bluesky Social", 173 CacheAge: 86400, 174 Width: &width, 175 Height: nil, 176 HTML: html, 177 } 178 if post.Author.DisplayName != nil { 179 data.AuthorName = fmt.Sprintf("%s (@%s)", *post.Author.DisplayName, post.Author.Handle) 180 } 181 return c.JSON(http.StatusOK, data) 182} 183 184func (srv *Server) WebPostEmbed(c echo.Context) error { 185 186 // sanity check arguments. don't 4xx, just let app handle if not expected format 187 rkeyParam := c.Param("rkey") 188 rkey, err := syntax.ParseRecordKey(rkeyParam) 189 if err != nil { 190 return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid RecordKey: %v", err)) 191 } 192 didParam := c.Param("did") 193 did, err := syntax.ParseDID(didParam) 194 if err != nil { 195 return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid DID: %v", err)) 196 } 197 _ = rkey 198 _ = did 199 200 // NOTE: this request was't really necessary; the JS will do the same fetch 201 /* 202 postView, err := srv.getBlueskyPost(ctx, did, rkey) 203 if err == ErrPostNotFound { 204 return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) 205 } else if err == ErrPostNotPublic { 206 return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) 207 } else if err != nil { 208 return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 209 } 210 */ 211 212 return c.Render(http.StatusOK, "postEmbed.html", nil) 213}