A locally focused bluesky appview
at master 7.6 kB view raw
1package xrpc 2 3import ( 4 "context" 5 "log/slog" 6 "net/http" 7 8 "github.com/bluesky-social/indigo/atproto/identity" 9 "github.com/labstack/echo/v4" 10 "github.com/labstack/echo/v4/middleware" 11 "github.com/whyrusleeping/konbini/backend" 12 "github.com/whyrusleeping/konbini/hydration" 13 "github.com/whyrusleeping/konbini/models" 14 "github.com/whyrusleeping/konbini/xrpc/actor" 15 "github.com/whyrusleeping/konbini/xrpc/feed" 16 "github.com/whyrusleeping/konbini/xrpc/graph" 17 "github.com/whyrusleeping/konbini/xrpc/labeler" 18 "github.com/whyrusleeping/konbini/xrpc/notification" 19 "github.com/whyrusleeping/konbini/xrpc/repo" 20 "github.com/whyrusleeping/konbini/xrpc/unspecced" 21 "gorm.io/gorm" 22) 23 24// Server represents the XRPC API server 25type Server struct { 26 e *echo.Echo 27 db *gorm.DB 28 dir identity.Directory 29 backend Backend 30 hydrator *hydration.Hydrator 31} 32 33// Backend interface for data access 34type Backend interface { 35 // Add methods as needed for data access 36 37 TrackMissingRecord(identifier string, wait bool) 38 GetOrCreateRepo(ctx context.Context, did string) (*models.Repo, error) 39} 40 41// NewServer creates a new XRPC server 42func NewServer(db *gorm.DB, dir identity.Directory, backend *backend.PostgresBackend) *Server { 43 e := echo.New() 44 e.HidePort = true 45 e.HideBanner = true 46 47 // CORS middleware 48 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 49 AllowOrigins: []string{"*"}, 50 AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions}, 51 AllowHeaders: []string{"*"}, 52 })) 53 54 // Logging middleware 55 e.Use(middleware.Logger()) 56 e.Use(middleware.Recover()) 57 58 s := &Server{ 59 e: e, 60 db: db, 61 dir: dir, 62 backend: backend, 63 hydrator: hydration.NewHydrator(db, dir, backend), 64 } 65 66 // Register XRPC endpoints 67 s.registerEndpoints() 68 69 return s 70} 71 72// Start starts the XRPC server 73func (s *Server) Start(addr string) error { 74 slog.Info("starting XRPC server", "addr", addr) 75 return s.e.Start(addr) 76} 77 78// registerEndpoints registers all XRPC endpoints 79func (s *Server) registerEndpoints() { 80 // XRPC endpoints follow the pattern: /xrpc/<namespace>.<method> 81 82 s.e.GET("/.well-known/did.json", func(c echo.Context) error { 83 return c.File("did.json") 84 }) 85 86 xrpcGroup := s.e.Group("/xrpc") 87 88 // com.atproto.identity.* 89 xrpcGroup.GET("/com.atproto.identity.resolveHandle", s.handleResolveHandle) 90 91 // com.atproto.repo.* 92 xrpcGroup.GET("/com.atproto.repo.getRecord", func(c echo.Context) error { 93 return repo.HandleGetRecord(c, s.db, s.hydrator) 94 }) 95 96 // app.bsky.actor.* 97 xrpcGroup.GET("/app.bsky.actor.getProfile", func(c echo.Context) error { 98 return actor.HandleGetProfile(c, s.hydrator) 99 }, s.optionalAuth) 100 xrpcGroup.GET("/app.bsky.actor.getProfiles", func(c echo.Context) error { 101 return actor.HandleGetProfiles(c, s.db, s.hydrator) 102 }, s.optionalAuth) 103 xrpcGroup.GET("/app.bsky.actor.getPreferences", func(c echo.Context) error { 104 return actor.HandleGetPreferences(c, s.db, s.hydrator) 105 }, s.requireAuth) 106 xrpcGroup.POST("/app.bsky.actor.putPreferences", func(c echo.Context) error { 107 return actor.HandlePutPreferences(c, s.db, s.hydrator) 108 }, s.requireAuth) 109 xrpcGroup.GET("/app.bsky.actor.searchActors", s.handleSearchActors) 110 xrpcGroup.GET("/app.bsky.actor.searchActorsTypeahead", s.handleSearchActorsTypeahead) 111 112 // app.bsky.feed.* 113 xrpcGroup.GET("/app.bsky.feed.getTimeline", func(c echo.Context) error { 114 return feed.HandleGetTimeline(c, s.db, s.hydrator) 115 }, s.requireAuth) 116 xrpcGroup.GET("/app.bsky.feed.getAuthorFeed", func(c echo.Context) error { 117 return feed.HandleGetAuthorFeed(c, s.db, s.hydrator) 118 }) 119 xrpcGroup.GET("/app.bsky.feed.getPostThread", func(c echo.Context) error { 120 return feed.HandleGetPostThread(c, s.db, s.hydrator) 121 }) 122 xrpcGroup.GET("/app.bsky.feed.getPosts", func(c echo.Context) error { 123 return feed.HandleGetPosts(c, s.hydrator) 124 }) 125 xrpcGroup.GET("/app.bsky.feed.getLikes", func(c echo.Context) error { 126 return feed.HandleGetLikes(c, s.db, s.hydrator) 127 }) 128 xrpcGroup.GET("/app.bsky.feed.getRepostedBy", func(c echo.Context) error { 129 return feed.HandleGetRepostedBy(c, s.db, s.hydrator) 130 }) 131 xrpcGroup.GET("/app.bsky.feed.getActorLikes", func(c echo.Context) error { 132 return feed.HandleGetActorLikes(c, s.db, s.hydrator) 133 }, s.requireAuth) 134 xrpcGroup.GET("/app.bsky.feed.getFeed", func(c echo.Context) error { 135 return feed.HandleGetFeed(c, s.db, s.hydrator, s.dir) 136 }, s.optionalAuth) 137 xrpcGroup.GET("/app.bsky.feed.getFeedGenerator", func(c echo.Context) error { 138 return feed.HandleGetFeedGenerator(c, s.db, s.hydrator, s.dir) 139 }) 140 141 // app.bsky.graph.* 142 xrpcGroup.GET("/app.bsky.graph.getFollows", func(c echo.Context) error { 143 return graph.HandleGetFollows(c, s.db, s.hydrator) 144 }) 145 xrpcGroup.GET("/app.bsky.graph.getFollowers", func(c echo.Context) error { 146 return graph.HandleGetFollowers(c, s.db, s.hydrator) 147 }) 148 xrpcGroup.GET("/app.bsky.graph.getBlocks", func(c echo.Context) error { 149 return graph.HandleGetBlocks(c, s.db, s.hydrator) 150 }, s.requireAuth) 151 xrpcGroup.GET("/app.bsky.graph.getMutes", func(c echo.Context) error { 152 return graph.HandleGetMutes(c, s.db, s.hydrator) 153 }, s.requireAuth) 154 xrpcGroup.GET("/app.bsky.graph.getRelationships", func(c echo.Context) error { 155 return graph.HandleGetRelationships(c, s.db, s.hydrator) 156 }) 157 xrpcGroup.GET("/app.bsky.graph.getLists", s.handleGetLists) 158 xrpcGroup.GET("/app.bsky.graph.getList", s.handleGetList) 159 160 // app.bsky.notification.* 161 xrpcGroup.GET("/app.bsky.notification.listNotifications", func(c echo.Context) error { 162 return notification.HandleListNotifications(c, s.db, s.hydrator) 163 }, s.requireAuth) 164 xrpcGroup.GET("/app.bsky.notification.getUnreadCount", func(c echo.Context) error { 165 return notification.HandleGetUnreadCount(c, s.db, s.hydrator) 166 }, s.requireAuth) 167 xrpcGroup.POST("/app.bsky.notification.updateSeen", func(c echo.Context) error { 168 return notification.HandleUpdateSeen(c, s.db, s.hydrator) 169 }, s.requireAuth) 170 171 // app.bsky.labeler.* 172 xrpcGroup.GET("/app.bsky.labeler.getServices", func(c echo.Context) error { 173 return labeler.HandleGetServices(c) 174 }) 175 176 // app.bsky.unspecced.* 177 xrpcGroup.GET("/app.bsky.unspecced.getConfig", func(c echo.Context) error { 178 return unspecced.HandleGetConfig(c) 179 }) 180 xrpcGroup.GET("/app.bsky.unspecced.getTrendingTopics", func(c echo.Context) error { 181 return unspecced.HandleGetTrendingTopics(c) 182 }) 183 xrpcGroup.GET("/app.bsky.unspecced.getPostThreadV2", func(c echo.Context) error { 184 return unspecced.HandleGetPostThreadV2(c, s.db, s.hydrator) 185 }) 186} 187 188// XRPCError creates a properly formatted XRPC error response 189func XRPCError(c echo.Context, statusCode int, errType, message string) error { 190 return c.JSON(statusCode, map[string]interface{}{ 191 "error": errType, 192 "message": message, 193 }) 194} 195 196// getUserDID extracts the viewer DID from the request context 197// Returns empty string if not authenticated 198func getUserDID(c echo.Context) string { 199 did := c.Get("viewer") 200 if did == nil { 201 return "" 202 } 203 if s, ok := did.(string); ok { 204 return s 205 } 206 return "" 207} 208 209func (s *Server) handleSearchActors(c echo.Context) error { 210 return XRPCError(c, http.StatusNotImplemented, "NotImplemented", "Not yet implemented") 211} 212 213func (s *Server) handleSearchActorsTypeahead(c echo.Context) error { 214 return XRPCError(c, http.StatusNotImplemented, "NotImplemented", "Not yet implemented") 215} 216 217func (s *Server) handleGetLists(c echo.Context) error { 218 return XRPCError(c, http.StatusNotImplemented, "NotImplemented", "Not yet implemented") 219} 220 221func (s *Server) handleGetList(c echo.Context) error { 222 return XRPCError(c, http.StatusNotImplemented, "NotImplemented", "Not yet implemented") 223}