this repo has no description

more endpoints

Changed files
+223 -20
api
generated
internal
helpers
+14 -10
api/server/feedgetlikes.go
··· 8 "github.com/labstack/echo/v4" 9 vyletdatabase "github.com/vylet-app/go/database/proto" 10 "github.com/vylet-app/go/generated/vylet" 11 ) 12 13 type GetFeedLikesBySubjectInput struct { 14 Uri string `query:"uri"` 15 - Limit int64 `query:"limit"` 16 Cursor *string `query:"cursor"` 17 } 18 19 - func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, error) { 20 logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri) 21 22 resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{ ··· 25 Cursor: cursor, 26 }) 27 if err != nil { 28 - return nil, fmt.Errorf("failed to get likes by subject: %w", err) 29 } 30 31 dids := make([]string, 0, len(resp.Likes)) ··· 35 36 profiles, err := s.getProfiles(ctx, dids) 37 if err != nil { 38 - return nil, fmt.Errorf("failed to get profiles for subject: %w", err) 39 } 40 41 likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes)) ··· 53 }) 54 } 55 56 - return likes, nil 57 } 58 59 - func (s *Server) handleGetLikesBySubject(e echo.Context) error { 60 ctx := e.Request().Context() 61 62 - logger := s.logger.With("name", "handleGetLikesByPost") 63 64 var input GetFeedLikesBySubjectInput 65 if err := e.Bind(&input); err != nil { ··· 71 return NewValidationError("uri", "URI must be provided") 72 } 73 74 - if input.Limit < 1 || input.Limit > 100 { 75 return NewValidationError("limit", "limit must be between 1 and 100") 76 } 77 78 logger = logger.With("uri", input.Uri) 79 80 - likes, err := s.getLikesBySubject(ctx, input.Uri, input.Limit, input.Cursor) 81 if err != nil { 82 logger.Error("failed to get subject likes", "err", err) 83 return ErrInternalServerErr 84 } 85 86 return e.JSON(200, vylet.FeedGetSubjectLikes_Output{ 87 - Likes: likes, 88 }) 89 }
··· 8 "github.com/labstack/echo/v4" 9 vyletdatabase "github.com/vylet-app/go/database/proto" 10 "github.com/vylet-app/go/generated/vylet" 11 + "github.com/vylet-app/go/internal/helpers" 12 ) 13 14 type GetFeedLikesBySubjectInput struct { 15 Uri string `query:"uri"` 16 + Limit *int64 `query:"limit"` 17 Cursor *string `query:"cursor"` 18 } 19 20 + func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, *string, error) { 21 logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri) 22 23 resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{ ··· 26 Cursor: cursor, 27 }) 28 if err != nil { 29 + return nil, nil, fmt.Errorf("failed to get likes by subject: %w", err) 30 } 31 32 dids := make([]string, 0, len(resp.Likes)) ··· 36 37 profiles, err := s.getProfiles(ctx, dids) 38 if err != nil { 39 + return nil, nil, fmt.Errorf("failed to get profiles for subject: %w", err) 40 } 41 42 likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes)) ··· 54 }) 55 } 56 57 + return likes, resp.Cursor, nil 58 } 59 60 + func (s *Server) handleGetSubjectLikes(e echo.Context) error { 61 ctx := e.Request().Context() 62 63 + logger := s.logger.With("name", "handleGetSubjectLikes") 64 65 var input GetFeedLikesBySubjectInput 66 if err := e.Bind(&input); err != nil { ··· 72 return NewValidationError("uri", "URI must be provided") 73 } 74 75 + if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 76 return NewValidationError("limit", "limit must be between 1 and 100") 77 + } else if input.Limit == nil { 78 + input.Limit = helpers.ToInt64Ptr(25) 79 } 80 81 logger = logger.With("uri", input.Uri) 82 83 + likes, cursor, err := s.getLikesBySubject(ctx, input.Uri, *input.Limit, input.Cursor) 84 if err != nil { 85 logger.Error("failed to get subject likes", "err", err) 86 return ErrInternalServerErr 87 } 88 89 return e.JSON(200, vylet.FeedGetSubjectLikes_Output{ 90 + Likes: likes, 91 + Cursor: cursor, 92 }) 93 }
+87 -9
api/server/feedgetposts.go
··· 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "time" 8 9 "github.com/bluesky-social/indigo/lex/util" ··· 14 "github.com/vylet-app/go/internal/helpers" 15 "golang.org/x/sync/errgroup" 16 ) 17 - 18 - type GetFeedPostsInput struct { 19 - Uris []string `query:"uris"` 20 - } 21 22 func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) { 23 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ ··· 82 } 83 84 func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) { 85 - logger := s.logger.With("name", "feedPostsToPostViews") 86 - 87 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ 88 Uris: uris, 89 }) ··· 94 return nil, ErrDatabaseNotFound 95 } 96 97 - dids := make([]string, 0, len(resp.Posts)) 98 addedDids := make(map[string]struct{}) 99 - for _, post := range resp.Posts { 100 if _, ok := addedDids[post.AuthorDid]; ok { 101 continue 102 } ··· 131 } 132 133 feedPostViews := make(map[string]*vylet.FeedDefs_PostView) 134 - for _, post := range resp.Posts { 135 profileBasic, ok := profiles[post.AuthorDid] 136 if !ok { 137 logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri) ··· 197 return feedPostViews, nil 198 } 199 200 func (s *Server) handleGetPosts(e echo.Context) error { 201 ctx := e.Request().Context() 202 ··· 245 Posts: orderedPostViews, 246 }) 247 }
··· 3 import ( 4 "context" 5 "encoding/json" 6 + "errors" 7 "fmt" 8 + "sort" 9 "time" 10 11 "github.com/bluesky-social/indigo/lex/util" ··· 16 "github.com/vylet-app/go/internal/helpers" 17 "golang.org/x/sync/errgroup" 18 ) 19 20 func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) { 21 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ ··· 80 } 81 82 func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) { 83 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ 84 Uris: uris, 85 }) ··· 90 return nil, ErrDatabaseNotFound 91 } 92 93 + feedPostViews, err := s.postsToPostViews(ctx, resp.Posts, viewer) 94 + if err != nil { 95 + return nil, err 96 + } 97 + 98 + return feedPostViews, nil 99 + } 100 + 101 + func (s *Server) postsToPostViews(ctx context.Context, posts map[string]*vyletdatabase.Post, viewer string) (map[string]*vylet.FeedDefs_PostView, error) { 102 + logger := s.logger.With("name", "postsToPostViews") 103 + 104 + uris := make([]string, 0, len(posts)) 105 + dids := make([]string, 0, len(posts)) 106 addedDids := make(map[string]struct{}) 107 + for uri, post := range posts { 108 + uris = append(uris, uri) 109 + 110 if _, ok := addedDids[post.AuthorDid]; ok { 111 continue 112 } ··· 141 } 142 143 feedPostViews := make(map[string]*vylet.FeedDefs_PostView) 144 + for _, post := range posts { 145 profileBasic, ok := profiles[post.AuthorDid] 146 if !ok { 147 logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri) ··· 207 return feedPostViews, nil 208 } 209 210 + type GetFeedPostsInput struct { 211 + Uris []string `query:"uris"` 212 + } 213 + 214 func (s *Server) handleGetPosts(e echo.Context) error { 215 ctx := e.Request().Context() 216 ··· 259 Posts: orderedPostViews, 260 }) 261 } 262 + 263 + type GetFeedActorPostsInput struct { 264 + Actor string `query:"actor"` 265 + Limit *int64 `query:"limit"` 266 + Cursor *string `query:"cursor"` 267 + } 268 + 269 + func (s *Server) handleGetActorPosts(e echo.Context) error { 270 + ctx := e.Request().Context() 271 + 272 + logger := s.logger.With("name", "handleGetActorPosts") 273 + 274 + var input GetFeedActorPostsInput 275 + if err := e.Bind(&input); err != nil { 276 + logger.Error("failed to bind", "err", err) 277 + return ErrInternalServerErr 278 + } 279 + 280 + if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 281 + return NewValidationError("limit", "limit must be between 1 and 100") 282 + } else if input.Limit == nil { 283 + input.Limit = helpers.ToInt64Ptr(25) 284 + } 285 + 286 + logger = logger.With("actor", input.Actor, "limit", *input.Limit, "cursor", input.Cursor) 287 + 288 + did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor) 289 + if err != nil { 290 + if errors.Is(err, ErrActorNotValid) { 291 + return NewValidationError("actor", "actor must be a valid DID or handle") 292 + } 293 + logger.Error("error fetching did and handle", "err", err) 294 + return ErrInternalServerErr 295 + } 296 + 297 + resp, err := s.client.Post.GetPostsByActor(ctx, &vyletdatabase.GetPostsByActorRequest{ 298 + Did: did, 299 + Limit: *input.Limit, 300 + Cursor: input.Cursor, 301 + }) 302 + if err != nil { 303 + logger.Error("failed to get posts", "did", did) 304 + return ErrInternalServerErr 305 + } 306 + 307 + postViews, err := s.postsToPostViews(ctx, resp.Posts, "") // TODO: set viewer 308 + if err != nil { 309 + s.logger.Error("failed to get post views", "err", err) 310 + return ErrInternalServerErr 311 + } 312 + 313 + sortedPostViews := make([]*vylet.FeedDefs_PostView, 0, len(postViews)) 314 + for _, postView := range postViews { 315 + sortedPostViews = append(sortedPostViews, postView) 316 + } 317 + sort.Slice(sortedPostViews, func(i, j int) bool { 318 + return sortedPostViews[i].CreatedAt > sortedPostViews[j].CreatedAt 319 + }) 320 + 321 + return e.JSON(200, vylet.FeedGetActorPosts_Output{ 322 + Posts: sortedPostViews, 323 + Cursor: resp.Cursor, 324 + }) 325 + }
+2 -1
api/server/server.go
··· 129 130 // app.vylet.feed 131 s.echo.GET("/xrpc/app.vylet.feed.getPosts", s.handleGetPosts) 132 - s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetLikesBySubject) 133 } 134 135 func (s *Server) errorHandler(err error, c echo.Context) {
··· 129 130 // app.vylet.feed 131 s.echo.GET("/xrpc/app.vylet.feed.getPosts", s.handleGetPosts) 132 + s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetSubjectLikes) 133 + s.echo.GET("/xrpc/app.vylet.feed.getActorPosts", s.handleGetActorPosts) 134 } 135 136 func (s *Server) errorHandler(err error, c echo.Context) {
+76
bench.py
···
··· 1 + import requests 2 + import time 3 + import sys 4 + from datetime import datetime 5 + 6 + # Configuration 7 + URL = "http://localhost:8080/xrpc/app.vylet.feed.getActorPosts?actor=hailey.at" 8 + INITIAL_DELAY = 5.0 9 + MIN_DELAY = 0.001 10 + RAMP_FACTOR = 0.5 11 + 12 + 13 + def make_request(session, request_num, delay): 14 + try: 15 + start_time = time.time() 16 + response = session.get(URL, timeout=10) 17 + elapsed = time.time() - start_time 18 + 19 + timestamp = datetime.now().strftime("%H:%M:%S") 20 + rate = 1 / delay if delay > 0 else float("inf") 21 + 22 + print( 23 + f"[{timestamp}] Request #{request_num:4d} | " 24 + f"Status: {response.status_code} | " 25 + f"Time: {elapsed:.3f}s | " 26 + f"Rate: {rate:.2f} req/s" 27 + ) 28 + 29 + return True 30 + except requests.exceptions.RequestException as e: 31 + timestamp = datetime.now().strftime("%H:%M:%S") 32 + print(f"[{timestamp}] Request #{request_num:4d} | ERROR: {e}") 33 + return False 34 + 35 + 36 + def main(): 37 + print("=" * 70) 38 + print("Gradual Load Tester") 39 + print("=" * 70) 40 + print(f"Target URL: {URL}") 41 + print(f"Starting delay: {INITIAL_DELAY}s ({1 / INITIAL_DELAY:.2f} req/s)") 42 + print(f"Min delay: {MIN_DELAY}s ({1 / MIN_DELAY:.2f} req/s)") 43 + print( 44 + f"Ramp factor: {RAMP_FACTOR} (gets {(1 - RAMP_FACTOR) * 100:.0f}% faster each cycle)" 45 + ) 46 + print("=" * 70) 47 + print("\nPress Ctrl+C to stop\n") 48 + 49 + session = requests.Session() 50 + delay = INITIAL_DELAY 51 + request_num = 0 52 + 53 + try: 54 + while True: 55 + request_num += 1 56 + make_request(session, request_num, delay) 57 + 58 + time.sleep(delay) 59 + 60 + if delay > MIN_DELAY: 61 + delay = max(delay * RAMP_FACTOR, MIN_DELAY) 62 + if delay == MIN_DELAY: 63 + print(f"\n{'=' * 70}") 64 + print(f"Reached maximum rate: {1 / MIN_DELAY:.2f} requests/second") 65 + print(f"{'=' * 70}\n") 66 + 67 + except KeyboardInterrupt: 68 + print(f"\n\n{'=' * 70}") 69 + print(f"Stopped after {request_num} requests") 70 + print(f"Final rate: {1 / delay:.2f} requests/second") 71 + print(f"{'=' * 70}") 72 + sys.exit(0) 73 + 74 + 75 + if __name__ == "__main__": 76 + main()
+36
generated/vylet/feedgetActorPosts.go
···
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: app.vylet.feed.getActorPosts 4 + 5 + package vylet 6 + 7 + import ( 8 + "context" 9 + 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // FeedGetActorPosts_Output is the output of a app.vylet.feed.getActorPosts call. 14 + type FeedGetActorPosts_Output struct { 15 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 16 + Posts []*FeedDefs_PostView `json:"posts" cborgen:"posts"` 17 + } 18 + 19 + // FeedGetActorPosts calls the XRPC method "app.vylet.feed.getActorPosts". 20 + func FeedGetActorPosts(ctx context.Context, c lexutil.LexClient, actor string, cursor string, limit int64) (*FeedGetActorPosts_Output, error) { 21 + var out FeedGetActorPosts_Output 22 + 23 + params := map[string]interface{}{} 24 + params["actor"] = actor 25 + if cursor != "" { 26 + params["cursor"] = cursor 27 + } 28 + if limit != 0 { 29 + params["limit"] = limit 30 + } 31 + if err := c.LexDo(ctx, lexutil.Query, "", "app.vylet.feed.getActorPosts", params, nil, &out); err != nil { 32 + return nil, err 33 + } 34 + 35 + return &out, nil 36 + }
+8
internal/helpers/helpers.go
··· 13 return &str 14 } 15 16 func ImageCidToCdnUrl(cid string, size string) string { 17 return fmt.Sprintf("https://cdn.vylet.app/%s/%s@png", cid, size) 18 }
··· 13 return &str 14 } 15 16 + func ToIntPtr(num int) *int { 17 + return &num 18 + } 19 + 20 + func ToInt64Ptr(num int64) *int64 { 21 + return &num 22 + } 23 + 24 func ImageCidToCdnUrl(cid string, size string) string { 25 return fmt.Sprintf("https://cdn.vylet.app/%s/%s@png", cid, size) 26 }