A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

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
(Dec 13, 2025, 10:29 PM -0800) 27d469e9 66a2e250

+101 -13
+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)