this repo has no description

more endpoints

Changed files
+223 -20
api
generated
internal
helpers
+14 -10
api/server/feedgetlikes.go
··· 8 8 "github.com/labstack/echo/v4" 9 9 vyletdatabase "github.com/vylet-app/go/database/proto" 10 10 "github.com/vylet-app/go/generated/vylet" 11 + "github.com/vylet-app/go/internal/helpers" 11 12 ) 12 13 13 14 type GetFeedLikesBySubjectInput struct { 14 15 Uri string `query:"uri"` 15 - Limit int64 `query:"limit"` 16 + Limit *int64 `query:"limit"` 16 17 Cursor *string `query:"cursor"` 17 18 } 18 19 19 - func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, error) { 20 + func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, *string, error) { 20 21 logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri) 21 22 22 23 resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{ ··· 25 26 Cursor: cursor, 26 27 }) 27 28 if err != nil { 28 - return nil, fmt.Errorf("failed to get likes by subject: %w", err) 29 + return nil, nil, fmt.Errorf("failed to get likes by subject: %w", err) 29 30 } 30 31 31 32 dids := make([]string, 0, len(resp.Likes)) ··· 35 36 36 37 profiles, err := s.getProfiles(ctx, dids) 37 38 if err != nil { 38 - return nil, fmt.Errorf("failed to get profiles for subject: %w", err) 39 + return nil, nil, fmt.Errorf("failed to get profiles for subject: %w", err) 39 40 } 40 41 41 42 likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes)) ··· 53 54 }) 54 55 } 55 56 56 - return likes, nil 57 + return likes, resp.Cursor, nil 57 58 } 58 59 59 - func (s *Server) handleGetLikesBySubject(e echo.Context) error { 60 + func (s *Server) handleGetSubjectLikes(e echo.Context) error { 60 61 ctx := e.Request().Context() 61 62 62 - logger := s.logger.With("name", "handleGetLikesByPost") 63 + logger := s.logger.With("name", "handleGetSubjectLikes") 63 64 64 65 var input GetFeedLikesBySubjectInput 65 66 if err := e.Bind(&input); err != nil { ··· 71 72 return NewValidationError("uri", "URI must be provided") 72 73 } 73 74 74 - if input.Limit < 1 || input.Limit > 100 { 75 + if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 75 76 return NewValidationError("limit", "limit must be between 1 and 100") 77 + } else if input.Limit == nil { 78 + input.Limit = helpers.ToInt64Ptr(25) 76 79 } 77 80 78 81 logger = logger.With("uri", input.Uri) 79 82 80 - likes, err := s.getLikesBySubject(ctx, input.Uri, input.Limit, input.Cursor) 83 + likes, cursor, err := s.getLikesBySubject(ctx, input.Uri, *input.Limit, input.Cursor) 81 84 if err != nil { 82 85 logger.Error("failed to get subject likes", "err", err) 83 86 return ErrInternalServerErr 84 87 } 85 88 86 89 return e.JSON(200, vylet.FeedGetSubjectLikes_Output{ 87 - Likes: likes, 90 + Likes: likes, 91 + Cursor: cursor, 88 92 }) 89 93 }
+87 -9
api/server/feedgetposts.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "errors" 6 7 "fmt" 8 + "sort" 7 9 "time" 8 10 9 11 "github.com/bluesky-social/indigo/lex/util" ··· 14 16 "github.com/vylet-app/go/internal/helpers" 15 17 "golang.org/x/sync/errgroup" 16 18 ) 17 - 18 - type GetFeedPostsInput struct { 19 - Uris []string `query:"uris"` 20 - } 21 19 22 20 func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) { 23 21 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ ··· 82 80 } 83 81 84 82 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 83 resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{ 88 84 Uris: uris, 89 85 }) ··· 94 90 return nil, ErrDatabaseNotFound 95 91 } 96 92 97 - dids := make([]string, 0, len(resp.Posts)) 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)) 98 106 addedDids := make(map[string]struct{}) 99 - for _, post := range resp.Posts { 107 + for uri, post := range posts { 108 + uris = append(uris, uri) 109 + 100 110 if _, ok := addedDids[post.AuthorDid]; ok { 101 111 continue 102 112 } ··· 131 141 } 132 142 133 143 feedPostViews := make(map[string]*vylet.FeedDefs_PostView) 134 - for _, post := range resp.Posts { 144 + for _, post := range posts { 135 145 profileBasic, ok := profiles[post.AuthorDid] 136 146 if !ok { 137 147 logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri) ··· 197 207 return feedPostViews, nil 198 208 } 199 209 210 + type GetFeedPostsInput struct { 211 + Uris []string `query:"uris"` 212 + } 213 + 200 214 func (s *Server) handleGetPosts(e echo.Context) error { 201 215 ctx := e.Request().Context() 202 216 ··· 245 259 Posts: orderedPostViews, 246 260 }) 247 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 129 130 130 // app.vylet.feed 131 131 s.echo.GET("/xrpc/app.vylet.feed.getPosts", s.handleGetPosts) 132 - s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetLikesBySubject) 132 + s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetSubjectLikes) 133 + s.echo.GET("/xrpc/app.vylet.feed.getActorPosts", s.handleGetActorPosts) 133 134 } 134 135 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 13 return &str 14 14 } 15 15 16 + func ToIntPtr(num int) *int { 17 + return &num 18 + } 19 + 20 + func ToInt64Ptr(num int64) *int64 { 21 + return &num 22 + } 23 + 16 24 func ImageCidToCdnUrl(cid string, size string) string { 17 25 return fmt.Sprintf("https://cdn.vylet.app/%s/%s@png", cid, size) 18 26 }