diff --git a/models/models.go b/models/models.go index c9c7236..807fc40 100644 --- a/models/models.go +++ b/models/models.go @@ -107,6 +107,15 @@ type Record struct { Value []byte } +type PrivateRecord struct { + Did string `gorm:"primaryKey:idx_private_record_did_created_at;index:idx_private_record_did_nsid"` + CreatedAt string `gorm:"index;index:idx_private_record_did_created_at,sort:desc"` + Nsid string `gorm:"primarKey;index:idx_private_record_did_nsid"` + Rkey string `gorm:"primaryKey"` + Cid string + Value []byte +} + type Block struct { Did string `gorm:"primaryKey;index:idx_blocks_by_rev"` Cid []byte `gorm:"primaryKey"` diff --git a/server/handle_repo_create_private.go b/server/handle_repo_create_private.go new file mode 100644 index 0000000..a9be59f --- /dev/null +++ b/server/handle_repo_create_private.go @@ -0,0 +1,57 @@ +package server + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest/to" + "github.com/haileyok/cocoon/models" + "github.com/labstack/echo/v4" +) + +type ComAtprotoUnspeccedCreatePrivateRecordInput struct { + Repo string `json:"repo" validate:"required,atproto-did"` + Collection string `json:"collection" validate:"required,atproto-nsid"` + Rkey *string `json:"rkey,omitempty"` + Validate *bool `json:"bool,omitempty"` + Record MarshalableMap `json:"record" validate:"required"` +} + +func (s *Server) handleServerCreatePrivate(e echo.Context) error { + ctx := e.Request().Context() + logger := s.logger.With("name", "handleCreatePrivate") + + repo := e.Get("repo").(*models.RepoActor) + + var input ComAtprotoUnspeccedCreatePrivateRecordInput + if err := e.Bind(&input); err != nil { + logger.Error("error binding", "err", err) + return fmt.Errorf("error binding: %w", err) + } + + if input.Rkey == nil { + input.Rkey = to.StringPtr(s.repoman.clock.Next().String()) + } + + b, err := json.Marshal(input.Record) + if err != nil { + logger.Error("failed to marshal input record", "err", err) + return fmt.Errorf("failed to marshal input record: %w", err) + } + + record := models.PrivateRecord{ + Did: repo.Repo.Did, + CreatedAt: time.Now().UTC().Format(time.RFC3339Nano), + Nsid: input.Collection, + Rkey: *input.Rkey, + Value: b, + } + + if err := s.db.Create(ctx, &record, nil).Error; err != nil { + logger.Error("failed to create record in db", "err", err) + return fmt.Errorf("failed to create record in db") + } + + return e.NoContent(200) +} diff --git a/server/handle_repo_get_private.go b/server/handle_repo_get_private.go new file mode 100644 index 0000000..12d69a2 --- /dev/null +++ b/server/handle_repo_get_private.go @@ -0,0 +1,41 @@ +package server + +import ( + "encoding/json" + "fmt" + + "github.com/haileyok/cocoon/models" + "github.com/labstack/echo/v4" +) + +type ComAtprotoUnspeccedGetPrivateRecordInput struct { + Collection string `query:"collection"` + Rkey string `query:"rkey"` +} + +func (s *Server) handleServerGetPrivate(e echo.Context) error { + ctx := e.Request().Context() + logger := s.logger.With("name", "handleGetPrivate") + + repo := e.Get("repo").(*models.RepoActor) + + var input ComAtprotoUnspeccedGetPrivateRecordInput + if err := e.Bind(&input); err != nil { + logger.Error("error binding", "err", err) + return fmt.Errorf("error binding: %w", err) + } + + var record models.PrivateRecord + if err := s.db.Raw(ctx, "SELECT * FROM private_records WHERE did = ? AND nsid = ? AND rkey = ?", nil, repo.Repo.Did, input.Collection, input.Rkey).Scan(&record).Error; err != nil { + logger.Error("error getting private record", "err", err) + return fmt.Errorf("failed to get private record: %w", err) + } + + var unmarshaled map[string]any + if err := json.Unmarshal(record.Value, &unmarshaled); err != nil { + logger.Error("error unmarshaling record", "err", err) + return fmt.Errorf("failed to unmarshal record: %w", err) + } + + return e.JSON(200, unmarshaled) +} diff --git a/server/handle_repo_list_private.go b/server/handle_repo_list_private.go new file mode 100644 index 0000000..96c0b7f --- /dev/null +++ b/server/handle_repo_list_private.go @@ -0,0 +1,81 @@ +package server + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest/to" + "github.com/haileyok/cocoon/models" + "github.com/labstack/echo/v4" +) + +type ComAtprotoUnspeccedListPrivateRecordsInput struct { + Collection string `query:"collection"` + Limit int `query:"limit"` + Cursor string `query:"cursor"` +} + +type ComAtprotoUnspeccedListPrivateRecordsResponse struct { + Cursor *string `json:"cursor,omitempty"` + Records []ComAtprotoUnspeccedListPrivateRecordsRecordItem `json:"records"` +} + +type ComAtprotoUnspeccedListPrivateRecordsRecordItem struct { + Uri string `json:"uri"` + Value map[string]any `json:"value"` +} + +func (s *Server) handleServerListPrivate(e echo.Context) error { + ctx := e.Request().Context() + logger := s.logger.With("name", "handleListPrivate") + + repo := e.Get("repo").(*models.RepoActor) + + var input ComAtprotoUnspeccedListPrivateRecordsInput + if err := e.Bind(&input); err != nil { + logger.Error("error binding", "err", err) + return fmt.Errorf("error binding: %w", err) + } + + limit, err := getLimitFromContext(e, 100) + if err != nil { + logger.Error("failed to parse limit from context", "err", err) + return fmt.Errorf("failed to parse limit from context: %w", err) + } + + if input.Cursor == "" { + input.Cursor = time.Now().UTC().Format(time.RFC3339Nano) + } + + var records []models.PrivateRecord + if err := s.db.Raw(ctx, "SELECT * FROM private_records WHERE did = ? AND nsid = ? AND created_at < ORDER BY created_at DESC LIMIT ?", nil, repo.Repo.Did, input.Collection, input.Cursor, limit).Scan(&records).Error; err != nil { + logger.Error("error getting private record", "err", err) + return fmt.Errorf("failed to get private record: %w", err) + } + + respRecords := make([]ComAtprotoUnspeccedListPrivateRecordsRecordItem, 0, len(records)) + + for _, rec := range records { + var unmarshaled map[string]any + if err := json.Unmarshal(rec.Value, &unmarshaled); err != nil { + logger.Error("failed to unmarshal record", "err", err) + return fmt.Errorf("failed to unmarshal record: %w", err) + } + + respRecords = append(respRecords, ComAtprotoUnspeccedListPrivateRecordsRecordItem{ + Uri: fmt.Sprintf("at://%s/%s/%s", repo.Repo.Did, input.Collection, rec.Rkey), + Value: unmarshaled, + }) + } + + var newcursor *string + if len(records) == limit { + newcursor = to.StringPtr(records[len(records)-1].CreatedAt) + } + + return e.JSON(200, ComAtprotoUnspeccedListPrivateRecordsResponse{ + Cursor: newcursor, + Records: respRecords, + }) +} diff --git a/server/server.go b/server/server.go index 953e243..c60b886 100644 --- a/server/server.go +++ b/server/server.go @@ -528,6 +528,11 @@ func (s *Server) addRoutes() { s.echo.POST("/xrpc/com.atproto.repo.uploadBlob", s.handleRepoUploadBlob, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.repo.importRepo", s.handleRepoImportRepo, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + // temp private record routes + s.echo.POST("/xrpc/com.atproto.unspecced.createPrivateRecord", s.handleServerCreatePrivate, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + s.echo.GET("/xrpc/com.atproto.unspecced.getPrivateRecord", s.handleServerGetPrivate, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + s.echo.GET("/xrpc/com.atproto.unspecced.listPrivateRecords", s.handleServerListPrivate, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + // stupid silly endpoints s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) @@ -560,6 +565,7 @@ func (s *Server) Serve(ctx context.Context) error { &models.Blob{}, &models.BlobPart{}, &models.ReservedKey{}, + &models.PrivateRecord{}, &provider.OauthToken{}, &provider.OauthAuthorizationRequest{}, )