mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}