An atproto PDS written in Go

implement queryLabels and add new COCOON_REQUIRE_INVITE env (#47)

* implement queryLabels

* add a COCOON_REQUIRE_INVITE env to make invite codes be able to not be required (still required by default)

* handle handles for http requests and stuff

authored by Scan and committed by GitHub 27d469e9 66a2e250

+1 -1
README.md
··· 256 256 257 257 ### Other 258 258 259 - - [ ] `com.atproto.label.queryLabels` 259 + - [x] `com.atproto.label.queryLabels` 260 260 - [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS) 261 261 - [x] `app.bsky.actor.getPreferences` 262 262 - [x] `app.bsky.actor.putPreferences`
+6
cmd/cocoon/main.go
··· 79 79 Name: "admin-password", 80 80 EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 81 81 }, 82 + &cli.BoolFlag{ 83 + Name: "require-invite", 84 + EnvVars: []string{"COCOON_REQUIRE_INVITE"}, 85 + Value: true, 86 + }, 82 87 &cli.StringFlag{ 83 88 Name: "smtp-user", 84 89 EnvVars: []string{"COCOON_SMTP_USER"}, ··· 185 190 Version: Version, 186 191 Relays: cmd.StringSlice("relays"), 187 192 AdminPassword: cmd.String("admin-password"), 193 + RequireInvite: cmd.Bool("require-invite"), 188 194 SmtpUser: cmd.String("smtp-user"), 189 195 SmtpPass: cmd.String("smtp-pass"), 190 196 SmtpHost: cmd.String("smtp-host"),
+34
server/handle_label_query_labels.go
··· 1 + package server 2 + 3 + import ( 4 + "github.com/labstack/echo/v4" 5 + ) 6 + 7 + type Label struct { 8 + Ver *int `json:"ver,omitempty"` 9 + Src string `json:"src"` 10 + Uri string `json:"uri"` 11 + Cid *string `json:"cid,omitempty"` 12 + Val string `json:"val"` 13 + Neg *bool `json:"neg,omitempty"` 14 + Cts string `json:"cts"` 15 + Exp *string `json:"exp,omitempty"` 16 + Sig []byte `json:"sig,omitempty"` 17 + } 18 + 19 + type ComAtprotoLabelQueryLabelsResponse struct { 20 + Cursor *string `json:"cursor,omitempty"` 21 + Labels []Label `json:"labels"` 22 + } 23 + 24 + func (s *Server) handleLabelQueryLabels(e echo.Context) error { 25 + svc := e.Request().Header.Get("atproto-proxy") 26 + if svc != "" || s.config.FallbackProxy != "" { 27 + return s.handleProxy(e) 28 + } 29 + 30 + return e.JSON(200, ComAtprotoLabelQueryLabelsResponse{ 31 + Cursor: nil, 32 + Labels: []Label{}, 33 + }) 34 + }
+19 -11
server/handle_server_create_account.go
··· 25 25 Handle string `json:"handle" validate:"required,atproto-handle"` 26 26 Did *string `json:"did" validate:"atproto-did"` 27 27 Password string `json:"password" validate:"required"` 28 - InviteCode string `json:"inviteCode" validate:"required"` 28 + InviteCode string `json:"inviteCode" validate:"omitempty"` 29 29 } 30 30 31 31 type ComAtprotoServerCreateAccountResponse struct { ··· 104 104 } 105 105 106 106 var ic models.InviteCode 107 - if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 108 - if err == gorm.ErrRecordNotFound { 107 + if s.config.RequireInvite { 108 + if strings.TrimSpace(request.InviteCode) == "" { 109 109 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 110 110 } 111 - s.logger.Error("error getting invite code from db", "error", err) 112 - return helpers.ServerError(e, nil) 113 - } 114 111 115 - if ic.RemainingUseCount < 1 { 116 - return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 112 + if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 113 + if err == gorm.ErrRecordNotFound { 114 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 115 + } 116 + s.logger.Error("error getting invite code from db", "error", err) 117 + return helpers.ServerError(e, nil) 118 + } 119 + 120 + if ic.RemainingUseCount < 1 { 121 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 122 + } 117 123 } 118 124 119 125 // see if the email is already taken ··· 234 240 }) 235 241 } 236 242 237 - if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 238 - s.logger.Error("error decrementing use count", "error", err) 239 - return helpers.ServerError(e, nil) 243 + if s.config.RequireInvite { 244 + if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 245 + s.logger.Error("error decrementing use count", "error", err) 246 + return helpers.ServerError(e, nil) 247 + } 240 248 } 241 249 242 250 sess, err := s.createSession(&urepo)
+1 -1
server/handle_server_describe_server.go
··· 22 22 23 23 func (s *Server) handleDescribeServer(e echo.Context) error { 24 24 return e.JSON(200, ComAtprotoServerDescribeServerResponse{ 25 - InviteCodeRequired: true, 25 + InviteCodeRequired: s.config.RequireInvite, 26 26 PhoneVerificationRequired: false, 27 27 AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more 28 28 Links: ComAtprotoServerDescribeServerResponseLinks{
+33
server/handle_well_known.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "strings" 5 6 6 7 "github.com/Azure/go-autorest/autorest/to" 8 + "github.com/haileyok/cocoon/internal/helpers" 7 9 "github.com/labstack/echo/v4" 10 + "gorm.io/gorm" 8 11 ) 9 12 10 13 var ( ··· 61 64 }, 62 65 }, 63 66 }) 67 + } 68 + 69 + func (s *Server) handleAtprotoDid(e echo.Context) error { 70 + host := e.Request().Host 71 + if host == "" { 72 + return helpers.InputError(e, to.StringPtr("Invalid handle.")) 73 + } 74 + 75 + host = strings.Split(host, ":")[0] 76 + host = strings.ToLower(strings.TrimSpace(host)) 77 + 78 + if host == s.config.Hostname { 79 + return e.String(200, s.config.Did) 80 + } 81 + 82 + suffix := "." + s.config.Hostname 83 + if !strings.HasSuffix(host, suffix) { 84 + return e.NoContent(404) 85 + } 86 + 87 + actor, err := s.getActorByHandle(host) 88 + if err != nil { 89 + if err == gorm.ErrRecordNotFound { 90 + return e.NoContent(404) 91 + } 92 + s.logger.Error("error looking up actor by handle", "error", err) 93 + return helpers.ServerError(e, nil) 94 + } 95 + 96 + return e.String(200, actor.Did) 64 97 } 65 98 66 99 func (s *Server) handleOauthProtectedResource(e echo.Context) error {
+7
server/server.go
··· 102 102 ContactEmail string 103 103 Relays []string 104 104 AdminPassword string 105 + RequireInvite bool 105 106 106 107 SmtpUser string 107 108 SmtpPass string ··· 126 127 EnforcePeering bool 127 128 Relays []string 128 129 AdminPassword string 130 + RequireInvite bool 129 131 SmtpEmail string 130 132 SmtpName string 131 133 BlockstoreVariant BlockstoreVariant ··· 379 381 EnforcePeering: false, 380 382 Relays: args.Relays, 381 383 AdminPassword: args.AdminPassword, 384 + RequireInvite: args.RequireInvite, 382 385 SmtpName: args.SmtpName, 383 386 SmtpEmail: args.SmtpEmail, 384 387 BlockstoreVariant: args.BlockstoreVariant, ··· 442 445 s.echo.GET("/", s.handleRoot) 443 446 s.echo.GET("/xrpc/_health", s.handleHealth) 444 447 s.echo.GET("/.well-known/did.json", s.handleWellKnown) 448 + s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid) 445 449 s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource) 446 450 s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer) 447 451 s.echo.GET("/robots.txt", s.handleRobots) ··· 465 469 s.echo.GET("/xrpc/com.atproto.sync.subscribeRepos", s.handleSyncSubscribeRepos) 466 470 s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs) 467 471 s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob) 472 + 473 + // labels 474 + s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels) 468 475 469 476 // account 470 477 s.echo.GET("/account", s.handleAccount)