Vow, uncensorable PDS written in Go

feat: implement com.atproto.server.getAccountInviteCodes

+122 -1
+9
models/models.go
··· 80 80 Code string `gorm:"primaryKey"` 81 81 Did string `gorm:"index"` 82 82 RemainingUseCount int 83 + CreatedAt time.Time 84 + Disabled bool `gorm:"default:false"` 85 + } 86 + 87 + type InviteCodeUse struct { 88 + ID uint `gorm:"primaryKey"` 89 + Code string `gorm:"index"` 90 + UsedBy string 91 + UsedAt time.Time 83 92 } 84 93 85 94 type Token struct {
+1 -1
readme.md
··· 188 188 - [x] `com.atproto.server.deleteAccount` 189 189 - [x] `com.atproto.server.deleteSession` 190 190 - [x] `com.atproto.server.describeServer` 191 - - [ ] `com.atproto.server.getAccountInviteCodes` 191 + - [x] `com.atproto.server.getAccountInviteCodes` 192 192 - [x] `com.atproto.server.getServiceAuth` 193 193 - [x] `com.atproto.server.refreshSession` 194 194 - [x] `com.atproto.server.requestAccountDelete`
+8
server/handle_server_create_account.go
··· 290 290 helpers.ServerError(w, nil) 291 291 return 292 292 } 293 + 294 + if err := s.db.Create(ctx, &models.InviteCodeUse{ 295 + Code: request.InviteCode, 296 + UsedBy: signupDid, 297 + UsedAt: time.Now(), 298 + }, nil).Error; err != nil { 299 + logger.Error("error recording invite code use", "error", err) 300 + } 293 301 } 294 302 295 303 sess, err := s.createSession(ctx, &urepo)
+2
server/handle_server_create_invite_code.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "net/http" 6 + "time" 6 7 7 8 "github.com/google/uuid" 8 9 "pkg.rbrt.fr/vow/internal/helpers" ··· 48 49 Code: ic, 49 50 Did: acc, 50 51 RemainingUseCount: req.UseCount, 52 + CreatedAt: time.Now(), 51 53 }, nil).Error; err != nil { 52 54 logger.Error("error creating invite code", "error", err) 53 55 helpers.ServerError(w, nil)
+2
server/handle_server_create_invite_codes.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "net/http" 6 + "time" 6 7 7 8 "github.com/google/uuid" 8 9 "pkg.rbrt.fr/vow/internal/helpers" ··· 60 61 Code: ic, 61 62 Did: did, 62 63 RemainingUseCount: req.UseCount, 64 + CreatedAt: time.Now(), 63 65 }, nil).Error; err != nil { 64 66 logger.Error("error creating invite code", "error", err) 65 67 helpers.ServerError(w, nil)
+98
server/handle_server_get_account_invite_codes.go
··· 1 + package server 2 + 3 + import ( 4 + "net/http" 5 + "time" 6 + 7 + "pkg.rbrt.fr/vow/internal/helpers" 8 + "pkg.rbrt.fr/vow/models" 9 + ) 10 + 11 + type ComAtprotoServerGetAccountInviteCodesResponse struct { 12 + Codes []InviteCodeView `json:"codes"` 13 + } 14 + 15 + type InviteCodeView struct { 16 + Code string `json:"code"` 17 + Available int `json:"available"` 18 + Disabled bool `json:"disabled"` 19 + ForAccount string `json:"forAccount"` 20 + CreatedBy string `json:"createdBy"` 21 + CreatedAt string `json:"createdAt"` 22 + Uses []InviteCodeUseView `json:"uses"` 23 + } 24 + 25 + type InviteCodeUseView struct { 26 + UsedBy string `json:"usedBy"` 27 + UsedAt string `json:"usedAt"` 28 + } 29 + 30 + func (s *Server) handleGetAccountInviteCodes(w http.ResponseWriter, r *http.Request) { 31 + ctx := r.Context() 32 + logger := s.logger.With("name", "handleGetAccountInviteCodes") 33 + 34 + repo, ok := getContextValue[*models.RepoActor](r, contextKeyRepo) 35 + if !ok { 36 + helpers.UnauthorizedError(w, nil) 37 + return 38 + } 39 + 40 + did := repo.Repo.Did 41 + 42 + includeUsed := r.URL.Query().Get("includeUsed") != "false" 43 + 44 + var codes []models.InviteCode 45 + if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE did = ?", nil, did).Scan(&codes).Error; err != nil { 46 + logger.Error("error fetching invite codes", "error", err) 47 + helpers.ServerError(w, nil) 48 + return 49 + } 50 + 51 + result := make([]InviteCodeView, 0, len(codes)) 52 + for _, code := range codes { 53 + if code.Code == "" { 54 + continue 55 + } 56 + 57 + var uses []models.InviteCodeUse 58 + if err := s.db.Raw(ctx, "SELECT * FROM invite_code_uses WHERE code = ?", nil, code.Code).Scan(&uses).Error; err != nil { 59 + logger.Error("error fetching invite code uses", "error", err) 60 + helpers.ServerError(w, nil) 61 + return 62 + } 63 + 64 + if !includeUsed && len(uses) > 0 && code.RemainingUseCount <= 0 { 65 + continue 66 + } 67 + 68 + useViews := make([]InviteCodeUseView, 0, len(uses)) 69 + for _, u := range uses { 70 + if u.UsedBy == "" { 71 + continue 72 + } 73 + useViews = append(useViews, InviteCodeUseView{ 74 + UsedBy: u.UsedBy, 75 + UsedAt: u.UsedAt.Format(time.RFC3339), 76 + }) 77 + } 78 + 79 + createdAt := code.CreatedAt 80 + if createdAt.IsZero() { 81 + createdAt = time.Now() 82 + } 83 + 84 + result = append(result, InviteCodeView{ 85 + Code: code.Code, 86 + Available: code.RemainingUseCount, 87 + Disabled: code.Disabled, 88 + ForAccount: did, 89 + CreatedBy: did, 90 + CreatedAt: createdAt.Format(time.RFC3339), 91 + Uses: useViews, 92 + }) 93 + } 94 + 95 + s.writeJSON(w, 200, ComAtprotoServerGetAccountInviteCodesResponse{ 96 + Codes: result, 97 + }) 98 + }
+2
server/server.go
··· 554 554 r.Post("/xrpc/com.atproto.server.requestEmailUpdate", authed(s.handleServerRequestEmailUpdate).ServeHTTP) 555 555 r.Post("/xrpc/com.atproto.server.resetPassword", authed(s.handleServerResetPassword).ServeHTTP) 556 556 r.Post("/xrpc/com.atproto.server.updateEmail", authed(s.handleServerUpdateEmail).ServeHTTP) 557 + r.Get("/xrpc/com.atproto.server.getAccountInviteCodes", authed(s.handleGetAccountInviteCodes).ServeHTTP) 557 558 r.Get("/xrpc/com.atproto.server.getServiceAuth", authed(s.handleServerGetServiceAuth).ServeHTTP) 558 559 r.Get("/xrpc/com.atproto.server.checkAccountStatus", authed(s.handleServerCheckAccountStatus).ServeHTTP) 559 560 r.Post("/xrpc/com.atproto.server.deactivateAccount", authed(s.handleServerDeactivateAccount).ServeHTTP) ··· 596 597 &models.Actor{}, 597 598 &models.Repo{}, 598 599 &models.InviteCode{}, 600 + &models.InviteCodeUse{}, 599 601 &models.Token{}, 600 602 &models.RefreshToken{}, 601 603 &models.Block{},