this repo has no description
at main 312 lines 9.0 kB view raw
1package server 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "sort" 9 "time" 10 11 "github.com/bluesky-social/indigo/lex/util" 12 "github.com/labstack/echo/v4" 13 "github.com/vylet-app/go/database/client" 14 vyletdatabase "github.com/vylet-app/go/database/proto" 15 "github.com/vylet-app/go/generated/handlers" 16 "github.com/vylet-app/go/generated/vylet" 17 "github.com/vylet-app/go/internal/helpers" 18 "golang.org/x/sync/errgroup" 19) 20 21func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) { 22 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ 23 Uris: uris, 24 }) 25 if err != nil { 26 return nil, fmt.Errorf("failed to get post: %w", err) 27 } 28 if client.IsNotFoundError(resp.Error) { 29 return nil, ErrDatabaseNotFound 30 } 31 32 feedPosts := make(map[string]*vylet.FeedPost) 33 for _, post := range resp.Posts { 34 feedPost := &vylet.FeedPost{ 35 Caption: post.Caption, 36 CreatedAt: post.CreatedAt.AsTime().Format(time.RFC3339Nano), 37 Media: &vylet.FeedPost_Media{}, 38 } 39 40 if post.Images == nil { 41 return nil, fmt.Errorf("bad post, contains no media") 42 } 43 44 media := vylet.FeedPost_Media{ 45 MediaImages: &vylet.MediaImages{ 46 Images: make([]*vylet.MediaImages_Image, 0, len(post.Images)), 47 }, 48 } 49 50 for _, img := range post.Images { 51 mediaImg := &vylet.MediaImages_Image{ 52 Image: &util.LexBlob{ 53 Ref: helpers.StrToLexLink(img.Cid), 54 MimeType: img.Mime, 55 Size: img.Size, 56 }, 57 } 58 if img.Alt != nil { 59 mediaImg.Alt = *img.Alt 60 } 61 if img.Width != nil && img.Height != nil { 62 mediaImg.AspectRatio = &vylet.MediaDefs_AspectRatio{ 63 Width: *img.Width, 64 Height: *img.Height, 65 } 66 } 67 68 media.MediaImages.Images = append(media.MediaImages.Images, mediaImg) 69 } 70 71 if post.Facets != nil { 72 var facets []*vylet.RichtextFacet 73 if err := json.Unmarshal(post.Facets, &facets); err != nil { 74 return nil, fmt.Errorf("failed to unmarshal post facets: %w", err) 75 } 76 feedPost.Facets = facets 77 } 78 } 79 80 return feedPosts, nil 81} 82 83func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) { 84 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ 85 Uris: uris, 86 }) 87 if err != nil { 88 return nil, fmt.Errorf("failed to get post: %w", err) 89 } 90 if client.IsNotFoundError(resp.Error) { 91 return nil, ErrDatabaseNotFound 92 } 93 94 feedPostViews, err := s.postsToPostViews(ctx, resp.Posts, viewer) 95 if err != nil { 96 return nil, err 97 } 98 99 return feedPostViews, nil 100} 101 102func (s *Server) postsToPostViews(ctx context.Context, posts map[string]*vyletdatabase.Post, viewer string) (map[string]*vylet.FeedDefs_PostView, error) { 103 logger := s.logger.With("name", "postsToPostViews") 104 105 uris := make([]string, 0, len(posts)) 106 dids := make([]string, 0, len(posts)) 107 addedDids := make(map[string]struct{}) 108 for uri, post := range posts { 109 uris = append(uris, uri) 110 111 if _, ok := addedDids[post.AuthorDid]; ok { 112 continue 113 } 114 dids = append(dids, post.AuthorDid) 115 addedDids[post.AuthorDid] = struct{}{} 116 } 117 118 g, gCtx := errgroup.WithContext(ctx) 119 var profiles map[string]*vylet.ActorDefs_ProfileViewBasic 120 var countsResp *vyletdatabase.GetPostsInteractionCountsResponse 121 g.Go(func() error { 122 maybeProfiles, err := s.getProfilesBasic(gCtx, dids) 123 if err != nil { 124 return err 125 } 126 profiles = maybeProfiles 127 return nil 128 }) 129 g.Go(func() error { 130 maybeCounts, err := s.client.Post.GetPostsInteractionCounts(gCtx, &vyletdatabase.GetPostsInteractionCountsRequest{Uris: uris}) 131 if err != nil { 132 return err 133 } 134 if maybeCounts.Error != nil { 135 return fmt.Errorf("failed to get post interaction counts: %s", *maybeCounts.Error) 136 } 137 countsResp = maybeCounts 138 return nil 139 }) 140 if err := g.Wait(); err != nil { 141 return nil, fmt.Errorf("error getting metadata: %w", err) 142 } 143 144 feedPostViews := make(map[string]*vylet.FeedDefs_PostView) 145 for _, post := range posts { 146 profileBasic, ok := profiles[post.AuthorDid] 147 if !ok { 148 logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri) 149 continue 150 } 151 counts, ok := countsResp.Counts[post.Uri] 152 if !ok { 153 logger.Warn("failed to get counts for post", "uri", post.Uri) 154 continue 155 } 156 157 postView := &vylet.FeedDefs_PostView{ 158 Author: profileBasic, 159 Caption: post.Caption, 160 Cid: post.Cid, 161 Facets: []*vylet.RichtextFacet{}, 162 // Labels: []*atproto.LabelDefs_Label{}, 163 Media: &vylet.FeedDefs_PostView_Media{}, 164 LikeCount: counts.Likes, 165 ReplyCount: counts.Replies, 166 Uri: post.Uri, 167 Viewer: &vylet.FeedDefs_ViewerState{}, 168 CreatedAt: post.CreatedAt.AsTime().Format(time.RFC3339Nano), 169 IndexedAt: post.IndexedAt.AsTime().Format(time.RFC3339Nano), 170 } 171 172 media := vylet.FeedDefs_PostView_Media{ 173 MediaImages_View: &vylet.MediaImages_View{ 174 Images: make([]*vylet.MediaImages_ViewImage, 0, len(post.Images)), 175 }, 176 } 177 for _, img := range post.Images { 178 mediaImg := &vylet.MediaImages_ViewImage{ 179 Alt: img.Alt, 180 Fullsize: helpers.ImageCidToCdnUrl(s.cdnHost, "fullsize", post.AuthorDid, img.Cid), 181 Thumbnail: helpers.ImageCidToCdnUrl(s.cdnHost, "thumb", post.AuthorDid, img.Cid), 182 } 183 if img.Width != nil && img.Height != nil { 184 mediaImg.AspectRatio = &vylet.MediaDefs_AspectRatio{ 185 Width: *img.Width, 186 Height: *img.Height, 187 } 188 } 189 190 media.MediaImages_View.Images = append(media.MediaImages_View.Images, mediaImg) 191 } 192 postView.Media = &media 193 194 if post.Facets != nil { 195 var facets []*vylet.RichtextFacet 196 if err := json.Unmarshal(post.Facets, &facets); err != nil { 197 logger.Error("failed to unmarshal post facets", "uri", post.Uri, "err", err) 198 continue 199 } 200 postView.Facets = facets 201 } 202 203 feedPostViews[post.Uri] = postView 204 } 205 206 return feedPostViews, nil 207} 208 209func (s *Server) FeedGetPostsRequiresAuth() bool { 210 return false 211} 212 213func (s *Server) HandleFeedGetPosts(e echo.Context, input *handlers.FeedGetPostsInput) (*vylet.FeedGetPosts_Output, *echo.HTTPError) { 214 ctx := e.Request().Context() 215 viewer := getViewer(e) 216 217 logger := s.logger.With("name", "HandleFeedGetPosts", "viewer", viewer) 218 219 if len(input.Uris) == 0 { 220 return nil, NewValidationError("uris", "must supply at least one AT-URI") 221 } 222 223 if len(input.Uris) > 25 { 224 return nil, NewValidationError("uris", "no more than 25 AT-URIs may be supplied") 225 } 226 227 if allValid, err := helpers.ValidateUris(input.Uris); !allValid { 228 logger.Warn("received invalid URIs", "uris", input.Uris, "err", err) 229 return nil, NewValidationError("uris", "all URIs must be valid AT-URIs") 230 } 231 232 postViews, err := s.getPostViews(ctx, input.Uris, viewer) 233 if err != nil { 234 logger.Error("failed to get posts", "err", err) 235 return nil, ErrInternalServerErr 236 } 237 238 if len(postViews) == 0 { 239 return nil, ErrNotFound 240 } 241 242 orderedPostViews := make([]*vylet.FeedDefs_PostView, 0, len(postViews)) 243 for _, uri := range input.Uris { 244 postView, ok := postViews[uri] 245 if !ok { 246 logger.Warn("failed to find post for uri", "uri", uri) 247 continue 248 } 249 orderedPostViews = append(orderedPostViews, postView) 250 } 251 252 return &vylet.FeedGetPosts_Output{ 253 Posts: orderedPostViews, 254 }, nil 255} 256 257func (s *Server) FeedGetActorPostsRequiresAuth() bool { 258 return false 259} 260 261func (s *Server) HandleFeedGetActorPosts(e echo.Context, input *handlers.FeedGetActorPostsInput) (*vylet.FeedGetActorPosts_Output, *echo.HTTPError) { 262 ctx := e.Request().Context() 263 viewer := getViewer(e) 264 265 logger := s.logger.With("name", "handleGetActorPosts", "viewer", viewer) 266 267 if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 268 return nil, NewValidationError("limit", "limit must be between 1 and 100") 269 } else if input.Limit == nil { 270 input.Limit = helpers.ToInt64Ptr(25) 271 } 272 273 logger = logger.With("actor", input.Actor, "limit", *input.Limit, "cursor", input.Cursor) 274 275 did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor) 276 if err != nil { 277 if errors.Is(err, ErrActorNotValid) { 278 return nil, NewValidationError("actor", "actor must be a valid DID or handle") 279 } 280 logger.Error("error fetching did and handle", "err", err) 281 return nil, ErrInternalServerErr 282 } 283 284 resp, err := s.client.Post.GetPostsByActor(ctx, &vyletdatabase.GetPostsByActorRequest{ 285 Did: did, 286 Limit: *input.Limit, 287 Cursor: input.Cursor, 288 }) 289 if err != nil { 290 logger.Error("failed to get posts", "did", did) 291 return nil, ErrInternalServerErr 292 } 293 294 postViews, err := s.postsToPostViews(ctx, resp.Posts, viewer) 295 if err != nil { 296 s.logger.Error("failed to get post views", "err", err) 297 return nil, ErrInternalServerErr 298 } 299 300 sortedPostViews := make([]*vylet.FeedDefs_PostView, 0, len(postViews)) 301 for _, postView := range postViews { 302 sortedPostViews = append(sortedPostViews, postView) 303 } 304 sort.Slice(sortedPostViews, func(i, j int) bool { 305 return sortedPostViews[i].CreatedAt > sortedPostViews[j].CreatedAt 306 }) 307 308 return &vylet.FeedGetActorPosts_Output{ 309 Posts: sortedPostViews, 310 Cursor: resp.Cursor, 311 }, nil 312}