From 21a531b97fba19197e5b7a1d9c45bc59d400fe64 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 6 Nov 2023 00:23:22 -0800 Subject: [PATCH 001/111] lexicon: initial work on schema parsing --- atproto/lexicon/cmd/lextool/main.go | 59 ++++ atproto/lexicon/docs.go | 4 + atproto/lexicon/extract.go | 20 ++ atproto/lexicon/language.go | 310 ++++++++++++++++++ atproto/lexicon/language_test.go | 47 +++ atproto/lexicon/lexicon.go | 12 + .../testdata/com_atproto_label_defs.json | 78 +++++ 7 files changed, 530 insertions(+) create mode 100644 atproto/lexicon/cmd/lextool/main.go create mode 100644 atproto/lexicon/docs.go create mode 100644 atproto/lexicon/extract.go create mode 100644 atproto/lexicon/language.go create mode 100644 atproto/lexicon/language_test.go create mode 100644 atproto/lexicon/lexicon.go create mode 100644 atproto/lexicon/testdata/com_atproto_label_defs.json diff --git a/atproto/lexicon/cmd/lextool/main.go b/atproto/lexicon/cmd/lextool/main.go new file mode 100644 index 000000000..a5e72ed0e --- /dev/null +++ b/atproto/lexicon/cmd/lextool/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + + "github.com/bluesky-social/indigo/atproto/lexicon" + + "github.com/urfave/cli/v2" +) + +func main() { + app := cli.App{ + Name: "lex-tool", + Usage: "informal debugging CLI tool for atproto lexicons", + } + app.Commands = []*cli.Command{ + &cli.Command{ + Name: "parse-schema", + Usage: "parse an individual lexicon schema file (JSON)", + Action: runParseSchema, + }, + } + h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) + slog.SetDefault(slog.New(h)) + app.RunAndExitOnError() +} + +func runParseSchema(cctx *cli.Context) error { + p := cctx.Args().First() + if p == "" { + return fmt.Errorf("need to provide path to a schema file as an argument") + } + + f, err := os.Open(p) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + var sf lexicon.SchemaFile + if err := json.Unmarshal(b, &sf); err != nil { + return err + } + out, err := json.MarshalIndent(sf, "", " ") + if err != nil { + return err + } + fmt.Println(string(out)) + return nil +} diff --git a/atproto/lexicon/docs.go b/atproto/lexicon/docs.go new file mode 100644 index 000000000..754179933 --- /dev/null +++ b/atproto/lexicon/docs.go @@ -0,0 +1,4 @@ +/* +Package atproto/lexicon provides generic Lexicon schema parsing and run-time validation. +*/ +package lexicon diff --git a/atproto/lexicon/extract.go b/atproto/lexicon/extract.go new file mode 100644 index 000000000..51d09ba93 --- /dev/null +++ b/atproto/lexicon/extract.go @@ -0,0 +1,20 @@ +package lexicon + +import ( + "encoding/json" +) + +// Helper type for extracting record type from JSON +type genericSchemaDef struct { + Type string `json:"type"` +} + +// Parses the top-level $type field from generic atproto JSON data +func ExtractTypeJSON(b []byte) (string, error) { + var gsd genericSchemaDef + if err := json.Unmarshal(b, &gsd); err != nil { + return "", err + } + + return gsd.Type, nil +} diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go new file mode 100644 index 000000000..e5f3a9dda --- /dev/null +++ b/atproto/lexicon/language.go @@ -0,0 +1,310 @@ +package lexicon + +import ( + "encoding/json" + "fmt" +) + +// Serialization helper for top-level Lexicon schema JSON objects (files) +type SchemaFile struct { + Lexicon int `json:"lexicon,const=1"` + ID string `json:"id"` + Revision *int `json:"revision,omitempty"` + Description *string `json:"description,omitempty"` + Defs map[string]SchemaDef `json:"defs"` +} + +// enum type to represent any of the schema fields +type SchemaDef struct { + Inner any +} + +func (s SchemaDef) MarshalJSON() ([]byte, error) { + return json.Marshal(s.Inner) +} + +func (s *SchemaDef) UnmarshalJSON(b []byte) error { + t, err := ExtractTypeJSON(b) + if err != nil { + return err + } + switch t { + case "record": + v := new(SchemaRecord) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "query": + v := new(SchemaQuery) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "procedure": + v := new(SchemaProcedure) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "subscription": + v := new(SchemaSubscription) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "null": + v := new(SchemaNull) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "boolean": + v := new(SchemaBoolean) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "integer": + v := new(SchemaInteger) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "string": + v := new(SchemaString) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "bytes": + v := new(SchemaBytes) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "cid-link": + v := new(SchemaCIDLink) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "array": + v := new(SchemaArray) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "object": + v := new(SchemaObject) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "blob": + v := new(SchemaBlob) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "params": + v := new(SchemaParams) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "token": + v := new(SchemaToken) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "ref": + v := new(SchemaRef) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "union": + v := new(SchemaUnion) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + case "unknown": + v := new(SchemaUnknown) + if err = json.Unmarshal(b, v); err != nil { + return err + } + s.Inner = v + return nil + default: + return fmt.Errorf("unexpected schema type: %s", t) + } + return fmt.Errorf("unexpected schema type: %s", t) +} + +type SchemaRecord struct { + Type string `json:"type,const=record"` + Description *string `json:"description,omitempty"` + Key string `json:"key"` + Record SchemaObject `json:"record"` +} + +type SchemaQuery struct { + Type string `json:"type,const=query"` + Description *string `json:"description,omitempty"` + Parameters SchemaParams `json:"parameters"` + Output *SchemaBody `json:"output"` + Errors []SchemaError `json:"errors,omitempty"` // optional +} + +type SchemaProcedure struct { + Type string `json:"type,const=procedure"` + Description *string `json:"description,omitempty"` + Parameters SchemaParams `json:"parameters"` + Output *SchemaBody `json:"output"` // optional + Errors []SchemaError `json:"errors,omitempty"` // optional + Input *SchemaBody `json:"input"` // optional +} + +type SchemaSubscription struct { + Type string `json:"type,const=subscription"` + Description *string `json:"description,omitempty"` + Parameters SchemaParams `json:"parameters"` + Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional? +} + +type SchemaBody struct { + Description *string `json:"description,omitempty"` + Encoding string `json:"encoding"` // required, mimetype + Schema *SchemaDef `json:"schema"` // optional; type:object, type:ref, or type:union +} + +type SchemaMessage struct { + Description *string `json:"description,omitempty"` + Schema SchemaDef `json:"schema"` // required; type:union only +} + +type SchemaError struct { + Name string `json:"name"` + Description *string `json:"description"` +} + +type SchemaNull struct { + Type string `json:"type,const=null"` + Description *string `json:"description,omitempty"` +} + +type SchemaBoolean struct { + Type string `json:"type,const=bool"` + Description *string `json:"description,omitempty"` + Default *bool `json:"default,omitempty"` + Const *bool `json:"const,omitempty"` +} + +type SchemaInteger struct { + Type string `json:"type,const=integer"` + Description *string `json:"description,omitempty"` + Minimum *int `json:"minimum,omitempty"` + Maximum *int `json:"maximum,omitempty"` + Enum []int `json:"enum,omitempty"` + Default *int `json:"default,omitempty"` + Const *int `json:"const,omitempty"` +} + +type SchemaString struct { + Type string `json:"type,const=string"` + Description *string `json:"description,omitempty"` + Format *string `json:"format,omitempty"` + MinLength *int `json:"minLength,omitempty"` + MaxLength *int `json:"maxLength,omitempty"` + MinGraphemes *int `json:"minGraphemes,omitempty"` + MaxGraphemes *int `json:"maxGraphemes,omitempty"` + KnownValues []string `json:"knownValues,omitempty"` + Enum []string `json:"enum,omitempty"` + Default *int `json:"default,omitempty"` + Const *int `json:"const,omitempty"` +} + +type SchemaBytes struct { + Type string `json:"type,const=bytes"` + Description *string `json:"description,omitempty"` + MinLength *int `json:"minLength,omitempty"` + MaxLength *int `json:"maxLength,omitempty"` +} + +type SchemaCIDLink struct { + Type string `json:"type,const=cid-link"` + Description *string `json:"description,omitempty"` +} + +type SchemaArray struct { + Type string `json:"type,const=array"` + Description *string `json:"description,omitempty"` + Items SchemaDef `json:"items"` + MinLength *int `json:"minLength,omitempty"` + MaxLength *int `json:"maxLength,omitempty"` +} + +type SchemaObject struct { + Type string `json:"type,const=object"` + Description *string `json:"description,omitempty"` + Properties map[string]SchemaDef `json:"properties"` + Required []string `json:"required,omitempty"` + Nullable []string `json:"nullable,omitempty"` +} + +type SchemaBlob struct { + Type string `json:"type,const=blob"` + Description *string `json:"description,omitempty"` + Accept []string `json:"accept,omitempty"` + MaxSize *int `json:"maxSize,omitempty"` +} + +type SchemaParams struct { + Type string `json:"type,const=params"` + Description *string `json:"description,omitempty"` + Properties map[string]SchemaDef `json:"properties"` // boolean, integer, string, or unknown; or an array of these types + Required []string `json:"required,omitempty"` +} + +type SchemaToken struct { + Type string `json:"type,const=token"` + Description *string `json:"description,omitempty"` +} + +type SchemaRef struct { + Type string `json:"type,const=ref"` + Description *string `json:"description,omitempty"` + Ref string `json:"ref"` +} + +type SchemaUnion struct { + Type string `json:"type,const=union"` + Description *string `json:"description,omitempty"` + Refs []string `json:"refs"` + Closed *bool `json:"closed,omitempty"` +} + +type SchemaUnknown struct { + Type string `json:"type,const=unknown"` + Description *string `json:"description,omitempty"` +} diff --git a/atproto/lexicon/language_test.go b/atproto/lexicon/language_test.go new file mode 100644 index 000000000..89882c86e --- /dev/null +++ b/atproto/lexicon/language_test.go @@ -0,0 +1,47 @@ +package lexicon + +import ( + "encoding/json" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicLabelLexicon(t *testing.T) { + assert := assert.New(t) + + f, err := os.Open("testdata/com_atproto_label_defs.json") + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + jsonBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var schema SchemaFile + if err := json.Unmarshal(jsonBytes, &schema); err != nil { + t.Fatal(err) + } + + outBytes, err := json.Marshal(schema) + if err != nil { + t.Fatal(err) + } + + var beforeMap map[string]any + if err := json.Unmarshal(jsonBytes, &beforeMap); err != nil { + t.Fatal(err) + } + + var afterMap map[string]any + if err := json.Unmarshal(outBytes, &afterMap); err != nil { + t.Fatal(err) + } + + assert.Equal(beforeMap, afterMap) +} diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go new file mode 100644 index 000000000..9b13a8599 --- /dev/null +++ b/atproto/lexicon/lexicon.go @@ -0,0 +1,12 @@ +package lexicon + +import ( +// "github.com/bluesky-social/indigo/atproto/syntax" +) + +// An aggregation of lexicon schemas, and methods for validating generic data against those schemas. +type Catalog struct { +} + +type Schema struct { +} diff --git a/atproto/lexicon/testdata/com_atproto_label_defs.json b/atproto/lexicon/testdata/com_atproto_label_defs.json new file mode 100644 index 000000000..57f06f81b --- /dev/null +++ b/atproto/lexicon/testdata/com_atproto_label_defs.json @@ -0,0 +1,78 @@ +{ + "defs": { + "label": { + "description": "Metadata tag on an atproto resource (eg, repo or record)", + "properties": { + "cid": { + "description": "optionally, CID specifying the specific version of 'uri' resource this label applies to", + "format": "cid", + "type": "string" + }, + "cts": { + "description": "timestamp when this label was created", + "format": "datetime", + "type": "string" + }, + "neg": { + "description": "if true, this is a negation label, overwriting a previous label", + "type": "boolean" + }, + "src": { + "description": "DID of the actor who created this label", + "format": "did", + "type": "string" + }, + "uri": { + "description": "AT URI of the record, repository (account), or other resource which this label applies to", + "format": "uri", + "type": "string" + }, + "val": { + "description": "the short string name of the value or type of this label", + "maxLength": 128, + "type": "string" + } + }, + "required": [ + "src", + "uri", + "val", + "cts" + ], + "type": "object" + }, + "selfLabel": { + "description": "Metadata tag on an atproto record, published by the author within the record. Note -- schemas should use #selfLabels, not #selfLabel.", + "properties": { + "val": { + "description": "the short string name of the value or type of this label", + "maxLength": 128, + "type": "string" + } + }, + "required": [ + "val" + ], + "type": "object" + }, + "selfLabels": { + "description": "Metadata tags on an atproto record, published by the author within the record.", + "properties": { + "values": { + "items": { + "ref": "#selfLabel", + "type": "ref" + }, + "maxLength": 10, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + } + }, + "id": "com.atproto.label.defs", + "lexicon": 1 +} From 53fcf07ef80976184af5d440ada3e9786a6ddf8a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 6 Nov 2023 23:15:41 -0800 Subject: [PATCH 002/111] lexicon: better validation schemas themselves --- atproto/lexicon/cmd/lextool/main.go | 21 ++ atproto/lexicon/language.go | 327 ++++++++++++++++++++++++++-- atproto/lexicon/lexicon.go | 99 ++++++++- 3 files changed, 426 insertions(+), 21 deletions(-) diff --git a/atproto/lexicon/cmd/lextool/main.go b/atproto/lexicon/cmd/lextool/main.go index a5e72ed0e..b5652b40c 100644 --- a/atproto/lexicon/cmd/lextool/main.go +++ b/atproto/lexicon/cmd/lextool/main.go @@ -23,6 +23,11 @@ func main() { Usage: "parse an individual lexicon schema file (JSON)", Action: runParseSchema, }, + &cli.Command{ + Name: "load-directory", + Usage: "try recursively loading all the schemas from a directory", + Action: runLoadDirectory, + }, } h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) slog.SetDefault(slog.New(h)) @@ -57,3 +62,19 @@ func runParseSchema(cctx *cli.Context) error { fmt.Println(string(out)) return nil } + +func runLoadDirectory(cctx *cli.Context) error { + p := cctx.Args().First() + if p == "" { + return fmt.Errorf("need to provide directory path as an argument") + } + + c := lexicon.NewCatalog() + err := c.LoadDirectory(p) + if err != nil { + return err + } + + fmt.Println("success!") + return nil +} diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index e5f3a9dda..f303d99ab 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -3,6 +3,8 @@ package lexicon import ( "encoding/json" "fmt" + "reflect" + "strings" ) // Serialization helper for top-level Lexicon schema JSON objects (files) @@ -19,6 +21,49 @@ type SchemaDef struct { Inner any } +func (s *SchemaDef) CheckSchema() error { + switch v := s.Inner.(type) { + case SchemaRecord: + return v.CheckSchema() + case SchemaQuery: + return v.CheckSchema() + case SchemaProcedure: + return v.CheckSchema() + case SchemaSubscription: + return v.CheckSchema() + case SchemaNull: + return v.CheckSchema() + case SchemaBoolean: + return v.CheckSchema() + case SchemaInteger: + return v.CheckSchema() + case SchemaString: + return v.CheckSchema() + case SchemaBytes: + return v.CheckSchema() + case SchemaCIDLink: + return v.CheckSchema() + case SchemaArray: + return v.CheckSchema() + case SchemaObject: + return v.CheckSchema() + case SchemaBlob: + return v.CheckSchema() + case SchemaParams: + return v.CheckSchema() + case SchemaToken: + return v.CheckSchema() + case SchemaRef: + return v.CheckSchema() + case SchemaUnion: + return v.CheckSchema() + case SchemaUnknown: + return v.CheckSchema() + default: + return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v)) + } +} + func (s SchemaDef) MarshalJSON() ([]byte, error) { return json.Marshal(s.Inner) } @@ -28,132 +73,133 @@ func (s *SchemaDef) UnmarshalJSON(b []byte) error { if err != nil { return err } + // TODO: should we call CheckSchema here, instead of in lexicon loading? switch t { case "record": v := new(SchemaRecord) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "query": v := new(SchemaQuery) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "procedure": v := new(SchemaProcedure) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "subscription": v := new(SchemaSubscription) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "null": v := new(SchemaNull) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "boolean": v := new(SchemaBoolean) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "integer": v := new(SchemaInteger) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "string": v := new(SchemaString) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "bytes": v := new(SchemaBytes) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "cid-link": v := new(SchemaCIDLink) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "array": v := new(SchemaArray) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "object": v := new(SchemaObject) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "blob": v := new(SchemaBlob) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "params": v := new(SchemaParams) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "token": v := new(SchemaToken) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "ref": v := new(SchemaRef) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "union": v := new(SchemaUnion) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil case "unknown": v := new(SchemaUnknown) if err = json.Unmarshal(b, v); err != nil { return err } - s.Inner = v + s.Inner = *v return nil default: return fmt.Errorf("unexpected schema type: %s", t) @@ -168,6 +214,18 @@ type SchemaRecord struct { Record SchemaObject `json:"record"` } +func (s *SchemaRecord) CheckSchema() error { + switch s.Key { + case "tid", "any": + // pass + default: + if !strings.HasPrefix(s.Key, "literal:") { + return fmt.Errorf("invalid record key specifier: %s", s.Key) + } + } + return s.Record.CheckSchema() +} + type SchemaQuery struct { Type string `json:"type,const=query"` Description *string `json:"description,omitempty"` @@ -176,6 +234,20 @@ type SchemaQuery struct { Errors []SchemaError `json:"errors,omitempty"` // optional } +func (s *SchemaQuery) CheckSchema() error { + if s.Output != nil { + if err := s.Output.CheckSchema(); err != nil { + return err + } + } + for _, e := range s.Errors { + if err := e.CheckSchema(); err != nil { + return err + } + } + return s.Parameters.CheckSchema() +} + type SchemaProcedure struct { Type string `json:"type,const=procedure"` Description *string `json:"description,omitempty"` @@ -185,6 +257,25 @@ type SchemaProcedure struct { Input *SchemaBody `json:"input"` // optional } +func (s *SchemaProcedure) CheckSchema() error { + if s.Input != nil { + if err := s.Input.CheckSchema(); err != nil { + return err + } + } + if s.Output != nil { + if err := s.Output.CheckSchema(); err != nil { + return err + } + } + for _, e := range s.Errors { + if err := e.CheckSchema(); err != nil { + return err + } + } + return s.Parameters.CheckSchema() +} + type SchemaSubscription struct { Type string `json:"type,const=subscription"` Description *string `json:"description,omitempty"` @@ -192,27 +283,67 @@ type SchemaSubscription struct { Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional? } +func (s *SchemaSubscription) CheckSchema() error { + if s.Message != nil { + if err := s.Message.CheckSchema(); err != nil { + return err + } + } + return s.Parameters.CheckSchema() +} + type SchemaBody struct { Description *string `json:"description,omitempty"` Encoding string `json:"encoding"` // required, mimetype Schema *SchemaDef `json:"schema"` // optional; type:object, type:ref, or type:union } +func (s *SchemaBody) CheckSchema() error { + // TODO: any validation of encoding? + if s.Schema != nil { + switch s.Schema.Inner.(type) { + case SchemaObject, SchemaRef, SchemaUnion: + // pass + default: + return fmt.Errorf("body type can only have object, ref, or union schema") + } + if err := s.Schema.CheckSchema(); err != nil { + return err + } + } + return nil +} + type SchemaMessage struct { Description *string `json:"description,omitempty"` Schema SchemaDef `json:"schema"` // required; type:union only } +func (s *SchemaMessage) CheckSchema() error { + if _, ok := s.Schema.Inner.(SchemaUnion); !ok { + return fmt.Errorf("message must have schema type union") + } + return s.Schema.CheckSchema() +} + type SchemaError struct { Name string `json:"name"` Description *string `json:"description"` } +func (s *SchemaError) CheckSchema() error { + return nil +} + type SchemaNull struct { Type string `json:"type,const=null"` Description *string `json:"description,omitempty"` } +func (s *SchemaNull) CheckSchema() error { + return nil +} + type SchemaBoolean struct { Type string `json:"type,const=bool"` Description *string `json:"description,omitempty"` @@ -220,6 +351,13 @@ type SchemaBoolean struct { Const *bool `json:"const,omitempty"` } +func (s *SchemaBoolean) CheckSchema() error { + if s.Default != nil && s.Const != nil { + return fmt.Errorf("schema can't have both 'default' and 'const'") + } + return nil +} + type SchemaInteger struct { Type string `json:"type,const=integer"` Description *string `json:"description,omitempty"` @@ -230,6 +368,17 @@ type SchemaInteger struct { Const *int `json:"const,omitempty"` } +func (s *SchemaInteger) CheckSchema() error { + // TODO: enforce min/max against enum, default, const + if s.Default != nil && s.Const != nil { + return fmt.Errorf("schema can't have both 'default' and 'const'") + } + if s.Minimum != nil && s.Maximum != nil && *s.Maximum < *s.Minimum { + return fmt.Errorf("schema max < min") + } + return nil +} + type SchemaString struct { Type string `json:"type,const=string"` Description *string `json:"description,omitempty"` @@ -240,8 +389,28 @@ type SchemaString struct { MaxGraphemes *int `json:"maxGraphemes,omitempty"` KnownValues []string `json:"knownValues,omitempty"` Enum []string `json:"enum,omitempty"` - Default *int `json:"default,omitempty"` - Const *int `json:"const,omitempty"` + Default *string `json:"default,omitempty"` + Const *string `json:"const,omitempty"` +} + +func (s *SchemaString) CheckSchema() error { + // TODO: enforce min/max against enum, default, const + if s.Default != nil && s.Const != nil { + return fmt.Errorf("schema can't have both 'default' and 'const'") + } + if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength { + return fmt.Errorf("schema max < min") + } + if s.MinGraphemes != nil && s.MaxGraphemes != nil && *s.MaxGraphemes < *s.MinGraphemes { + return fmt.Errorf("schema max < min") + } + if (s.MinLength != nil && *s.MinLength < 0) || + (s.MaxLength != nil && *s.MaxLength < 0) || + (s.MinGraphemes != nil && *s.MinGraphemes < 0) || + (s.MaxGraphemes != nil && *s.MaxGraphemes < 0) { + return fmt.Errorf("string schema min or max below zero") + } + return nil } type SchemaBytes struct { @@ -251,11 +420,26 @@ type SchemaBytes struct { MaxLength *int `json:"maxLength,omitempty"` } +func (s *SchemaBytes) CheckSchema() error { + if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength { + return fmt.Errorf("schema max < min") + } + if (s.MinLength != nil && *s.MinLength < 0) || + (s.MaxLength != nil && *s.MaxLength < 0) { + return fmt.Errorf("bytes schema min or max below zero") + } + return nil +} + type SchemaCIDLink struct { Type string `json:"type,const=cid-link"` Description *string `json:"description,omitempty"` } +func (s *SchemaCIDLink) CheckSchema() error { + return nil +} + type SchemaArray struct { Type string `json:"type,const=array"` Description *string `json:"description,omitempty"` @@ -264,6 +448,17 @@ type SchemaArray struct { MaxLength *int `json:"maxLength,omitempty"` } +func (s *SchemaArray) CheckSchema() error { + if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength { + return fmt.Errorf("schema max < min") + } + if (s.MinLength != nil && *s.MinLength < 0) || + (s.MaxLength != nil && *s.MaxLength < 0) { + return fmt.Errorf("array schema min or max below zero") + } + return s.Items.CheckSchema() +} + type SchemaObject struct { Type string `json:"type,const=object"` Description *string `json:"description,omitempty"` @@ -272,6 +467,31 @@ type SchemaObject struct { Nullable []string `json:"nullable,omitempty"` } +func (s *SchemaObject) CheckSchema() error { + // TODO: check for set intersection between required and nullable + // TODO: check for set uniqueness of required and nullable + for _, k := range s.Required { + if _, ok := s.Properties[k]; !ok { + fmt.Errorf("object 'required' field not in properties: %s", k) + } + } + for _, k := range s.Nullable { + if _, ok := s.Properties[k]; !ok { + fmt.Errorf("object 'nullable' field not in properties: %s", k) + } + } + for k, def := range s.Properties { + // TODO: more checks on field name? + if len(k) == 0 { + return fmt.Errorf("empty object schema field name not allowed") + } + if err := def.CheckSchema(); err != nil { + return err + } + } + return nil +} + type SchemaBlob struct { Type string `json:"type,const=blob"` Description *string `json:"description,omitempty"` @@ -279,6 +499,14 @@ type SchemaBlob struct { MaxSize *int `json:"maxSize,omitempty"` } +func (s *SchemaBlob) CheckSchema() error { + // TODO: validate Accept (mimetypes)? + if s.MaxSize != nil && *s.MaxSize <= 0 { + return fmt.Errorf("blob max size less or equal to zero") + } + return nil +} + type SchemaParams struct { Type string `json:"type,const=params"` Description *string `json:"description,omitempty"` @@ -286,17 +514,61 @@ type SchemaParams struct { Required []string `json:"required,omitempty"` } +func (s *SchemaParams) CheckSchema() error { + // TODO: check for set uniqueness of required + for _, k := range s.Required { + if _, ok := s.Properties[k]; !ok { + fmt.Errorf("object 'required' field not in properties: %s", k) + } + } + for k, def := range s.Properties { + // TODO: more checks on field name? + if len(k) == 0 { + return fmt.Errorf("empty object schema field name not allowed") + } + switch v := def.Inner.(type) { + case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown: + // pass + case SchemaArray: + switch v.Items.Inner.(type) { + case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown: + // pass + default: + return fmt.Errorf("params array item type must be boolean, integer, string, or unknown") + } + default: + return fmt.Errorf("params field type must be boolean, integer, string, or unknown") + } + if err := def.CheckSchema(); err != nil { + return err + } + } + return nil +} + type SchemaToken struct { Type string `json:"type,const=token"` Description *string `json:"description,omitempty"` } +func (s *SchemaToken) CheckSchema() error { + return nil +} + type SchemaRef struct { Type string `json:"type,const=ref"` Description *string `json:"description,omitempty"` Ref string `json:"ref"` } +func (s *SchemaRef) CheckSchema() error { + // TODO: more validation of ref string? + if len(s.Ref) == 0 { + return fmt.Errorf("empty schema ref") + } + return nil +} + type SchemaUnion struct { Type string `json:"type,const=union"` Description *string `json:"description,omitempty"` @@ -304,7 +576,22 @@ type SchemaUnion struct { Closed *bool `json:"closed,omitempty"` } +func (s *SchemaUnion) CheckSchema() error { + // TODO: uniqueness check on refs + for _, ref := range s.Refs { + // TODO: more validation of ref string? + if len(ref) == 0 { + return fmt.Errorf("empty schema ref") + } + } + return nil +} + type SchemaUnknown struct { Type string `json:"type,const=unknown"` Description *string `json:"description,omitempty"` } + +func (s *SchemaUnknown) CheckSchema() error { + return nil +} diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 9b13a8599..f07aea6c7 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -1,12 +1,109 @@ package lexicon import ( -// "github.com/bluesky-social/indigo/atproto/syntax" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" ) // An aggregation of lexicon schemas, and methods for validating generic data against those schemas. type Catalog struct { + // TODO: not safe zero value; hide this field? seems aggressive + Schemas map[string]Schema +} + +func NewCatalog() Catalog { + return Catalog{ + Schemas: make(map[string]Schema), + } } type Schema struct { + ID string + Revision *int + Def any +} + +func (c *Catalog) Resolve(name string) (*Schema, error) { + // default to #main if name doesn't have a fragment + if !strings.Contains(name, "#") { + name = name + "#main" + } + s, ok := c.Schemas[name] + if !ok { + return nil, fmt.Errorf("schema not found in catalog: %s", name) + } + return &s, nil +} + +func (c *Catalog) AddSchemaFile(sf SchemaFile) error { + base := sf.ID + for frag, def := range sf.Defs { + if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { + // TODO: more validation here? + return fmt.Errorf("schema name invalid: %s", frag) + } + name := base + "#" + frag + if _, ok := c.Schemas[name]; ok { + return fmt.Errorf("catalog already contained a schema with name: %s", name) + } + if err := def.CheckSchema(); err != nil { + return err + } + // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." + switch def.Inner.(type) { + case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: + if frag != "main" { + return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) + } + } + s := Schema{ + ID: name, + Revision: sf.Revision, + Def: def.Inner, + } + c.Schemas[name] = s + } + return nil } + +func (c *Catalog) LoadDirectory(dirPath string) error { + return filepath.WalkDir(dirPath, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if !strings.HasSuffix(p, ".json") { + return nil + } + // TODO: logging + fmt.Println(p) + f, err := os.Open(p) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + var sf SchemaFile + if err = json.Unmarshal(b, &sf); err != nil { + return err + } + if err = c.AddSchemaFile(sf); err != nil { + return err + } + return nil + }) +} + +//func (c *Catalog) ValidateData(d map[string]any) error From 99dd213958c29f7475602995c48812e6790dbbc5 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 7 Nov 2023 00:28:38 -0800 Subject: [PATCH 003/111] lexicon: progress on validation --- atproto/lexicon/language.go | 154 ++++++++++++++++++ atproto/lexicon/language_test.go | 2 +- atproto/lexicon/lexicon.go | 107 +++++++++++- atproto/lexicon/lexicon_test.go | 39 +++++ .../{ => valid}/com_atproto_label_defs.json | 0 5 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 atproto/lexicon/lexicon_test.go rename atproto/lexicon/testdata/{ => valid}/com_atproto_label_defs.json (100%) diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index f303d99ab..579e45dc9 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -5,6 +5,9 @@ import ( "fmt" "reflect" "strings" + + "github.com/bluesky-social/indigo/atproto/data" + "github.com/bluesky-social/indigo/atproto/syntax" ) // Serialization helper for top-level Lexicon schema JSON objects (files) @@ -334,6 +337,20 @@ type SchemaError struct { func (s *SchemaError) CheckSchema() error { return nil } +func (s *SchemaError) Validate(d any) error { + e, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("expected an object in error position") + } + n, ok := e["error"] + if !ok { + return fmt.Errorf("expected error type") + } + if n != s.Name { + return fmt.Errorf("error type mis-match: %s", n) + } + return nil +} type SchemaNull struct { Type string `json:"type,const=null"` @@ -344,6 +361,13 @@ func (s *SchemaNull) CheckSchema() error { return nil } +func (s *SchemaNull) Validate(d any) error { + if d != nil { + return fmt.Errorf("expected null data, got: %s", reflect.TypeOf(d)) + } + return nil +} + type SchemaBoolean struct { Type string `json:"type,const=bool"` Description *string `json:"description,omitempty"` @@ -358,6 +382,17 @@ func (s *SchemaBoolean) CheckSchema() error { return nil } +func (s *SchemaBoolean) Validate(d any) error { + v, ok := d.(bool) + if !ok { + return fmt.Errorf("expected a boolean") + } + if s.Const != nil && v != *s.Const { + return fmt.Errorf("boolean val didn't match constant (%v): %v", *s.Const, v) + } + return nil +} + type SchemaInteger struct { Type string `json:"type,const=integer"` Description *string `json:"description,omitempty"` @@ -379,6 +414,21 @@ func (s *SchemaInteger) CheckSchema() error { return nil } +func (s *SchemaInteger) Validate(d any) error { + v, ok := d.(int) + if !ok { + return fmt.Errorf("expected an integer") + } + // TODO: enforce enum + if s.Const != nil && v != *s.Const { + return fmt.Errorf("integer val didn't match constant (%d): %d", *s.Const, v) + } + if (s.Minimum != nil && v < *s.Minimum) || (s.Maximum != nil && v > *s.Maximum) { + return fmt.Errorf("integer val outside specified range: %d", v) + } + return nil +} + type SchemaString struct { Type string `json:"type,const=string"` Description *string `json:"description,omitempty"` @@ -410,6 +460,71 @@ func (s *SchemaString) CheckSchema() error { (s.MaxGraphemes != nil && *s.MaxGraphemes < 0) { return fmt.Errorf("string schema min or max below zero") } + if s.Format != nil { + switch *s.Format { + case "at-identifier", "at-uri", "cid", "datetime", "did", "handle", "nsid", "uri", "language": + // pass + default: + return fmt.Errorf("unknown string format: %s", *s.Format) + } + } + return nil +} + +func (s *SchemaString) Validate(d any) error { + v, ok := d.(string) + if !ok { + return fmt.Errorf("expected a string") + } + // TODO: enforce enum + if s.Const != nil && v != *s.Const { + return fmt.Errorf("string val didn't match constant (%s): %s", *s.Const, v) + } + // TODO: is this actually counting UTF-8 length? + if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) { + return fmt.Errorf("string length outside specified range: %d", len(v)) + } + // TODO: grapheme length + if s.Format != nil { + switch *s.Format { + case "at-identifier": + if _, err := syntax.ParseAtIdentifier(v); err != nil { + return err + } + case "at-uri": + if _, err := syntax.ParseATURI(v); err != nil { + return err + } + case "cid": + if _, err := syntax.ParseCID(v); err != nil { + return err + } + case "datetime": + if _, err := syntax.ParseDatetime(v); err != nil { + return err + } + case "did": + if _, err := syntax.ParseDID(v); err != nil { + return err + } + case "handle": + if _, err := syntax.ParseHandle(v); err != nil { + return err + } + case "nsid": + if _, err := syntax.ParseNSID(v); err != nil { + return err + } + case "uri": + if _, err := syntax.ParseURI(v); err != nil { + return err + } + case "language": + if _, err := syntax.ParseLanguage(v); err != nil { + return err + } + } + } return nil } @@ -431,6 +546,17 @@ func (s *SchemaBytes) CheckSchema() error { return nil } +func (s *SchemaBytes) Validate(d any) error { + v, ok := d.(data.Bytes) + if !ok { + return fmt.Errorf("expecting bytes") + } + if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) { + return fmt.Errorf("bytes size out of bounds: %d", len(v)) + } + return nil +} + type SchemaCIDLink struct { Type string `json:"type,const=cid-link"` Description *string `json:"description,omitempty"` @@ -440,6 +566,14 @@ func (s *SchemaCIDLink) CheckSchema() error { return nil } +func (s *SchemaCIDLink) Validate(d any) error { + _, ok := d.(data.CIDLink) + if !ok { + return fmt.Errorf("expecting a cid-link") + } + return nil +} + type SchemaArray struct { Type string `json:"type,const=array"` Description *string `json:"description,omitempty"` @@ -507,6 +641,18 @@ func (s *SchemaBlob) CheckSchema() error { return nil } +func (s *SchemaBlob) Validate(d any) error { + v, ok := d.(data.Blob) + if !ok { + return fmt.Errorf("expected a blob") + } + // TODO: validate accept mimetype + if s.MaxSize != nil && int(v.Size) > *s.MaxSize { + return fmt.Errorf("blob size too large: %d", v.Size) + } + return nil +} + type SchemaParams struct { Type string `json:"type,const=params"` Description *string `json:"description,omitempty"` @@ -546,6 +692,10 @@ func (s *SchemaParams) CheckSchema() error { return nil } +func (s *SchemaParams) Validate(d any) error { + return nil +} + type SchemaToken struct { Type string `json:"type,const=token"` Description *string `json:"description,omitempty"` @@ -595,3 +745,7 @@ type SchemaUnknown struct { func (s *SchemaUnknown) CheckSchema() error { return nil } + +func (s *SchemaUnknown) Validate(d any) error { + return nil +} diff --git a/atproto/lexicon/language_test.go b/atproto/lexicon/language_test.go index 89882c86e..982653a79 100644 --- a/atproto/lexicon/language_test.go +++ b/atproto/lexicon/language_test.go @@ -12,7 +12,7 @@ import ( func TestBasicLabelLexicon(t *testing.T) { assert := assert.New(t) - f, err := os.Open("testdata/com_atproto_label_defs.json") + f, err := os.Open("testdata/valid/com_atproto_label_defs.json") if err != nil { t.Fatal(err) } diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index f07aea6c7..22bfdc99c 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path/filepath" + "reflect" "strings" ) @@ -106,4 +107,108 @@ func (c *Catalog) LoadDirectory(dirPath string) error { }) } -//func (c *Catalog) ValidateData(d map[string]any) error +func (c *Catalog) Validate(d any, id string) error { + schema, err := c.Resolve(id) + if err != nil { + return nil + } + switch v := schema.Def.(type) { + case SchemaRecord: + obj, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) + } + // XXX: return v.Validate(d, schema.ID) + _ = obj + _ = v + return nil + case SchemaQuery: + // XXX: return v.Validate(d) + return nil + case SchemaProcedure: + // XXX: return v.Validate(d) + return nil + case SchemaSubscription: + obj, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) + } + // XXX: return v.Validate(d) + _ = obj + return nil + case SchemaToken: + str, ok := d.(string) + if !ok { + return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) + } + if str != id { + return fmt.Errorf("expected token (%s), got: %s", id, str) + } + return nil + default: + return c.validateDef(schema.Def, d) + } +} + +func (c *Catalog) validateDef(def any, d any) error { + // TODO: + switch v := def.(type) { + case SchemaNull: + return v.Validate(d) + case SchemaBoolean: + return v.Validate(d) + case SchemaInteger: + return v.Validate(d) + case SchemaString: + return v.Validate(d) + case SchemaBytes: + return v.Validate(d) + case SchemaCIDLink: + return v.Validate(d) + case SchemaArray: + arr, ok := d.([]any) + if !ok { + return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d)) + } + // XXX: return v.ValidateArray(d, v) + _ = arr + return nil + case SchemaObject: + obj, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) + } + return c.ValidateObject(v, obj) + case SchemaBlob: + return v.Validate(d) + case SchemaParams: + return v.Validate(d) + case SchemaRef: + // recurse + return c.Validate(d, v.Ref) + case SchemaUnion: + // XXX: special ValidateUnion helper + return nil + case SchemaUnknown: + return v.Validate(d) + default: + return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v)) + } +} + +func (c *Catalog) ValidateObject(s SchemaObject, d map[string]any) error { + for _, k := range s.Required { + if _, ok := d[k]; !ok { + return fmt.Errorf("required field missing: %s", k) + } + } + for k, def := range s.Properties { + if v, ok := d[k]; ok { + err := c.validateDef(def.Inner, v) + if err != nil { + return err + } + } + } + return nil +} diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/lexicon_test.go new file mode 100644 index 000000000..ba3eeec75 --- /dev/null +++ b/atproto/lexicon/lexicon_test.go @@ -0,0 +1,39 @@ +package lexicon + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicCatalog(t *testing.T) { + assert := assert.New(t) + + cat := NewCatalog() + if err := cat.LoadDirectory("testdata/valid"); err != nil { + t.Fatal(err) + } + + assert.NoError(cat.Validate( + map[string]any{ + "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "cts": "2000-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", + "val": "test-label", + }, + "com.atproto.label.defs#label", + )) + + assert.Error(cat.Validate( + map[string]any{ + "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + "cts": "2000-01-01T00:00:00.000Z", + "neg": false, + "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", + "val": "test-label", + }, + "com.atproto.label.defs#label", + )) +} diff --git a/atproto/lexicon/testdata/com_atproto_label_defs.json b/atproto/lexicon/testdata/valid/com_atproto_label_defs.json similarity index 100% rename from atproto/lexicon/testdata/com_atproto_label_defs.json rename to atproto/lexicon/testdata/valid/com_atproto_label_defs.json From 23d8f85014bc677c7a2d982ebac3288d02de4d93 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 7 Nov 2023 22:51:10 -0800 Subject: [PATCH 004/111] lexicon: more progress on testing and validation --- atproto/lexicon/interop_language_test.go | 95 ++++++++ atproto/lexicon/interop_record_test.go | 89 ++++++++ atproto/lexicon/language.go | 24 +- atproto/lexicon/language_test.go | 2 +- atproto/lexicon/lexicon.go | 100 ++++---- atproto/lexicon/lexicon_test.go | 14 +- .../com_atproto_label_defs.json | 6 +- atproto/lexicon/testdata/catalog/query.json | 70 ++++++ atproto/lexicon/testdata/catalog/record.json | 213 ++++++++++++++++++ atproto/lexicon/testdata/lexicon-invalid.json | 18 ++ atproto/lexicon/testdata/lexicon-valid.json | 10 + .../lexicon/testdata/record-data-invalid.json | 21 ++ .../lexicon/testdata/record-data-valid.json | 91 ++++++++ 13 files changed, 690 insertions(+), 63 deletions(-) create mode 100644 atproto/lexicon/interop_language_test.go create mode 100644 atproto/lexicon/interop_record_test.go rename atproto/lexicon/testdata/{valid => catalog}/com_atproto_label_defs.json (99%) create mode 100644 atproto/lexicon/testdata/catalog/query.json create mode 100644 atproto/lexicon/testdata/catalog/record.json create mode 100644 atproto/lexicon/testdata/lexicon-invalid.json create mode 100644 atproto/lexicon/testdata/lexicon-valid.json create mode 100644 atproto/lexicon/testdata/record-data-invalid.json create mode 100644 atproto/lexicon/testdata/record-data-valid.json diff --git a/atproto/lexicon/interop_language_test.go b/atproto/lexicon/interop_language_test.go new file mode 100644 index 000000000..fa9f347ee --- /dev/null +++ b/atproto/lexicon/interop_language_test.go @@ -0,0 +1,95 @@ +package lexicon + +import ( + "encoding/json" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type LexiconFixture struct { + Name string `json:"name"` + Lexicon json.RawMessage `json:"lexicon"` +} + +func TestInteropLexiconValid(t *testing.T) { + + f, err := os.Open("testdata/lexicon-valid.json") + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + jsonBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var fixtures []LexiconFixture + if err := json.Unmarshal(jsonBytes, &fixtures); err != nil { + t.Fatal(err) + } + + for _, f := range fixtures { + testLexiconFixtureValid(t, f) + } +} + +func testLexiconFixtureValid(t *testing.T, fixture LexiconFixture) { + assert := assert.New(t) + + var schema SchemaFile + if err := json.Unmarshal(fixture.Lexicon, &schema); err != nil { + t.Fatal(err) + } + + outBytes, err := json.Marshal(schema) + if err != nil { + t.Fatal(err) + } + + var beforeMap map[string]any + if err := json.Unmarshal(fixture.Lexicon, &beforeMap); err != nil { + t.Fatal(err) + } + + var afterMap map[string]any + if err := json.Unmarshal(outBytes, &afterMap); err != nil { + t.Fatal(err) + } + + assert.Equal(beforeMap, afterMap) +} + +func TestInteropLexiconInvalid(t *testing.T) { + + f, err := os.Open("testdata/lexicon-invalid.json") + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + jsonBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var fixtures []LexiconFixture + if err := json.Unmarshal(jsonBytes, &fixtures); err != nil { + t.Fatal(err) + } + + for _, f := range fixtures { + testLexiconFixtureInvalid(t, f) + } +} + +func testLexiconFixtureInvalid(t *testing.T, fixture LexiconFixture) { + assert := assert.New(t) + + var schema SchemaFile + err := json.Unmarshal(fixture.Lexicon, &schema) + assert.Error(err) +} diff --git a/atproto/lexicon/interop_record_test.go b/atproto/lexicon/interop_record_test.go new file mode 100644 index 000000000..8a2282ff8 --- /dev/null +++ b/atproto/lexicon/interop_record_test.go @@ -0,0 +1,89 @@ +package lexicon + +import ( + "encoding/json" + "fmt" + "io" + "os" + "testing" + + "github.com/bluesky-social/indigo/atproto/data" + + "github.com/stretchr/testify/assert" +) + +type RecordFixture struct { + Name string `json:"name"` + RecordKey string `json:"rkey"` + Data json.RawMessage `json:"data"` +} + +func TestInteropRecordValid(t *testing.T) { + assert := assert.New(t) + + cat := NewCatalog() + if err := cat.LoadDirectory("testdata/catalog"); err != nil { + t.Fatal(err) + } + + f, err := os.Open("testdata/record-data-valid.json") + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + jsonBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var fixtures []RecordFixture + if err := json.Unmarshal(jsonBytes, &fixtures); err != nil { + t.Fatal(err) + } + + for _, fixture := range fixtures { + fmt.Println(fixture.Name) + d, err := data.UnmarshalJSON(fixture.Data) + if err != nil { + t.Fatal(err) + } + + assert.NoError(cat.ValidateRecord(d, "example.lexicon.record")) + } +} + +func TestInteropRecordInvalid(t *testing.T) { + assert := assert.New(t) + + cat := NewCatalog() + if err := cat.LoadDirectory("testdata/catalog"); err != nil { + t.Fatal(err) + } + + f, err := os.Open("testdata/record-data-invalid.json") + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + jsonBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var fixtures []RecordFixture + if err := json.Unmarshal(jsonBytes, &fixtures); err != nil { + t.Fatal(err) + } + + for _, fixture := range fixtures { + fmt.Println(fixture.Name) + d, err := data.UnmarshalJSON(fixture.Data) + if err != nil { + t.Fatal(err) + } + + assert.Error(cat.ValidateRecord(d, "example.lexicon.record")) + } +} diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 579e45dc9..7f10e54fe 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -63,7 +63,7 @@ func (s *SchemaDef) CheckSchema() error { case SchemaUnknown: return v.CheckSchema() default: - return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v)) + return fmt.Errorf("unhandled schema type: %v", reflect.TypeOf(v)) } } @@ -207,7 +207,6 @@ func (s *SchemaDef) UnmarshalJSON(b []byte) error { default: return fmt.Errorf("unexpected schema type: %s", t) } - return fmt.Errorf("unexpected schema type: %s", t) } type SchemaRecord struct { @@ -415,10 +414,11 @@ func (s *SchemaInteger) CheckSchema() error { } func (s *SchemaInteger) Validate(d any) error { - v, ok := d.(int) + v64, ok := d.(int64) if !ok { return fmt.Errorf("expected an integer") } + v := int(v64) // TODO: enforce enum if s.Const != nil && v != *s.Const { return fmt.Errorf("integer val didn't match constant (%d): %d", *s.Const, v) @@ -474,7 +474,7 @@ func (s *SchemaString) CheckSchema() error { func (s *SchemaString) Validate(d any) error { v, ok := d.(string) if !ok { - return fmt.Errorf("expected a string") + return fmt.Errorf("expected a string: %v", reflect.TypeOf(d)) } // TODO: enforce enum if s.Const != nil && v != *s.Const { @@ -606,12 +606,12 @@ func (s *SchemaObject) CheckSchema() error { // TODO: check for set uniqueness of required and nullable for _, k := range s.Required { if _, ok := s.Properties[k]; !ok { - fmt.Errorf("object 'required' field not in properties: %s", k) + return fmt.Errorf("object 'required' field not in properties: %s", k) } } for _, k := range s.Nullable { if _, ok := s.Properties[k]; !ok { - fmt.Errorf("object 'nullable' field not in properties: %s", k) + return fmt.Errorf("object 'nullable' field not in properties: %s", k) } } for k, def := range s.Properties { @@ -626,6 +626,16 @@ func (s *SchemaObject) CheckSchema() error { return nil } +// Checks if a field name 'k' is one of the Nullable fields for this object +func (s *SchemaObject) IsNullable(k string) bool { + for _, el := range s.Nullable { + if el == k { + return true + } + } + return false +} + type SchemaBlob struct { Type string `json:"type,const=blob"` Description *string `json:"description,omitempty"` @@ -664,7 +674,7 @@ func (s *SchemaParams) CheckSchema() error { // TODO: check for set uniqueness of required for _, k := range s.Required { if _, ok := s.Properties[k]; !ok { - fmt.Errorf("object 'required' field not in properties: %s", k) + return fmt.Errorf("object 'required' field not in properties: %s", k) } } for k, def := range s.Properties { diff --git a/atproto/lexicon/language_test.go b/atproto/lexicon/language_test.go index 982653a79..cd33396f2 100644 --- a/atproto/lexicon/language_test.go +++ b/atproto/lexicon/language_test.go @@ -12,7 +12,7 @@ import ( func TestBasicLabelLexicon(t *testing.T) { assert := assert.New(t) - f, err := os.Open("testdata/valid/com_atproto_label_defs.json") + f, err := os.Open("testdata/catalog/com_atproto_label_defs.json") if err != nil { t.Fatal(err) } diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 22bfdc99c..bee5cbc3a 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -107,50 +107,29 @@ func (c *Catalog) LoadDirectory(dirPath string) error { }) } -func (c *Catalog) Validate(d any, id string) error { - schema, err := c.Resolve(id) +// TODO: rkey? is nsid always known? +// TODO: nsid as syntax.NSID +func (c *Catalog) ValidateRecord(raw any, id string) error { + def, err := c.Resolve(id) if err != nil { - return nil + return err } - switch v := schema.Def.(type) { - case SchemaRecord: - obj, ok := d.(map[string]any) - if !ok { - return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) - } - // XXX: return v.Validate(d, schema.ID) - _ = obj - _ = v - return nil - case SchemaQuery: - // XXX: return v.Validate(d) - return nil - case SchemaProcedure: - // XXX: return v.Validate(d) - return nil - case SchemaSubscription: - obj, ok := d.(map[string]any) - if !ok { - return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) - } - // XXX: return v.Validate(d) - _ = obj - return nil - case SchemaToken: - str, ok := d.(string) - if !ok { - return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) - } - if str != id { - return fmt.Errorf("expected token (%s), got: %s", id, str) - } - return nil - default: - return c.validateDef(schema.Def, d) + s, ok := def.Def.(SchemaRecord) + if !ok { + return fmt.Errorf("schema is not of record type: %s", id) } + d, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("record data is not object type") + } + t, ok := d["$type"] + if !ok || t != id { + return fmt.Errorf("record data missing $type, or didn't match expected NSID") + } + return c.validateObject(s.Record, d) } -func (c *Catalog) validateDef(def any, d any) error { +func (c *Catalog) validateData(def any, d any) error { // TODO: switch v := def.(type) { case SchemaNull: @@ -170,33 +149,44 @@ func (c *Catalog) validateDef(def any, d any) error { if !ok { return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d)) } - // XXX: return v.ValidateArray(d, v) - _ = arr - return nil + return c.validateArray(v, arr) case SchemaObject: obj, ok := d.(map[string]any) if !ok { return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) } - return c.ValidateObject(v, obj) + return c.validateObject(v, obj) case SchemaBlob: return v.Validate(d) case SchemaParams: return v.Validate(d) case SchemaRef: + // XXX: relative refs (in-file) // recurse - return c.Validate(d, v.Ref) + next, err := c.Resolve(v.Ref) + if err != nil { + return err + } + return c.validateData(next.Def, d) case SchemaUnion: - // XXX: special ValidateUnion helper + //return fmt.Errorf("XXX: union validation not implemented") return nil case SchemaUnknown: return v.Validate(d) + case SchemaToken: + str, ok := d.(string) + if !ok { + return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) + } + // XXX: token validation not implemented + _ = str + return nil default: return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v)) } } -func (c *Catalog) ValidateObject(s SchemaObject, d map[string]any) error { +func (c *Catalog) validateObject(s SchemaObject, d map[string]any) error { for _, k := range s.Required { if _, ok := d[k]; !ok { return fmt.Errorf("required field missing: %s", k) @@ -204,7 +194,10 @@ func (c *Catalog) ValidateObject(s SchemaObject, d map[string]any) error { } for k, def := range s.Properties { if v, ok := d[k]; ok { - err := c.validateDef(def.Inner, v) + if v == nil && s.IsNullable(k) { + continue + } + err := c.validateData(def.Inner, v) if err != nil { return err } @@ -212,3 +205,16 @@ func (c *Catalog) ValidateObject(s SchemaObject, d map[string]any) error { } return nil } + +func (c *Catalog) validateArray(s SchemaArray, arr []any) error { + if (s.MinLength != nil && len(arr) < *s.MinLength) || (s.MaxLength != nil && len(arr) > *s.MaxLength) { + return fmt.Errorf("array length out of bounds: %d", len(arr)) + } + for _, v := range arr { + err := c.validateData(s.Items.Inner, v) + if err != nil { + return err + } + } + return nil +} diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/lexicon_test.go index ba3eeec75..81953e04a 100644 --- a/atproto/lexicon/lexicon_test.go +++ b/atproto/lexicon/lexicon_test.go @@ -10,11 +10,16 @@ func TestBasicCatalog(t *testing.T) { assert := assert.New(t) cat := NewCatalog() - if err := cat.LoadDirectory("testdata/valid"); err != nil { + if err := cat.LoadDirectory("testdata/catalog"); err != nil { t.Fatal(err) } - assert.NoError(cat.Validate( + def, err := cat.Resolve("com.atproto.label.defs#label") + if err != nil { + t.Fatal(err) + } + assert.NoError(cat.validateData( + def.Def, map[string]any{ "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", "cts": "2000-01-01T00:00:00.000Z", @@ -23,10 +28,10 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, - "com.atproto.label.defs#label", )) - assert.Error(cat.Validate( + assert.Error(cat.validateData( + def.Def, map[string]any{ "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", "cts": "2000-01-01T00:00:00.000Z", @@ -34,6 +39,5 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, - "com.atproto.label.defs#label", )) } diff --git a/atproto/lexicon/testdata/valid/com_atproto_label_defs.json b/atproto/lexicon/testdata/catalog/com_atproto_label_defs.json similarity index 99% rename from atproto/lexicon/testdata/valid/com_atproto_label_defs.json rename to atproto/lexicon/testdata/catalog/com_atproto_label_defs.json index 57f06f81b..f8677dc37 100644 --- a/atproto/lexicon/testdata/valid/com_atproto_label_defs.json +++ b/atproto/lexicon/testdata/catalog/com_atproto_label_defs.json @@ -1,4 +1,6 @@ { + "lexicon": 1, + "id": "com.atproto.label.defs", "defs": { "label": { "description": "Metadata tag on an atproto resource (eg, repo or record)", @@ -72,7 +74,5 @@ ], "type": "object" } - }, - "id": "com.atproto.label.defs", - "lexicon": 1 + } } diff --git a/atproto/lexicon/testdata/catalog/query.json b/atproto/lexicon/testdata/catalog/query.json new file mode 100644 index 000000000..b337fc007 --- /dev/null +++ b/atproto/lexicon/testdata/catalog/query.json @@ -0,0 +1,70 @@ +{ + "lexicon": 1, + "id": "example.lexicon.query", + "revision": 1, + "description": "exersizes many lexicon features for the query type", + "defs": { + "main": { + "type": "query", + "description": "a query type", + "parameters": { + "type": "params", + "description": "a params type", + "required": ["string"], + "properties": { + "boolean": { + "type": "boolean", + "description": "field of type boolean" + }, + "integer": { + "type": "integer", + "description": "field of type integer" + }, + "string": { + "type": "string", + "description": "field of type string" + }, + "handle": { + "type": "string", + "format": "handle", + "description": "field of type string, format handle" + }, + "unknown": { + "type": "unknown", + "description": "field of type unknown" + }, + "array": { + "type": "array", + "description": "field of type array", + "items": { "type": "integer" } + } + } + }, + "output": { + "description": "output body type", + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "a": { + "type": "integer" + }, + "b": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "DemoError", + "description": "demo error value" + }, + { + "name": "AnotherDemoError", + "description": "another demo error value" + } + ] + } + } +} diff --git a/atproto/lexicon/testdata/catalog/record.json b/atproto/lexicon/testdata/catalog/record.json new file mode 100644 index 000000000..11351fd40 --- /dev/null +++ b/atproto/lexicon/testdata/catalog/record.json @@ -0,0 +1,213 @@ +{ + "lexicon": 1, + "id": "example.lexicon.record", + "revision": 1, + "description": "exersizes many lexicon features for the record type", + "defs": { + "main": { + "type": "record", + "key": "literal:demo", + "description": "a record type with many field", + "record": { + "required": [ "integer" ], + "nullable": [ "nullableString" ], + "properties": { + "null": { + "type": "null", + "description": "field of type null" + }, + "boolean": { + "type": "boolean", + "description": "field of type boolean" + }, + "integer": { + "type": "integer", + "description": "field of type integer" + }, + "string": { + "type": "string", + "description": "field of type string" + }, + "nullableString": { + "type": "string", + "description": "field of type string; value is nullable" + }, + "bytes": { + "type": "bytes", + "description": "field of type bytes" + }, + "cid-link": { + "type": "cid-link", + "description": "field of type cid-link" + }, + "blob": { + "type": "blob", + "description": "field of type blob" + }, + "unknown": { + "type": "unknown", + "description": "field of type unknown" + }, + "array": { + "type": "array", + "description": "field of type array", + "items": { "type": "integer" } + }, + "object": { + "type": "object", + "description": "field of type null", + "properties": { + "a": { "type": "integer" }, + "b": { "type": "integer" } + } + }, + "ref": { + "type": "ref", + "description": "field of type ref", + "ref": "example.lexicon.record#demoToken" + }, + "union": { + "type": "union", + "refs": [ + "example.lexicon.record#demoToken", + "example.lexicon.record#demoObject" + ] + }, + "formats": { + "type": "ref", + "ref": "example.lexicon.record#stringFormats" + }, + "constInteger": { + "type": "integer", + "const": 42 + }, + "defaultInteger": { + "type": "integer", + "default": 42 + }, + "enumInteger": { + "type": "integer", + "enum": [4, 9, 16, 25] + }, + "rangeInteger": { + "type": "integer", + "minimum": 10, + "maximum": 20 + }, + "lenString": { + "type": "string", + "minLength": 10, + "maxLength": 20 + }, + "graphemeString": { + "type": "string", + "minGraphemes": 10, + "maxGraphemes": 20 + }, + "enumString": { + "type": "string", + "knownValues": ["fish", "tree", "rock"] + }, + "knownString": { + "type": "string", + "knownValues": ["blue", "green", "red"] + }, + "sizeBytes": { + "type": "bytes", + "minLength": 10, + "maxLength": 20 + }, + "lenArray": { + "type": "array", + "items": { "type": "integer" }, + "minLength": 2, + "maxLength": 5 + }, + "sizeBlob": { + "type": "blob", + "maxSize": 20 + }, + "acceptBlob": { + "type": "blob", + "accept": [ "image/*" ] + }, + "closedUnion": { + "type": "union", + "refs": [ + "example.lexicon.record#demoToken", + "example.lexicon.record#demoObject" + ], + "closed": true + } + } + } + }, + "stringFormats": { + "type": "object", + "description": "all the various string format types", + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "a did string" + }, + "handle": { + "type": "string", + "format": "handle", + "description": "a did string" + }, + "atidentifier": { + "type": "string", + "format": "at-identifier", + "description": "an at-identifier string" + }, + "nsid": { + "type": "string", + "format": "nsid", + "description": "an nsid string" + }, + "aturi": { + "type": "string", + "format": "at-uri", + "description": "an at-uri string" + }, + "cid": { + "type": "string", + "format": "cid", + "description": "a cid string (not a cid-link)" + }, + "datetime": { + "type": "string", + "format": "datetime", + "description": "a datetime string" + }, + "language": { + "type": "string", + "format": "language", + "description": "a language string" + }, + "uri": { + "type": "string", + "format": "uri", + "description": "a generic URI field" + } + } + }, + "demoToken": { + "type": "token", + "description": "an example of what a token looks like" + }, + "demoObject": { + "type": "object", + "description": "smaller object schema for unions", + "parameters": { + "a": { + "type": "integer" + }, + "b": { + "type": "integer" + } + } + } + } +} diff --git a/atproto/lexicon/testdata/lexicon-invalid.json b/atproto/lexicon/testdata/lexicon-invalid.json new file mode 100644 index 000000000..2c80fedbc --- /dev/null +++ b/atproto/lexicon/testdata/lexicon-invalid.json @@ -0,0 +1,18 @@ +[ +{ + "name": "invalid lexicon field", + "lexicon": { + "lexicon": "one", + "id": "example.lexicon", + "defs": { "demo": { "type": "integer" } } + } +}, +{ + "name": "invalid id field", + "lexicon": { + "lexicon": 1, + "id": 2, + "defs": { "demo": { "type": "integer" } } + } +} +] diff --git a/atproto/lexicon/testdata/lexicon-valid.json b/atproto/lexicon/testdata/lexicon-valid.json new file mode 100644 index 000000000..bfecaeb73 --- /dev/null +++ b/atproto/lexicon/testdata/lexicon-valid.json @@ -0,0 +1,10 @@ +[ +{ + "name": "minimal", + "lexicon": { + "lexicon": 1, + "id": "example.lexicon", + "defs": { "demo": { "type": "integer" } } + } +} +] diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json new file mode 100644 index 000000000..05c8f514a --- /dev/null +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -0,0 +1,21 @@ +[ + { "name": "missing required field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record" } + }, + { "name": "invalid string field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "string": 2 } }, + { "name": "invalid string format handle", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "handle": "123" } } + }, + { "name": "invalid string format did", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "did": "123" } } + }, + { "name": "invalid array element", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "array": [true, false] } + } +] diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json new file mode 100644 index 000000000..a5c4c2f00 --- /dev/null +++ b/atproto/lexicon/testdata/record-data-valid.json @@ -0,0 +1,91 @@ +[ + { + "name": "minimal", + "rkey": "demo", + "data": { + "$type": "example.lexicon.record", + "integer": 1 + } + }, + { + "name": "full", + "rkey": "demo", + "data": { + "$type": "example.lexicon.record", + "null": null, + "boolean": true, + "integer": 3, + "string": "blah", + "nullableString": null, + "bytes": { + "$bytes": "123" + }, + "cidlink": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + }, + "blob": { + "$type": "blob", + "mimeType": "text/plain", + "size": 12345, + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }, + "unknown": { + "a": "alphabet", + "b": 3 + }, + "array": [1,2,3], + "object": { + "a": 1, + "b": 2 + }, + "ref": "example.lexicon.record#demoToken", + "union": { + "$type": "example.lexicon.record#demoObject", + "a": 1, + "b": 2 + }, + "formats": { + "did": "did:web:example.com", + "handle": "handle.example.com", + "atidentifier": "handle.example.com", + "aturi": "at://handle.example.com/com.example.nsid/asdf123", + "nsid": "com.example.nsid", + "cid": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq", + "datetime": "2023-10-30T22:25:23Z", + "language": "en", + "uri": "https://example.com/file.txt" + }, + "constInteger": 42, + "defaultInteger": 123, + "enumInteger": 16, + "rangeInteger": 16, + "lenString": "1234567890ABC", + "graphemeString": "abcde", + "enumString": "fish", + "knownString": "blue", + "sizeBytes": { + "$bytes": "asdfasdfasdfasdf" + }, + "lenArray": [1,2,3], + "sizeBlob": { + "$type": "blob", + "mimeType": "text/plain", + "size": 8, + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }, + "acceptBlob": { + "$type": "blob", + "mimeType": "image/png", + "size": 12345, + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }, + "closedUnion": "example.lexicon.record#demoToken" + } + } +] From 0a7d59700ee75cd83f4e1457338dd4423f0d4507 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 28 Feb 2024 20:32:44 -0800 Subject: [PATCH 005/111] lexicon: progress on validation --- atproto/lexicon/interop_record_test.go | 7 ++- atproto/lexicon/language.go | 65 ++++++++++++++++++++++++-- atproto/lexicon/lexicon.go | 34 ++++++++++---- atproto/lexicon/mimetype.go | 19 ++++++++ atproto/lexicon/mimetype_test.go | 21 +++++++++ 5 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 atproto/lexicon/mimetype.go create mode 100644 atproto/lexicon/mimetype_test.go diff --git a/atproto/lexicon/interop_record_test.go b/atproto/lexicon/interop_record_test.go index 8a2282ff8..f2bd14d44 100644 --- a/atproto/lexicon/interop_record_test.go +++ b/atproto/lexicon/interop_record_test.go @@ -83,7 +83,10 @@ func TestInteropRecordInvalid(t *testing.T) { if err != nil { t.Fatal(err) } - - assert.Error(cat.ValidateRecord(d, "example.lexicon.record")) + err = cat.ValidateRecord(d, "example.lexicon.record") + if err == nil { + fmt.Println(" FAIL") + } + assert.Error(err) } } diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 7f10e54fe..eeed9b1eb 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -8,6 +8,8 @@ import ( "github.com/bluesky-social/indigo/atproto/data" "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/rivo/uniseg" ) // Serialization helper for top-level Lexicon schema JSON objects (files) @@ -419,13 +421,24 @@ func (s *SchemaInteger) Validate(d any) error { return fmt.Errorf("expected an integer") } v := int(v64) - // TODO: enforce enum if s.Const != nil && v != *s.Const { return fmt.Errorf("integer val didn't match constant (%d): %d", *s.Const, v) } if (s.Minimum != nil && v < *s.Minimum) || (s.Maximum != nil && v > *s.Maximum) { return fmt.Errorf("integer val outside specified range: %d", v) } + if len(s.Enum) != 0 { + inEnum := false + for _, e := range s.Enum { + if e == v { + inEnum = true + break + } + } + if !inEnum { + return fmt.Errorf("integer val not in required enum: %d", v) + } + } return nil } @@ -476,7 +489,6 @@ func (s *SchemaString) Validate(d any) error { if !ok { return fmt.Errorf("expected a string: %v", reflect.TypeOf(d)) } - // TODO: enforce enum if s.Const != nil && v != *s.Const { return fmt.Errorf("string val didn't match constant (%s): %s", *s.Const, v) } @@ -484,7 +496,24 @@ func (s *SchemaString) Validate(d any) error { if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) { return fmt.Errorf("string length outside specified range: %d", len(v)) } - // TODO: grapheme length + if len(s.Enum) != 0 { + inEnum := false + for _, e := range s.Enum { + if e == v { + inEnum = true + break + } + } + if !inEnum { + return fmt.Errorf("string val not in required enum: %s", v) + } + } + if s.MinGraphemes != nil || s.MaxGraphemes != nil { + lenG := uniseg.GraphemeClusterCount(v) + if (s.MinGraphemes != nil && lenG < *s.MinGraphemes) || (s.MaxGraphemes != nil && lenG > *s.MaxGraphemes) { + return fmt.Errorf("string length (graphemes) outside specified range: %d", lenG) + } + } if s.Format != nil { switch *s.Format { case "at-identifier": @@ -656,7 +685,18 @@ func (s *SchemaBlob) Validate(d any) error { if !ok { return fmt.Errorf("expected a blob") } - // TODO: validate accept mimetype + if len(s.Accept) > 0 { + typeOk := false + for _, pat := range s.Accept { + if acceptableMimeType(pat, v.MimeType) { + typeOk = true + break + } + } + if !typeOk { + return fmt.Errorf("blob mimetype doesn't match accepted: %s", v.MimeType) + } + } if s.MaxSize != nil && int(v.Size) > *s.MaxSize { return fmt.Errorf("blob size too large: %d", v.Size) } @@ -702,6 +742,7 @@ func (s *SchemaParams) CheckSchema() error { return nil } +// XXX: implementation? func (s *SchemaParams) Validate(d any) error { return nil } @@ -709,12 +750,28 @@ func (s *SchemaParams) Validate(d any) error { type SchemaToken struct { Type string `json:"type,const=token"` Description *string `json:"description,omitempty"` + // the fully-qualified identifier of this token + Name string } func (s *SchemaToken) CheckSchema() error { return nil } +func (s *SchemaToken) Validate(d any) error { + str, ok := d.(string) + if !ok { + return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) + } + if s.Name == "" { + return fmt.Errorf("token name was not populated at parse time") + } + if str != s.Name { + return fmt.Errorf("token name did not match expected: %s", str) + } + return nil +} + type SchemaRef struct { Type string `json:"type,const=ref"` Description *string `json:"description,omitempty"` diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index bee5cbc3a..02be9994d 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -61,6 +61,10 @@ func (c *Catalog) AddSchemaFile(sf SchemaFile) error { if frag != "main" { return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) } + case SchemaToken: + token := def.Inner.(SchemaToken) + token.Name = name + def.Inner = token } s := Schema{ ID: name, @@ -169,18 +173,11 @@ func (c *Catalog) validateData(def any, d any) error { } return c.validateData(next.Def, d) case SchemaUnion: - //return fmt.Errorf("XXX: union validation not implemented") - return nil + return c.validateUnion(v, d) case SchemaUnknown: return v.Validate(d) case SchemaToken: - str, ok := d.(string) - if !ok { - return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) - } - // XXX: token validation not implemented - _ = str - return nil + return v.Validate(d) default: return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v)) } @@ -218,3 +215,22 @@ func (c *Catalog) validateArray(s SchemaArray, arr []any) error { } return nil } + +func (c *Catalog) validateUnion(s SchemaUnion, d any) error { + closed := s.Closed != nil && *s.Closed == true + for _, ref := range s.Refs { + def, err := c.Resolve(ref) + if err != nil { + // TODO: how to actually handle unknown defs? + return err + } + if err = c.validateData(def.Def, d); nil == err { // if success + return nil + } + } + if closed { + return fmt.Errorf("data did not match any variant of closed union") + } + // TODO: anything matches if an open union? + return nil +} diff --git a/atproto/lexicon/mimetype.go b/atproto/lexicon/mimetype.go new file mode 100644 index 000000000..0038003c7 --- /dev/null +++ b/atproto/lexicon/mimetype.go @@ -0,0 +1,19 @@ +package lexicon + +import ( + "strings" +) + +// checks if val matches pattern, with optional trailing glob on pattern. case-sensitive. +func acceptableMimeType(pattern, val string) bool { + if val == "" || pattern == "" { + return false + } + if strings.HasSuffix(pattern, "*") { + prefix := pattern[:len(pattern)-1] + return strings.HasPrefix(val, prefix) + } else { + return pattern == val + } + return false +} diff --git a/atproto/lexicon/mimetype_test.go b/atproto/lexicon/mimetype_test.go new file mode 100644 index 000000000..db2be81d5 --- /dev/null +++ b/atproto/lexicon/mimetype_test.go @@ -0,0 +1,21 @@ +package lexicon + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAcceptableMimeType(t *testing.T) { + assert := assert.New(t) + + assert.True(acceptableMimeType("image/*", "image/png")) + assert.True(acceptableMimeType("text/plain", "text/plain")) + + assert.False(acceptableMimeType("image/*", "text/plain")) + assert.False(acceptableMimeType("text/plain", "image/png")) + assert.False(acceptableMimeType("text/plain", "")) + assert.False(acceptableMimeType("", "text/plain")) + + // TODO: application/json, application/json+thing +} From f35a69d2c7bf5a2c52baadfa391fa765291d1261 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 28 Feb 2024 20:33:07 -0800 Subject: [PATCH 006/111] lexicons: real grapheme test string --- atproto/lexicon/testdata/record-data-valid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json index a5c4c2f00..8c8fd78fc 100644 --- a/atproto/lexicon/testdata/record-data-valid.json +++ b/atproto/lexicon/testdata/record-data-valid.json @@ -62,7 +62,7 @@ "enumInteger": 16, "rangeInteger": 16, "lenString": "1234567890ABC", - "graphemeString": "abcde", + "graphemeString": "🇩🇪🏳️‍🌈🇩🇪🏳️‍🌈🇩🇪🏳️‍🌈🇩🇪🏳️‍🌈🇩🇪🏳️‍🌈🇩🇪🏳️‍🌈", "enumString": "fish", "knownString": "blue", "sizeBytes": { From 7f80d1a1456398be16f2a8e139c140c0e5fd0868 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 28 Feb 2024 20:33:25 -0800 Subject: [PATCH 007/111] lexicon: enumString actually an enum --- atproto/lexicon/testdata/catalog/record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/lexicon/testdata/catalog/record.json b/atproto/lexicon/testdata/catalog/record.json index 11351fd40..dcf3b7b0e 100644 --- a/atproto/lexicon/testdata/catalog/record.json +++ b/atproto/lexicon/testdata/catalog/record.json @@ -106,7 +106,7 @@ }, "enumString": { "type": "string", - "knownValues": ["fish", "tree", "rock"] + "enum": ["fish", "tree", "rock"] }, "knownString": { "type": "string", From 9928fd1fac1e8bb5c138749f05c3357a832b0831 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 28 Feb 2024 20:33:44 -0800 Subject: [PATCH 008/111] lexicon: many more invalid test cases --- .../lexicon/testdata/record-data-invalid.json | 159 +++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json index 05c8f514a..e5b90d2a2 100644 --- a/atproto/lexicon/testdata/record-data-invalid.json +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -3,9 +3,68 @@ "rkey": "demo", "data": { "$type": "example.lexicon.record" } }, + { "name": "invalid null field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "null": true } }, + { "name": "invalid boolean field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "boolean": "green"} }, + { "name": "invalid integer field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "integer": "green"} }, + { "name": "invalid non-nullable string field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "string": null } }, { "name": "invalid string field", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "string": 2 } }, + { "name": "invalid bytes field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "bytes": "green" } }, + { "name": "invalid bytes: empty object", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "bytes": {}}}, + { "name": "invalid bytes: wrong type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "bytes": { + "bytes": "asdfasdfasdfasdf" + }}}, + { "name": "invalid cid-link field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "cid-link": "green" } }, + { "name": "invalid blob field", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "blob": "green" } }, + { "name": "invalid blob: wrong type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "bytes": { + "type": "blob", + "size": 123, + "mimeType": false, + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }}}, + { "name": "invalid array", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "array": 123 } + }, + { "name": "invalid array element", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "array": [true, false] } + }, + { "name": "invalid object", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "object": 123 } + }, + { "name": "invalid token ref type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "ref": 123 } + }, + { "name": "invalid ref value", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "ref": "example.lexicon.record#wrongToken" } + }, { "name": "invalid string format handle", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "handle": "123" } } @@ -14,8 +73,104 @@ "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "did": "123" } } }, - { "name": "invalid array element", + { "name": "invalid string format atidentifier", "rkey": "demo", - "data": { "$type": "example.lexicon.record", "integer": 1, "array": [true, false] } + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "atidentifier": "123" } } + }, + { "name": "invalid string format nsid", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "nsid": "123" } } + }, + { "name": "invalid string format aturi", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "aturi": "123" } } + }, + { "name": "invalid string format cid", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "cid": "123" } } + }, + { "name": "invalid string format datetime", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "datetime": "123" } } + }, + { "name": "invalid string format language", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "language": "123" } } + }, + { "name": "invalid string format uri", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "uri": "123" } } + }, + { "name": "wrong const value", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "constInteger": 41 } + }, + { "name": "integer not in enum", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "enumInteger": 7 } + }, + { "name": "out of integer range", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "rangeInteger": 9000 } + }, + { "name": "string too short", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "lenString": "." } + }, + { "name": "string too long", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "lenString": "abcdefg-abcdefg-abcdefg" } + }, + { "name": "string too short (graphemes)", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "graphemeString": "👩‍👩‍👦‍👦👩‍👩‍👦‍👦" } + }, + { "name": "string too long (graphemes)", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "graphemeString": "abcdefg-abcdefg-abcdefg" } + }, + { "name": "out of enum string", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "enumString": "unexpected" } + }, + { "name": "bytes too short", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "sizeBytes": { "$bytes": "b25l" }} + }, + { "name": "bytes too long", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "sizeBytes": { "$bytes": "b25lb25lb25lb25lb25lb25lb25lb25lb25lb25lb25l" }} + }, + { "name": "array too short", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "lenArray": [0]} + }, + { "name": "array too long", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "lenArray": [0,0,0,0,0,0,0,0,0,0]} + }, + { "name": "blob too large", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "sizeBlob": { + "$type": "blob", + "size": 12345, + "mimeType": "text/plain", + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }}}, + { "name": "blob wrong type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "acceptBlob": { + "$type": "blob", + "size": 12345, + "mimeType": "text/plain", + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }}}, + { "name": "out of closed union", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "closedUnion": "other" } } ] From 4927f7c25ef7eeab9c7195dc624680dcc149af1f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 28 Feb 2024 21:05:59 -0800 Subject: [PATCH 009/111] more progress --- atproto/lexicon/language.go | 11 +++-------- atproto/lexicon/lexicon.go | 14 +++++++------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index eeed9b1eb..7963045b1 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -742,16 +742,11 @@ func (s *SchemaParams) CheckSchema() error { return nil } -// XXX: implementation? -func (s *SchemaParams) Validate(d any) error { - return nil -} - type SchemaToken struct { Type string `json:"type,const=token"` Description *string `json:"description,omitempty"` // the fully-qualified identifier of this token - Name string + fullName string } func (s *SchemaToken) CheckSchema() error { @@ -763,10 +758,10 @@ func (s *SchemaToken) Validate(d any) error { if !ok { return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) } - if s.Name == "" { + if s.fullName == "" { return fmt.Errorf("token name was not populated at parse time") } - if str != s.Name { + if str != s.fullName { return fmt.Errorf("token name did not match expected: %s", str) } return nil diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 02be9994d..c35662007 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -30,6 +30,9 @@ type Schema struct { } func (c *Catalog) Resolve(name string) (*Schema, error) { + if name == "" { + return nil, fmt.Errorf("tried to resolve empty string name") + } // default to #main if name doesn't have a fragment if !strings.Contains(name, "#") { name = name + "#main" @@ -56,15 +59,15 @@ func (c *Catalog) AddSchemaFile(sf SchemaFile) error { return err } // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." - switch def.Inner.(type) { + switch s := def.Inner.(type) { case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: if frag != "main" { return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) } case SchemaToken: - token := def.Inner.(SchemaToken) - token.Name = name - def.Inner = token + // add fully-qualified name to token + s.fullName = name + def.Inner = s } s := Schema{ ID: name, @@ -162,10 +165,7 @@ func (c *Catalog) validateData(def any, d any) error { return c.validateObject(v, obj) case SchemaBlob: return v.Validate(d) - case SchemaParams: - return v.Validate(d) case SchemaRef: - // XXX: relative refs (in-file) // recurse next, err := c.Resolve(v.Ref) if err != nil { From 92c10baee2e91547b5bbb352b412f5c69d71f654 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 29 Feb 2024 00:51:17 -0800 Subject: [PATCH 010/111] lextool: start on network helpers --- atproto/lexicon/cmd/lextool/main.go | 10 ++++ atproto/lexicon/cmd/lextool/net.go | 92 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 atproto/lexicon/cmd/lextool/net.go diff --git a/atproto/lexicon/cmd/lextool/main.go b/atproto/lexicon/cmd/lextool/main.go index b5652b40c..88bee2389 100644 --- a/atproto/lexicon/cmd/lextool/main.go +++ b/atproto/lexicon/cmd/lextool/main.go @@ -28,6 +28,16 @@ func main() { Usage: "try recursively loading all the schemas from a directory", Action: runLoadDirectory, }, + &cli.Command{ + Name: "validate-record", + Usage: "fetch from network, validate against catalog", + Action: runValidateRecord, + }, + &cli.Command{ + Name: "validate-firehose", + Usage: "subscribe to a firehose, validate every known record against catalog", + Action: runValidateFirehose, + }, } h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) slog.SetDefault(slog.New(h)) diff --git a/atproto/lexicon/cmd/lextool/net.go b/atproto/lexicon/cmd/lextool/net.go new file mode 100644 index 000000000..1ea2dfbb3 --- /dev/null +++ b/atproto/lexicon/cmd/lextool/net.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/bluesky-social/indigo/atproto/data" + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/lexicon" + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/urfave/cli/v2" +) + +func runValidateRecord(cctx *cli.Context) error { + ctx := context.Background() + args := cctx.Args().Slice() + if len(args) != 2 { + return fmt.Errorf("expected two args (catalog path and AT-URI)") + } + p := args[0] + if p == "" { + return fmt.Errorf("need to provide directory path as an argument") + } + + c := lexicon.NewCatalog() + err := c.LoadDirectory(p) + if err != nil { + return err + } + + aturi, err := syntax.ParseATURI(args[1]) + if err != nil { + return err + } + if aturi.RecordKey() == "" { + return fmt.Errorf("need a full, not partial, AT-URI: %s", aturi) + } + dir := identity.DefaultDirectory() + ident, err := dir.Lookup(ctx, aturi.Authority()) + if err != nil { + return fmt.Errorf("resolving AT-URI authority: %v", err) + } + pdsURL := ident.PDSEndpoint() + if pdsURL == "" { + return fmt.Errorf("could not resolve PDS endpoint for AT-URI account: %s", ident.DID.String()) + } + + slog.Info("fetching record", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String()) + url := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s", + pdsURL, ident.DID, aturi.Collection(), aturi.RecordKey()) + resp, err := http.Get(url) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("fetch failed") + } + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + body, err := data.UnmarshalJSON(respBytes) + record := body["value"].(map[string]any) + + slog.Info("validating", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String()) + err = c.ValidateRecord(record, aturi.Collection().String()) + if err != nil { + return err + } + fmt.Println("success!") + return nil +} + +func runValidateFirehose(cctx *cli.Context) error { + p := cctx.Args().First() + if p == "" { + return fmt.Errorf("need to provide directory path as an argument") + } + + c := lexicon.NewCatalog() + err := c.LoadDirectory(p) + if err != nil { + return err + } + + return fmt.Errorf("UNIMPLEMENTED") +} From e22c44065952f1149ab237f44d6f547165eec6b7 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 29 Feb 2024 00:52:09 -0800 Subject: [PATCH 011/111] lexicon fixes --- atproto/lexicon/language.go | 88 +++++++++++++++++++++++++++++++++++++ atproto/lexicon/lexicon.go | 11 ++--- atproto/lexicon/mimetype.go | 1 - 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 7963045b1..96c889ecf 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -69,6 +69,81 @@ func (s *SchemaDef) CheckSchema() error { } } +// Helper to recurse down the definition tree and set full references on any sub-schemas which need to embed that metadata +func (s *SchemaDef) SetBase(base string) { + switch v := s.Inner.(type) { + case SchemaRecord: + for i, val := range v.Record.Properties { + val.SetBase(base) + v.Record.Properties[i] = val + } + s.Inner = v + case SchemaQuery: + for i, val := range v.Parameters.Properties { + val.SetBase(base) + v.Parameters.Properties[i] = val + } + if v.Output != nil && v.Output.Schema != nil { + v.Output.Schema.SetBase(base) + } + s.Inner = v + case SchemaProcedure: + for i, val := range v.Parameters.Properties { + val.SetBase(base) + v.Parameters.Properties[i] = val + } + if v.Input != nil && v.Input.Schema != nil { + v.Input.Schema.SetBase(base) + } + if v.Output != nil && v.Output.Schema != nil { + v.Output.Schema.SetBase(base) + } + s.Inner = v + case SchemaSubscription: + for i, val := range v.Parameters.Properties { + val.SetBase(base) + v.Parameters.Properties[i] = val + } + if v.Message != nil { + v.Message.Schema.SetBase(base) + } + s.Inner = v + case SchemaArray: + v.Items.SetBase(base) + s.Inner = v + case SchemaObject: + for i, val := range v.Properties { + val.SetBase(base) + v.Properties[i] = val + } + s.Inner = v + case SchemaParams: + for i, val := range v.Properties { + val.SetBase(base) + v.Properties[i] = val + } + s.Inner = v + case SchemaRef: + // add fully-qualified name + if strings.HasPrefix(v.Ref, "#") { + v.fullRef = base + v.Ref + } else { + v.fullRef = v.Ref + } + s.Inner = v + case SchemaUnion: + // add fully-qualified name + for _, ref := range v.Refs { + if strings.HasPrefix(ref, "#") { + ref = base + ref + } + v.fullRefs = append(v.fullRefs, ref) + } + s.Inner = v + } + return +} + func (s SchemaDef) MarshalJSON() ([]byte, error) { return json.Marshal(s.Inner) } @@ -750,6 +825,9 @@ type SchemaToken struct { } func (s *SchemaToken) CheckSchema() error { + if s.fullName == "" { + return fmt.Errorf("expected fully-qualified token name") + } return nil } @@ -771,6 +849,8 @@ type SchemaRef struct { Type string `json:"type,const=ref"` Description *string `json:"description,omitempty"` Ref string `json:"ref"` + // full path of reference + fullRef string } func (s *SchemaRef) CheckSchema() error { @@ -778,6 +858,9 @@ func (s *SchemaRef) CheckSchema() error { if len(s.Ref) == 0 { return fmt.Errorf("empty schema ref") } + if len(s.fullRef) == 0 { + return fmt.Errorf("empty full schema ref") + } return nil } @@ -786,6 +869,8 @@ type SchemaUnion struct { Description *string `json:"description,omitempty"` Refs []string `json:"refs"` Closed *bool `json:"closed,omitempty"` + // fully qualified + fullRefs []string } func (s *SchemaUnion) CheckSchema() error { @@ -796,6 +881,9 @@ func (s *SchemaUnion) CheckSchema() error { return fmt.Errorf("empty schema ref") } } + if len(s.fullRefs) != len(s.Refs) { + return fmt.Errorf("union refs were not expanded") + } return nil } diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index c35662007..329060a5c 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -55,9 +55,6 @@ func (c *Catalog) AddSchemaFile(sf SchemaFile) error { if _, ok := c.Schemas[name]; ok { return fmt.Errorf("catalog already contained a schema with name: %s", name) } - if err := def.CheckSchema(); err != nil { - return err - } // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." switch s := def.Inner.(type) { case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: @@ -69,6 +66,10 @@ func (c *Catalog) AddSchemaFile(sf SchemaFile) error { s.fullName = name def.Inner = s } + def.SetBase(base) + if err := def.CheckSchema(); err != nil { + return err + } s := Schema{ ID: name, Revision: sf.Revision, @@ -167,7 +168,7 @@ func (c *Catalog) validateData(def any, d any) error { return v.Validate(d) case SchemaRef: // recurse - next, err := c.Resolve(v.Ref) + next, err := c.Resolve(v.fullRef) if err != nil { return err } @@ -218,7 +219,7 @@ func (c *Catalog) validateArray(s SchemaArray, arr []any) error { func (c *Catalog) validateUnion(s SchemaUnion, d any) error { closed := s.Closed != nil && *s.Closed == true - for _, ref := range s.Refs { + for _, ref := range s.fullRefs { def, err := c.Resolve(ref) if err != nil { // TODO: how to actually handle unknown defs? diff --git a/atproto/lexicon/mimetype.go b/atproto/lexicon/mimetype.go index 0038003c7..0e42d5edb 100644 --- a/atproto/lexicon/mimetype.go +++ b/atproto/lexicon/mimetype.go @@ -15,5 +15,4 @@ func acceptableMimeType(pattern, val string) bool { } else { return pattern == val } - return false } From e1a8f3a74cdf2a2368860d65d982f9c17d6770eb Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 19 Mar 2024 22:39:57 -0700 Subject: [PATCH 012/111] refactor catalog and methods --- atproto/lexicon/catalog.go | 112 +++++++++++++++++++ atproto/lexicon/cmd/lextool/main.go | 2 +- atproto/lexicon/cmd/lextool/net.go | 10 +- atproto/lexicon/interop_record_test.go | 8 +- atproto/lexicon/lexicon.go | 144 ++++--------------------- atproto/lexicon/lexicon_test.go | 8 +- 6 files changed, 146 insertions(+), 138 deletions(-) create mode 100644 atproto/lexicon/catalog.go diff --git a/atproto/lexicon/catalog.go b/atproto/lexicon/catalog.go new file mode 100644 index 000000000..22b4004e2 --- /dev/null +++ b/atproto/lexicon/catalog.go @@ -0,0 +1,112 @@ +package lexicon + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// An aggregation of lexicon schemas, and methods for validating generic data against those schemas. +type Catalog interface { + Resolve(ref string) (*Schema, error) +} + +type BaseCatalog struct { + schemas map[string]Schema +} + +func NewBaseCatalog() BaseCatalog { + return BaseCatalog{ + schemas: make(map[string]Schema), + } +} + +func (c *BaseCatalog) Resolve(ref string) (*Schema, error) { + if ref == "" { + return nil, fmt.Errorf("tried to resolve empty string name") + } + // default to #main if name doesn't have a fragment + if !strings.Contains(ref, "#") { + ref = ref + "#main" + } + s, ok := c.schemas[ref] + if !ok { + return nil, fmt.Errorf("schema not found in catalog: %s", ref) + } + return &s, nil +} + +func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error { + base := sf.ID + for frag, def := range sf.Defs { + if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { + // TODO: more validation here? + return fmt.Errorf("schema name invalid: %s", frag) + } + name := base + "#" + frag + if _, ok := c.schemas[name]; ok { + return fmt.Errorf("catalog already contained a schema with name: %s", name) + } + // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." + switch s := def.Inner.(type) { + case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: + if frag != "main" { + return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) + } + case SchemaToken: + // add fully-qualified name to token + s.fullName = name + def.Inner = s + } + def.SetBase(base) + if err := def.CheckSchema(); err != nil { + return err + } + s := Schema{ + ID: name, + Revision: sf.Revision, + Def: def.Inner, + } + c.schemas[name] = s + } + return nil +} + +func (c *BaseCatalog) LoadDirectory(dirPath string) error { + return filepath.WalkDir(dirPath, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if !strings.HasSuffix(p, ".json") { + return nil + } + // TODO: logging + fmt.Println(p) + f, err := os.Open(p) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + var sf SchemaFile + if err = json.Unmarshal(b, &sf); err != nil { + return err + } + if err = c.AddSchemaFile(sf); err != nil { + return err + } + return nil + }) +} diff --git a/atproto/lexicon/cmd/lextool/main.go b/atproto/lexicon/cmd/lextool/main.go index 88bee2389..85f379360 100644 --- a/atproto/lexicon/cmd/lextool/main.go +++ b/atproto/lexicon/cmd/lextool/main.go @@ -79,7 +79,7 @@ func runLoadDirectory(cctx *cli.Context) error { return fmt.Errorf("need to provide directory path as an argument") } - c := lexicon.NewCatalog() + c := lexicon.NewBaseCatalog() err := c.LoadDirectory(p) if err != nil { return err diff --git a/atproto/lexicon/cmd/lextool/net.go b/atproto/lexicon/cmd/lextool/net.go index 1ea2dfbb3..92525458e 100644 --- a/atproto/lexicon/cmd/lextool/net.go +++ b/atproto/lexicon/cmd/lextool/net.go @@ -26,8 +26,8 @@ func runValidateRecord(cctx *cli.Context) error { return fmt.Errorf("need to provide directory path as an argument") } - c := lexicon.NewCatalog() - err := c.LoadDirectory(p) + cat := lexicon.NewBaseCatalog() + err := cat.LoadDirectory(p) if err != nil { return err } @@ -68,7 +68,7 @@ func runValidateRecord(cctx *cli.Context) error { record := body["value"].(map[string]any) slog.Info("validating", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String()) - err = c.ValidateRecord(record, aturi.Collection().String()) + err = lexicon.ValidateRecord(&cat, record, aturi.Collection().String()) if err != nil { return err } @@ -82,8 +82,8 @@ func runValidateFirehose(cctx *cli.Context) error { return fmt.Errorf("need to provide directory path as an argument") } - c := lexicon.NewCatalog() - err := c.LoadDirectory(p) + cat := lexicon.NewBaseCatalog() + err := cat.LoadDirectory(p) if err != nil { return err } diff --git a/atproto/lexicon/interop_record_test.go b/atproto/lexicon/interop_record_test.go index f2bd14d44..747f0c02a 100644 --- a/atproto/lexicon/interop_record_test.go +++ b/atproto/lexicon/interop_record_test.go @@ -21,7 +21,7 @@ type RecordFixture struct { func TestInteropRecordValid(t *testing.T) { assert := assert.New(t) - cat := NewCatalog() + cat := NewBaseCatalog() if err := cat.LoadDirectory("testdata/catalog"); err != nil { t.Fatal(err) } @@ -49,14 +49,14 @@ func TestInteropRecordValid(t *testing.T) { t.Fatal(err) } - assert.NoError(cat.ValidateRecord(d, "example.lexicon.record")) + assert.NoError(ValidateRecord(&cat, d, "example.lexicon.record")) } } func TestInteropRecordInvalid(t *testing.T) { assert := assert.New(t) - cat := NewCatalog() + cat := NewBaseCatalog() if err := cat.LoadDirectory("testdata/catalog"); err != nil { t.Fatal(err) } @@ -83,7 +83,7 @@ func TestInteropRecordInvalid(t *testing.T) { if err != nil { t.Fatal(err) } - err = cat.ValidateRecord(d, "example.lexicon.record") + err = ValidateRecord(&cat, d, "example.lexicon.record") if err == nil { fmt.Println(" FAIL") } diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 329060a5c..6a48d1cb4 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -1,143 +1,37 @@ package lexicon import ( - "encoding/json" "fmt" - "io" - "io/fs" - "os" - "path/filepath" "reflect" - "strings" ) -// An aggregation of lexicon schemas, and methods for validating generic data against those schemas. -type Catalog struct { - // TODO: not safe zero value; hide this field? seems aggressive - Schemas map[string]Schema -} - -func NewCatalog() Catalog { - return Catalog{ - Schemas: make(map[string]Schema), - } -} - type Schema struct { ID string Revision *int Def any } -func (c *Catalog) Resolve(name string) (*Schema, error) { - if name == "" { - return nil, fmt.Errorf("tried to resolve empty string name") - } - // default to #main if name doesn't have a fragment - if !strings.Contains(name, "#") { - name = name + "#main" - } - s, ok := c.Schemas[name] - if !ok { - return nil, fmt.Errorf("schema not found in catalog: %s", name) - } - return &s, nil -} - -func (c *Catalog) AddSchemaFile(sf SchemaFile) error { - base := sf.ID - for frag, def := range sf.Defs { - if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { - // TODO: more validation here? - return fmt.Errorf("schema name invalid: %s", frag) - } - name := base + "#" + frag - if _, ok := c.Schemas[name]; ok { - return fmt.Errorf("catalog already contained a schema with name: %s", name) - } - // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." - switch s := def.Inner.(type) { - case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription: - if frag != "main" { - return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) - } - case SchemaToken: - // add fully-qualified name to token - s.fullName = name - def.Inner = s - } - def.SetBase(base) - if err := def.CheckSchema(); err != nil { - return err - } - s := Schema{ - ID: name, - Revision: sf.Revision, - Def: def.Inner, - } - c.Schemas[name] = s - } - return nil -} - -func (c *Catalog) LoadDirectory(dirPath string) error { - return filepath.WalkDir(dirPath, func(p string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - if !strings.HasSuffix(p, ".json") { - return nil - } - // TODO: logging - fmt.Println(p) - f, err := os.Open(p) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - - b, err := io.ReadAll(f) - if err != nil { - return err - } - - var sf SchemaFile - if err = json.Unmarshal(b, &sf); err != nil { - return err - } - if err = c.AddSchemaFile(sf); err != nil { - return err - } - return nil - }) -} - -// TODO: rkey? is nsid always known? -// TODO: nsid as syntax.NSID -func (c *Catalog) ValidateRecord(raw any, id string) error { - def, err := c.Resolve(id) +func ValidateRecord(cat Catalog, recordData any, ref string) error { + def, err := cat.Resolve(ref) if err != nil { return err } s, ok := def.Def.(SchemaRecord) if !ok { - return fmt.Errorf("schema is not of record type: %s", id) + return fmt.Errorf("schema is not of record type: %s", ref) } - d, ok := raw.(map[string]any) + d, ok := recordData.(map[string]any) if !ok { return fmt.Errorf("record data is not object type") } t, ok := d["$type"] - if !ok || t != id { + if !ok || t != ref { return fmt.Errorf("record data missing $type, or didn't match expected NSID") } - return c.validateObject(s.Record, d) + return validateObject(cat, s.Record, d) } -func (c *Catalog) validateData(def any, d any) error { +func validateData(cat Catalog, def any, d any) error { // TODO: switch v := def.(type) { case SchemaNull: @@ -157,24 +51,24 @@ func (c *Catalog) validateData(def any, d any) error { if !ok { return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d)) } - return c.validateArray(v, arr) + return validateArray(cat, v, arr) case SchemaObject: obj, ok := d.(map[string]any) if !ok { return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) } - return c.validateObject(v, obj) + return validateObject(cat, v, obj) case SchemaBlob: return v.Validate(d) case SchemaRef: // recurse - next, err := c.Resolve(v.fullRef) + next, err := cat.Resolve(v.fullRef) if err != nil { return err } - return c.validateData(next.Def, d) + return validateData(cat, next.Def, d) case SchemaUnion: - return c.validateUnion(v, d) + return validateUnion(cat, v, d) case SchemaUnknown: return v.Validate(d) case SchemaToken: @@ -184,7 +78,7 @@ func (c *Catalog) validateData(def any, d any) error { } } -func (c *Catalog) validateObject(s SchemaObject, d map[string]any) error { +func validateObject(cat Catalog, s SchemaObject, d map[string]any) error { for _, k := range s.Required { if _, ok := d[k]; !ok { return fmt.Errorf("required field missing: %s", k) @@ -195,7 +89,7 @@ func (c *Catalog) validateObject(s SchemaObject, d map[string]any) error { if v == nil && s.IsNullable(k) { continue } - err := c.validateData(def.Inner, v) + err := validateData(cat, def.Inner, v) if err != nil { return err } @@ -204,12 +98,12 @@ func (c *Catalog) validateObject(s SchemaObject, d map[string]any) error { return nil } -func (c *Catalog) validateArray(s SchemaArray, arr []any) error { +func validateArray(cat Catalog, s SchemaArray, arr []any) error { if (s.MinLength != nil && len(arr) < *s.MinLength) || (s.MaxLength != nil && len(arr) > *s.MaxLength) { return fmt.Errorf("array length out of bounds: %d", len(arr)) } for _, v := range arr { - err := c.validateData(s.Items.Inner, v) + err := validateData(cat, s.Items.Inner, v) if err != nil { return err } @@ -217,15 +111,15 @@ func (c *Catalog) validateArray(s SchemaArray, arr []any) error { return nil } -func (c *Catalog) validateUnion(s SchemaUnion, d any) error { +func validateUnion(cat Catalog, s SchemaUnion, d any) error { closed := s.Closed != nil && *s.Closed == true for _, ref := range s.fullRefs { - def, err := c.Resolve(ref) + def, err := cat.Resolve(ref) if err != nil { // TODO: how to actually handle unknown defs? return err } - if err = c.validateData(def.Def, d); nil == err { // if success + if err = validateData(cat, def.Def, d); nil == err { // if success return nil } } diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/lexicon_test.go index 81953e04a..eef75b04e 100644 --- a/atproto/lexicon/lexicon_test.go +++ b/atproto/lexicon/lexicon_test.go @@ -9,7 +9,7 @@ import ( func TestBasicCatalog(t *testing.T) { assert := assert.New(t) - cat := NewCatalog() + cat := NewBaseCatalog() if err := cat.LoadDirectory("testdata/catalog"); err != nil { t.Fatal(err) } @@ -18,7 +18,8 @@ func TestBasicCatalog(t *testing.T) { if err != nil { t.Fatal(err) } - assert.NoError(cat.validateData( + assert.NoError(validateData( + &cat, def.Def, map[string]any{ "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", @@ -30,7 +31,8 @@ func TestBasicCatalog(t *testing.T) { }, )) - assert.Error(cat.validateData( + assert.Error(validateData( + &cat, def.Def, map[string]any{ "cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", From 53c3e8332936120b8355313b966c82654a8e25c2 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 19 Mar 2024 23:03:20 -0700 Subject: [PATCH 013/111] data: support parsing legacy blobs --- atproto/data/parse.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/atproto/data/parse.go b/atproto/data/parse.go index 5924ff2a3..2f1f4305e 100644 --- a/atproto/data/parse.go +++ b/atproto/data/parse.go @@ -111,6 +111,18 @@ func parseMap(obj map[string]any) (any, error) { return nil, fmt.Errorf("$type field must contain a non-empty string") } } + // legacy blob type + if len(obj) == 2 { + if _, ok := obj["mimeType"]; ok { + if _, ok := obj["cid"]; ok { + b, err := parseLegacyBlob(obj) + if err != nil { + return nil, err + } + return *b, nil + } + } + } out := make(map[string]any, len(obj)) for k, val := range obj { if len(k) > MAX_OBJECT_KEY_LEN { @@ -213,6 +225,30 @@ func parseBlob(obj map[string]any) (*Blob, error) { }, nil } +func parseLegacyBlob(obj map[string]any) (*Blob, error) { + if len(obj) != 2 { + return nil, fmt.Errorf("legacy blobs expected to have 2 fields") + } + var err error + mimeType, ok := obj["mimeType"].(string) + if !ok { + return nil, fmt.Errorf("blob 'mimeType' missing or not a string") + } + cidStr, ok := obj["cid"] + if !ok { + return nil, fmt.Errorf("blob 'cid' missing") + } + c, err := cid.Parse(cidStr) + if err != nil { + return nil, fmt.Errorf("invalid CID: %w", err) + } + return &Blob{ + Size: -1, + MimeType: mimeType, + Ref: CIDLink(c), + }, nil +} + func parseObject(obj map[string]any) (map[string]any, error) { out, err := parseMap(obj) if err != nil { From 586731a33e7a8715b86a38f8827e902b22710d56 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 19 Mar 2024 23:03:54 -0700 Subject: [PATCH 014/111] lexicon: support lenient parsing (datetime and legacy blobs) --- atproto/lexicon/cmd/lextool/net.go | 7 ++++-- atproto/lexicon/language.go | 19 ++++++++++++--- atproto/lexicon/lexicon.go | 39 +++++++++++++++++++----------- atproto/lexicon/lexicon_test.go | 2 ++ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/atproto/lexicon/cmd/lextool/net.go b/atproto/lexicon/cmd/lextool/net.go index 92525458e..25fd9118a 100644 --- a/atproto/lexicon/cmd/lextool/net.go +++ b/atproto/lexicon/cmd/lextool/net.go @@ -65,10 +65,13 @@ func runValidateRecord(cctx *cli.Context) error { } body, err := data.UnmarshalJSON(respBytes) - record := body["value"].(map[string]any) + record, ok := body["value"].(map[string]any) + if !ok { + return fmt.Errorf("fetched record was not an object") + } slog.Info("validating", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String()) - err = lexicon.ValidateRecord(&cat, record, aturi.Collection().String()) + err = lexicon.ValidateRecordLenient(&cat, record, aturi.Collection().String()) if err != nil { return err } diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 96c889ecf..6f13597ea 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -559,7 +559,8 @@ func (s *SchemaString) CheckSchema() error { return nil } -func (s *SchemaString) Validate(d any) error { +// lenient mode is only for datetimes, and hopefully will be deprecated soon +func (s *SchemaString) Validate(d any, lenient bool) error { v, ok := d.(string) if !ok { return fmt.Errorf("expected a string: %v", reflect.TypeOf(d)) @@ -604,8 +605,14 @@ func (s *SchemaString) Validate(d any) error { return err } case "datetime": - if _, err := syntax.ParseDatetime(v); err != nil { - return err + if lenient { + if _, err := syntax.ParseDatetimeLenient(v); err != nil { + return err + } + } else { + if _, err := syntax.ParseDatetime(v); err != nil { + return err + } } case "did": if _, err := syntax.ParseDID(v); err != nil { @@ -755,11 +762,15 @@ func (s *SchemaBlob) CheckSchema() error { return nil } -func (s *SchemaBlob) Validate(d any) error { +// lenient flag allows legacy blobs (if true) +func (s *SchemaBlob) Validate(d any, lenient bool) error { v, ok := d.(data.Blob) if !ok { return fmt.Errorf("expected a blob") } + if !lenient && v.Size < 0 { + return fmt.Errorf("legacy blobs not allowed") + } if len(s.Accept) > 0 { typeOk := false for _, pat := range s.Accept { diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 6a48d1cb4..f53edaf41 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -12,6 +12,17 @@ type Schema struct { } func ValidateRecord(cat Catalog, recordData any, ref string) error { + return validateRecordConfig(cat, recordData, ref, false) +} + +// Variation of ValidateRecord which allows "legacy" blob format, and flexible string datetimes. +// +// Hope is to deprecate this lenient variation in the near future! +func ValidateRecordLenient(cat Catalog, recordData any, ref string) error { + return validateRecordConfig(cat, recordData, ref, true) +} + +func validateRecordConfig(cat Catalog, recordData any, ref string, lenient bool) error { def, err := cat.Resolve(ref) if err != nil { return err @@ -28,10 +39,10 @@ func ValidateRecord(cat Catalog, recordData any, ref string) error { if !ok || t != ref { return fmt.Errorf("record data missing $type, or didn't match expected NSID") } - return validateObject(cat, s.Record, d) + return validateObject(cat, s.Record, d, lenient) } -func validateData(cat Catalog, def any, d any) error { +func validateData(cat Catalog, def any, d any, lenient bool) error { // TODO: switch v := def.(type) { case SchemaNull: @@ -41,7 +52,7 @@ func validateData(cat Catalog, def any, d any) error { case SchemaInteger: return v.Validate(d) case SchemaString: - return v.Validate(d) + return v.Validate(d, lenient) case SchemaBytes: return v.Validate(d) case SchemaCIDLink: @@ -51,24 +62,24 @@ func validateData(cat Catalog, def any, d any) error { if !ok { return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d)) } - return validateArray(cat, v, arr) + return validateArray(cat, v, arr, lenient) case SchemaObject: obj, ok := d.(map[string]any) if !ok { return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) } - return validateObject(cat, v, obj) + return validateObject(cat, v, obj, lenient) case SchemaBlob: - return v.Validate(d) + return v.Validate(d, lenient) case SchemaRef: // recurse next, err := cat.Resolve(v.fullRef) if err != nil { return err } - return validateData(cat, next.Def, d) + return validateData(cat, next.Def, d, lenient) case SchemaUnion: - return validateUnion(cat, v, d) + return validateUnion(cat, v, d, lenient) case SchemaUnknown: return v.Validate(d) case SchemaToken: @@ -78,7 +89,7 @@ func validateData(cat Catalog, def any, d any) error { } } -func validateObject(cat Catalog, s SchemaObject, d map[string]any) error { +func validateObject(cat Catalog, s SchemaObject, d map[string]any, lenient bool) error { for _, k := range s.Required { if _, ok := d[k]; !ok { return fmt.Errorf("required field missing: %s", k) @@ -89,7 +100,7 @@ func validateObject(cat Catalog, s SchemaObject, d map[string]any) error { if v == nil && s.IsNullable(k) { continue } - err := validateData(cat, def.Inner, v) + err := validateData(cat, def.Inner, v, lenient) if err != nil { return err } @@ -98,12 +109,12 @@ func validateObject(cat Catalog, s SchemaObject, d map[string]any) error { return nil } -func validateArray(cat Catalog, s SchemaArray, arr []any) error { +func validateArray(cat Catalog, s SchemaArray, arr []any, lenient bool) error { if (s.MinLength != nil && len(arr) < *s.MinLength) || (s.MaxLength != nil && len(arr) > *s.MaxLength) { return fmt.Errorf("array length out of bounds: %d", len(arr)) } for _, v := range arr { - err := validateData(cat, s.Items.Inner, v) + err := validateData(cat, s.Items.Inner, v, lenient) if err != nil { return err } @@ -111,7 +122,7 @@ func validateArray(cat Catalog, s SchemaArray, arr []any) error { return nil } -func validateUnion(cat Catalog, s SchemaUnion, d any) error { +func validateUnion(cat Catalog, s SchemaUnion, d any, lenient bool) error { closed := s.Closed != nil && *s.Closed == true for _, ref := range s.fullRefs { def, err := cat.Resolve(ref) @@ -119,7 +130,7 @@ func validateUnion(cat Catalog, s SchemaUnion, d any) error { // TODO: how to actually handle unknown defs? return err } - if err = validateData(cat, def.Def, d); nil == err { // if success + if err = validateData(cat, def.Def, d, lenient); nil == err { // if success return nil } } diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/lexicon_test.go index eef75b04e..3d12e40b8 100644 --- a/atproto/lexicon/lexicon_test.go +++ b/atproto/lexicon/lexicon_test.go @@ -29,6 +29,7 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, + false, )) assert.Error(validateData( @@ -41,5 +42,6 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, + false, )) } From f4adea5f0cebaf42e30ca9053e7859d79f35b04d Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 13 Aug 2024 20:48:48 -0700 Subject: [PATCH 015/111] atproto/identity: correct 'catalog' ref in pkg readme --- atproto/identity/doc.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/atproto/identity/doc.go b/atproto/identity/doc.go index 911b2811c..32bef3313 100644 --- a/atproto/identity/doc.go +++ b/atproto/identity/doc.go @@ -1,8 +1,6 @@ /* Package identity provides types and routines for resolving handles and DIDs from the network -The two main abstractions are a Catalog interface for identity service implementations, and an Identity structure which represents core identity information relevant to atproto. The Catalog interface can be nested, somewhat like HTTP middleware, to provide caching, observability, or other bespoke needs in more complex systems. - -Much of the implementation of this SDK is based on existing code in indigo:api/extra.go +The two main abstractions are a Directory interface for identity service implementations, and an Identity struct which represents core identity information relevant to atproto. The Directory interface can be nested, somewhat like HTTP middleware, to provide caching, observability, or other bespoke needs in more complex systems. */ package identity From cfbbce39d7f635ac6da0d42cc95a3d3cb351a1e2 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 13 Aug 2024 20:53:54 -0700 Subject: [PATCH 016/111] TID and record-key string format support --- atproto/lexicon/language.go | 10 +++++++++- atproto/lexicon/testdata/catalog/record.json | 10 ++++++++++ atproto/lexicon/testdata/record-data-invalid.json | 8 ++++++++ atproto/lexicon/testdata/record-data-valid.json | 3 ++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 6f13597ea..7ea46e34c 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -550,7 +550,7 @@ func (s *SchemaString) CheckSchema() error { } if s.Format != nil { switch *s.Format { - case "at-identifier", "at-uri", "cid", "datetime", "did", "handle", "nsid", "uri", "language": + case "at-identifier", "at-uri", "cid", "datetime", "did", "handle", "nsid", "uri", "language", "tid", "record-key": // pass default: return fmt.Errorf("unknown string format: %s", *s.Format) @@ -634,6 +634,14 @@ func (s *SchemaString) Validate(d any, lenient bool) error { if _, err := syntax.ParseLanguage(v); err != nil { return err } + case "tid": + if _, err := syntax.ParseTID(v); err != nil { + return err + } + case "record-key": + if _, err := syntax.ParseRecordKey(v); err != nil { + return err + } } } return nil diff --git a/atproto/lexicon/testdata/catalog/record.json b/atproto/lexicon/testdata/catalog/record.json index dcf3b7b0e..cb32a07ee 100644 --- a/atproto/lexicon/testdata/catalog/record.json +++ b/atproto/lexicon/testdata/catalog/record.json @@ -190,6 +190,16 @@ "type": "string", "format": "uri", "description": "a generic URI field" + }, + "tid": { + "type": "string", + "format": "tid", + "description": "a generic TID field" + }, + "recordkey": { + "type": "string", + "format": "record-key", + "description": "a generic record-key field" } } }, diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json index e5b90d2a2..623ca2395 100644 --- a/atproto/lexicon/testdata/record-data-invalid.json +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -101,6 +101,14 @@ "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "uri": "123" } } }, + { "name": "invalid string format tid", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "tid": "000" } } + }, + { "name": "invalid string format recordkey", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "formats": { "recordkey": "." } } + }, { "name": "wrong const value", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "constInteger": 41 } diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json index 8c8fd78fc..bbdd89b3d 100644 --- a/atproto/lexicon/testdata/record-data-valid.json +++ b/atproto/lexicon/testdata/record-data-valid.json @@ -55,7 +55,8 @@ "cid": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq", "datetime": "2023-10-30T22:25:23Z", "language": "en", - "uri": "https://example.com/file.txt" + "tid": "3kznmn7xqxl22", + "recordkey": "simple" }, "constInteger": 42, "defaultInteger": 123, From f19a2a447258dd3c6cdc6d64449a39283fac33e8 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 15 Aug 2024 23:46:11 -0700 Subject: [PATCH 017/111] switch to bitmask for validation config --- atproto/lexicon/cmd/lextool/net.go | 2 +- atproto/lexicon/interop_record_test.go | 4 +- atproto/lexicon/language.go | 10 ++--- atproto/lexicon/lexicon.go | 51 ++++++++++++++------------ atproto/lexicon/lexicon_test.go | 4 +- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/atproto/lexicon/cmd/lextool/net.go b/atproto/lexicon/cmd/lextool/net.go index 25fd9118a..1600aaee8 100644 --- a/atproto/lexicon/cmd/lextool/net.go +++ b/atproto/lexicon/cmd/lextool/net.go @@ -71,7 +71,7 @@ func runValidateRecord(cctx *cli.Context) error { } slog.Info("validating", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String()) - err = lexicon.ValidateRecordLenient(&cat, record, aturi.Collection().String()) + err = lexicon.ValidateRecord(&cat, record, aturi.Collection().String(), lexicon.LenientMode) if err != nil { return err } diff --git a/atproto/lexicon/interop_record_test.go b/atproto/lexicon/interop_record_test.go index 747f0c02a..cb6546998 100644 --- a/atproto/lexicon/interop_record_test.go +++ b/atproto/lexicon/interop_record_test.go @@ -49,7 +49,7 @@ func TestInteropRecordValid(t *testing.T) { t.Fatal(err) } - assert.NoError(ValidateRecord(&cat, d, "example.lexicon.record")) + assert.NoError(ValidateRecord(&cat, d, "example.lexicon.record", 0)) } } @@ -83,7 +83,7 @@ func TestInteropRecordInvalid(t *testing.T) { if err != nil { t.Fatal(err) } - err = ValidateRecord(&cat, d, "example.lexicon.record") + err = ValidateRecord(&cat, d, "example.lexicon.record", 0) if err == nil { fmt.Println(" FAIL") } diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 7ea46e34c..5f5325be9 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -559,8 +559,7 @@ func (s *SchemaString) CheckSchema() error { return nil } -// lenient mode is only for datetimes, and hopefully will be deprecated soon -func (s *SchemaString) Validate(d any, lenient bool) error { +func (s *SchemaString) Validate(d any, flags ValidateFlags) error { v, ok := d.(string) if !ok { return fmt.Errorf("expected a string: %v", reflect.TypeOf(d)) @@ -605,7 +604,7 @@ func (s *SchemaString) Validate(d any, lenient bool) error { return err } case "datetime": - if lenient { + if flags&AllowLenientDatetime != 0 { if _, err := syntax.ParseDatetimeLenient(v); err != nil { return err } @@ -770,13 +769,12 @@ func (s *SchemaBlob) CheckSchema() error { return nil } -// lenient flag allows legacy blobs (if true) -func (s *SchemaBlob) Validate(d any, lenient bool) error { +func (s *SchemaBlob) Validate(d any, flags ValidateFlags) error { v, ok := d.(data.Blob) if !ok { return fmt.Errorf("expected a blob") } - if !lenient && v.Size < 0 { + if !(flags&AllowLegacyBlob != 0) && v.Size < 0 { return fmt.Errorf("legacy blobs not allowed") } if len(s.Accept) > 0 { diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index f53edaf41..71b47f8f2 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -5,24 +5,27 @@ import ( "reflect" ) +type ValidateFlags int + +const ( + AllowLegacyBlob = 1 << iota + AllowLenientDatetime + StrictRecursiveValidation +) + +var LenientMode ValidateFlags = AllowLegacyBlob | AllowLenientDatetime + type Schema struct { ID string Revision *int Def any } -func ValidateRecord(cat Catalog, recordData any, ref string) error { - return validateRecordConfig(cat, recordData, ref, false) -} - -// Variation of ValidateRecord which allows "legacy" blob format, and flexible string datetimes. -// -// Hope is to deprecate this lenient variation in the near future! -func ValidateRecordLenient(cat Catalog, recordData any, ref string) error { - return validateRecordConfig(cat, recordData, ref, true) +func ValidateRecord(cat Catalog, recordData any, ref string, flags ValidateFlags) error { + return validateRecordConfig(cat, recordData, ref, flags) } -func validateRecordConfig(cat Catalog, recordData any, ref string, lenient bool) error { +func validateRecordConfig(cat Catalog, recordData any, ref string, flags ValidateFlags) error { def, err := cat.Resolve(ref) if err != nil { return err @@ -39,10 +42,10 @@ func validateRecordConfig(cat Catalog, recordData any, ref string, lenient bool) if !ok || t != ref { return fmt.Errorf("record data missing $type, or didn't match expected NSID") } - return validateObject(cat, s.Record, d, lenient) + return validateObject(cat, s.Record, d, flags) } -func validateData(cat Catalog, def any, d any, lenient bool) error { +func validateData(cat Catalog, def any, d any, flags ValidateFlags) error { // TODO: switch v := def.(type) { case SchemaNull: @@ -52,7 +55,7 @@ func validateData(cat Catalog, def any, d any, lenient bool) error { case SchemaInteger: return v.Validate(d) case SchemaString: - return v.Validate(d, lenient) + return v.Validate(d, flags) case SchemaBytes: return v.Validate(d) case SchemaCIDLink: @@ -62,24 +65,24 @@ func validateData(cat Catalog, def any, d any, lenient bool) error { if !ok { return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d)) } - return validateArray(cat, v, arr, lenient) + return validateArray(cat, v, arr, flags) case SchemaObject: obj, ok := d.(map[string]any) if !ok { return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d)) } - return validateObject(cat, v, obj, lenient) + return validateObject(cat, v, obj, flags) case SchemaBlob: - return v.Validate(d, lenient) + return v.Validate(d, flags) case SchemaRef: // recurse next, err := cat.Resolve(v.fullRef) if err != nil { return err } - return validateData(cat, next.Def, d, lenient) + return validateData(cat, next.Def, d, flags) case SchemaUnion: - return validateUnion(cat, v, d, lenient) + return validateUnion(cat, v, d, flags) case SchemaUnknown: return v.Validate(d) case SchemaToken: @@ -89,7 +92,7 @@ func validateData(cat Catalog, def any, d any, lenient bool) error { } } -func validateObject(cat Catalog, s SchemaObject, d map[string]any, lenient bool) error { +func validateObject(cat Catalog, s SchemaObject, d map[string]any, flags ValidateFlags) error { for _, k := range s.Required { if _, ok := d[k]; !ok { return fmt.Errorf("required field missing: %s", k) @@ -100,7 +103,7 @@ func validateObject(cat Catalog, s SchemaObject, d map[string]any, lenient bool) if v == nil && s.IsNullable(k) { continue } - err := validateData(cat, def.Inner, v, lenient) + err := validateData(cat, def.Inner, v, flags) if err != nil { return err } @@ -109,12 +112,12 @@ func validateObject(cat Catalog, s SchemaObject, d map[string]any, lenient bool) return nil } -func validateArray(cat Catalog, s SchemaArray, arr []any, lenient bool) error { +func validateArray(cat Catalog, s SchemaArray, arr []any, flags ValidateFlags) error { if (s.MinLength != nil && len(arr) < *s.MinLength) || (s.MaxLength != nil && len(arr) > *s.MaxLength) { return fmt.Errorf("array length out of bounds: %d", len(arr)) } for _, v := range arr { - err := validateData(cat, s.Items.Inner, v, lenient) + err := validateData(cat, s.Items.Inner, v, flags) if err != nil { return err } @@ -122,7 +125,7 @@ func validateArray(cat Catalog, s SchemaArray, arr []any, lenient bool) error { return nil } -func validateUnion(cat Catalog, s SchemaUnion, d any, lenient bool) error { +func validateUnion(cat Catalog, s SchemaUnion, d any, flags ValidateFlags) error { closed := s.Closed != nil && *s.Closed == true for _, ref := range s.fullRefs { def, err := cat.Resolve(ref) @@ -130,7 +133,7 @@ func validateUnion(cat Catalog, s SchemaUnion, d any, lenient bool) error { // TODO: how to actually handle unknown defs? return err } - if err = validateData(cat, def.Def, d, lenient); nil == err { // if success + if err = validateData(cat, def.Def, d, flags); nil == err { // if success return nil } } diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/lexicon_test.go index 3d12e40b8..ce5c490ec 100644 --- a/atproto/lexicon/lexicon_test.go +++ b/atproto/lexicon/lexicon_test.go @@ -29,7 +29,7 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, - false, + 0, )) assert.Error(validateData( @@ -42,6 +42,6 @@ func TestBasicCatalog(t *testing.T) { "uri": "at://did:plc:asdf123/com.atproto.feed.post/asdf123", "val": "test-label", }, - false, + 0, )) } From 415b0921e97292f09512f7d2c8ee673952fd0062 Mon Sep 17 00:00:00 2001 From: Jaz Volpert Date: Thu, 19 Sep 2024 22:55:53 +0000 Subject: [PATCH 018/111] Document sonar and proivde a dashboard --- cmd/sonar/README.md | 24 + cmd/sonar/grafana-dashboard.json | 1541 ++++++++++++++++++++++++++++++ cmd/sonar/main.go | 28 +- cmd/sonar/sonar_dash.png | Bin 0 -> 763925 bytes 4 files changed, 1581 insertions(+), 12 deletions(-) create mode 100644 cmd/sonar/README.md create mode 100644 cmd/sonar/grafana-dashboard.json create mode 100644 cmd/sonar/sonar_dash.png diff --git a/cmd/sonar/README.md b/cmd/sonar/README.md new file mode 100644 index 000000000..dbd8ccfda --- /dev/null +++ b/cmd/sonar/README.md @@ -0,0 +1,24 @@ +# Sonar +Sonar is an AT Proto Firehose Montioring tool + +Sonar connects to an AT Proto Firehose (either from a PDS or a Relay) following the semantics of `com.atproto.sync.subscribeRepos`. + +Sonar monitors the throughput of events, producing prometheus metrics on the frequency of different kinds of events. + +Sonar additionally walks through repo operations and tracks the frequency of creation/update/deletion of different record collections. + +Sonar's main use is to provide an operational dashboard of activity on the network, allowing us to view changes in event rate over time and understand what kinds of traffic flow through the firehose over time. + +## Running Sonar + +To run sonar in Docker locally, you can run: `make sonar-up` from the root of the `indigo` directory. + +This will start a sonar instance that connects to the Bluesky-operated Relay firehose at `bsky.network` and will expose metrics at `http://localhost:8345` + +Feel free to modify the `docker-compose.yml` in this directory to change any settings via environment variables i.e. to change the firehose host `SONAR_WS_URL` or the listen port `SONAR_PORT`. + +## Dashboard + +Sonar emits Prometheus metrics which you can scrape and then visualize with the Grafana dashboard (JSON template provided in this directory) shown below: + +![A dashboard for Sonar showing event throughput and distribution](./sonar_dash.png) diff --git a/cmd/sonar/grafana-dashboard.json b/cmd/sonar/grafana-dashboard.json new file mode 100644 index 000000000..2ce8c712e --- /dev/null +++ b/cmd/sonar/grafana-dashboard.json @@ -0,0 +1,1541 @@ +{ + "__inputs": [ + { + "name": "DS_MIMIR", + "label": "Mimir", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.4.1" + }, + { + "type": "panel", + "id": "piechart", + "name": "Pie chart", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 55, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1000 + }, + { + "color": "orange", + "value": 1200 + }, + { + "color": "red", + "value": 1500 + } + ] + }, + "unit": "cps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "repo_commit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "identity" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "repo_tombstone" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "avg(rate(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval])) by (event_type)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Event Throughput", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1000 + }, + { + "color": "orange", + "value": 1200 + }, + { + "color": "red", + "value": 1500 + } + ] + }, + "unit": "cps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1 Day Ago" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.lineStyle", + "value": { + "fill": "solid" + } + }, + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "1 Week Ago" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "4 Weeks Ago" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Current", + "1 Day Ago" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 12, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval]))", + "legendFormat": "Current", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval] offset 1d))", + "hide": false, + "instant": false, + "legendFormat": "1 Day Ago", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval] offset 1w))", + "hide": false, + "instant": false, + "legendFormat": "1 Week Ago", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval] offset 4w))", + "hide": false, + "instant": false, + "legendFormat": "4 Weeks Ago", + "range": true, + "refId": "D" + } + ], + "title": "Total Event Throughput", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 55, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1000 + }, + { + "color": "orange", + "value": 1200 + }, + { + "color": "red", + "value": 1500 + } + ] + }, + "unit": "cps" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "avg(rate(sonar_records_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval])) by (record_type)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Record Throughput", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 15000 + }, + { + "color": "orange", + "value": 60000 + }, + { + "color": "red", + "value": 300000 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Since Event Creation" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 12, + "y": 9 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sonar_last_evt_created_evt_processed_gap{box=~\"${box}\",socket_url=~\"${socket}\"}", + "legendFormat": "Since Event Creation", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sonar_last_record_created_evt_processed_gap{box=~\"${box}\",socket_url=~\"${socket}\"}", + "hide": false, + "legendFormat": "Since Record Creation", + "range": true, + "refId": "B" + } + ], + "title": "Firehose Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [], + "unit": "locale" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 20, + "y": 9 + }, + "id": 9, + "options": { + "displayLabels": [ + "name" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "avg(increase(sonar_ops_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"}[$__range])) by (kind)", + "format": "time_series", + "instant": false, + "legendFormat": "{{kind}}", + "range": true, + "refId": "A" + } + ], + "title": "Operation Kind Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 17 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "rate(process_cpu_seconds_total{job=\"sonar\", box=~\"${box}\",instance=~\"${instance}\"}[$__rate_interval])", + "instant": false, + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [], + "unit": "locale" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 17 + }, + "id": 7, + "options": { + "displayLabels": [ + "name" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "avg(increase(sonar_records_processed_total{box=~\"${box}\",socket_url=~\"${socket}\", action=\"create\"}[$__range])) by (record_type)", + "format": "time_series", + "instant": false, + "legendFormat": "{{record_type}}", + "range": true, + "refId": "A" + } + ], + "title": "Record Distribution", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binbps" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(indigo_repo_stream_bytes_total{box=~\"${box}\",instance=~\"${instance}\"}[$__rate_interval])) by (instance) * 8", + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Socket Bandwidth", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 25 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "process_resident_memory_bytes{job=\"sonar\", box=~\"${box}\",instance=~\"${instance}\"}", + "instant": false, + "legendFormat": "{{instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 26 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "sum(rate(process_cpu_seconds_total{job=\"sonar\", box=~\"${box}\",instance=~\"${instance}\"}[$__rate_interval])) by (instance) / sum(rate(sonar_events_processed_total{box=~\"${box}\",instance=~\"${instance}\"}[$__rate_interval])) by (instance)", + "format": "time_series", + "instant": false, + "interval": "", + "legendFormat": "CPU Seconds per Event", + "range": true, + "refId": "A" + } + ], + "title": "CPU per Event", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 500000 + }, + { + "color": "#EAB839", + "value": 850000 + }, + { + "color": "red", + "value": 1000000 + } + ] + }, + "unit": "locale" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 10, + "y": 27 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "increase(sonar_ops_processed_total{kind=~\"create\",socket_url=~\"wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos\", op_path=\"app.bsky.actor.profile\"}[$__range])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Profiles Created", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 55, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1000 + }, + { + "color": "orange", + "value": 1200 + }, + { + "color": "red", + "value": 1500 + } + ] + }, + "unit": "cps" + }, + "overrides": [] + }, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "editorMode": "code", + "expr": "avg(rate(sonar_ops_processed_total{kind=~\"${op_kind}\", box=~\"${box}\",socket_url=~\"${socket}\"}[$__rate_interval])) by (op_path, kind) > 0", + "legendFormat": "{{kind}} {{op_path}}", + "range": true, + "refId": "A" + } + ], + "title": "Operation Throughput", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "(.*)(app.bsky.)(.*)", + "renamePattern": "$1 $3" + } + } + ], + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "definition": "label_values(sonar_ops_processed_total,kind)", + "hide": 0, + "includeAll": true, + "label": "Operation Kind", + "multi": true, + "name": "op_kind", + "options": [], + "query": { + "query": "label_values(sonar_ops_processed_total,kind)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "definition": "label_values(indigo_repo_stream_bytes_total{job=\"sonar\"},box)", + "hide": 0, + "includeAll": false, + "label": "Sonar Host", + "multi": false, + "name": "box", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(indigo_repo_stream_bytes_total{job=\"sonar\"},box)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "definition": "label_values(socket_url)", + "hide": 0, + "includeAll": false, + "label": "Socket", + "multi": false, + "name": "socket", + "options": [], + "query": { + "query": "label_values(socket_url)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_MIMIR}" + }, + "definition": "label_values(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"},instance)", + "hide": 2, + "includeAll": false, + "label": "Instance", + "multi": false, + "name": "instance", + "options": [], + "query": { + "query": "label_values(sonar_events_processed_total{box=~\"${box}\",socket_url=~\"${socket}\"},instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sonar", + "uid": "e5c542d0-07d6-44e9-bc30-a4b973bedd86", + "version": 24, + "weekStart": "" +} diff --git a/cmd/sonar/main.go b/cmd/sonar/main.go index 2eb898f95..a867646fe 100644 --- a/cmd/sonar/main.go +++ b/cmd/sonar/main.go @@ -33,19 +33,22 @@ func main() { app.Flags = []cli.Flag{ &cli.StringFlag{ - Name: "ws-url", - Usage: "full websocket path to the ATProto SubscribeRepos XRPC endpoint", - Value: "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos", + Name: "ws-url", + Usage: "full websocket path to the ATProto SubscribeRepos XRPC endpoint", + Value: "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos", + EnvVars: []string{"SONAR_WS_URL"}, }, &cli.StringFlag{ - Name: "log-level", - Usage: "log level", - Value: "info", + Name: "log-level", + Usage: "log level", + Value: "info", + EnvVars: []string{"SONAR_LOG_LEVEL"}, }, &cli.IntFlag{ - Name: "port", - Usage: "listen port for metrics server", - Value: 8345, + Name: "port", + Usage: "listen port for metrics server", + Value: 8345, + EnvVars: []string{"SONAR_PORT"}, }, &cli.IntFlag{ Name: "max-queue-size", @@ -53,9 +56,10 @@ func main() { Value: 10, }, &cli.StringFlag{ - Name: "cursor-file", - Usage: "path to cursor file", - Value: "sonar_cursor.json", + Name: "cursor-file", + Usage: "path to cursor file", + Value: "sonar_cursor.json", + EnvVars: []string{"SONAR_CURSOR_FILE"}, }, } diff --git a/cmd/sonar/sonar_dash.png b/cmd/sonar/sonar_dash.png new file mode 100644 index 0000000000000000000000000000000000000000..b4570cbe74089855b471cef71aba8374255c9130 GIT binary patch literal 763925 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

y5)XDHvJie0ECpML8mqbf$DQ30 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

)zAWNzTI$?@X1_gwrxuOc zNA68|`7;RE`0}8(0glQ(PIak2`P>hI5~xd&JfE$f07N-zFYzcC*kFhA2iFmIFAp=J z)!c2UD_99Q!MArcmYpWVXa5P+D1MD(M=)KVECle|`TApzo3V06Zr(Yh!&gN9sBvPk zfu&T~IM}ROybKIT8xoB*317Z^VKLr)yWxI)+%wxAK95tTQ!V3Ub-zAdpp;KkYkiAK zM3k``KKf%?eE{edgtjK)w%2YZLRO^(J={iHOwF>t7Hv|88U?r^*^f; z)49Bw3#H@x4-4J~<8MSRCFq#BQ*<2RKcE)i5ti@1(!KbiT5lQZaSh?2{a(WuI9?N0 zXraDSl5S3-dZs`HV%QIgUG{!Ur2~v-T4ktkfrml{?;r#tyk!k zGy8j*vEJ5sq=^rdl?nZ~*JLN=Mzv*z7M}VAz{v6JOzpCeP;iDB*T769@6!rc-kluj z?k0yFIOCthOn^8YXur)u_MBK#P2&H+-U|W0Xn|x4o%PHH7e5B`J&vyhyQ^zB3N;!| z4h?m6cpy(#5_;aXq(q2_fx*w~@UHmy*PWetK5pM-jNj88{kv9-@HzA%r=Ur}OrUi7p-*mwzNN(4u&9&8~O9HK#jga|B?eOTV9ciaFV<=z63@%CA zKeGTNpjKGn8 z&7(uj!2}>5OG`onLrb5wOdFdA-sX{-ZRx1;EW`a>9jOJRUABVa_IDA4pL9?g<}J

D37uwA$p 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 C6YT&`lJ`K4L% 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�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 IGw^$d1f)g|BlVB6c7rk}dQ~Cy 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≦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

GGnYi2X=c6+z;Fc)bVP=~B&=BDdQUT@w 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 Date: Thu, 19 Sep 2024 16:53:08 -0400 Subject: [PATCH 019/111] document admin endpoints --- cmd/bigsky/README.md | 187 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/cmd/bigsky/README.md b/cmd/bigsky/README.md index 25df32ccf..73218e3c5 100644 --- a/cmd/bigsky/README.md +++ b/cmd/bigsky/README.md @@ -150,3 +150,190 @@ Lastly, can monitor progress of any ongoing re-syncs: # check sync progress for all hosts cat hosts.txt | parallel -j1 ./sync_pds.sh {} + + +## Admin API + +The relay has a number of admin HTTP API endpoints. Given a relay setup listening on port 2470 and with a reasonably secure admin secret: + +``` +RELAY_ADMIN_PASSWORD=$(openssl rand --hex 16) +bigsky --api-listen :2470 --admin-key ${RELAY_ADMIN_PASSWORD} ... +``` + +One can, for example, begin compaction of all repos + +``` +curl -H 'Authorization: Bearer '${RELAY_ADMIN_PASSWORD} -H 'Content-Type: application/x-www-form-urlencoded' --data '' http://127.0.0.1:2470/admin/repo/compactAll +``` + +### /admin/subs/getUpstreamConns + +Return list of PDS host names in json array of strings: ["host", ...] + +### /admin/subs/perDayLimit + +Return `{"limit": int}` for the number of new PDS subscriptions that the relay may start in a rolling 24 hour window. + +### /admin/subs/setPerDayLimit + +POST with `?limit={int}` to set the number of new PDS subscriptions that the relay may start in a rolling 24 hour window. + +### /admin/subs/setEnabled + +POST with param `?enabled=true` or `?enabled=false` to enable or disable PDS-requested new-PDS crawling. + +### /admin/subs/getEnabled + +Return `{"enabled": bool}` if non-admin new PDS crawl requests are enabled + +### /admin/subs/killUpstream + +POST with `?host={pds host name}` to disconnect from their firehose. + +Optionally add `&block=true` to prevent connecting to them in the future. + +### /admin/subs/listDomainBans + +Return `{"banned_domains": ["host name", ...]}` + +### /admin/subs/banDomain + +POST `{"Domain": "host name"}` to ban a domain + +### /admin/subs/unbanDomain + +POST `{"Domain": "host name"}` to un-ban a domain + +### /admin/repo/takeDown + +POST `{"did": "did:..."}` to take-down a bad repo; deletes all local data for the repo + +### /admin/repo/reverseTakedown + +POST `?did={did:...}` to reverse a repo take-down + +### /admin/repo/compact + +POST `?did={did:...}` to compact a repo. Optionally `&fast=true`. HTTP blocks until the compaction finishes. + +### /admin/repo/compactAll + +POST to begin compaction of all repos. Optional query params: + + * `fast=true` + * `limit={int}` maximum number of repos to compact (biggest first) (default 50) + * `threhsold={int}` minimum number of shard files a repo must have on disk to merit compaction (default 20) + +### /admin/repo/reset + +POST `?did={did:...}` deletes all local data for the repo + +### /admin/repo/verify + +POST `?did={did:...}` checks that all repo data is accessible. HTTP blocks until done. + +### /admin/pds/requestCrawl + +POST `{"hostname":"pds host"}` to start crawling a PDS + +### /admin/pds/list + +GET returns JSON list of records +```json +[{ + "Host": string, + "Did": string, + "SSL": bool, + "Cursor": int, + "Registered": bool, + "Blocked": bool, + "RateLimit": float, + "CrawlRateLimit": float, + "RepoCount": int, + "RepoLimit": int, + "HourlyEventLimit": int, + "DailyEventLimit": int, + + "HasActiveConnection": bool, + "EventsSeenSinceStartup": int, + "PerSecondEventRate": {"Max": float, "Window": float seconds}, + "PerHourEventRate": {"Max": float, "Window": float seconds}, + "PerDayEventRate": {"Max": float, "Window": float seconds}, + "CrawlRate": {"Max": float, "Window": float seconds}, + "UserCount": int, +}, ...] +``` + +### /admin/pds/resync + +POST `?host={host}` to start a resync of a PDS + +GET `?host={host}` to get status of a PDS resync, return + +```json +{"resync": { + "pds": { + "Host": string, + "Did": string, + "SSL": bool, + "Cursor": int, + "Registered": bool, + "Blocked": bool, + "RateLimit": float, + "CrawlRateLimit": float, + "RepoCount": int, + "RepoLimit": int, + "HourlyEventLimit": int, + "DailyEventLimit": int, + }, + "numRepoPages": int, + "numRepos": int, + "numReposChecked": int, + "numReposToResync": int, + "status": string, + "statusChangedAt": time, +}} +``` + +### /admin/pds/changeLimits + +POST to set the limits for a PDS. body: + +```json +{ + "host": string, + "per_second": int, + "per_hour": int, + "per_day": int, + "crawl_rate": int, + "repo_limit": int, +} +``` + +### /admin/pds/block + +POST `?host={host}` to block a PDS + +### /admin/pds/unblock + +POST `?host={host}` to un-block a PDS + + +### /admin/pds/addTrustedDomain + +POST `?domain={}` to make a domain trusted + +### /admin/consumers/list + +GET returns list json of clients currently reading from the relay firehose + +```json +[{ + "id": int, + "remote_addr": string, + "user_agent": string, + "events_consumed": int, + "connected_at": time, +}, ...] +``` From daa3810f5d5d94226d4af570c4377968345bb2b3 Mon Sep 17 00:00:00 2001 From: Brian Olson Date: Thu, 12 Sep 2024 09:14:53 -0400 Subject: [PATCH 020/111] refactor carstore meta into a class --- carstore/bs.go | 400 ++++++------------------------------------ carstore/meta_gorm.go | 346 ++++++++++++++++++++++++++++++++++++ carstore/util.go | 32 ++++ 3 files changed, 432 insertions(+), 346 deletions(-) create mode 100644 carstore/meta_gorm.go create mode 100644 carstore/util.go diff --git a/carstore/bs.go b/carstore/bs.go index 1f86b6612..894f41c5b 100644 --- a/carstore/bs.go +++ b/carstore/bs.go @@ -4,14 +4,11 @@ import ( "bufio" "bytes" "context" - "encoding/binary" "fmt" "io" "os" "path/filepath" "sort" - "strconv" - "strings" "sync" "sync/atomic" "time" @@ -33,7 +30,6 @@ import ( cbg "github.com/whyrusleeping/cbor-gen" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -53,7 +49,7 @@ const MaxSliceLength = 2 << 20 const BigShardThreshold = 2 << 20 type CarStore struct { - meta *gorm.DB + meta *CarStoreGormMeta rootDir string lscLk sync.Mutex @@ -78,79 +74,12 @@ func NewCarStore(meta *gorm.DB, root string) (*CarStore, error) { } return &CarStore{ - meta: meta, + meta: &CarStoreGormMeta{meta: meta}, rootDir: root, lastShardCache: make(map[models.Uid]*CarShard), }, nil } -type UserInfo struct { - gorm.Model - Head string -} - -type CarShard struct { - ID uint `gorm:"primarykey"` - CreatedAt time.Time - - Root models.DbCID `gorm:"index"` - DataStart int64 - Seq int `gorm:"index:idx_car_shards_seq;index:idx_car_shards_usr_seq,priority:2,sort:desc"` - Path string - Usr models.Uid `gorm:"index:idx_car_shards_usr;index:idx_car_shards_usr_seq,priority:1"` - Rev string -} - -type blockRef struct { - ID uint `gorm:"primarykey"` - Cid models.DbCID `gorm:"index"` - Shard uint `gorm:"index"` - Offset int64 - //User uint `gorm:"index"` -} - -type staleRef struct { - ID uint `gorm:"primarykey"` - Cid *models.DbCID - Cids []byte - Usr models.Uid `gorm:"index"` -} - -func (sr *staleRef) getCids() ([]cid.Cid, error) { - if sr.Cid != nil { - return []cid.Cid{sr.Cid.CID}, nil - } - - return unpackCids(sr.Cids) -} - -func packCids(cids []cid.Cid) []byte { - buf := new(bytes.Buffer) - for _, c := range cids { - buf.Write(c.Bytes()) - } - - return buf.Bytes() -} - -func unpackCids(b []byte) ([]cid.Cid, error) { - br := bytes.NewReader(b) - var out []cid.Cid - for { - _, c, err := cid.CidFromReader(br) - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - out = append(out, c) - } - - return out, nil -} - type userView struct { cs *CarStore user models.Uid @@ -166,17 +95,7 @@ func (uv *userView) HashOnRead(hor bool) { } func (uv *userView) Has(ctx context.Context, k cid.Cid) (bool, error) { - var count int64 - if err := uv.cs.meta. - Model(blockRef{}). - Select("path, block_refs.offset"). - Joins("left join car_shards on block_refs.shard = car_shards.id"). - Where("usr = ? AND cid = ?", uv.user, models.DbCID{CID: k}). - Count(&count).Error; err != nil { - return false, err - } - - return count > 0, nil + return uv.cs.meta.HasUidCid(ctx, uv.user, k) } var CacheHits int64 @@ -197,30 +116,16 @@ func (uv *userView) Get(ctx context.Context, k cid.Cid) (blockformat.Block, erro } atomic.AddInt64(&CacheMiss, 1) - // TODO: for now, im using a join to ensure we only query blocks from the - // correct user. maybe it makes sense to put the user in the blockRef - // directly? tradeoff of time vs space - var info struct { - Path string - Offset int64 - Usr models.Uid - } - if err := uv.cs.meta.Raw(`SELECT - (select path from car_shards where id = block_refs.shard) as path, - block_refs.offset, - (select usr from car_shards where id = block_refs.shard) as usr -FROM block_refs -WHERE - block_refs.cid = ? -LIMIT 1;`, models.DbCID{CID: k}).Scan(&info).Error; err != nil { + path, offset, user, err := uv.cs.meta.LookupBlockRef(ctx, k) + if err != nil { return nil, err } - if info.Path == "" { + if path == "" { return nil, ipld.ErrNotFound{Cid: k} } prefetch := uv.prefetch - if info.Usr != uv.user { + if user != uv.user { blockGetTotalCounterUsrskip.Add(1) prefetch = false } else { @@ -228,9 +133,9 @@ LIMIT 1;`, models.DbCID{CID: k}).Scan(&info).Error; err != nil { } if prefetch { - return uv.prefetchRead(ctx, k, info.Path, info.Offset) + return uv.prefetchRead(ctx, k, path, offset) } else { - return uv.singleRead(ctx, k, info.Path, info.Offset) + return uv.singleRead(ctx, k, path, offset) } } @@ -390,18 +295,13 @@ func (cs *CarStore) getLastShard(ctx context.Context, user models.Uid) (*CarShar return maybeLs, nil } - var lastShard CarShard - // this is often slow (which is why we're caching it) but could be sped up with an extra index: - // CREATE INDEX idx_car_shards_usr_id ON car_shards (usr, seq DESC); - if err := cs.meta.WithContext(ctx).Model(CarShard{}).Limit(1).Order("seq desc").Find(&lastShard, "usr = ?", user).Error; err != nil { - //if err := cs.meta.Model(CarShard{}).Where("user = ?", user).Last(&lastShard).Error; err != nil { - //if err != gorm.ErrRecordNotFound { + lastShard, err := cs.meta.GetLastShard(ctx, user) + if err != nil { return nil, err - //} } - cs.putLastShardCache(&lastShard) - return &lastShard, nil + cs.putLastShardCache(lastShard) + return lastShard, nil } var ErrRepoBaseMismatch = fmt.Errorf("attempted a delta session on top of the wrong previous head") @@ -452,24 +352,27 @@ func (cs *CarStore) ReadOnlySession(user models.Uid) (*DeltaSession, error) { }, nil } +// TODO: incremental is only ever called true, remove the param func (cs *CarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, w io.Writer) error { ctx, span := otel.Tracer("carstore").Start(ctx, "ReadUserCar") defer span.End() var earlySeq int if sinceRev != "" { - var untilShard CarShard - if err := cs.meta.Where("rev >= ? AND usr = ?", sinceRev, user).Order("rev").First(&untilShard).Error; err != nil { - return fmt.Errorf("finding early shard: %w", err) + var err error + earlySeq, err = cs.meta.SeqForRev(ctx, user, sinceRev) + if err != nil { + return err } - earlySeq = untilShard.Seq } - var shards []CarShard - if err := cs.meta.Order("seq desc").Where("usr = ? AND seq >= ?", user, earlySeq).Find(&shards).Error; err != nil { + // TODO: Why does ReadUserCar want shards seq DESC but CompactUserShards wants seq ASC ? + shards, err := cs.meta.GetUserShardsDesc(ctx, user, earlySeq) + if err != nil { return err } + // TODO: incremental is only ever called true, so this is fine and we can remove the error check if !incremental && earlySeq > 0 { // have to do it the ugly way return fmt.Errorf("nyi") @@ -496,6 +399,8 @@ func (cs *CarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev s return nil } +// inner loop part of ReadUserCar +// copy shard blocks from disk to Writer func (cs *CarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Writer) error { ctx, span := otel.Tracer("carstore").Start(ctx, "writeShardBlocks") defer span.End() @@ -519,31 +424,7 @@ func (cs *CarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Wri return nil } -func (cs *CarStore) writeBlockFromShard(ctx context.Context, sh *CarShard, w io.Writer, c cid.Cid) error { - fi, err := os.Open(sh.Path) - if err != nil { - return err - } - defer fi.Close() - - rr, err := car.NewCarReader(fi) - if err != nil { - return err - } - - for { - blk, err := rr.Next() - if err != nil { - return err - } - - if blk.Cid() == c { - _, err := LdWrite(w, c.Bytes(), blk.RawData()) - return err - } - } -} - +// inner loop part of compactBucket func (cs *CarStore) iterateShardBlocks(ctx context.Context, sh *CarShard, cb func(blk blockformat.Block) error) error { fi, err := os.Open(sh.Path) if err != nil { @@ -769,123 +650,18 @@ func (cs *CarStore) putShard(ctx context.Context, shard *CarShard, brefs []map[s ctx, span := otel.Tracer("carstore").Start(ctx, "putShard") defer span.End() - // TODO: there should be a way to create the shard and block_refs that - // reference it in the same query, would save a lot of time - tx := cs.meta.WithContext(ctx).Begin() - - if err := tx.WithContext(ctx).Create(shard).Error; err != nil { - return fmt.Errorf("failed to create shard in DB tx: %w", err) - } - - if !nocache { - cs.putLastShardCache(shard) - } - - for _, ref := range brefs { - ref["shard"] = shard.ID - } - - if err := createBlockRefs(ctx, tx, brefs); err != nil { - return fmt.Errorf("failed to create block refs: %w", err) - } - - if len(rmcids) > 0 { - cids := make([]cid.Cid, 0, len(rmcids)) - for c := range rmcids { - cids = append(cids, c) - } - - if err := tx.Create(&staleRef{ - Cids: packCids(cids), - Usr: shard.Usr, - }).Error; err != nil { - return err - } - } - - err := tx.WithContext(ctx).Commit().Error + err := cs.meta.PutShardAndRefs(ctx, shard, brefs, rmcids) if err != nil { - return fmt.Errorf("failed to commit shard DB transaction: %w", err) - } - - return nil -} - -func createBlockRefs(ctx context.Context, tx *gorm.DB, brefs []map[string]any) error { - ctx, span := otel.Tracer("carstore").Start(ctx, "createBlockRefs") - defer span.End() - - if err := createInBatches(ctx, tx, brefs, 2000); err != nil { return err } - return nil -} - -func generateInsertQuery(data []map[string]any) (string, []any) { - placeholders := strings.Repeat("(?, ?, ?),", len(data)) - placeholders = placeholders[:len(placeholders)-1] // trim trailing comma - - query := "INSERT INTO block_refs (\"cid\", \"offset\", \"shard\") VALUES " + placeholders - - values := make([]any, 0, 3*len(data)) - for _, entry := range data { - values = append(values, entry["cid"], entry["offset"], entry["shard"]) + if !nocache { + cs.putLastShardCache(shard) } - return query, values -} - -// Function to create in batches -func createInBatches(ctx context.Context, tx *gorm.DB, data []map[string]any, batchSize int) error { - for i := 0; i < len(data); i += batchSize { - batch := data[i:] - if len(batch) > batchSize { - batch = batch[:batchSize] - } - - query, values := generateInsertQuery(batch) - - if err := tx.WithContext(ctx).Exec(query, values...).Error; err != nil { - return err - } - } return nil } -func LdWrite(w io.Writer, d ...[]byte) (int64, error) { - var sum uint64 - for _, s := range d { - sum += uint64(len(s)) - } - - buf := make([]byte, 8) - n := binary.PutUvarint(buf, sum) - nw, err := w.Write(buf[:n]) - if err != nil { - return 0, err - } - - for _, s := range d { - onw, err := w.Write(s) - if err != nil { - return int64(nw), err - } - nw += onw - } - - return int64(nw), nil -} - -func setToSlice(s map[cid.Cid]bool) []cid.Cid { - out := make([]cid.Cid, 0, len(s)) - for c := range s { - out = append(out, c) - } - - return out -} - func BlockDiff(ctx context.Context, bs blockstore.Blockstore, oldroot cid.Cid, newcids map[cid.Cid]blockformat.Block, skipcids map[cid.Cid]bool) (map[cid.Cid]bool, error) { ctx, span := otel.Tracer("repo").Start(ctx, "BlockDiff") defer span.End() @@ -1029,8 +805,8 @@ type UserStat struct { } func (cs *CarStore) Stat(ctx context.Context, usr models.Uid) ([]UserStat, error) { - var shards []CarShard - if err := cs.meta.Order("seq asc").Find(&shards, "usr = ?", usr).Error; err != nil { + shards, err := cs.meta.GetUserShards(ctx, usr) + if err != nil { return nil, err } @@ -1047,8 +823,8 @@ func (cs *CarStore) Stat(ctx context.Context, usr models.Uid) ([]UserStat, error } func (cs *CarStore) WipeUserData(ctx context.Context, user models.Uid) error { - var shards []*CarShard - if err := cs.meta.Find(&shards, "usr = ?", user).Error; err != nil { + shards, err := cs.meta.GetUserShards(ctx, user) + if err != nil { return err } @@ -1063,32 +839,23 @@ func (cs *CarStore) WipeUserData(ctx context.Context, user models.Uid) error { return nil } -func (cs *CarStore) deleteShards(ctx context.Context, shs []*CarShard) error { +func (cs *CarStore) deleteShards(ctx context.Context, shs []CarShard) error { ctx, span := otel.Tracer("carstore").Start(ctx, "deleteShards") defer span.End() - deleteSlice := func(ctx context.Context, subs []*CarShard) error { - var ids []uint - for _, sh := range subs { - ids = append(ids, sh.ID) - } - - txn := cs.meta.Begin() - - if err := txn.Delete(&CarShard{}, "id in (?)", ids).Error; err != nil { - return err - } - - if err := txn.Delete(&blockRef{}, "shard in (?)", ids).Error; err != nil { - return err + deleteSlice := func(ctx context.Context, subs []CarShard) error { + ids := make([]uint, len(subs)) + for i, sh := range subs { + ids[i] = sh.ID } - if err := txn.Commit().Error; err != nil { + err := cs.meta.DeleteShardsAndRefs(ctx, ids) + if err != nil { return err } for _, sh := range subs { - if err := cs.deleteShardFile(ctx, sh); err != nil { + if err := cs.deleteShardFile(ctx, &sh); err != nil { if !os.IsNotExist(err) { return err } @@ -1200,6 +967,8 @@ func (cb *compBucket) isEmpty() bool { func (cs *CarStore) openNewCompactedShardFile(ctx context.Context, user models.Uid, seq int) (*os.File, string, error) { // TODO: some overwrite protections + // NOTE CreateTemp is used for creating a non-colliding file, but we keep it and don't delete it so don't think of it as "temporary". + // This creates "sh-%d-%d%s" with some random stuff in the last position fi, err := os.CreateTemp(cs.rootDir, fnameForShard(user, seq)) if err != nil { return nil, "", err @@ -1217,31 +986,19 @@ func (cs *CarStore) GetCompactionTargets(ctx context.Context, shardCount int) ([ ctx, span := otel.Tracer("carstore").Start(ctx, "GetCompactionTargets") defer span.End() - var targets []CompactionTarget - if err := cs.meta.Raw(`select usr, count(*) as num_shards from car_shards group by usr having count(*) > ? order by num_shards desc`, shardCount).Scan(&targets).Error; err != nil { - return nil, err - } - - return targets, nil + return cs.meta.GetCompactionTargets(ctx, shardCount) } +// getBlockRefsForShards is a prep function for CompactUserShards func (cs *CarStore) getBlockRefsForShards(ctx context.Context, shardIds []uint) ([]blockRef, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "getBlockRefsForShards") defer span.End() span.SetAttributes(attribute.Int("shards", len(shardIds))) - chunkSize := 2000 - out := make([]blockRef, 0, len(shardIds)) - for i := 0; i < len(shardIds); i += chunkSize { - sl := shardIds[i:] - if len(sl) > chunkSize { - sl = sl[:chunkSize] - } - - if err := blockRefsForShards(ctx, cs.meta, sl, &out); err != nil { - return nil, fmt.Errorf("getting block refs: %w", err) - } + out, err := cs.meta.GetBlockRefsForShards(ctx, shardIds) + if err != nil { + return nil, err } span.SetAttributes(attribute.Int("refs", len(out))) @@ -1249,31 +1006,6 @@ func (cs *CarStore) getBlockRefsForShards(ctx context.Context, shardIds []uint) return out, nil } -func valuesStatementForShards(shards []uint) string { - sb := new(strings.Builder) - for i, v := range shards { - sb.WriteByte('(') - sb.WriteString(strconv.Itoa(int(v))) - sb.WriteByte(')') - if i != len(shards)-1 { - sb.WriteByte(',') - } - } - return sb.String() -} - -func blockRefsForShards(ctx context.Context, db *gorm.DB, shards []uint, obuf *[]blockRef) error { - // Check the database driver - switch db.Dialector.(type) { - case *postgres.Dialector: - sval := valuesStatementForShards(shards) - q := fmt.Sprintf(`SELECT block_refs.* FROM block_refs INNER JOIN (VALUES %s) AS vals(v) ON block_refs.shard = v`, sval) - return db.Raw(q).Scan(obuf).Error - default: - return db.Raw(`SELECT * FROM block_refs WHERE shard IN (?)`, shards).Scan(obuf).Error - } -} - func shardSize(sh *CarShard) (int64, error) { st, err := os.Stat(sh.Path) if err != nil { @@ -1302,15 +1034,11 @@ func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skip span.SetAttributes(attribute.Int64("user", int64(user))) - var shards []CarShard - if err := cs.meta.WithContext(ctx).Find(&shards, "usr = ?", user).Error; err != nil { + shards, err := cs.meta.GetUserShards(ctx, user) + if err != nil { return nil, err } - sort.Slice(shards, func(i, j int) bool { - return shards[i].Seq < shards[j].Seq - }) - if skipBigShards { // Since we generally expect shards to start bigger and get smaller, // and because we want to avoid compacting non-adjacent shards @@ -1356,8 +1084,8 @@ func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skip span.SetAttributes(attribute.Int("blockRefs", len(brefs))) - var staleRefs []staleRef - if err := cs.meta.WithContext(ctx).Find(&staleRefs, "usr = ?", user).Error; err != nil { + staleRefs, err := cs.meta.GetUserStaleRefs(ctx, user) + if err != nil { return nil, err } @@ -1486,7 +1214,7 @@ func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skip stats.NewShards++ - var todelete []*CarShard + todelete := make([]CarShard, 0, len(b.shards)) for _, s := range b.shards { removedShards[s.ID] = true sh, ok := shardsById[s.ID] @@ -1494,7 +1222,7 @@ func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skip return nil, fmt.Errorf("missing shard to delete") } - todelete = append(todelete, &sh) + todelete = append(todelete, sh) } stats.ShardsDeleted += len(todelete) @@ -1546,27 +1274,7 @@ func (cs *CarStore) deleteStaleRefs(ctx context.Context, uid models.Uid, brefs [ } } - txn := cs.meta.Begin() - - if err := txn.Delete(&staleRef{}, "usr = ?", uid).Error; err != nil { - return err - } - - // now create a new staleRef with all the refs we couldn't clear out - if len(staleToKeep) > 0 { - if err := txn.Create(&staleRef{ - Usr: uid, - Cids: packCids(staleToKeep), - }).Error; err != nil { - return err - } - } - - if err := txn.Commit().Error; err != nil { - return fmt.Errorf("failed to commit staleRef updates: %w", err) - } - - return nil + return cs.meta.SetStaleRef(ctx, uid, staleToKeep) } func (cs *CarStore) compactBucket(ctx context.Context, user models.Uid, b *compBucket, shardsById map[uint]CarShard, keep map[cid.Cid]bool) error { diff --git a/carstore/meta_gorm.go b/carstore/meta_gorm.go new file mode 100644 index 000000000..eb9ff7bbc --- /dev/null +++ b/carstore/meta_gorm.go @@ -0,0 +1,346 @@ +package carstore + +import ( + "bytes" + "context" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/bluesky-social/indigo/models" + "github.com/ipfs/go-cid" + "go.opentelemetry.io/otel" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type CarStoreGormMeta struct { + meta *gorm.DB +} + +func (cs *CarStoreGormMeta) Init() error { + if err := cs.meta.AutoMigrate(&CarShard{}, &blockRef{}); err != nil { + return err + } + if err := cs.meta.AutoMigrate(&staleRef{}); err != nil { + return err + } + return nil +} + +// Return true if any known record matches (Uid, Cid) +func (cs *CarStoreGormMeta) HasUidCid(ctx context.Context, user models.Uid, k cid.Cid) (bool, error) { + var count int64 + if err := cs.meta. + Model(blockRef{}). + Select("path, block_refs.offset"). + Joins("left join car_shards on block_refs.shard = car_shards.id"). + Where("usr = ? AND cid = ?", user, models.DbCID{CID: k}). + Count(&count).Error; err != nil { + return false, err + } + + return count > 0, nil +} + +// For some Cid, lookup the block ref. +// Return the path of the file written, the offset within the file, and the user associated with the Cid. +func (cs *CarStoreGormMeta) LookupBlockRef(ctx context.Context, k cid.Cid) (path string, offset int64, user models.Uid, err error) { + // TODO: for now, im using a join to ensure we only query blocks from the + // correct user. maybe it makes sense to put the user in the blockRef + // directly? tradeoff of time vs space + var info struct { + Path string + Offset int64 + Usr models.Uid + } + if err := cs.meta.Raw(`SELECT + (select path from car_shards where id = block_refs.shard) as path, + block_refs.offset, + (select usr from car_shards where id = block_refs.shard) as usr +FROM block_refs +WHERE + block_refs.cid = ? +LIMIT 1;`, models.DbCID{CID: k}).Scan(&info).Error; err != nil { + var defaultUser models.Uid + return "", -1, defaultUser, err + } + return info.Path, info.Offset, info.Usr, nil +} + +func (cs *CarStoreGormMeta) GetLastShard(ctx context.Context, user models.Uid) (*CarShard, error) { + var lastShard CarShard + if err := cs.meta.WithContext(ctx).Model(CarShard{}).Limit(1).Order("seq desc").Find(&lastShard, "usr = ?", user).Error; err != nil { + return nil, err + } + return &lastShard, nil +} + +// return all of a users's shards, ascending by Seq +func (cs *CarStoreGormMeta) GetUserShards(ctx context.Context, usr models.Uid) ([]CarShard, error) { + var shards []CarShard + if err := cs.meta.Order("seq asc").Find(&shards, "usr = ?", usr).Error; err != nil { + return nil, err + } + return shards, nil +} + +// return all of a users's shards, descending by Seq +func (cs *CarStoreGormMeta) GetUserShardsDesc(ctx context.Context, usr models.Uid, minSeq int) ([]CarShard, error) { + var shards []CarShard + if err := cs.meta.Order("seq desc").Find(&shards, "usr = ? AND seq >= ?", usr, minSeq).Error; err != nil { + return nil, err + } + return shards, nil +} + +func (cs *CarStoreGormMeta) GetUserStaleRefs(ctx context.Context, user models.Uid) ([]staleRef, error) { + var staleRefs []staleRef + if err := cs.meta.WithContext(ctx).Find(&staleRefs, "usr = ?", user).Error; err != nil { + return nil, err + } + return staleRefs, nil +} + +func (cs *CarStoreGormMeta) SeqForRev(ctx context.Context, user models.Uid, sinceRev string) (int, error) { + var untilShard CarShard + if err := cs.meta.Where("rev >= ? AND usr = ?", sinceRev, user).Order("rev").First(&untilShard).Error; err != nil { + return 0, fmt.Errorf("finding early shard: %w", err) + } + return untilShard.Seq, nil +} + +func (cs *CarStoreGormMeta) GetCompactionTargets(ctx context.Context, minShardCount int) ([]CompactionTarget, error) { + var targets []CompactionTarget + if err := cs.meta.Raw(`select usr, count(*) as num_shards from car_shards group by usr having count(*) > ? order by num_shards desc`, minShardCount).Scan(&targets).Error; err != nil { + return nil, err + } + + return targets, nil +} + +func (cs *CarStoreGormMeta) PutShardAndRefs(ctx context.Context, shard *CarShard, brefs []map[string]any, rmcids map[cid.Cid]bool) error { + // TODO: there should be a way to create the shard and block_refs that + // reference it in the same query, would save a lot of time + tx := cs.meta.WithContext(ctx).Begin() + + if err := tx.WithContext(ctx).Create(shard).Error; err != nil { + return fmt.Errorf("failed to create shard in DB tx: %w", err) + } + + for _, ref := range brefs { + ref["shard"] = shard.ID + } + + if err := createBlockRefs(ctx, tx, brefs); err != nil { + return fmt.Errorf("failed to create block refs: %w", err) + } + + if len(rmcids) > 0 { + cids := make([]cid.Cid, 0, len(rmcids)) + for c := range rmcids { + cids = append(cids, c) + } + + if err := tx.Create(&staleRef{ + Cids: packCids(cids), + Usr: shard.Usr, + }).Error; err != nil { + return err + } + } + + err := tx.WithContext(ctx).Commit().Error + if err != nil { + return fmt.Errorf("failed to commit shard DB transaction: %w", err) + } + return nil +} + +func (cs *CarStoreGormMeta) DeleteShardsAndRefs(ctx context.Context, ids []uint) error { + txn := cs.meta.Begin() + + if err := txn.Delete(&CarShard{}, "id in (?)", ids).Error; err != nil { + txn.Rollback() + return err + } + + if err := txn.Delete(&blockRef{}, "shard in (?)", ids).Error; err != nil { + txn.Rollback() + return err + } + + return txn.Commit().Error +} + +func (cs *CarStoreGormMeta) GetBlockRefsForShards(ctx context.Context, shardIds []uint) ([]blockRef, error) { + chunkSize := 2000 + out := make([]blockRef, 0, len(shardIds)) + for i := 0; i < len(shardIds); i += chunkSize { + sl := shardIds[i:] + if len(sl) > chunkSize { + sl = sl[:chunkSize] + } + + if err := blockRefsForShards(ctx, cs.meta, sl, &out); err != nil { + return nil, fmt.Errorf("getting block refs: %w", err) + } + } + return out, nil +} + +// blockRefsForShards is an inner loop helper for GetBlockRefsForShards +func blockRefsForShards(ctx context.Context, db *gorm.DB, shards []uint, obuf *[]blockRef) error { + // Check the database driver + switch db.Dialector.(type) { + case *postgres.Dialector: + sval := valuesStatementForShards(shards) + q := fmt.Sprintf(`SELECT block_refs.* FROM block_refs INNER JOIN (VALUES %s) AS vals(v) ON block_refs.shard = v`, sval) + return db.Raw(q).Scan(obuf).Error + default: + return db.Raw(`SELECT * FROM block_refs WHERE shard IN (?)`, shards).Scan(obuf).Error + } +} + +// valuesStatementForShards builds a postgres compatible statement string from int literals +func valuesStatementForShards(shards []uint) string { + sb := new(strings.Builder) + for i, v := range shards { + sb.WriteByte('(') + sb.WriteString(strconv.Itoa(int(v))) + sb.WriteByte(')') + if i != len(shards)-1 { + sb.WriteByte(',') + } + } + return sb.String() +} + +func (cs *CarStoreGormMeta) SetStaleRef(ctx context.Context, uid models.Uid, staleToKeep []cid.Cid) error { + txn := cs.meta.Begin() + + if err := txn.Delete(&staleRef{}, "usr = ?", uid).Error; err != nil { + return err + } + + // now create a new staleRef with all the refs we couldn't clear out + if len(staleToKeep) > 0 { + if err := txn.Create(&staleRef{ + Usr: uid, + Cids: packCids(staleToKeep), + }).Error; err != nil { + return err + } + } + + if err := txn.Commit().Error; err != nil { + return fmt.Errorf("failed to commit staleRef updates: %w", err) + } + return nil +} + +type CarShard struct { + ID uint `gorm:"primarykey"` + CreatedAt time.Time + + Root models.DbCID `gorm:"index"` + DataStart int64 + Seq int `gorm:"index:idx_car_shards_seq;index:idx_car_shards_usr_seq,priority:2,sort:desc"` + Path string + Usr models.Uid `gorm:"index:idx_car_shards_usr;index:idx_car_shards_usr_seq,priority:1"` + Rev string +} + +type blockRef struct { + ID uint `gorm:"primarykey"` + Cid models.DbCID `gorm:"index"` + Shard uint `gorm:"index"` + Offset int64 + //User uint `gorm:"index"` +} + +type staleRef struct { + ID uint `gorm:"primarykey"` + Cid *models.DbCID + Cids []byte + Usr models.Uid `gorm:"index"` +} + +func (sr *staleRef) getCids() ([]cid.Cid, error) { + if sr.Cid != nil { + return []cid.Cid{sr.Cid.CID}, nil + } + + return unpackCids(sr.Cids) +} + +func unpackCids(b []byte) ([]cid.Cid, error) { + br := bytes.NewReader(b) + var out []cid.Cid + for { + _, c, err := cid.CidFromReader(br) + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + out = append(out, c) + } + + return out, nil +} + +func packCids(cids []cid.Cid) []byte { + buf := new(bytes.Buffer) + for _, c := range cids { + buf.Write(c.Bytes()) + } + + return buf.Bytes() +} + +func createBlockRefs(ctx context.Context, tx *gorm.DB, brefs []map[string]any) error { + ctx, span := otel.Tracer("carstore").Start(ctx, "createBlockRefs") + defer span.End() + + if err := createInBatches(ctx, tx, brefs, 2000); err != nil { + return err + } + + return nil +} + +// Function to create in batches +func createInBatches(ctx context.Context, tx *gorm.DB, brefs []map[string]any, batchSize int) error { + for i := 0; i < len(brefs); i += batchSize { + batch := brefs[i:] + if len(batch) > batchSize { + batch = batch[:batchSize] + } + + query, values := generateInsertQuery(batch) + + if err := tx.WithContext(ctx).Exec(query, values...).Error; err != nil { + return err + } + } + return nil +} + +func generateInsertQuery(brefs []map[string]any) (string, []any) { + placeholders := strings.Repeat("(?, ?, ?),", len(brefs)) + placeholders = placeholders[:len(placeholders)-1] // trim trailing comma + + query := "INSERT INTO block_refs (\"cid\", \"offset\", \"shard\") VALUES " + placeholders + + values := make([]any, 0, 3*len(brefs)) + for _, entry := range brefs { + values = append(values, entry["cid"], entry["offset"], entry["shard"]) + } + + return query, values +} diff --git a/carstore/util.go b/carstore/util.go new file mode 100644 index 000000000..56396501f --- /dev/null +++ b/carstore/util.go @@ -0,0 +1,32 @@ +package carstore + +import ( + "encoding/binary" + "io" +) + +// Length-delimited Write +// Writer stream gets Uvarint length then concatenated data +func LdWrite(w io.Writer, d ...[]byte) (int64, error) { + var sum uint64 + for _, s := range d { + sum += uint64(len(s)) + } + + buf := make([]byte, 8) + n := binary.PutUvarint(buf, sum) + nw, err := w.Write(buf[:n]) + if err != nil { + return 0, err + } + + for _, s := range d { + onw, err := w.Write(s) + if err != nil { + return int64(nw), err + } + nw += onw + } + + return int64(nw), nil +} From 5c2cb480b4367f3781491a1da0f6313904abfc48 Mon Sep 17 00:00:00 2001 From: Brian Olson Date: Thu, 12 Sep 2024 12:45:29 -0400 Subject: [PATCH 021/111] carstore.CarStore is an interface, carstore.FileCarStore implements it --- carstore/bs.go | 75 ++++++++++++++++++++++---------------- carstore/repo_test.go | 4 +- events/dbpersist.go | 4 +- events/dbpersist_test.go | 2 +- events/diskpersist_test.go | 6 +-- pds/handlers_test.go | 2 +- pds/server.go | 4 +- repomgr/ingest_test.go | 4 +- repomgr/repomgr.go | 6 +-- 9 files changed, 60 insertions(+), 47 deletions(-) diff --git a/carstore/bs.go b/carstore/bs.go index 894f41c5b..e7af35d12 100644 --- a/carstore/bs.go +++ b/carstore/bs.go @@ -48,7 +48,20 @@ const MaxSliceLength = 2 << 20 const BigShardThreshold = 2 << 20 -type CarStore struct { +type CarStore interface { + CompactUserShards(ctx context.Context, user models.Uid, skipBigShards bool) (*CompactionStats, error) + GetCompactionTargets(ctx context.Context, shardCount int) ([]CompactionTarget, error) + GetUserRepoHead(ctx context.Context, user models.Uid) (cid.Cid, error) + GetUserRepoRev(ctx context.Context, user models.Uid) (string, error) + ImportSlice(ctx context.Context, uid models.Uid, since *string, carslice []byte) (cid.Cid, *DeltaSession, error) + NewDeltaSession(ctx context.Context, user models.Uid, since *string) (*DeltaSession, error) + ReadOnlySession(user models.Uid) (*DeltaSession, error) + ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, w io.Writer) error + Stat(ctx context.Context, usr models.Uid) ([]UserStat, error) + WipeUserData(ctx context.Context, user models.Uid) error +} + +type FileCarStore struct { meta *CarStoreGormMeta rootDir string @@ -56,7 +69,7 @@ type CarStore struct { lastShardCache map[models.Uid]*CarShard } -func NewCarStore(meta *gorm.DB, root string) (*CarStore, error) { +func NewCarStore(meta *gorm.DB, root string) (CarStore, error) { if _, err := os.Stat(root); err != nil { if !os.IsNotExist(err) { return nil, err @@ -73,7 +86,7 @@ func NewCarStore(meta *gorm.DB, root string) (*CarStore, error) { return nil, err } - return &CarStore{ + return &FileCarStore{ meta: &CarStoreGormMeta{meta: meta}, rootDir: root, lastShardCache: make(map[models.Uid]*CarShard), @@ -81,7 +94,7 @@ func NewCarStore(meta *gorm.DB, root string) (*CarStore, error) { } type userView struct { - cs *CarStore + cs *FileCarStore user models.Uid cache map[cid.Cid]blockformat.Block @@ -256,11 +269,11 @@ type DeltaSession struct { baseCid cid.Cid seq int readonly bool - cs *CarStore + cs *FileCarStore lastRev string } -func (cs *CarStore) checkLastShardCache(user models.Uid) *CarShard { +func (cs *FileCarStore) checkLastShardCache(user models.Uid) *CarShard { cs.lscLk.Lock() defer cs.lscLk.Unlock() @@ -272,21 +285,21 @@ func (cs *CarStore) checkLastShardCache(user models.Uid) *CarShard { return nil } -func (cs *CarStore) removeLastShardCache(user models.Uid) { +func (cs *FileCarStore) removeLastShardCache(user models.Uid) { cs.lscLk.Lock() defer cs.lscLk.Unlock() delete(cs.lastShardCache, user) } -func (cs *CarStore) putLastShardCache(ls *CarShard) { +func (cs *FileCarStore) putLastShardCache(ls *CarShard) { cs.lscLk.Lock() defer cs.lscLk.Unlock() cs.lastShardCache[ls.Usr] = ls } -func (cs *CarStore) getLastShard(ctx context.Context, user models.Uid) (*CarShard, error) { +func (cs *FileCarStore) getLastShard(ctx context.Context, user models.Uid) (*CarShard, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "getLastShard") defer span.End() @@ -306,7 +319,7 @@ func (cs *CarStore) getLastShard(ctx context.Context, user models.Uid) (*CarShar var ErrRepoBaseMismatch = fmt.Errorf("attempted a delta session on top of the wrong previous head") -func (cs *CarStore) NewDeltaSession(ctx context.Context, user models.Uid, since *string) (*DeltaSession, error) { +func (cs *FileCarStore) NewDeltaSession(ctx context.Context, user models.Uid, since *string) (*DeltaSession, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "NewSession") defer span.End() @@ -338,7 +351,7 @@ func (cs *CarStore) NewDeltaSession(ctx context.Context, user models.Uid, since }, nil } -func (cs *CarStore) ReadOnlySession(user models.Uid) (*DeltaSession, error) { +func (cs *FileCarStore) ReadOnlySession(user models.Uid) (*DeltaSession, error) { return &DeltaSession{ base: &userView{ user: user, @@ -353,7 +366,7 @@ func (cs *CarStore) ReadOnlySession(user models.Uid) (*DeltaSession, error) { } // TODO: incremental is only ever called true, remove the param -func (cs *CarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, w io.Writer) error { +func (cs *FileCarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev string, incremental bool, w io.Writer) error { ctx, span := otel.Tracer("carstore").Start(ctx, "ReadUserCar") defer span.End() @@ -401,7 +414,7 @@ func (cs *CarStore) ReadUserCar(ctx context.Context, user models.Uid, sinceRev s // inner loop part of ReadUserCar // copy shard blocks from disk to Writer -func (cs *CarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Writer) error { +func (cs *FileCarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Writer) error { ctx, span := otel.Tracer("carstore").Start(ctx, "writeShardBlocks") defer span.End() @@ -425,7 +438,7 @@ func (cs *CarStore) writeShardBlocks(ctx context.Context, sh *CarShard, w io.Wri } // inner loop part of compactBucket -func (cs *CarStore) iterateShardBlocks(ctx context.Context, sh *CarShard, cb func(blk blockformat.Block) error) error { +func (cs *FileCarStore) iterateShardBlocks(ctx context.Context, sh *CarShard, cb func(blk blockformat.Block) error) error { fi, err := os.Open(sh.Path) if err != nil { return err @@ -528,7 +541,7 @@ func (ds *DeltaSession) GetSize(ctx context.Context, c cid.Cid) (int, error) { func fnameForShard(user models.Uid, seq int) string { return fmt.Sprintf("sh-%d-%d", user, seq) } -func (cs *CarStore) openNewShardFile(ctx context.Context, user models.Uid, seq int) (*os.File, string, error) { +func (cs *FileCarStore) openNewShardFile(ctx context.Context, user models.Uid, seq int) (*os.File, string, error) { // TODO: some overwrite protections fname := filepath.Join(cs.rootDir, fnameForShard(user, seq)) fi, err := os.Create(fname) @@ -539,7 +552,7 @@ func (cs *CarStore) openNewShardFile(ctx context.Context, user models.Uid, seq i return fi, fname, nil } -func (cs *CarStore) writeNewShardFile(ctx context.Context, user models.Uid, seq int, data []byte) (string, error) { +func (cs *FileCarStore) writeNewShardFile(ctx context.Context, user models.Uid, seq int, data []byte) (string, error) { _, span := otel.Tracer("carstore").Start(ctx, "writeNewShardFile") defer span.End() @@ -552,7 +565,7 @@ func (cs *CarStore) writeNewShardFile(ctx context.Context, user models.Uid, seq return fname, nil } -func (cs *CarStore) deleteShardFile(ctx context.Context, sh *CarShard) error { +func (cs *FileCarStore) deleteShardFile(ctx context.Context, sh *CarShard) error { return os.Remove(sh.Path) } @@ -587,7 +600,7 @@ func WriteCarHeader(w io.Writer, root cid.Cid) (int64, error) { return hnw, nil } -func (cs *CarStore) writeNewShard(ctx context.Context, root cid.Cid, rev string, user models.Uid, seq int, blks map[cid.Cid]blockformat.Block, rmcids map[cid.Cid]bool) ([]byte, error) { +func (cs *FileCarStore) writeNewShard(ctx context.Context, root cid.Cid, rev string, user models.Uid, seq int, blks map[cid.Cid]blockformat.Block, rmcids map[cid.Cid]bool) ([]byte, error) { buf := new(bytes.Buffer) hnw, err := WriteCarHeader(buf, root) @@ -646,7 +659,7 @@ func (cs *CarStore) writeNewShard(ctx context.Context, root cid.Cid, rev string, return buf.Bytes(), nil } -func (cs *CarStore) putShard(ctx context.Context, shard *CarShard, brefs []map[string]any, rmcids map[cid.Cid]bool, nocache bool) error { +func (cs *FileCarStore) putShard(ctx context.Context, shard *CarShard, brefs []map[string]any, rmcids map[cid.Cid]bool, nocache bool) error { ctx, span := otel.Tracer("carstore").Start(ctx, "putShard") defer span.End() @@ -726,7 +739,7 @@ func BlockDiff(ctx context.Context, bs blockstore.Blockstore, oldroot cid.Cid, n return dropset, nil } -func (cs *CarStore) ImportSlice(ctx context.Context, uid models.Uid, since *string, carslice []byte) (cid.Cid, *DeltaSession, error) { +func (cs *FileCarStore) ImportSlice(ctx context.Context, uid models.Uid, since *string, carslice []byte) (cid.Cid, *DeltaSession, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "ImportSlice") defer span.End() @@ -774,7 +787,7 @@ func (ds *DeltaSession) CalcDiff(ctx context.Context, skipcids map[cid.Cid]bool) return nil } -func (cs *CarStore) GetUserRepoHead(ctx context.Context, user models.Uid) (cid.Cid, error) { +func (cs *FileCarStore) GetUserRepoHead(ctx context.Context, user models.Uid) (cid.Cid, error) { lastShard, err := cs.getLastShard(ctx, user) if err != nil { return cid.Undef, err @@ -786,7 +799,7 @@ func (cs *CarStore) GetUserRepoHead(ctx context.Context, user models.Uid) (cid.C return lastShard.Root.CID, nil } -func (cs *CarStore) GetUserRepoRev(ctx context.Context, user models.Uid) (string, error) { +func (cs *FileCarStore) GetUserRepoRev(ctx context.Context, user models.Uid) (string, error) { lastShard, err := cs.getLastShard(ctx, user) if err != nil { return "", err @@ -804,7 +817,7 @@ type UserStat struct { Created time.Time } -func (cs *CarStore) Stat(ctx context.Context, usr models.Uid) ([]UserStat, error) { +func (cs *FileCarStore) Stat(ctx context.Context, usr models.Uid) ([]UserStat, error) { shards, err := cs.meta.GetUserShards(ctx, usr) if err != nil { return nil, err @@ -822,7 +835,7 @@ func (cs *CarStore) Stat(ctx context.Context, usr models.Uid) ([]UserStat, error return out, nil } -func (cs *CarStore) WipeUserData(ctx context.Context, user models.Uid) error { +func (cs *FileCarStore) WipeUserData(ctx context.Context, user models.Uid) error { shards, err := cs.meta.GetUserShards(ctx, user) if err != nil { return err @@ -839,7 +852,7 @@ func (cs *CarStore) WipeUserData(ctx context.Context, user models.Uid) error { return nil } -func (cs *CarStore) deleteShards(ctx context.Context, shs []CarShard) error { +func (cs *FileCarStore) deleteShards(ctx context.Context, shs []CarShard) error { ctx, span := otel.Tracer("carstore").Start(ctx, "deleteShards") defer span.End() @@ -965,7 +978,7 @@ func (cb *compBucket) isEmpty() bool { return len(cb.shards) == 0 } -func (cs *CarStore) openNewCompactedShardFile(ctx context.Context, user models.Uid, seq int) (*os.File, string, error) { +func (cs *FileCarStore) openNewCompactedShardFile(ctx context.Context, user models.Uid, seq int) (*os.File, string, error) { // TODO: some overwrite protections // NOTE CreateTemp is used for creating a non-colliding file, but we keep it and don't delete it so don't think of it as "temporary". // This creates "sh-%d-%d%s" with some random stuff in the last position @@ -982,7 +995,7 @@ type CompactionTarget struct { NumShards int } -func (cs *CarStore) GetCompactionTargets(ctx context.Context, shardCount int) ([]CompactionTarget, error) { +func (cs *FileCarStore) GetCompactionTargets(ctx context.Context, shardCount int) ([]CompactionTarget, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "GetCompactionTargets") defer span.End() @@ -990,7 +1003,7 @@ func (cs *CarStore) GetCompactionTargets(ctx context.Context, shardCount int) ([ } // getBlockRefsForShards is a prep function for CompactUserShards -func (cs *CarStore) getBlockRefsForShards(ctx context.Context, shardIds []uint) ([]blockRef, error) { +func (cs *FileCarStore) getBlockRefsForShards(ctx context.Context, shardIds []uint) ([]blockRef, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "getBlockRefsForShards") defer span.End() @@ -1028,7 +1041,7 @@ type CompactionStats struct { DupeCount int `json:"dupeCount"` } -func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skipBigShards bool) (*CompactionStats, error) { +func (cs *FileCarStore) CompactUserShards(ctx context.Context, user models.Uid, skipBigShards bool) (*CompactionStats, error) { ctx, span := otel.Tracer("carstore").Start(ctx, "CompactUserShards") defer span.End() @@ -1242,7 +1255,7 @@ func (cs *CarStore) CompactUserShards(ctx context.Context, user models.Uid, skip return stats, nil } -func (cs *CarStore) deleteStaleRefs(ctx context.Context, uid models.Uid, brefs []blockRef, staleRefs []staleRef, removedShards map[uint]bool) error { +func (cs *FileCarStore) deleteStaleRefs(ctx context.Context, uid models.Uid, brefs []blockRef, staleRefs []staleRef, removedShards map[uint]bool) error { ctx, span := otel.Tracer("carstore").Start(ctx, "deleteStaleRefs") defer span.End() @@ -1277,7 +1290,7 @@ func (cs *CarStore) deleteStaleRefs(ctx context.Context, uid models.Uid, brefs [ return cs.meta.SetStaleRef(ctx, uid, staleToKeep) } -func (cs *CarStore) compactBucket(ctx context.Context, user models.Uid, b *compBucket, shardsById map[uint]CarShard, keep map[cid.Cid]bool) error { +func (cs *FileCarStore) compactBucket(ctx context.Context, user models.Uid, b *compBucket, shardsById map[uint]CarShard, keep map[cid.Cid]bool) error { ctx, span := otel.Tracer("carstore").Start(ctx, "compactBucket") defer span.End() diff --git a/carstore/repo_test.go b/carstore/repo_test.go index 084f16f36..a4d2c8cb8 100644 --- a/carstore/repo_test.go +++ b/carstore/repo_test.go @@ -24,7 +24,7 @@ import ( "gorm.io/gorm" ) -func testCarStore() (*CarStore, func(), error) { +func testCarStore() (CarStore, func(), error) { tempdir, err := os.MkdirTemp("", "msttest-") if err != nil { return nil, nil, err @@ -250,7 +250,7 @@ func TestRepeatedCompactions(t *testing.T) { checkRepo(t, cs, buf, recs) } -func checkRepo(t *testing.T, cs *CarStore, r io.Reader, expRecs []cid.Cid) { +func checkRepo(t *testing.T, cs CarStore, r io.Reader, expRecs []cid.Cid) { t.Helper() rep, err := repo.ReadRepoFromCar(context.TODO(), r) if err != nil { diff --git a/events/dbpersist.go b/events/dbpersist.go index c534057d4..a9d6288f2 100644 --- a/events/dbpersist.go +++ b/events/dbpersist.go @@ -51,7 +51,7 @@ func DefaultOptions() *Options { type DbPersistence struct { db *gorm.DB - cs *carstore.CarStore + cs carstore.CarStore lk sync.Mutex @@ -86,7 +86,7 @@ type RepoEventRecord struct { Ops []byte } -func NewDbPersistence(db *gorm.DB, cs *carstore.CarStore, options *Options) (*DbPersistence, error) { +func NewDbPersistence(db *gorm.DB, cs carstore.CarStore, options *Options) (*DbPersistence, error) { if err := db.AutoMigrate(&RepoEventRecord{}); err != nil { return nil, err } diff --git a/events/dbpersist_test.go b/events/dbpersist_test.go index 6954812df..c299569da 100644 --- a/events/dbpersist_test.go +++ b/events/dbpersist_test.go @@ -268,7 +268,7 @@ func BenchmarkPlayback(b *testing.B) { } } -func setupDBs(t testing.TB) (*gorm.DB, *gorm.DB, *carstore.CarStore, string, error) { +func setupDBs(t testing.TB) (*gorm.DB, *gorm.DB, carstore.CarStore, string, error) { dir, err := os.MkdirTemp("", "integtest") if err != nil { return nil, nil, nil, "", err diff --git a/events/diskpersist_test.go b/events/diskpersist_test.go index baefd379d..5d09c0fc2 100644 --- a/events/diskpersist_test.go +++ b/events/diskpersist_test.go @@ -187,7 +187,7 @@ func BenchmarkDiskPersist(b *testing.B) { } -func runPersisterBenchmark(b *testing.B, cs *carstore.CarStore, db *gorm.DB, p events.EventPersistence) { +func runPersisterBenchmark(b *testing.B, cs carstore.CarStore, db *gorm.DB, p events.EventPersistence) { ctx := context.Background() db.AutoMigrate(&pds.User{}) @@ -302,7 +302,7 @@ func TestDiskPersister(t *testing.T) { runEventManagerTest(t, cs, db, dp) } -func runEventManagerTest(t *testing.T, cs *carstore.CarStore, db *gorm.DB, p events.EventPersistence) { +func runEventManagerTest(t *testing.T, cs carstore.CarStore, db *gorm.DB, p events.EventPersistence) { ctx := context.Background() db.AutoMigrate(&pds.User{}) @@ -409,7 +409,7 @@ func TestDiskPersisterTakedowns(t *testing.T) { runTakedownTest(t, cs, db, dp) } -func runTakedownTest(t *testing.T, cs *carstore.CarStore, db *gorm.DB, p events.EventPersistence) { +func runTakedownTest(t *testing.T, cs carstore.CarStore, db *gorm.DB, p events.EventPersistence) { ctx := context.TODO() db.AutoMigrate(&pds.User{}) diff --git a/pds/handlers_test.go b/pds/handlers_test.go index d83c23fb4..fe2bb14b8 100644 --- a/pds/handlers_test.go +++ b/pds/handlers_test.go @@ -17,7 +17,7 @@ import ( "gorm.io/gorm" ) -func testCarStore(t *testing.T, db *gorm.DB) (*carstore.CarStore, func()) { +func testCarStore(t *testing.T, db *gorm.DB) (carstore.CarStore, func()) { t.Helper() tempdir, err := os.MkdirTemp("", "msttest-") if err != nil { diff --git a/pds/server.go b/pds/server.go index 77c58f024..b9d1c903b 100644 --- a/pds/server.go +++ b/pds/server.go @@ -41,7 +41,7 @@ var log = logging.Logger("pds") type Server struct { db *gorm.DB - cs *carstore.CarStore + cs carstore.CarStore repoman *repomgr.RepoManager feedgen *FeedGenerator notifman notifs.NotificationManager @@ -65,7 +65,7 @@ type Server struct { // NewServer. const serverListenerBootTimeout = 5 * time.Second -func NewServer(db *gorm.DB, cs *carstore.CarStore, serkey *did.PrivKey, handleSuffix, serviceUrl string, didr plc.PLCClient, jwtkey []byte) (*Server, error) { +func NewServer(db *gorm.DB, cs carstore.CarStore, serkey *did.PrivKey, handleSuffix, serviceUrl string, didr plc.PLCClient, jwtkey []byte) (*Server, error) { db.AutoMigrate(&User{}) db.AutoMigrate(&Peering{}) diff --git a/repomgr/ingest_test.go b/repomgr/ingest_test.go index 4296cd949..dcb9097ac 100644 --- a/repomgr/ingest_test.go +++ b/repomgr/ingest_test.go @@ -69,7 +69,7 @@ func TestLoadNewRepo(t *testing.T) { } } -func testCarstore(t *testing.T, dir string) *carstore.CarStore { +func testCarstore(t *testing.T, dir string) carstore.CarStore { cardb, err := gorm.Open(sqlite.Open(filepath.Join(dir, "car.sqlite"))) if err != nil { t.Fatal(err) @@ -151,7 +151,7 @@ func TestIngestWithGap(t *testing.T) { } } -func doPost(t *testing.T, cs *carstore.CarStore, did string, prev *string, postid int) ([]byte, cid.Cid, string, string) { +func doPost(t *testing.T, cs carstore.CarStore, did string, prev *string, postid int) ([]byte, cid.Cid, string, string) { ctx := context.TODO() ds, err := cs.NewDeltaSession(ctx, 1, prev) if err != nil { diff --git a/repomgr/repomgr.go b/repomgr/repomgr.go index 59f90b13d..ad90a43b2 100644 --- a/repomgr/repomgr.go +++ b/repomgr/repomgr.go @@ -33,7 +33,7 @@ import ( var log = logging.Logger("repomgr") -func NewRepoManager(cs *carstore.CarStore, kmgr KeyManager) *RepoManager { +func NewRepoManager(cs carstore.CarStore, kmgr KeyManager) *RepoManager { return &RepoManager{ cs: cs, @@ -53,7 +53,7 @@ func (rm *RepoManager) SetEventHandler(cb func(context.Context, *RepoEvent), hyd } type RepoManager struct { - cs *carstore.CarStore + cs carstore.CarStore kmgr KeyManager lklk sync.Mutex @@ -140,7 +140,7 @@ func (rm *RepoManager) lockUser(ctx context.Context, user models.Uid) func() { } } -func (rm *RepoManager) CarStore() *carstore.CarStore { +func (rm *RepoManager) CarStore() carstore.CarStore { return rm.cs } From 442474aebe8b6497141bd8252c8aef8d0823950e Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 00:16:21 -0700 Subject: [PATCH 022/111] automod: fix segfault in harassment rule --- automod/rules/harassment.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/automod/rules/harassment.go b/automod/rules/harassment.go index d5a35008e..5212b69e1 100644 --- a/automod/rules/harassment.go +++ b/automod/rules/harassment.go @@ -76,10 +76,12 @@ func HarassmentTargetInteractionPostRule(c *automod.RecordContext, post *appbsky if targetAccount == nil { continue } - for _, t := range targetAccount.Private.AccountTags { - if t == "harassment-protection" { - targetIsProtected = true - break + if targetAccount.Private != nil { + for _, t := range targetAccount.Private.AccountTags { + if t == "harassment-protection" { + targetIsProtected = true + break + } } } } From f24fe0287ed4bb621800354eebb0a64cc1c9ed8c Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 24 Jan 2024 18:21:34 -0800 Subject: [PATCH 023/111] redisdir: handle purge of uncached directory info --- atproto/identity/redisdir/redis_directory.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 3ea9f354a..90817b6dc 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -317,11 +317,19 @@ func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error handle, err := a.AsHandle() if nil == err { // if not an error, is a handle handle = handle.Normalize() - return d.handleCache.Delete(ctx, handle.String()) + err = d.handleCache.Delete(ctx, handle.String()) + if err == cache.ErrCacheMiss { + return nil + } + return err } did, err := a.AsDID() if nil == err { // if not an error, is a DID - return d.identityCache.Delete(ctx, did.String()) + err = d.identityCache.Delete(ctx, did.String()) + if err == cache.ErrCacheMiss { + return nil + } + return err } return fmt.Errorf("at-identifier neither a Handle nor a DID") } From 2e695b86ae15c9347acad9958fd1ae87992e8f30 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 01:45:31 -0700 Subject: [PATCH 024/111] identity: simplify type sig --- atproto/identity/cache_directory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/identity/cache_directory.go b/atproto/identity/cache_directory.go index 55951bbf0..dad78fe82 100644 --- a/atproto/identity/cache_directory.go +++ b/atproto/identity/cache_directory.go @@ -68,7 +68,7 @@ var handleRequestsCoalesced = promauto.NewCounter(prometheus.CounterOpts{ var _ Directory = (*CacheDirectory)(nil) // Capacity of zero means unlimited size. Similarly, ttl of zero means unlimited duration. -func NewCacheDirectory(inner Directory, capacity int, hitTTL, errTTL time.Duration, invalidHandleTTL time.Duration) CacheDirectory { +func NewCacheDirectory(inner Directory, capacity int, hitTTL, errTTL, invalidHandleTTL time.Duration) CacheDirectory { return CacheDirectory{ ErrTTL: errTTL, InvalidHandleTTL: invalidHandleTTL, From 122453159b8c8d0b0ad565c6b52fa99320cbd1d8 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:39:41 -0700 Subject: [PATCH 025/111] syntax: clearer error for trivial empty string case --- atproto/syntax/atidentifier.go | 3 +++ atproto/syntax/cid.go | 3 +++ atproto/syntax/datetime.go | 3 +++ atproto/syntax/did.go | 3 +++ atproto/syntax/handle.go | 3 +++ atproto/syntax/language.go | 3 +++ atproto/syntax/nsid.go | 3 +++ atproto/syntax/recordkey.go | 3 +++ atproto/syntax/tid.go | 3 +++ atproto/syntax/uri.go | 3 +++ 10 files changed, 30 insertions(+) diff --git a/atproto/syntax/atidentifier.go b/atproto/syntax/atidentifier.go index 21a0f8879..be5315c3f 100644 --- a/atproto/syntax/atidentifier.go +++ b/atproto/syntax/atidentifier.go @@ -10,6 +10,9 @@ type AtIdentifier struct { } func ParseAtIdentifier(raw string) (*AtIdentifier, error) { + if raw == "" { + return nil, fmt.Errorf("expected AT account identifier, got empty string") + } if strings.HasPrefix(raw, "did:") { did, err := ParseDID(raw) if err != nil { diff --git a/atproto/syntax/cid.go b/atproto/syntax/cid.go index bd2c7a9b3..8f994c876 100644 --- a/atproto/syntax/cid.go +++ b/atproto/syntax/cid.go @@ -16,6 +16,9 @@ type CID string var cidRegex = regexp.MustCompile(`^[a-zA-Z0-9+=]{8,256}$`) func ParseCID(raw string) (CID, error) { + if raw == "" { + return "", fmt.Errorf("expected CID, got empty string") + } if len(raw) > 256 { return "", fmt.Errorf("CID is too long (256 chars max)") } diff --git a/atproto/syntax/datetime.go b/atproto/syntax/datetime.go index 631918e6a..f1a819bc8 100644 --- a/atproto/syntax/datetime.go +++ b/atproto/syntax/datetime.go @@ -24,6 +24,9 @@ type Datetime string var datetimeRegex = regexp.MustCompile(`^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]:[0-6][0-9](.[0-9]{1,20})?(Z|([+-][0-2][0-9]:[0-5][0-9]))$`) func ParseDatetime(raw string) (Datetime, error) { + if raw == "" { + return "", fmt.Errorf("expected datetime, got empty string") + } if len(raw) > 64 { return "", fmt.Errorf("Datetime too long (max 64 chars)") } diff --git a/atproto/syntax/did.go b/atproto/syntax/did.go index 567c58ede..d356dc4fe 100644 --- a/atproto/syntax/did.go +++ b/atproto/syntax/did.go @@ -16,6 +16,9 @@ type DID string var didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) func ParseDID(raw string) (DID, error) { + if raw == "" { + return "", fmt.Errorf("expected DID, got empty string") + } if len(raw) > 2*1024 { return "", fmt.Errorf("DID is too long (2048 chars max)") } diff --git a/atproto/syntax/handle.go b/atproto/syntax/handle.go index 63322b7fc..2c52f9dbe 100644 --- a/atproto/syntax/handle.go +++ b/atproto/syntax/handle.go @@ -21,6 +21,9 @@ var ( type Handle string func ParseHandle(raw string) (Handle, error) { + if raw == "" { + return "", fmt.Errorf("expected handle, got empty string") + } if len(raw) > 253 { return "", fmt.Errorf("handle is too long (253 chars max)") } diff --git a/atproto/syntax/language.go b/atproto/syntax/language.go index 0d263eec6..f65683d27 100644 --- a/atproto/syntax/language.go +++ b/atproto/syntax/language.go @@ -15,6 +15,9 @@ type Language string var langRegex = regexp.MustCompile(`^(i|[a-z]{2,3})(-[a-zA-Z0-9]+)*$`) func ParseLanguage(raw string) (Language, error) { + if raw == "" { + return "", fmt.Errorf("expected language code, got empty string") + } if len(raw) > 128 { return "", fmt.Errorf("Language is too long (128 chars max)") } diff --git a/atproto/syntax/nsid.go b/atproto/syntax/nsid.go index a87778eba..9b0279265 100644 --- a/atproto/syntax/nsid.go +++ b/atproto/syntax/nsid.go @@ -16,6 +16,9 @@ var nsidRegex = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\. type NSID string func ParseNSID(raw string) (NSID, error) { + if raw == "" { + return "", fmt.Errorf("expected NSID, got empty string") + } if len(raw) > 317 { return "", fmt.Errorf("NSID is too long (317 chars max)") } diff --git a/atproto/syntax/recordkey.go b/atproto/syntax/recordkey.go index 24606803b..541a99c38 100644 --- a/atproto/syntax/recordkey.go +++ b/atproto/syntax/recordkey.go @@ -15,6 +15,9 @@ var recordKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9_~.:-]{1,512}$`) type RecordKey string func ParseRecordKey(raw string) (RecordKey, error) { + if raw == "" { + return "", fmt.Errorf("expected record key, got empty string") + } if len(raw) > 512 { return "", fmt.Errorf("recordkey is too long (512 chars max)") } diff --git a/atproto/syntax/tid.go b/atproto/syntax/tid.go index c67b4b216..53a131482 100644 --- a/atproto/syntax/tid.go +++ b/atproto/syntax/tid.go @@ -27,6 +27,9 @@ type TID string var tidRegex = regexp.MustCompile(`^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$`) func ParseTID(raw string) (TID, error) { + if raw == "" { + return "", fmt.Errorf("expected TID, got empty string") + } if len(raw) != 13 { return "", fmt.Errorf("TID is wrong length (expected 13 chars)") } diff --git a/atproto/syntax/uri.go b/atproto/syntax/uri.go index d2cc6ce7e..4b069d3cc 100644 --- a/atproto/syntax/uri.go +++ b/atproto/syntax/uri.go @@ -13,6 +13,9 @@ import ( type URI string func ParseURI(raw string) (URI, error) { + if raw == "" { + return "", fmt.Errorf("expected URI, got empty string") + } if len(raw) > 8192 { return "", fmt.Errorf("URI is too long (8192 chars max)") } From 048c0b9af1ad57fc31f57596f56c1272feabb3c1 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 01:46:24 -0700 Subject: [PATCH 026/111] redisdir: add invalid handle TTL --- atproto/identity/redisdir/redis_directory.go | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 90817b6dc..698939c7b 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -20,9 +20,10 @@ var redisDirPrefix string = "dir/" // // Includes an in-process LRU cache as well (provided by the redis client library), for hot key (identities). type RedisDirectory struct { - Inner identity.Directory - ErrTTL time.Duration - HitTTL time.Duration + Inner identity.Directory + ErrTTL time.Duration + HitTTL time.Duration + InvalidHandleTTL time.Duration handleCache *cache.Cache identityCache *cache.Cache @@ -49,7 +50,7 @@ var _ identity.Directory = (*RedisDirectory)(nil) // `redisURL` contains all the redis connection config options. // `hitTTL` and `errTTL` define how long successful and errored identity metadata should be cached (respectively). errTTL is expected to be shorted than hitTTL. // `lruSize` is the size of the in-process cache, for each of the handle and identity caches. 10000 is a reasonable default. -func NewRedisDirectory(inner identity.Directory, redisURL string, hitTTL, errTTL time.Duration, lruSize int) (*RedisDirectory, error) { +func NewRedisDirectory(inner identity.Directory, redisURL string, hitTTL, errTTL, invalidHandleTTL time.Duration, lruSize int) (*RedisDirectory, error) { opt, err := redis.ParseURL(redisURL) if err != nil { return nil, err @@ -69,11 +70,12 @@ func NewRedisDirectory(inner identity.Directory, redisURL string, hitTTL, errTTL LocalCache: cache.NewTinyLFU(lruSize, hitTTL), }) return &RedisDirectory{ - Inner: inner, - ErrTTL: errTTL, - HitTTL: hitTTL, - handleCache: handleCache, - identityCache: identityCache, + Inner: inner, + ErrTTL: errTTL, + HitTTL: hitTTL, + InvalidHandleTTL: invalidHandleTTL, + handleCache: handleCache, + identityCache: identityCache, }, nil } @@ -88,6 +90,9 @@ func (d *RedisDirectory) isIdentityStale(e *identityEntry) bool { if e.Err != nil && time.Since(e.Updated) > d.ErrTTL { return true } + if e.Identity != nil && e.Identity.Handle.IsInvalidHandle() && time.Since(e.Updated) > d.InvalidHandleTTL { + return true + } return false } From e30e51b329d68f46c768f1a52dc8bac517aa1694 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 01:47:40 -0700 Subject: [PATCH 027/111] redisdir: have cache update code match caching directory --- atproto/identity/redisdir/redis_directory.go | 43 ++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 698939c7b..0eee1dde0 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -96,7 +96,7 @@ func (d *RedisDirectory) isIdentityStale(e *identityEntry) bool { return false } -func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) (*handleEntry, error) { +func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) handleEntry { h = h.Normalize() ident, err := d.Inner.LookupHandle(ctx, h) if err != nil { @@ -112,9 +112,10 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) (*ha TTL: d.ErrTTL, }) if err != nil { - return nil, err + he.DID = "" + he.Err = err + return he } - return &he, nil } entry := identityEntry{ @@ -135,7 +136,9 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) (*ha TTL: d.HitTTL, }) if err != nil { - return nil, err + he.DID = "" + he.Err = err + return he } err = d.handleCache.Set(&cache.Item{ Ctx: ctx, @@ -144,9 +147,11 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) (*ha TTL: d.HitTTL, }) if err != nil { - return nil, err + he.DID = "" + he.Err = err + return he } - return &he, nil + return he } func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { @@ -183,21 +188,23 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy } } - var did syntax.DID // Update the Handle Entry from PLC and cache the result - newEntry, err := d.updateHandle(ctx, h) - if err == nil && newEntry != nil { - did = newEntry.DID - } + newEntry := d.updateHandle(ctx, h) // Cleanup the coalesce map and close the results channel d.handleLookupChans.Delete(h.String()) // Callers waiting will now get the result from the cache close(res) - return did, err + if newEntry.Err != nil { + return "", newEntry.Err + } + if newEntry.DID != "" { + return newEntry.DID, nil + } + return "", fmt.Errorf("unexpected control-flow error") } -func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) (*identityEntry, error) { +func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identityEntry { ident, err := d.Inner.LookupDID(ctx, did) // persist the identity lookup error, instead of processing it immediately entry := identityEntry{ @@ -222,7 +229,9 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) (*identi TTL: d.HitTTL, }) if err != nil { - return nil, err + entry.Identity = nil + entry.Err = err + return entry } if he != nil { err = d.handleCache.Set(&cache.Item{ @@ -232,10 +241,12 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) (*identi TTL: d.HitTTL, }) if err != nil { - return nil, err + entry.Identity = nil + entry.Err = err + return entry } } - return &entry, nil + return entry } func (d *RedisDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { From 23497fa3579ff5708f47ef498b51318269483436 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 01:48:14 -0700 Subject: [PATCH 028/111] redisdir: refactor to *WithCacheState, matching cached directory --- atproto/identity/redisdir/redis_directory.go | 50 +++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 0eee1dde0..936b743f3 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -250,14 +250,19 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identity } func (d *RedisDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { + id, _, err := d.LookupDIDWithCacheState(ctx, did) + return id, err +} + +func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax.DID) (*identity.Identity, bool, error) { var entry identityEntry err := d.identityCache.Get(ctx, redisDirPrefix+did.String(), &entry) if err != nil && err != cache.ErrCacheMiss { - return nil, err + return nil, false, err } if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { identityCacheHits.Inc() - return entry.Identity, entry.Err + return entry.Identity, true, entry.Err } identityCacheMisses.Inc() @@ -272,51 +277,60 @@ func (d *RedisDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identi // The result should now be in the cache err = d.identityCache.Get(ctx, redisDirPrefix+did.String(), &entry) if err != nil && err != cache.ErrCacheMiss { - return nil, err + return nil, false, err } if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { - return entry.Identity, entry.Err + return entry.Identity, false, entry.Err } - return nil, fmt.Errorf("identity not found in cache after coalesce returned") + return nil, false, fmt.Errorf("identity not found in cache after coalesce returned") case <-ctx.Done(): - return nil, ctx.Err() + return nil, false, ctx.Err() } } - var doc *identity.Identity // Update the Identity Entry from PLC and cache the result - newEntry, err := d.updateDID(ctx, did) - if err == nil && newEntry != nil { - doc = newEntry.Identity - } + newEntry := d.updateDID(ctx, did) // Cleanup the coalesce map and close the results channel d.didLookupChans.Delete(did.String()) // Callers waiting will now get the result from the cache close(res) - return doc, err + if newEntry.Err != nil { + return nil, false, newEntry.Err + } + if newEntry.Identity != nil { + return newEntry.Identity, false, nil + } + return nil, false, fmt.Errorf("unexpected control-flow error") } func (d *RedisDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*identity.Identity, error) { + ident, _, err := d.LookupHandleWithCacheState(ctx, h) + return ident, err +} + +func (d *RedisDirectory) LookupHandleWithCacheState(ctx context.Context, h syntax.Handle) (*identity.Identity, bool, error) { + h = h.Normalize() did, err := d.ResolveHandle(ctx, h) if err != nil { - return nil, err + return nil, false, err } - ident, err := d.LookupDID(ctx, did) + ident, hit, err := d.LookupDIDWithCacheState(ctx, did) if err != nil { - return nil, err + return nil, hit, err } declared, err := ident.DeclaredHandle() if err != nil { - return nil, err + return nil, hit, err } if declared != h { - return nil, fmt.Errorf("handle does not match that declared in DID document") + return nil, hit, identity.ErrHandleMismatch } - return ident, nil + return ident, hit, nil } + func (d *RedisDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*identity.Identity, error) { handle, err := a.AsHandle() if nil == err { // if not an error, is a handle From 22f38fbadd908e769ab95bf7218452d432bf49bc Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 02:10:43 -0700 Subject: [PATCH 029/111] fix missing return --- atproto/identity/redisdir/redis_directory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 936b743f3..8344ce783 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -116,6 +116,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand he.Err = err return he } + return he } entry := identityEntry{ From 618b150227f8ede21a710b04e88145f09f8fdf19 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:25:50 -0700 Subject: [PATCH 030/111] fix redisdir purging --- atproto/identity/redisdir/redis_directory.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 8344ce783..b0460f990 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -348,7 +348,7 @@ func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error handle, err := a.AsHandle() if nil == err { // if not an error, is a handle handle = handle.Normalize() - err = d.handleCache.Delete(ctx, handle.String()) + err = d.handleCache.Delete(ctx, redisDirPrefix+handle.String()) if err == cache.ErrCacheMiss { return nil } @@ -356,7 +356,7 @@ func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error } did, err := a.AsDID() if nil == err { // if not an error, is a DID - err = d.identityCache.Delete(ctx, did.String()) + err = d.identityCache.Delete(ctx, redisDirPrefix+did.String()) if err == cache.ErrCacheMiss { return nil } From 7e2b115921c66a0d711d66601f38117d8eb1b18e Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:26:33 -0700 Subject: [PATCH 031/111] redisdir: DID as nullable This fixes a bug where de-serializing the cached object was confusingly resulting in an unmarshal error. --- atproto/identity/redisdir/redis_directory.go | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index b0460f990..5aef9806b 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -33,8 +33,9 @@ type RedisDirectory struct { type handleEntry struct { Updated time.Time - DID syntax.DID - Err error + // needs to be pointer type, because unmarshalling empty string would be an error + DID *syntax.DID + Err error } type identityEntry struct { @@ -102,7 +103,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand if err != nil { he := handleEntry{ Updated: time.Now(), - DID: "", + DID: nil, Err: err, } err = d.handleCache.Set(&cache.Item{ @@ -112,7 +113,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand TTL: d.ErrTTL, }) if err != nil { - he.DID = "" + he.DID = nil he.Err = err return he } @@ -126,7 +127,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand } he := handleEntry{ Updated: time.Now(), - DID: ident.DID, + DID: &ident.DID, Err: nil, } @@ -137,7 +138,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand TTL: d.HitTTL, }) if err != nil { - he.DID = "" + he.DID = nil he.Err = err return he } @@ -148,7 +149,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand TTL: d.HitTTL, }) if err != nil { - he.DID = "" + he.DID = nil he.Err = err return he } @@ -199,8 +200,8 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if newEntry.Err != nil { return "", newEntry.Err } - if newEntry.DID != "" { - return newEntry.DID, nil + if newEntry.DID != nil { + return *newEntry.DID, nil } return "", fmt.Errorf("unexpected control-flow error") } @@ -218,7 +219,7 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identity if nil == err && !ident.Handle.IsInvalidHandle() { he = &handleEntry{ Updated: time.Now(), - DID: did, + DID: &did, Err: nil, } } From 64033934dfcbf04f828c31209458032d0fa46a56 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:34:32 -0700 Subject: [PATCH 032/111] redisdir: better meta error messages --- atproto/identity/redisdir/redis_directory.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 5aef9806b..476a17ed8 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -114,7 +114,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand }) if err != nil { he.DID = nil - he.Err = err + he.Err = fmt.Errorf("identity cache write: %w", err) return he } return he @@ -139,7 +139,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand }) if err != nil { he.DID = nil - he.Err = err + he.Err = fmt.Errorf("identity cache write: %w", err) return he } err = d.handleCache.Set(&cache.Item{ @@ -150,7 +150,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand }) if err != nil { he.DID = nil - he.Err = err + he.Err = fmt.Errorf("identity cache write: %w", err) return he } return he @@ -160,7 +160,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy var entry handleEntry err := d.handleCache.Get(ctx, redisDirPrefix+h.String(), &entry) if err != nil && err != cache.ErrCacheMiss { - return "", err + return "", fmt.Errorf("identity cache read: %w", err) } if err != cache.ErrCacheMiss && !d.isHandleStale(&entry) { handleCacheHits.Inc() @@ -179,7 +179,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy // The result should now be in the cache err := d.handleCache.Get(ctx, redisDirPrefix+h.String(), entry) if err != nil && err != cache.ErrCacheMiss { - return "", err + return "", fmt.Errorf("identity cache read: %w", err) } if err != cache.ErrCacheMiss && !d.isHandleStale(&entry) { return entry.DID, entry.Err @@ -232,7 +232,7 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identity }) if err != nil { entry.Identity = nil - entry.Err = err + entry.Err = fmt.Errorf("identity cache write: %v", err) return entry } if he != nil { @@ -244,7 +244,7 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identity }) if err != nil { entry.Identity = nil - entry.Err = err + entry.Err = fmt.Errorf("identity cache write: %v", err) return entry } } @@ -260,7 +260,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax var entry identityEntry err := d.identityCache.Get(ctx, redisDirPrefix+did.String(), &entry) if err != nil && err != cache.ErrCacheMiss { - return nil, false, err + return nil, false, fmt.Errorf("identity cache read: %v", err) } if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { identityCacheHits.Inc() @@ -279,7 +279,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax // The result should now be in the cache err = d.identityCache.Get(ctx, redisDirPrefix+did.String(), &entry) if err != nil && err != cache.ErrCacheMiss { - return nil, false, err + return nil, false, fmt.Errorf("identity cache read: %v", err) } if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { return entry.Identity, false, entry.Err From f658332e03b819769be9bf5d57605084fc3ecd26 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:35:49 -0700 Subject: [PATCH 033/111] cleanups --- atproto/identity/redisdir/redis_directory.go | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 476a17ed8..6eef690c4 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -162,9 +162,15 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if err != nil && err != cache.ErrCacheMiss { return "", fmt.Errorf("identity cache read: %w", err) } - if err != cache.ErrCacheMiss && !d.isHandleStale(&entry) { + if nil == err && !d.isHandleStale(&entry) { handleCacheHits.Inc() - return entry.DID, entry.Err + if entry.Err != nil { + return "", entry.Err + } else if entry.DID != nil { + return *entry.DID, nil + } else { + return "", fmt.Errorf("code flow error in redis identity directory") + } } handleCacheMisses.Inc() @@ -181,8 +187,14 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if err != nil && err != cache.ErrCacheMiss { return "", fmt.Errorf("identity cache read: %w", err) } - if err != cache.ErrCacheMiss && !d.isHandleStale(&entry) { - return entry.DID, entry.Err + if nil == err && !d.isHandleStale(&entry) { + if entry.Err != nil { + return "", entry.Err + } else if entry.DID != nil { + return *entry.DID, nil + } else { + return "", fmt.Errorf("code flow error in redis identity directory") + } } return "", fmt.Errorf("identity not found in cache after coalesce returned") case <-ctx.Done(): @@ -192,6 +204,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy // Update the Handle Entry from PLC and cache the result newEntry := d.updateHandle(ctx, h) + // Cleanup the coalesce map and close the results channel d.handleLookupChans.Delete(h.String()) // Callers waiting will now get the result from the cache @@ -262,7 +275,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if err != nil && err != cache.ErrCacheMiss { return nil, false, fmt.Errorf("identity cache read: %v", err) } - if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { + if nil == err && !d.isIdentityStale(&entry) { identityCacheHits.Inc() return entry.Identity, true, entry.Err } @@ -281,7 +294,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if err != nil && err != cache.ErrCacheMiss { return nil, false, fmt.Errorf("identity cache read: %v", err) } - if err != cache.ErrCacheMiss && !d.isIdentityStale(&entry) { + if nil == err && !d.isIdentityStale(&entry) { return entry.Identity, false, entry.Err } return nil, false, fmt.Errorf("identity not found in cache after coalesce returned") @@ -292,6 +305,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax // Update the Identity Entry from PLC and cache the result newEntry := d.updateDID(ctx, did) + // Cleanup the coalesce map and close the results channel d.didLookupChans.Delete(did.String()) // Callers waiting will now get the result from the cache @@ -332,7 +346,6 @@ func (d *RedisDirectory) LookupHandleWithCacheState(ctx context.Context, h synta return ident, hit, nil } - func (d *RedisDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*identity.Identity, error) { handle, err := a.AsHandle() if nil == err { // if not an error, is a handle From a2efc12f5185e673b6b44c687e8d7439e131cf56 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:37:07 -0700 Subject: [PATCH 034/111] identity: break out cache metrics declarations --- atproto/identity/cache_directory.go | 32 ------------------------- atproto/identity/metrics.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 32 deletions(-) create mode 100644 atproto/identity/metrics.go diff --git a/atproto/identity/cache_directory.go b/atproto/identity/cache_directory.go index dad78fe82..6989049a3 100644 --- a/atproto/identity/cache_directory.go +++ b/atproto/identity/cache_directory.go @@ -7,8 +7,6 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "github.com/hashicorp/golang-lru/v2/expirable" ) @@ -35,36 +33,6 @@ type IdentityEntry struct { Err error } -var handleCacheHits = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_handle_cache_hits", - Help: "Number of cache hits for ATProto handle lookups", -}) - -var handleCacheMisses = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_handle_cache_misses", - Help: "Number of cache misses for ATProto handle lookups", -}) - -var identityCacheHits = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_identity_cache_hits", - Help: "Number of cache hits for ATProto identity lookups", -}) - -var identityCacheMisses = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_identity_cache_misses", - Help: "Number of cache misses for ATProto identity lookups", -}) - -var identityRequestsCoalesced = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_identity_requests_coalesced", - Help: "Number of identity requests coalesced", -}) - -var handleRequestsCoalesced = promauto.NewCounter(prometheus.CounterOpts{ - Name: "atproto_directory_handle_requests_coalesced", - Help: "Number of handle requests coalesced", -}) - var _ Directory = (*CacheDirectory)(nil) // Capacity of zero means unlimited size. Similarly, ttl of zero means unlimited duration. diff --git a/atproto/identity/metrics.go b/atproto/identity/metrics.go new file mode 100644 index 000000000..d6061c9ce --- /dev/null +++ b/atproto/identity/metrics.go @@ -0,0 +1,36 @@ +package identity + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var handleCacheHits = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_handle_cache_hits", + Help: "Number of cache hits for ATProto handle lookups", +}) + +var handleCacheMisses = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_handle_cache_misses", + Help: "Number of cache misses for ATProto handle lookups", +}) + +var identityCacheHits = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_identity_cache_hits", + Help: "Number of cache hits for ATProto identity lookups", +}) + +var identityCacheMisses = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_identity_cache_misses", + Help: "Number of cache misses for ATProto identity lookups", +}) + +var identityRequestsCoalesced = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_identity_requests_coalesced", + Help: "Number of identity requests coalesced", +}) + +var handleRequestsCoalesced = promauto.NewCounter(prometheus.CounterOpts{ + Name: "atproto_directory_handle_requests_coalesced", + Help: "Number of handle requests coalesced", +}) From de7eb52ac7454d264ae0849e230b83200dfa749b Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:37:48 -0700 Subject: [PATCH 035/111] identity: catch invalid handle sooner --- atproto/identity/cache_directory.go | 3 +++ atproto/identity/handle.go | 4 ++++ atproto/identity/redisdir/redis_directory.go | 3 +++ 3 files changed, 10 insertions(+) diff --git a/atproto/identity/cache_directory.go b/atproto/identity/cache_directory.go index 6989049a3..2df1ac5c5 100644 --- a/atproto/identity/cache_directory.go +++ b/atproto/identity/cache_directory.go @@ -92,6 +92,9 @@ func (d *CacheDirectory) updateHandle(ctx context.Context, h syntax.Handle) Hand } func (d *CacheDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { + if h.IsInvalidHandle() { + return "", fmt.Errorf("invalid handle") + } entry, ok := d.handleCache.Get(h) if ok && !d.IsHandleStale(&entry) { handleCacheHits.Inc() diff --git a/atproto/identity/handle.go b/atproto/identity/handle.go index 024ac6e32..77a157f85 100644 --- a/atproto/identity/handle.go +++ b/atproto/identity/handle.go @@ -168,6 +168,10 @@ func (d *BaseDirectory) ResolveHandle(ctx context.Context, handle syntax.Handle) var dnsErr error var did syntax.DID + if handle.IsInvalidHandle() { + return "", fmt.Errorf("invalid handle") + } + if !handle.AllowedTLD() { return "", ErrHandleReservedTLD } diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 6eef690c4..d8369382e 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -157,6 +157,9 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand } func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { + if h.IsInvalidHandle() { + return "", fmt.Errorf("invalid handle") + } var entry handleEntry err := d.handleCache.Get(ctx, redisDirPrefix+h.String(), &entry) if err != nil && err != cache.ErrCacheMiss { From f570fec249bbbaf9f7241a9d4d803860de10d544 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:41:47 -0700 Subject: [PATCH 036/111] redisdir live test (skipped) --- atproto/identity/redisdir/live_test.go | 136 +++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 atproto/identity/redisdir/live_test.go diff --git a/atproto/identity/redisdir/live_test.go b/atproto/identity/redisdir/live_test.go new file mode 100644 index 000000000..433c45b8c --- /dev/null +++ b/atproto/identity/redisdir/live_test.go @@ -0,0 +1,136 @@ +package redisdir + +import ( + "context" + "log/slog" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" + "golang.org/x/time/rate" + + "github.com/stretchr/testify/assert" +) + +var redisLocalTestURL string = "redis://localhost:6379/0" + +// NOTE: this hits the open internet! marked as skip below by default +func testDirectoryLive(t *testing.T, d identity.Directory) { + assert := assert.New(t) + ctx := context.Background() + + handle := syntax.Handle("atproto.com") + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + pdsSuffix := "host.bsky.network" + + resp, err := d.LookupHandle(ctx, handle) + assert.NoError(err) + assert.Equal(handle, resp.Handle) + assert.Equal(did, resp.DID) + assert.True(strings.HasSuffix(resp.PDSEndpoint(), pdsSuffix)) + dh, err := resp.DeclaredHandle() + assert.NoError(err) + assert.Equal(handle, dh) + pk, err := resp.PublicKey() + assert.NoError(err) + assert.NotNil(pk) + + resp, err = d.LookupDID(ctx, did) + assert.NoError(err) + assert.Equal(handle, resp.Handle) + assert.Equal(did, resp.DID) + assert.True(strings.HasSuffix(resp.PDSEndpoint(), pdsSuffix)) + + _, err = d.LookupHandle(ctx, syntax.Handle("fake-dummy-no-resolve.atproto.com")) + assert.Error(err) + //assert.ErrorIs(err, identity.ErrHandleNotFound) + + _, err = d.LookupDID(ctx, syntax.DID("did:web:fake-dummy-no-resolve.atproto.com")) + assert.Error(err) + //assert.ErrorIs(err, identity.ErrDIDNotFound) + + _, err = d.LookupDID(ctx, syntax.DID("did:plc:fake-dummy-no-resolve.atproto.com")) + assert.Error(err) + //assert.ErrorIs(err, identity.ErrDIDNotFound) + + _, err = d.LookupHandle(ctx, syntax.HandleInvalid) + assert.Error(err) +} + +func TestRedisDirectory(t *testing.T) { + t.Skip("TODO: skipping live network test") + assert := assert.New(t) + ctx := context.Background() + inner := identity.BaseDirectory{} + d, err := NewRedisDirectory(&inner, redisLocalTestURL, time.Hour*1, time.Hour*1, time.Hour*1, 1000) + if err != nil { + t.Fatal(err) + } + + err = d.Purge(ctx, syntax.Handle("atproto.com").AtIdentifier()) + assert.NoError(err) + err = d.Purge(ctx, syntax.Handle("fake-dummy-no-resolve.atproto.com").AtIdentifier()) + assert.NoError(err) + err = d.Purge(ctx, syntax.DID("did:web:fake-dummy-no-resolve.atproto.com").AtIdentifier()) + assert.NoError(err) + err = d.Purge(ctx, syntax.DID("did:plc:fake-dummy-no-resolve.atproto.com").AtIdentifier()) + assert.NoError(err) + + for i := 0; i < 3; i = i + 1 { + testDirectoryLive(t, d) + } +} + +func TestRedisCoalesce(t *testing.T) { + t.Skip("TODO: skipping live network test") + + assert := assert.New(t) + handle := syntax.Handle("atproto.com") + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + + base := identity.BaseDirectory{ + PLCURL: "https://plc.directory", + HTTPClient: http.Client{ + Timeout: time.Second * 15, + }, + // Limit the number of requests we can make to the PLC to 1 per second + PLCLimiter: rate.NewLimiter(1, 1), + TryAuthoritativeDNS: true, + SkipDNSDomainSuffixes: []string{".bsky.social"}, + } + dir, err := NewRedisDirectory(&base, redisLocalTestURL, time.Hour*1, time.Hour*1, time.Hour*1, 1000) + if err != nil { + t.Fatal(err) + } + // All 60 routines launch at the same time, so they should all miss the cache initially + routines := 60 + wg := sync.WaitGroup{} + + // Cancel the context after 2 seconds, if we're coalescing correctly, we should only make 1 request + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + for i := 0; i < routines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + ident, err := dir.LookupDID(ctx, did) + if err != nil { + slog.Error("Failed lookup", "error", err) + } + assert.NoError(err) + assert.Equal(handle, ident.Handle) + + ident, err = dir.LookupHandle(ctx, handle) + if err != nil { + slog.Error("Failed lookup", "error", err) + } + assert.NoError(err) + assert.Equal(did, ident.DID) + }() + } + wg.Wait() +} From a8f3a4edaeb606d8467fe21dfc5594f11be6160e Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 25 Sep 2024 03:53:06 -0700 Subject: [PATCH 037/111] hepa: set invalid handle TTL to 5min --- cmd/hepa/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/hepa/main.go b/cmd/hepa/main.go index 6cc0bcb82..dbef6c488 100644 --- a/cmd/hepa/main.go +++ b/cmd/hepa/main.go @@ -172,7 +172,7 @@ func configDirectory(cctx *cli.Context) (identity.Directory, error) { } var dir identity.Directory if cctx.String("redis-url") != "" { - rdir, err := redisdir.NewRedisDirectory(&baseDir, cctx.String("redis-url"), time.Hour*24, time.Minute*2, 10_000) + rdir, err := redisdir.NewRedisDirectory(&baseDir, cctx.String("redis-url"), time.Hour*24, time.Minute*2, time.Minute*5, 10_000) if err != nil { return nil, err } From e022b4976f566510365cbc589fcd04438d3e77af Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 26 Sep 2024 13:35:51 -0700 Subject: [PATCH 038/111] switch from fmt.Errorf to errors.New for static strings --- atproto/syntax/atidentifier.go | 8 ++++---- atproto/syntax/aturi.go | 5 +++-- atproto/syntax/cid.go | 12 ++++++------ atproto/syntax/datetime.go | 9 +++++---- atproto/syntax/did.go | 8 ++++---- atproto/syntax/handle.go | 5 +++-- atproto/syntax/language.go | 8 ++++---- atproto/syntax/nsid.go | 8 ++++---- atproto/syntax/recordkey.go | 10 +++++----- atproto/syntax/tid.go | 8 ++++---- atproto/syntax/uri.go | 8 ++++---- 11 files changed, 46 insertions(+), 43 deletions(-) diff --git a/atproto/syntax/atidentifier.go b/atproto/syntax/atidentifier.go index be5315c3f..ee7e34e53 100644 --- a/atproto/syntax/atidentifier.go +++ b/atproto/syntax/atidentifier.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "strings" ) @@ -11,7 +11,7 @@ type AtIdentifier struct { func ParseAtIdentifier(raw string) (*AtIdentifier, error) { if raw == "" { - return nil, fmt.Errorf("expected AT account identifier, got empty string") + return nil, errors.New("expected AT account identifier, got empty string") } if strings.HasPrefix(raw, "did:") { did, err := ParseDID(raw) @@ -37,7 +37,7 @@ func (n AtIdentifier) AsHandle() (Handle, error) { if ok { return handle, nil } - return "", fmt.Errorf("AT Identifier is not a Handle") + return "", errors.New("AT Identifier is not a Handle") } func (n AtIdentifier) IsDID() bool { @@ -50,7 +50,7 @@ func (n AtIdentifier) AsDID() (DID, error) { if ok { return did, nil } - return "", fmt.Errorf("AT Identifier is not a DID") + return "", errors.New("AT Identifier is not a DID") } func (n AtIdentifier) Normalize() AtIdentifier { diff --git a/atproto/syntax/aturi.go b/atproto/syntax/aturi.go index 0084a67c9..0ca801f33 100644 --- a/atproto/syntax/aturi.go +++ b/atproto/syntax/aturi.go @@ -1,6 +1,7 @@ package syntax import ( + "errors" "fmt" "regexp" "strings" @@ -17,11 +18,11 @@ type ATURI string func ParseATURI(raw string) (ATURI, error) { if len(raw) > 8192 { - return "", fmt.Errorf("ATURI is too long (8192 chars max)") + return "", errors.New("ATURI is too long (8192 chars max)") } parts := aturiRegex.FindStringSubmatch(raw) if parts == nil || len(parts) < 2 || parts[0] == "" { - return "", fmt.Errorf("AT-URI syntax didn't validate via regex") + return "", errors.New("AT-URI syntax didn't validate via regex") } // verify authority as either a DID or NSID _, err := ParseAtIdentifier(parts[1]) diff --git a/atproto/syntax/cid.go b/atproto/syntax/cid.go index 8f994c876..c6ad616c6 100644 --- a/atproto/syntax/cid.go +++ b/atproto/syntax/cid.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" "strings" ) @@ -17,20 +17,20 @@ var cidRegex = regexp.MustCompile(`^[a-zA-Z0-9+=]{8,256}$`) func ParseCID(raw string) (CID, error) { if raw == "" { - return "", fmt.Errorf("expected CID, got empty string") + return "", errors.New("expected CID, got empty string") } if len(raw) > 256 { - return "", fmt.Errorf("CID is too long (256 chars max)") + return "", errors.New("CID is too long (256 chars max)") } if len(raw) < 8 { - return "", fmt.Errorf("CID is too short (8 chars min)") + return "", errors.New("CID is too short (8 chars min)") } if !cidRegex.MatchString(raw) { - return "", fmt.Errorf("CID syntax didn't validate via regex") + return "", errors.New("CID syntax didn't validate via regex") } if strings.HasPrefix(raw, "Qmb") { - return "", fmt.Errorf("CIDv0 not allowed in this version of atproto") + return "", errors.New("CIDv0 not allowed in this version of atproto") } return CID(raw), nil } diff --git a/atproto/syntax/datetime.go b/atproto/syntax/datetime.go index f1a819bc8..21b33fcde 100644 --- a/atproto/syntax/datetime.go +++ b/atproto/syntax/datetime.go @@ -1,6 +1,7 @@ package syntax import ( + "errors" "fmt" "regexp" "strings" @@ -25,17 +26,17 @@ var datetimeRegex = regexp.MustCompile(`^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9 func ParseDatetime(raw string) (Datetime, error) { if raw == "" { - return "", fmt.Errorf("expected datetime, got empty string") + return "", errors.New("expected datetime, got empty string") } if len(raw) > 64 { - return "", fmt.Errorf("Datetime too long (max 64 chars)") + return "", errors.New("Datetime too long (max 64 chars)") } if !datetimeRegex.MatchString(raw) { - return "", fmt.Errorf("Datetime syntax didn't validate via regex") + return "", errors.New("Datetime syntax didn't validate via regex") } if strings.HasSuffix(raw, "-00:00") { - return "", fmt.Errorf("Datetime can't use '-00:00' for UTC timezone, must use '+00:00', per ISO-8601") + return "", errors.New("Datetime can't use '-00:00' for UTC timezone, must use '+00:00', per ISO-8601") } // ensure that the datetime actually parses using golang time lib _, err := time.Parse(time.RFC3339Nano, raw) diff --git a/atproto/syntax/did.go b/atproto/syntax/did.go index d356dc4fe..ef9c0e04f 100644 --- a/atproto/syntax/did.go +++ b/atproto/syntax/did.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" "strings" ) @@ -17,13 +17,13 @@ var didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$` func ParseDID(raw string) (DID, error) { if raw == "" { - return "", fmt.Errorf("expected DID, got empty string") + return "", errors.New("expected DID, got empty string") } if len(raw) > 2*1024 { - return "", fmt.Errorf("DID is too long (2048 chars max)") + return "", errors.New("DID is too long (2048 chars max)") } if !didRegex.MatchString(raw) { - return "", fmt.Errorf("DID syntax didn't validate via regex") + return "", errors.New("DID syntax didn't validate via regex") } return DID(raw), nil } diff --git a/atproto/syntax/handle.go b/atproto/syntax/handle.go index 2c52f9dbe..b903698b2 100644 --- a/atproto/syntax/handle.go +++ b/atproto/syntax/handle.go @@ -1,6 +1,7 @@ package syntax import ( + "errors" "fmt" "regexp" "strings" @@ -22,10 +23,10 @@ type Handle string func ParseHandle(raw string) (Handle, error) { if raw == "" { - return "", fmt.Errorf("expected handle, got empty string") + return "", errors.New("expected handle, got empty string") } if len(raw) > 253 { - return "", fmt.Errorf("handle is too long (253 chars max)") + return "", errors.New("handle is too long (253 chars max)") } if !handleRegex.MatchString(raw) { return "", fmt.Errorf("handle syntax didn't validate via regex: %s", raw) diff --git a/atproto/syntax/language.go b/atproto/syntax/language.go index f65683d27..59a9d3322 100644 --- a/atproto/syntax/language.go +++ b/atproto/syntax/language.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" ) @@ -16,13 +16,13 @@ var langRegex = regexp.MustCompile(`^(i|[a-z]{2,3})(-[a-zA-Z0-9]+)*$`) func ParseLanguage(raw string) (Language, error) { if raw == "" { - return "", fmt.Errorf("expected language code, got empty string") + return "", errors.New("expected language code, got empty string") } if len(raw) > 128 { - return "", fmt.Errorf("Language is too long (128 chars max)") + return "", errors.New("Language is too long (128 chars max)") } if !langRegex.MatchString(raw) { - return "", fmt.Errorf("Language syntax didn't validate via regex") + return "", errors.New("Language syntax didn't validate via regex") } return Language(raw), nil } diff --git a/atproto/syntax/nsid.go b/atproto/syntax/nsid.go index 9b0279265..ba5c51620 100644 --- a/atproto/syntax/nsid.go +++ b/atproto/syntax/nsid.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" "strings" ) @@ -17,13 +17,13 @@ type NSID string func ParseNSID(raw string) (NSID, error) { if raw == "" { - return "", fmt.Errorf("expected NSID, got empty string") + return "", errors.New("expected NSID, got empty string") } if len(raw) > 317 { - return "", fmt.Errorf("NSID is too long (317 chars max)") + return "", errors.New("NSID is too long (317 chars max)") } if !nsidRegex.MatchString(raw) { - return "", fmt.Errorf("NSID syntax didn't validate via regex") + return "", errors.New("NSID syntax didn't validate via regex") } return NSID(raw), nil } diff --git a/atproto/syntax/recordkey.go b/atproto/syntax/recordkey.go index 541a99c38..c8af66893 100644 --- a/atproto/syntax/recordkey.go +++ b/atproto/syntax/recordkey.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" ) @@ -16,16 +16,16 @@ type RecordKey string func ParseRecordKey(raw string) (RecordKey, error) { if raw == "" { - return "", fmt.Errorf("expected record key, got empty string") + return "", errors.New("expected record key, got empty string") } if len(raw) > 512 { - return "", fmt.Errorf("recordkey is too long (512 chars max)") + return "", errors.New("recordkey is too long (512 chars max)") } if raw == "" || raw == "." || raw == ".." { - return "", fmt.Errorf("recordkey can not be empty, '.', or '..'") + return "", errors.New("recordkey can not be empty, '.', or '..'") } if !recordKeyRegex.MatchString(raw) { - return "", fmt.Errorf("recordkey syntax didn't validate via regex") + return "", errors.New("recordkey syntax didn't validate via regex") } return RecordKey(raw), nil } diff --git a/atproto/syntax/tid.go b/atproto/syntax/tid.go index 53a131482..ebcb853ac 100644 --- a/atproto/syntax/tid.go +++ b/atproto/syntax/tid.go @@ -2,7 +2,7 @@ package syntax import ( "encoding/base32" - "fmt" + "errors" "regexp" "strings" "sync" @@ -28,13 +28,13 @@ var tidRegex = regexp.MustCompile(`^[234567abcdefghij][234567abcdefghijklmnopqrs func ParseTID(raw string) (TID, error) { if raw == "" { - return "", fmt.Errorf("expected TID, got empty string") + return "", errors.New("expected TID, got empty string") } if len(raw) != 13 { - return "", fmt.Errorf("TID is wrong length (expected 13 chars)") + return "", errors.New("TID is wrong length (expected 13 chars)") } if !tidRegex.MatchString(raw) { - return "", fmt.Errorf("TID syntax didn't validate via regex") + return "", errors.New("TID syntax didn't validate via regex") } return TID(raw), nil } diff --git a/atproto/syntax/uri.go b/atproto/syntax/uri.go index 4b069d3cc..fbf8807c4 100644 --- a/atproto/syntax/uri.go +++ b/atproto/syntax/uri.go @@ -1,7 +1,7 @@ package syntax import ( - "fmt" + "errors" "regexp" ) @@ -14,14 +14,14 @@ type URI string func ParseURI(raw string) (URI, error) { if raw == "" { - return "", fmt.Errorf("expected URI, got empty string") + return "", errors.New("expected URI, got empty string") } if len(raw) > 8192 { - return "", fmt.Errorf("URI is too long (8192 chars max)") + return "", errors.New("URI is too long (8192 chars max)") } var uriRegex = regexp.MustCompile(`^[a-z][a-z.-]{0,80}:[[:graph:]]+$`) if !uriRegex.MatchString(raw) { - return "", fmt.Errorf("URI syntax didn't validate via regex") + return "", errors.New("URI syntax didn't validate via regex") } return URI(raw), nil } From 4e43ca6562c13350c80f99fc17a18b44c38a823f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 26 Sep 2024 16:32:45 -0700 Subject: [PATCH 039/111] mst: clarify in comment that fields are nullable, not optional --- mst/mst.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mst/mst.go b/mst/mst.go index ee648add4..23810b01e 100644 --- a/mst/mst.go +++ b/mst/mst.go @@ -113,7 +113,7 @@ func CBORTypes() []reflect.Type { // MST tree node as gets serialized to CBOR. Note that the CBOR fields are all // single-character. type nodeData struct { - Left *cid.Cid `cborgen:"l"` // [optional] pointer to lower-level subtree to the "left" of this path/key + Left *cid.Cid `cborgen:"l"` // [nullable] pointer to lower-level subtree to the "left" of this path/key Entries []treeEntry `cborgen:"e"` // ordered list of entries at this node } @@ -122,7 +122,7 @@ type treeEntry struct { PrefixLen int64 `cborgen:"p"` // count of characters shared with previous path/key in tree KeySuffix []byte `cborgen:"k"` // remaining part of path/key (appended to "previous key") Val cid.Cid `cborgen:"v"` // CID pointer at this path/key - Tree *cid.Cid `cborgen:"t"` // [optional] pointer to lower-level subtree to the "right" of this path/key entry + Tree *cid.Cid `cborgen:"t"` // [nullable] pointer to lower-level subtree to the "right" of this path/key entry } // MerkleSearchTree represents an MST tree node (NodeData type). It can be in From eb4057413c327e49242e614d546118ef90f591a0 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 27 Sep 2024 17:22:00 -0700 Subject: [PATCH 040/111] add warning about error equality --- atproto/identity/redisdir/redis_directory.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index d8369382e..26e937abf 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -51,6 +51,8 @@ var _ identity.Directory = (*RedisDirectory)(nil) // `redisURL` contains all the redis connection config options. // `hitTTL` and `errTTL` define how long successful and errored identity metadata should be cached (respectively). errTTL is expected to be shorted than hitTTL. // `lruSize` is the size of the in-process cache, for each of the handle and identity caches. 10000 is a reasonable default. +// +// NOTE: Errors returned may be inconsistent with the base directory, or between calls. This is because cached errors are serialized/deserialized and that may break equality checks. func NewRedisDirectory(inner identity.Directory, redisURL string, hitTTL, errTTL, invalidHandleTTL time.Duration, lruSize int) (*RedisDirectory, error) { opt, err := redis.ParseURL(redisURL) if err != nil { From 873a7392139277a3d20fd446940ff1275f2c5577 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 27 Sep 2024 17:23:19 -0700 Subject: [PATCH 041/111] flip err/nill checks back around --- atproto/identity/redisdir/redis_directory.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index 26e937abf..c769d42c6 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -167,7 +167,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if err != nil && err != cache.ErrCacheMiss { return "", fmt.Errorf("identity cache read: %w", err) } - if nil == err && !d.isHandleStale(&entry) { + if err == nil && !d.isHandleStale(&entry) { // if no error... handleCacheHits.Inc() if entry.Err != nil { return "", entry.Err @@ -192,7 +192,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if err != nil && err != cache.ErrCacheMiss { return "", fmt.Errorf("identity cache read: %w", err) } - if nil == err && !d.isHandleStale(&entry) { + if err == nil && !d.isHandleStale(&entry) { // if no error... if entry.Err != nil { return "", entry.Err } else if entry.DID != nil { @@ -234,7 +234,7 @@ func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identity } var he *handleEntry // if *not* an error, then also update the handle cache - if nil == err && !ident.Handle.IsInvalidHandle() { + if err == nil && !ident.Handle.IsInvalidHandle() { he = &handleEntry{ Updated: time.Now(), DID: &did, @@ -280,7 +280,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if err != nil && err != cache.ErrCacheMiss { return nil, false, fmt.Errorf("identity cache read: %v", err) } - if nil == err && !d.isIdentityStale(&entry) { + if err == nil && !d.isIdentityStale(&entry) { // if no error... identityCacheHits.Inc() return entry.Identity, true, entry.Err } @@ -299,7 +299,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if err != nil && err != cache.ErrCacheMiss { return nil, false, fmt.Errorf("identity cache read: %v", err) } - if nil == err && !d.isIdentityStale(&entry) { + if err == nil && !d.isIdentityStale(&entry) { // if no error... return entry.Identity, false, entry.Err } return nil, false, fmt.Errorf("identity not found in cache after coalesce returned") @@ -353,11 +353,11 @@ func (d *RedisDirectory) LookupHandleWithCacheState(ctx context.Context, h synta func (d *RedisDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*identity.Identity, error) { handle, err := a.AsHandle() - if nil == err { // if not an error, is a handle + if err == nil { // if not an error, is a handle return d.LookupHandle(ctx, handle) } did, err := a.AsDID() - if nil == err { // if not an error, is a DID + if err == nil { // if not an error, is a DID return d.LookupDID(ctx, did) } return nil, fmt.Errorf("at-identifier neither a Handle nor a DID") @@ -365,7 +365,7 @@ func (d *RedisDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*id func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error { handle, err := a.AsHandle() - if nil == err { // if not an error, is a handle + if err == nil { // if not an error, is a handle handle = handle.Normalize() err = d.handleCache.Delete(ctx, redisDirPrefix+handle.String()) if err == cache.ErrCacheMiss { @@ -374,7 +374,7 @@ func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error return err } did, err := a.AsDID() - if nil == err { // if not an error, is a DID + if err == nil { // if not an error, is a DID err = d.identityCache.Delete(ctx, redisDirPrefix+did.String()) if err == cache.ErrCacheMiss { return nil From dc4176cd475c5d891f61daf48ba9f2b20fdfefd4 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 27 Sep 2024 17:24:29 -0700 Subject: [PATCH 042/111] replace fmt.Errorf with errors.New for static strings --- atproto/identity/redisdir/redis_directory.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/atproto/identity/redisdir/redis_directory.go b/atproto/identity/redisdir/redis_directory.go index c769d42c6..f1d67f256 100644 --- a/atproto/identity/redisdir/redis_directory.go +++ b/atproto/identity/redisdir/redis_directory.go @@ -2,6 +2,7 @@ package redisdir import ( "context" + "errors" "fmt" "sync" "time" @@ -160,7 +161,7 @@ func (d *RedisDirectory) updateHandle(ctx context.Context, h syntax.Handle) hand func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { if h.IsInvalidHandle() { - return "", fmt.Errorf("invalid handle") + return "", errors.New("invalid handle") } var entry handleEntry err := d.handleCache.Get(ctx, redisDirPrefix+h.String(), &entry) @@ -174,7 +175,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy } else if entry.DID != nil { return *entry.DID, nil } else { - return "", fmt.Errorf("code flow error in redis identity directory") + return "", errors.New("code flow error in redis identity directory") } } handleCacheMisses.Inc() @@ -198,10 +199,10 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy } else if entry.DID != nil { return *entry.DID, nil } else { - return "", fmt.Errorf("code flow error in redis identity directory") + return "", errors.New("code flow error in redis identity directory") } } - return "", fmt.Errorf("identity not found in cache after coalesce returned") + return "", errors.New("identity not found in cache after coalesce returned") case <-ctx.Done(): return "", ctx.Err() } @@ -221,7 +222,7 @@ func (d *RedisDirectory) ResolveHandle(ctx context.Context, h syntax.Handle) (sy if newEntry.DID != nil { return *newEntry.DID, nil } - return "", fmt.Errorf("unexpected control-flow error") + return "", errors.New("unexpected control-flow error") } func (d *RedisDirectory) updateDID(ctx context.Context, did syntax.DID) identityEntry { @@ -302,7 +303,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if err == nil && !d.isIdentityStale(&entry) { // if no error... return entry.Identity, false, entry.Err } - return nil, false, fmt.Errorf("identity not found in cache after coalesce returned") + return nil, false, errors.New("identity not found in cache after coalesce returned") case <-ctx.Done(): return nil, false, ctx.Err() } @@ -322,7 +323,7 @@ func (d *RedisDirectory) LookupDIDWithCacheState(ctx context.Context, did syntax if newEntry.Identity != nil { return newEntry.Identity, false, nil } - return nil, false, fmt.Errorf("unexpected control-flow error") + return nil, false, errors.New("unexpected control-flow error") } func (d *RedisDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*identity.Identity, error) { @@ -360,7 +361,7 @@ func (d *RedisDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*id if err == nil { // if not an error, is a DID return d.LookupDID(ctx, did) } - return nil, fmt.Errorf("at-identifier neither a Handle nor a DID") + return nil, errors.New("at-identifier neither a Handle nor a DID") } func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error { @@ -381,5 +382,5 @@ func (d *RedisDirectory) Purge(ctx context.Context, a syntax.AtIdentifier) error } return err } - return fmt.Errorf("at-identifier neither a Handle nor a DID") + return errors.New("at-identifier neither a Handle nor a DID") } From bca3b61578d13a1a1fcf656f1cf9f2bfb06d9bb0 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 00:35:40 -0700 Subject: [PATCH 043/111] early work on account migration --- cmd/goat/account.go | 1 + cmd/goat/account_migrate.go | 215 ++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 cmd/goat/account_migrate.go diff --git a/cmd/goat/account.go b/cmd/goat/account.go index 10952f9f1..de11686e2 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -53,6 +53,7 @@ var cmdAccount = &cli.Command{ ArgsUsage: ``, Action: runAccountStatus, }, + cmdAccountMigrate, }, } diff --git a/cmd/goat/account_migrate.go b/cmd/goat/account_migrate.go new file mode 100644 index 000000000..128df1ecd --- /dev/null +++ b/cmd/goat/account_migrate.go @@ -0,0 +1,215 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + appbsky "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" + + "github.com/urfave/cli/v2" +) + +var cmdAccountMigrate = &cli.Command{ + Name: "migrate", + Usage: "move account to a new PDS. requires full auth.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "new-handle", + Required: true, + Usage: "handle on new PDS", + EnvVars: []string{"NEW_ACCOUNT_HANDLE"}, + }, + &cli.StringFlag{ + Name: "new-password", + Required: true, + Usage: "password on new PDS", + EnvVars: []string{"NEW_ACCOUNT_PASSWORD"}, + }, + }, + Action: runAccountMigrate, +} + +func runAccountMigrate(cctx *cli.Context) error { + // TODO: this could check rev / commit before and after + ctx := context.Background() + + oldClient, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + did := oldClient.Auth.Did + + // connect to new host to discover service DID + newHostURL := "XXX" + newHandle := "XXX" + newPassword := "XXX" + newEmail := "XXX" + inviteCode := "XXX" + newClient := xrpc.Client{ + Host: newHostURL, + } + newHostDesc, err := comatproto.ServerDescribeServer(ctx, &newClient) + if err != nil { + return fmt.Errorf("failed connecting to new host: %w", err) + } + newHostDID, err := syntax.ParseDID(newHostDesc.Did) + if err != nil { + return err + } + slog.Info("new host", "did", newHostDID, "url", newHostURL) + + // 1. Create New Account + slog.Info("creating account on new host", "handle", newHandle) + + // get service auth token from old host + // args: (ctx, client, aud string, exp int64, lxm string) + expTimestamp := time.Now().Unix() + 60 + createAuthResp, err := comatproto.ServerGetServiceAuth(ctx, oldClient, newHostDID.String(), expTimestamp, "com.atproto.server.createAccount") + if err != nil { + return fmt.Errorf("failed getting service auth token from old host: %w", err) + } + + // then create the new account + // XXX: "Authorization Bearer " + _ = createAuthResp + createAccountResp, err := comatproto.ServerCreateAccount(ctx, &newClient, &comatproto.ServerCreateAccount_Input{ + Did: &did, + Email: &newEmail, + Handle: newHandle, + InviteCode: &inviteCode, + Password: &newPassword, + }) + if err != nil { + return fmt.Errorf("failed getting service auth token from old host: %w", err) + } + // TODO: validate response? + _ = createAccountResp + + // login client on the new host + sess, err := comatproto.ServerCreateSession(ctx, &newClient, &comatproto.ServerCreateSession_Input{ + Identifier: did, + Password: newPassword, + }) + if err != nil { + return fmt.Errorf("failed login to newly created account on new host: %w", err) + } + newClient.Auth = &xrpc.AuthInfo{ + Did: did, + AccessJwt: sess.AccessJwt, + RefreshJwt: sess.RefreshJwt, + } + + // 2. Migrate Data + + slog.Info("migrating repo") + repoBytes, err := comatproto.SyncGetRepo(ctx, oldClient, did, "") + if err != nil { + return fmt.Errorf("failed exporting repo: %w", err) + } + err = comatproto.RepoImportRepo(ctx, &newClient, bytes.NewReader(repoBytes)) + if err != nil { + return fmt.Errorf("failed importing repo: %w", err) + } + + slog.Info("migrating preferences") + // TODO: service proxy header + prefResp, err := appbsky.ActorGetPreferences(ctx, oldClient) + if err != nil { + return fmt.Errorf("failed fetching old preferences: %w", err) + } + err = appbsky.ActorPutPreferences(ctx, &newClient, &appbsky.ActorPutPreferences_Input{ + Preferences: prefResp.Preferences, + }) + + slog.Info("migrating blobs") + blobCursor := "" + for { + listResp, err := comatproto.SyncListBlobs(ctx, oldClient, blobCursor, did, 100, "") + if err != nil { + return fmt.Errorf("failed listing blobs: %w", err) + } + for _, blobCID := range listResp.Cids { + blobBytes, err := comatproto.SyncGetBlob(ctx, oldClient, blobCID, did) + if err != nil { + slog.Warn("failed downloading blob", "cid", blobCID, "err", err) + continue + } + _, err = comatproto.RepoUploadBlob(ctx, &newClient, bytes.NewReader(blobBytes)) + if err != nil { + slog.Warn("failed uploading blob", "cid", blobCID, "err", err, "size", len(blobBytes)) + } + slog.Info("transfered blob", "cid", blobCID, "size", len(blobBytes)) + } + if listResp.Cursor == nil || *listResp.Cursor == "" { + break + } + blobCursor = *listResp.Cursor + } + + // display migration status + // TODO: should this loop? with a delay? + statusResp, err := comatproto.ServerCheckAccountStatus(ctx, &newClient) + if err != nil { + return fmt.Errorf("failed checking account status: %w", err) + } + slog.Info("account migration status", "status", statusResp) + + // 3. Migrate Identity + // TODO: support did:web etc + slog.Info("updating identity to new host") + + credsResp, err := comatproto.IdentityGetRecommendedDidCredentials(ctx, &newClient) + if err != nil { + return fmt.Errorf("failed fetching new credentials: %w", err) + } + + // TODO: add some safety checks here around rotation key set intersection? + + err = comatproto.IdentityRequestPlcOperationSignature(ctx, oldClient) + if err != nil { + return fmt.Errorf("failed requesting PLC operation token: %w", err) + } + + plcAuthToken := "XXX" + signedPlcOpResp, err := comatproto.IdentitySignPlcOperation(ctx, oldClient, &comatproto.IdentitySignPlcOperation_Input{ + // XXX: not just pass-through + AlsoKnownAs: credsResp.AlsoKnownAs, + RotationKeys: credsResp.RotationKeys, + Services: credsResp.Services, + Token: &plcAuthToken, + VerificationMethods: credsResp.VerificationMethods, + }) + if err != nil { + return fmt.Errorf("failed requesting PLC operation signature: %w", err) + } + + err = comatproto.IdentitySubmitPlcOperation(ctx, &newClient, &comatproto.IdentitySubmitPlcOperation_Input{ + Operation: signedPlcOpResp.Operation, + }) + if err != nil { + return fmt.Errorf("failed submitting PLC operation: %w", err) + } + + // 4. Finalize Migration + slog.Info("activating new account") + + err = comatproto.ServerActivateAccount(ctx, &newClient) + if err != nil { + return fmt.Errorf("failed activating new host: %w", err) + } + err = comatproto.ServerDeactivateAccount(ctx, oldClient, nil) + if err != nil { + return fmt.Errorf("failed deactivating old host: %w", err) + } + + slog.Info("account migration completed") + return nil +} From a35423da119fc6e79cf878b9cddccad3b7953b76 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 01:02:23 -0700 Subject: [PATCH 044/111] basic bsky prefs export/import --- cmd/goat/bsky.go | 1 + cmd/goat/bsky_prefs.go | 92 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 cmd/goat/bsky_prefs.go diff --git a/cmd/goat/bsky.go b/cmd/goat/bsky.go index 607e31075..410c70d0c 100644 --- a/cmd/goat/bsky.go +++ b/cmd/goat/bsky.go @@ -23,6 +23,7 @@ var cmdBsky = &cli.Command{ ArgsUsage: ``, Action: runBskyPost, }, + cmdBskyPrefs, }, } diff --git a/cmd/goat/bsky_prefs.go b/cmd/goat/bsky_prefs.go new file mode 100644 index 000000000..1eba7012d --- /dev/null +++ b/cmd/goat/bsky_prefs.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + appbsky "github.com/bluesky-social/indigo/api/bsky" + + "github.com/urfave/cli/v2" +) + +var cmdBskyPrefs = &cli.Command{ + Name: "prefs", + Usage: "sub-commands for preferences", + Flags: []cli.Flag{}, + Subcommands: []*cli.Command{ + &cli.Command{ + Name: "export", + Usage: "dump preferences out as JSON", + Action: runBskyPrefsExport, + }, + &cli.Command{ + Name: "import", + Usage: "upload preferences from JSON file", + ArgsUsage: ``, + Action: runBskyPrefsImport, + }, + }, +} + +func runBskyPrefsExport(cctx *cli.Context) error { + ctx := context.Background() + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + // TODO: does this crash with unsupported preference types? + resp, err := appbsky.ActorGetPreferences(ctx, xrpcc) + if err != nil { + return fmt.Errorf("failed fetching old preferences: %w", err) + } + + b, err := json.MarshalIndent(resp.Preferences, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + + return nil +} + +func runBskyPrefsImport(cctx *cli.Context) error { + ctx := context.Background() + prefsPath := cctx.Args().First() + if prefsPath == "" { + return fmt.Errorf("need to provide file path as an argument") + } + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + prefsBytes, err := os.ReadFile(prefsPath) + if err != nil { + return err + } + + var prefsArray []appbsky.ActorDefs_Preferences_Elem + err = json.Unmarshal(prefsBytes, &prefsArray) + if err != nil { + return err + } + + // TODO: does this clobber unsupported preference types? + err = appbsky.ActorPutPreferences(ctx, xrpcc, &appbsky.ActorPutPreferences_Input{ + Preferences: prefsArray, + }) + if err != nil { + return fmt.Errorf("failed fetching old preferences: %w", err) + } + + return nil +} From b871b69932f65f33ca71053de892d4976853642d Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 01:03:07 -0700 Subject: [PATCH 045/111] account lookup vs status --- cmd/goat/account.go | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/cmd/goat/account.go b/cmd/goat/account.go index de11686e2..f2e6f625e 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -48,10 +49,15 @@ var cmdAccount = &cli.Command{ Action: runAccountLogout, }, &cli.Command{ - Name: "status", - Usage: "show account status at PDS", + Name: "lookup", + Usage: "show basic account status for any account", ArgsUsage: ``, - Action: runAccountStatus, + Action: runAccountLookup, + }, + &cli.Command{ + Name: "status", + Usage: "show current account status at PDS", + Action: runAccountStatus, }, cmdAccountMigrate, }, @@ -89,7 +95,7 @@ func runAccountLogout(cctx *cli.Context) error { return wipeAuthSession() } -func runAccountStatus(cctx *cli.Context) error { +func runAccountLookup(cctx *cli.Context) error { ctx := context.Background() username := cctx.Args().First() if username == "" { @@ -123,3 +129,27 @@ func runAccountStatus(cctx *cli.Context) error { } return nil } + +func runAccountStatus(cctx *cli.Context) error { + ctx := context.Background() + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + status, err := comatproto.ServerCheckAccountStatus(ctx, client) + if err != nil { + return fmt.Errorf("failed checking account status: %w", err) + } + + b, err := json.MarshalIndent(status, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + + return nil +} From 0a82c6c7fa11fd457fcc61af23a90a3a4e16bdd1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 29 Sep 2024 02:24:05 -0700 Subject: [PATCH 046/111] add tokenize while keeping common censor chars --- automod/keyword/tokenize.go | 23 ++++++++++++---- automod/keyword/tokenize_test.go | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/automod/keyword/tokenize.go b/automod/keyword/tokenize.go index 8c477432b..a2a0c627e 100644 --- a/automod/keyword/tokenize.go +++ b/automod/keyword/tokenize.go @@ -12,18 +12,19 @@ import ( ) var ( - puncChars = regexp.MustCompile(`[[:punct:]]+`) - nonTokenChars = regexp.MustCompile(`[^\pL\pN\s]+`) + puncChars = regexp.MustCompile(`[[:punct:]]+`) + nonTokenChars = regexp.MustCompile(`[^\pL\pN\s]+`) + nonTokenCharsSkipCensorChars = regexp.MustCompile(`[^\pL\pN\s#*_-]`) ) // Splits free-form text in to tokens, including lower-case, unicode normalization, and some unicode folding. // // The intent is for this to work similarly to an NLP tokenizer, as might be used in a fulltext search engine, and enable fast matching to a list of known tokens. It might eventually even do stemming, removing pluralization (trailing "s" for English), etc. -func TokenizeText(text string) []string { +func tokenizeText(text string, nonTokenCharsRegex *regexp.Regexp) []string { // this function needs to be re-defined in every function call to prevent a race condition normFunc := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) - split := strings.ToLower(nonTokenChars.ReplaceAllString(text, " ")) - bare := strings.ToLower(nonTokenChars.ReplaceAllString(split, "")) + split := strings.ToLower(nonTokenCharsRegex.ReplaceAllString(text, " ")) + bare := strings.ToLower(nonTokenCharsRegex.ReplaceAllString(split, "")) norm, _, err := transform.String(normFunc, bare) if err != nil { slog.Warn("unicode normalization error", "err", err) @@ -32,6 +33,18 @@ func TokenizeText(text string) []string { return strings.Fields(norm) } +func TokenizeText(text string) []string { + return tokenizeText(text, nonTokenChars) +} + +func TokenizeTextSkippingCensorChars(text string) []string { + return tokenizeText(text, nonTokenCharsSkipCensorChars) +} + +func TokenizeTextWithCustomNonTokenRegex(text string, regex *regexp.Regexp) []string { + return tokenizeText(text, regex) +} + func splitIdentRune(c rune) bool { return !unicode.IsLetter(c) && !unicode.IsNumber(c) } diff --git a/automod/keyword/tokenize_test.go b/automod/keyword/tokenize_test.go index 89d5f79b1..66f0178b8 100644 --- a/automod/keyword/tokenize_test.go +++ b/automod/keyword/tokenize_test.go @@ -1,6 +1,7 @@ package keyword import ( + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +18,9 @@ func TestTokenizeText(t *testing.T) { {text: "Hello, โลก!", out: []string{"hello", "โลก"}}, {text: "Gdańsk", out: []string{"gdansk"}}, {text: " foo1;bar2,baz3...", out: []string{"foo1", "bar2", "baz3"}}, + {text: "foo*bar", out: []string{"foo", "bar"}}, + {text: "foo-bar", out: []string{"foo", "bar"}}, + {text: "foo_bar", out: []string{"foo", "bar"}}, } for _, fix := range fixtures { @@ -24,6 +28,49 @@ func TestTokenizeText(t *testing.T) { } } +func TestTokenizeTextWithCensorChars(t *testing.T) { + assert := assert.New(t) + + fixtures := []struct { + text string + out []string + }{ + {text: "", out: []string{}}, + {text: "Hello, โลก!", out: []string{"hello", "โลก"}}, + {text: "Gdańsk", out: []string{"gdansk"}}, + {text: " foo1;bar2,baz3...", out: []string{"foo1", "bar2", "baz3"}}, + {text: "foo*bar,foo&bar", out: []string{"foo*bar", "foo", "bar"}}, + {text: "foo-bar,foo&bar", out: []string{"foo-bar", "foo", "bar"}}, + {text: "foo_bar,foo&bar", out: []string{"foo_bar", "foo", "bar"}}, + {text: "foo#bar,foo&bar", out: []string{"foo#bar", "foo", "bar"}}, + } + + for _, fix := range fixtures { + assert.Equal(fix.out, TokenizeTextSkippingCensorChars(fix.text)) + } +} + +func TestTokenizeTextWithCustomRegex(t *testing.T) { + assert := assert.New(t) + + fixtures := []struct { + text string + out []string + }{ + {text: "", out: []string{}}, + {text: "Hello, โลก!", out: []string{"hello", "โลก"}}, + {text: "Gdańsk", out: []string{"gdansk"}}, + {text: " foo1;bar2,baz3...", out: []string{"foo1", "bar2", "baz3"}}, + {text: "foo*bar", out: []string{"foo", "bar"}}, + {text: "foo&bar,foo*bar", out: []string{"foo&bar", "foo", "bar"}}, + } + + regex := regexp.MustCompile(`[^\pL\pN\s&]`) + for _, fix := range fixtures { + assert.Equal(fix.out, TokenizeTextWithCustomNonTokenRegex(fix.text, regex)) + } +} + func TestTokenizeIdentifier(t *testing.T) { assert := assert.New(t) From 4da4e794d52d374dfd2ef19772aa46269c5fed1b Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 29 Sep 2024 02:27:20 -0700 Subject: [PATCH 047/111] rename --- automod/keyword/tokenize.go | 4 ++-- automod/keyword/tokenize_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/automod/keyword/tokenize.go b/automod/keyword/tokenize.go index a2a0c627e..9a2edd9ec 100644 --- a/automod/keyword/tokenize.go +++ b/automod/keyword/tokenize.go @@ -41,8 +41,8 @@ func TokenizeTextSkippingCensorChars(text string) []string { return tokenizeText(text, nonTokenCharsSkipCensorChars) } -func TokenizeTextWithCustomNonTokenRegex(text string, regex *regexp.Regexp) []string { - return tokenizeText(text, regex) +func TokenizeTextWithRegex(text string, nonTokenCharsRegex *regexp.Regexp) []string { + return tokenizeText(text, nonTokenCharsRegex) } func splitIdentRune(c rune) bool { diff --git a/automod/keyword/tokenize_test.go b/automod/keyword/tokenize_test.go index 66f0178b8..45b477f6d 100644 --- a/automod/keyword/tokenize_test.go +++ b/automod/keyword/tokenize_test.go @@ -67,7 +67,7 @@ func TestTokenizeTextWithCustomRegex(t *testing.T) { regex := regexp.MustCompile(`[^\pL\pN\s&]`) for _, fix := range fixtures { - assert.Equal(fix.out, TokenizeTextWithCustomNonTokenRegex(fix.text, regex)) + assert.Equal(fix.out, TokenizeTextWithRegex(fix.text, regex)) } } From c8238e1043309c94117e520c4b256be84588256f Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 29 Sep 2024 02:35:07 -0700 Subject: [PATCH 048/111] clean --- automod/keyword/tokenize.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/automod/keyword/tokenize.go b/automod/keyword/tokenize.go index 9a2edd9ec..0b5a33ca4 100644 --- a/automod/keyword/tokenize.go +++ b/automod/keyword/tokenize.go @@ -20,7 +20,7 @@ var ( // Splits free-form text in to tokens, including lower-case, unicode normalization, and some unicode folding. // // The intent is for this to work similarly to an NLP tokenizer, as might be used in a fulltext search engine, and enable fast matching to a list of known tokens. It might eventually even do stemming, removing pluralization (trailing "s" for English), etc. -func tokenizeText(text string, nonTokenCharsRegex *regexp.Regexp) []string { +func TokenizeTextWithRegex(text string, nonTokenCharsRegex *regexp.Regexp) []string { // this function needs to be re-defined in every function call to prevent a race condition normFunc := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) split := strings.ToLower(nonTokenCharsRegex.ReplaceAllString(text, " ")) @@ -34,15 +34,11 @@ func tokenizeText(text string, nonTokenCharsRegex *regexp.Regexp) []string { } func TokenizeText(text string) []string { - return tokenizeText(text, nonTokenChars) + return TokenizeTextWithRegex(text, nonTokenChars) } func TokenizeTextSkippingCensorChars(text string) []string { - return tokenizeText(text, nonTokenCharsSkipCensorChars) -} - -func TokenizeTextWithRegex(text string, nonTokenCharsRegex *regexp.Regexp) []string { - return tokenizeText(text, nonTokenCharsRegex) + return TokenizeTextWithRegex(text, nonTokenCharsSkipCensorChars) } func splitIdentRune(c rune) bool { From ec438e77df2be7846c438b022ef163034087fb45 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 11:27:56 -0700 Subject: [PATCH 049/111] remove account check (redundant with status) --- cmd/goat/account.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/cmd/goat/account.go b/cmd/goat/account.go index f2e6f625e..714b1c25e 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -17,11 +17,6 @@ var cmdAccount = &cli.Command{ Usage: "sub-commands for auth and account management", Flags: []cli.Flag{}, Subcommands: []*cli.Command{ - &cli.Command{ - Name: "check", - Usage: "verifies current auth session is functional", - Action: runAccountCheck, - }, &cli.Command{ Name: "login", Usage: "create session with PDS instance", @@ -63,22 +58,6 @@ var cmdAccount = &cli.Command{ }, } -func runAccountCheck(cctx *cli.Context) error { - ctx := context.Background() - - client, err := loadAuthClient(ctx) - if err == ErrNoAuthSession { - return fmt.Errorf("auth required, but not logged in") - } else if err != nil { - return err - } - // TODO: more explicit check? - fmt.Printf("DID: %s\n", client.Auth.Did) - fmt.Printf("PDS: %s\n", client.Host) - - return nil -} - func runAccountLogin(cctx *cli.Context) error { ctx := context.Background() From 088d48b7db0bd5f2d636ba63e68085ce9cbcae56 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 11:28:59 -0700 Subject: [PATCH 050/111] more account helpers --- cmd/goat/account.go | 81 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/cmd/goat/account.go b/cmd/goat/account.go index 714b1c25e..4f99715e2 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -43,9 +43,19 @@ var cmdAccount = &cli.Command{ Usage: "delete any current session", Action: runAccountLogout, }, + &cli.Command{ + Name: "activate", + Usage: "(re)activate current account", + Action: runAccountActivate, + }, + &cli.Command{ + Name: "deactivate", + Usage: "deactivate current account", + Action: runAccountDeactivate, + }, &cli.Command{ Name: "lookup", - Usage: "show basic account status for any account", + Usage: "show basic account hosting status for any account", ArgsUsage: ``, Action: runAccountLookup, }, @@ -54,6 +64,11 @@ var cmdAccount = &cli.Command{ Usage: "show current account status at PDS", Action: runAccountStatus, }, + &cli.Command{ + Name: "missing-blobs", + Usage: "list any missing blobs for current account", + Action: runAccountMissingBlobs, + }, cmdAccountMigrate, }, } @@ -132,3 +147,67 @@ func runAccountStatus(cctx *cli.Context) error { return nil } + +func runAccountMissingBlobs(cctx *cli.Context) error { + ctx := context.Background() + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + cursor := "" + for { + resp, err := comatproto.RepoListMissingBlobs(ctx, client, cursor, 500) + if err != nil { + return err + } + for _, missing := range resp.Blobs { + fmt.Printf("%s\t%s\n", missing.Cid, missing.RecordUri) + } + if resp.Cursor != nil && *resp.Cursor != "" { + cursor = *resp.Cursor + } else { + break + } + } + return nil +} + +func runAccountActivate(cctx *cli.Context) error { + ctx := context.Background() + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + err = comatproto.ServerActivateAccount(ctx, client) + if err != nil { + return fmt.Errorf("failed activating account: %w", err) + } + + return nil +} + +func runAccountDeactivate(cctx *cli.Context) error { + ctx := context.Background() + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + err = comatproto.ServerDeactivateAccount(ctx, client, &comatproto.ServerDeactivateAccount_Input{}) + if err != nil { + return fmt.Errorf("failed deactivating account: %w", err) + } + + return nil +} From 712a4fed444028a1568ca620094c938b5511f6c5 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 16:53:59 -0700 Subject: [PATCH 051/111] initial PDS command --- cmd/goat/main.go | 1 + cmd/goat/pds.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 cmd/goat/pds.go diff --git a/cmd/goat/main.go b/cmd/goat/main.go index 538a783e7..c211b698a 100644 --- a/cmd/goat/main.go +++ b/cmd/goat/main.go @@ -37,6 +37,7 @@ func run(args []string) error { cmdRecord, cmdSyntax, cmdCrypto, + cmdPds, } return app.Run(args) } diff --git a/cmd/goat/pds.go b/cmd/goat/pds.go new file mode 100644 index 000000000..dc3485756 --- /dev/null +++ b/cmd/goat/pds.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/xrpc" + + "github.com/urfave/cli/v2" +) + +var cmdPds = &cli.Command{ + Name: "pds", + Usage: "sub-commands for pds hosts", + Flags: []cli.Flag{}, + Subcommands: []*cli.Command{ + &cli.Command{ + Name: "describe", + Usage: "shows info about a PDS info", + ArgsUsage: ``, + Action: runPdsDescribe, + }, + }, +} + +func runPdsDescribe(cctx *cli.Context) error { + ctx := context.Background() + + pdsHost := cctx.Args().First() + if pdsHost== "" { + return fmt.Errorf("need to provide new handle as argument") + } + if !strings.Contains(pdsHost, "://") { + return fmt.Errorf("PDS host is not a url: %s", pdsHost) + } + client := xrpc.Client{ + Host: pdsHost, + } + + resp, err := comatproto.ServerDescribeServer(ctx, &client) + if err != nil { + return err + } + + b, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + + return nil +} From f66c55375ab59a12d2ba380d1aa5cff7aa301ee7 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 16:54:36 -0700 Subject: [PATCH 052/111] allow overriding PDS discovery on login --- cmd/goat/account.go | 10 +++++++++- cmd/goat/auth.go | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cmd/goat/account.go b/cmd/goat/account.go index 4f99715e2..6f01a33d6 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "strings" + "time" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" @@ -35,6 +37,12 @@ var cmdAccount = &cli.Command{ Usage: "password (app password recommended)", EnvVars: []string{"ATP_AUTH_PASSWORD"}, }, + &cli.StringFlag{ + Name: "pds-host", + Usage: "URL of the PDS to create account on (overrides DID doc)", + Required: true, + EnvVars: []string{"ATP_PDS_HOST"}, + }, }, Action: runAccountLogin, }, @@ -81,7 +89,7 @@ func runAccountLogin(cctx *cli.Context) error { return err } - _, err = refreshAuthSession(ctx, *username, cctx.String("app-password")) + _, err = refreshAuthSession(ctx, *username, cctx.String("app-password"), cctx.String("pds-host")) return err } diff --git a/cmd/goat/auth.go b/cmd/goat/auth.go index 460117b83..f827bbe5c 100644 --- a/cmd/goat/auth.go +++ b/cmd/goat/auth.go @@ -79,7 +79,7 @@ func loadAuthClient(ctx context.Context) (*xrpc.Client, error) { if err != nil { // TODO: if failure, try creating a new session from password fmt.Println("trying to refresh auth from password...") - as, err := refreshAuthSession(ctx, sess.DID.AtIdentifier(), sess.Password) + as, err := refreshAuthSession(ctx, sess.DID.AtIdentifier(), sess.Password, sess.PDS) if err != nil { return nil, err } @@ -96,23 +96,32 @@ func loadAuthClient(ctx context.Context) (*xrpc.Client, error) { return &client, nil } -func refreshAuthSession(ctx context.Context, username syntax.AtIdentifier, password string) (*AuthSession, error) { - dir := identity.DefaultDirectory() - ident, err := dir.Lookup(ctx, username) - if err != nil { - return nil, err - } +func refreshAuthSession(ctx context.Context, username syntax.AtIdentifier, password, pdsURL string) (*AuthSession, error) { - pdsURL := ident.PDSEndpoint() + var did syntax.DID if pdsURL == "" { - return nil, fmt.Errorf("empty PDS URL") + dir := identity.DefaultDirectory() + ident, err := dir.Lookup(ctx, username) + if err != nil { + return nil, err + } + + pdsURL := ident.PDSEndpoint() + if pdsURL == "" { + return nil, fmt.Errorf("empty PDS URL") + } + did = ident.DID + } + + if did == "" && username.IsDID() { + did, _ = username.AsDID() } client := xrpc.Client{ Host: pdsURL, } sess, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{ - Identifier: ident.DID.String(), + Identifier: username.String(), Password: password, }) if err != nil { @@ -121,9 +130,18 @@ func refreshAuthSession(ctx context.Context, username syntax.AtIdentifier, passw // TODO: check account status? // TODO: warn if email isn't verified? + // TODO: check that sess.Did matches username + if did == "" { + did, err = syntax.ParseDID(sess.Did) + if err != nil { + return nil, err + } + } else if sess.Did != did.String() { + return nil, fmt.Errorf("session DID didn't match expected: %s != %s", sess.Did, did) + } authSession := AuthSession{ - DID: ident.DID, + DID: did, Password: password, PDS: pdsURL, RefreshToken: sess.RefreshJwt, From a5a683ab3441c7c40a1f02375972d75405d3e663 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 16:55:01 -0700 Subject: [PATCH 053/111] goat repo import --- cmd/goat/repo.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cmd/goat/repo.go b/cmd/goat/repo.go index df5f85348..e207c3e24 100644 --- a/cmd/goat/repo.go +++ b/cmd/goat/repo.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "fmt" @@ -36,6 +37,12 @@ var cmdRepo = &cli.Command{ }, Action: runRepoExport, }, + &cli.Command{ + Name: "import", + Usage: "upload CAR file for current account", + ArgsUsage: ``, + Action: runRepoImport, + }, &cli.Command{ Name: "ls", Aliases: []string{"list"}, @@ -104,6 +111,34 @@ func runRepoExport(cctx *cli.Context) error { return os.WriteFile(carPath, repoBytes, 0666) } +func runRepoImport(cctx *cli.Context) error { + ctx := context.Background() + + carPath := cctx.Args().First() + if carPath == "" { + return fmt.Errorf("need to provide CAR file path as an argument") + } + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + fileBytes, err := os.ReadFile(carPath) + if err != nil { + return err + } + + err = comatproto.RepoImportRepo(ctx, xrpcc, bytes.NewReader(fileBytes)) + if err != nil { + return fmt.Errorf("failed to import repo: %w", err) + } + + return nil +} + func runRepoList(cctx *cli.Context) error { ctx := context.Background() carPath := cctx.Args().First() From 5a876ccd660e1aa0aeb7e03d35ca1d686474823c Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 16:55:11 -0700 Subject: [PATCH 054/111] more account helpers --- cmd/goat/account.go | 210 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/cmd/goat/account.go b/cmd/goat/account.go index 6f01a33d6..19e5d811e 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -67,6 +67,12 @@ var cmdAccount = &cli.Command{ ArgsUsage: ``, Action: runAccountLookup, }, + &cli.Command{ + Name: "update-handle", + Usage: "change handle for current account", + ArgsUsage: ``, + Action: runAccountUpdateHandle, + }, &cli.Command{ Name: "status", Usage: "show current account status at PDS", @@ -77,6 +83,74 @@ var cmdAccount = &cli.Command{ Usage: "list any missing blobs for current account", Action: runAccountMissingBlobs, }, + &cli.Command{ + Name: "service-auth", + Usage: "create service auth token", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "endpoint", + Aliases: []string{"lxm"}, + Usage: "restrict token to API endpoint (NSID, optional)", + }, + &cli.StringFlag{ + Name: "audience", + Aliases: []string{"aud"}, + Required: true, + Usage: "DID of service that will receive and validate token", + }, + &cli.IntFlag{ + Name: "duration-sec", + Value: 60, + Usage: "validity time window of token (seconds)", + }, + }, + Action: runAccountServiceAuth, + }, + &cli.Command{ + Name: "create", + Usage: "create a new account on the indicated PDS host", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pds-host", + Usage: "URL of the PDS to create account on", + Required: true, + EnvVars: []string{"ATP_PDS_HOST"}, + }, + &cli.StringFlag{ + Name: "handle", + Usage: "handle for new account", + Required: true, + EnvVars: []string{"ATP_AUTH_HANDLE"}, + }, + &cli.StringFlag{ + Name: "password", + Usage: "initial account password", + Required: true, + EnvVars: []string{"ATP_AUTH_PASSWORD"}, + }, + &cli.StringFlag{ + Name: "invite-code", + Usage: "invite code for account signup", + }, + &cli.StringFlag{ + Name: "email", + Usage: "email address for new account", + }, + &cli.StringFlag{ + Name: "existing-did", + Usage: "an existing DID to use (eg, non-PLC DID, or migration)", + }, + &cli.StringFlag{ + Name: "recovery-key", + Usage: "public cryptographic key (did:key) to add as PLC recovery", + }, + &cli.StringFlag{ + Name: "service-auth", + Usage: "service auth token (for account migration)", + }, + }, + Action: runAccountCreate, + }, cmdAccountMigrate, }, } @@ -151,6 +225,8 @@ func runAccountStatus(cctx *cli.Context) error { if err != nil { return err } + fmt.Printf("DID: %s\n", client.Auth.Did) + fmt.Printf("Host: %s\n", client.Host) fmt.Println(string(b)) return nil @@ -219,3 +295,137 @@ func runAccountDeactivate(cctx *cli.Context) error { return nil } + +func runAccountUpdateHandle(cctx *cli.Context) error { + ctx := context.Background() + + raw := cctx.Args().First() + if raw == "" { + return fmt.Errorf("need to provide new handle as argument") + } + handle, err := syntax.ParseHandle(raw) + if err != nil { + return err + } + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + err = comatproto.IdentityUpdateHandle(ctx, client, &comatproto.IdentityUpdateHandle_Input{ + Handle: handle.String(), + }) + if err != nil { + return fmt.Errorf("failed updating handle: %w", err) + } + + return nil +} + +func runAccountServiceAuth(cctx *cli.Context) error { + ctx := context.Background() + + client, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + lxm := cctx.String("endpoint") + if lxm != "" { + _, err := syntax.ParseNSID(lxm) + if err != nil { + return fmt.Errorf("lxm argument must be a valid NSID: %w", err) + } + } + + aud := cctx.String("audience") + // TODO: can aud DID have a fragment? + _, err = syntax.ParseDID(aud) + if err != nil { + return fmt.Errorf("aud argument must be a valid DID: %w", err) + } + + durSec := cctx.Int("duration-sec") + expTimestamp := time.Now().Unix() + int64(durSec) + + resp, err := comatproto.ServerGetServiceAuth(ctx, client, aud, expTimestamp, lxm) + if err != nil { + return fmt.Errorf("failed updating handle: %w", err) + } + + fmt.Println(resp.Token) + + return nil +} + +func runAccountCreate(cctx *cli.Context) error { + ctx := context.Background() + + // validate args + pdsHost := cctx.String("pds-host") + if !strings.Contains(pdsHost, "://") { + return fmt.Errorf("PDS host is not a url: %s", pdsHost) + } + handle := cctx.String("handle") + _, err := syntax.ParseHandle(handle) + if err != nil { + return err + } + password := cctx.String("password") + params := &comatproto.ServerCreateAccount_Input{ + Handle: handle, + Password: &password, + } + raw := cctx.String("existing-did") + if raw != "" { + _, err := syntax.ParseDID(raw) + if err != nil { + return err + } + s := raw + params.Did = &s + } + raw = cctx.String("email") + if raw != "" { + s := raw + params.Email = &s + } + raw = cctx.String("invite-code") + if raw != "" { + s := raw + params.InviteCode = &s + } + raw = cctx.String("recovery-key") + if raw != "" { + s := raw + params.RecoveryKey = &s + } + + // create a new API client to connect to the account's PDS + xrpcc := xrpc.Client{ + Host: pdsHost, + } + + raw = cctx.String("service-auth") + if raw != "" && params.Did != nil { + xrpcc.Auth = &xrpc.AuthInfo{ + Did: *params.Did, + AccessJwt: raw, + } + } + + resp, err := comatproto.ServerCreateAccount(ctx, &xrpcc, params) + if err != nil { + return fmt.Errorf("failed to create account: %w", err) + } + + fmt.Println("Success!") + fmt.Printf("DID: %s\n", resp.Did) + fmt.Printf("Handle: %s\n", resp.Handle) + return nil +} From b69d973520d1a910bf52195df2beed9b663bbdf2 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 17:58:41 -0700 Subject: [PATCH 055/111] more progress on manual migration --- cmd/goat/account.go | 10 +- cmd/goat/account_migrate.go | 3 + cmd/goat/account_plc.go | 169 ++++++++++++++++++ cmd/goat/auth.go | 2 +- .../identitygetRecommendedDidCredentials.go | 23 +++ cmd/goat/identitysignPlcOperation.go | 38 ++++ cmd/goat/identitysubmitPlcOperation.go | 26 +++ cmd/goat/pds.go | 4 +- cmd/goat/repo.go | 2 +- 9 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 cmd/goat/account_plc.go create mode 100644 cmd/goat/identitygetRecommendedDidCredentials.go create mode 100644 cmd/goat/identitysignPlcOperation.go create mode 100644 cmd/goat/identitysubmitPlcOperation.go diff --git a/cmd/goat/account.go b/cmd/goat/account.go index 19e5d811e..347ebc02f 100644 --- a/cmd/goat/account.go +++ b/cmd/goat/account.go @@ -38,10 +38,9 @@ var cmdAccount = &cli.Command{ EnvVars: []string{"ATP_AUTH_PASSWORD"}, }, &cli.StringFlag{ - Name: "pds-host", - Usage: "URL of the PDS to create account on (overrides DID doc)", - Required: true, - EnvVars: []string{"ATP_PDS_HOST"}, + Name: "pds-host", + Usage: "URL of the PDS to create account on (overrides DID doc)", + EnvVars: []string{"ATP_PDS_HOST"}, }, }, Action: runAccountLogin, @@ -152,6 +151,7 @@ var cmdAccount = &cli.Command{ Action: runAccountCreate, }, cmdAccountMigrate, + cmdAccountPlc, }, } @@ -414,7 +414,7 @@ func runAccountCreate(cctx *cli.Context) error { raw = cctx.String("service-auth") if raw != "" && params.Did != nil { xrpcc.Auth = &xrpc.AuthInfo{ - Did: *params.Did, + Did: *params.Did, AccessJwt: raw, } } diff --git a/cmd/goat/account_migrate.go b/cmd/goat/account_migrate.go index 128df1ecd..f570886cd 100644 --- a/cmd/goat/account_migrate.go +++ b/cmd/goat/account_migrate.go @@ -128,6 +128,9 @@ func runAccountMigrate(cctx *cli.Context) error { err = appbsky.ActorPutPreferences(ctx, &newClient, &appbsky.ActorPutPreferences_Input{ Preferences: prefResp.Preferences, }) + if err != nil { + return fmt.Errorf("failed importing preferences: %w", err) + } slog.Info("migrating blobs") blobCursor := "" diff --git a/cmd/goat/account_plc.go b/cmd/goat/account_plc.go new file mode 100644 index 000000000..a0dde653b --- /dev/null +++ b/cmd/goat/account_plc.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + + "github.com/urfave/cli/v2" +) + +var cmdAccountPlc = &cli.Command{ + Name: "plc", + Usage: "sub-commands for managing PLC DID via PDS host", + Subcommands: []*cli.Command{ + &cli.Command{ + Name: "recommended", + Usage: "list recommended DID fields for current account", + Action: runAccountPlcRecommended, + }, + &cli.Command{ + Name: "request-token", + Usage: "request a 2FA token (by email) for signing op", + Action: runAccountPlcRequestToken, + }, + &cli.Command{ + Name: "sign", + Usage: "sign a PLC operation", + ArgsUsage: ``, + Action: runAccountPlcSign, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Usage: "2FA token for signing request", + }, + }, + }, + &cli.Command{ + Name: "submit", + Usage: "submit a PLC operation (via PDS)", + ArgsUsage: ``, + Action: runAccountPlcSubmit, + }, + }, +} + +func runAccountPlcRecommended(cctx *cli.Context) error { + ctx := context.Background() + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + resp, err := IdentityGetRecommendedDidCredentials(ctx, xrpcc) + if err != nil { + return err + } + + b, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + + fmt.Println(string(b)) + return nil +} + +func runAccountPlcRequestToken(cctx *cli.Context) error { + ctx := context.Background() + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + err = comatproto.IdentityRequestPlcOperationSignature(ctx, xrpcc) + if err != nil { + return err + } + + fmt.Println("Success; check email for token.") + return nil +} + +func runAccountPlcSign(cctx *cli.Context) error { + ctx := context.Background() + + opPath := cctx.Args().First() + if opPath == "" { + return fmt.Errorf("need to provide JSON file path as an argument") + } + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + fileBytes, err := os.ReadFile(opPath) + if err != nil { + return err + } + + var body IdentitySignPlcOperation_Input + if err = json.Unmarshal(fileBytes, &body); err != nil { + return fmt.Errorf("failed decoding PLC op JSON: %w", err) + } + + token := cctx.String("token") + if token != "" { + body.Token = &token + } + + resp, err := IdentitySignPlcOperation(ctx, xrpcc, &body) + if err != nil { + return err + } + + b, err := json.MarshalIndent(resp.Operation, "", " ") + if err != nil { + return err + } + + fmt.Println(string(b)) + return nil +} + +func runAccountPlcSubmit(cctx *cli.Context) error { + ctx := context.Background() + + opPath := cctx.Args().First() + if opPath == "" { + return fmt.Errorf("need to provide JSON file path as an argument") + } + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + fileBytes, err := os.ReadFile(opPath) + if err != nil { + return err + } + + var op json.RawMessage + if err = json.Unmarshal(fileBytes, &op); err != nil { + return fmt.Errorf("failed decoding PLC op JSON: %w", err) + } + + err = IdentitySubmitPlcOperation(ctx, xrpcc, &IdentitySubmitPlcOperation_Input{ + Operation: &op, + }) + if err != nil { + return fmt.Errorf("failed submitting PLC op via PDS: %w", err) + } + + return nil +} diff --git a/cmd/goat/auth.go b/cmd/goat/auth.go index f827bbe5c..5eaf2b29e 100644 --- a/cmd/goat/auth.go +++ b/cmd/goat/auth.go @@ -106,7 +106,7 @@ func refreshAuthSession(ctx context.Context, username syntax.AtIdentifier, passw return nil, err } - pdsURL := ident.PDSEndpoint() + pdsURL = ident.PDSEndpoint() if pdsURL == "" { return nil, fmt.Errorf("empty PDS URL") } diff --git a/cmd/goat/identitygetRecommendedDidCredentials.go b/cmd/goat/identitygetRecommendedDidCredentials.go new file mode 100644 index 000000000..75abcc522 --- /dev/null +++ b/cmd/goat/identitygetRecommendedDidCredentials.go @@ -0,0 +1,23 @@ +// Copied from indigo:api/atproto/identitygetRecommendedDidCredentials.go + +package main + +// schema: com.atproto.identity.getRecommendedDidCredentials + +import ( + "context" + "encoding/json" + + "github.com/bluesky-social/indigo/xrpc" +) + +// IdentityGetRecommendedDidCredentials calls the XRPC method "com.atproto.identity.getRecommendedDidCredentials". +func IdentityGetRecommendedDidCredentials(ctx context.Context, c *xrpc.Client) (*json.RawMessage, error) { + var out json.RawMessage + + if err := c.Do(ctx, xrpc.Query, "", "com.atproto.identity.getRecommendedDidCredentials", nil, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/cmd/goat/identitysignPlcOperation.go b/cmd/goat/identitysignPlcOperation.go new file mode 100644 index 000000000..f9c96bea2 --- /dev/null +++ b/cmd/goat/identitysignPlcOperation.go @@ -0,0 +1,38 @@ +// Copied from indigo:api/atproto/identitysignPlcOperation.go + +package main + +// schema: com.atproto.identity.signPlcOperation + +import ( + "context" + "encoding/json" + + "github.com/bluesky-social/indigo/xrpc" +) + +// IdentitySignPlcOperation_Input is the input argument to a com.atproto.identity.signPlcOperation call. +type IdentitySignPlcOperation_Input struct { + AlsoKnownAs []string `json:"alsoKnownAs,omitempty" cborgen:"alsoKnownAs,omitempty"` + RotationKeys []string `json:"rotationKeys,omitempty" cborgen:"rotationKeys,omitempty"` + Services *json.RawMessage `json:"services,omitempty" cborgen:"services,omitempty"` + // token: A token received through com.atproto.identity.requestPlcOperationSignature + Token *string `json:"token,omitempty" cborgen:"token,omitempty"` + VerificationMethods *json.RawMessage `json:"verificationMethods,omitempty" cborgen:"verificationMethods,omitempty"` +} + +// IdentitySignPlcOperation_Output is the output of a com.atproto.identity.signPlcOperation call. +type IdentitySignPlcOperation_Output struct { + // operation: A signed DID PLC operation. + Operation *json.RawMessage `json:"operation" cborgen:"operation"` +} + +// IdentitySignPlcOperation calls the XRPC method "com.atproto.identity.signPlcOperation". +func IdentitySignPlcOperation(ctx context.Context, c *xrpc.Client, input *IdentitySignPlcOperation_Input) (*IdentitySignPlcOperation_Output, error) { + var out IdentitySignPlcOperation_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.identity.signPlcOperation", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/cmd/goat/identitysubmitPlcOperation.go b/cmd/goat/identitysubmitPlcOperation.go new file mode 100644 index 000000000..9f15e8adc --- /dev/null +++ b/cmd/goat/identitysubmitPlcOperation.go @@ -0,0 +1,26 @@ +// Copied from indigo:api/atproto/identitysubmitPlcOperation.go + +package main + +// schema: com.atproto.identity.submitPlcOperation + +import ( + "context" + "encoding/json" + + "github.com/bluesky-social/indigo/xrpc" +) + +// IdentitySubmitPlcOperation_Input is the input argument to a com.atproto.identity.submitPlcOperation call. +type IdentitySubmitPlcOperation_Input struct { + Operation *json.RawMessage `json:"operation" cborgen:"operation"` +} + +// IdentitySubmitPlcOperation calls the XRPC method "com.atproto.identity.submitPlcOperation". +func IdentitySubmitPlcOperation(ctx context.Context, c *xrpc.Client, input *IdentitySubmitPlcOperation_Input) error { + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.identity.submitPlcOperation", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/cmd/goat/pds.go b/cmd/goat/pds.go index dc3485756..9a2b96324 100644 --- a/cmd/goat/pds.go +++ b/cmd/goat/pds.go @@ -21,7 +21,7 @@ var cmdPds = &cli.Command{ Name: "describe", Usage: "shows info about a PDS info", ArgsUsage: ``, - Action: runPdsDescribe, + Action: runPdsDescribe, }, }, } @@ -30,7 +30,7 @@ func runPdsDescribe(cctx *cli.Context) error { ctx := context.Background() pdsHost := cctx.Args().First() - if pdsHost== "" { + if pdsHost == "" { return fmt.Errorf("need to provide new handle as argument") } if !strings.Contains(pdsHost, "://") { diff --git a/cmd/goat/repo.go b/cmd/goat/repo.go index e207c3e24..97f2b81a2 100644 --- a/cmd/goat/repo.go +++ b/cmd/goat/repo.go @@ -41,7 +41,7 @@ var cmdRepo = &cli.Command{ Name: "import", Usage: "upload CAR file for current account", ArgsUsage: ``, - Action: runRepoImport, + Action: runRepoImport, }, &cli.Command{ Name: "ls", From 77db84f70ed232b65f1e5d52b86c61b4b964922f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 29 Sep 2024 18:54:47 -0700 Subject: [PATCH 056/111] goat migrate fixes --- cmd/goat/account_migrate.go | 115 +++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/cmd/goat/account_migrate.go b/cmd/goat/account_migrate.go index f570886cd..6453de6f3 100644 --- a/cmd/goat/account_migrate.go +++ b/cmd/goat/account_migrate.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "strings" "context" + "encoding/json" "fmt" "log/slog" "time" @@ -19,6 +21,12 @@ var cmdAccountMigrate = &cli.Command{ Name: "migrate", Usage: "move account to a new PDS. requires full auth.", Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pds-host", + Usage: "URL of the new PDS to create account on", + Required: true, + EnvVars: []string{"ATP_PDS_HOST"}, + }, &cli.StringFlag{ Name: "new-handle", Required: true, @@ -31,6 +39,20 @@ var cmdAccountMigrate = &cli.Command{ Usage: "password on new PDS", EnvVars: []string{"NEW_ACCOUNT_PASSWORD"}, }, + &cli.StringFlag{ + Name: "plc-token", + Required: true, + Usage: "token from old PDS authorizing token signature", + EnvVars: []string{"PLC_SIGN_TOKEN"}, + }, + &cli.StringFlag{ + Name: "invite-code", + Usage: "invite code for account signup", + }, + &cli.StringFlag{ + Name: "new-email", + Usage: "email address for new account", + }, }, Action: runAccountMigrate, } @@ -47,15 +69,26 @@ func runAccountMigrate(cctx *cli.Context) error { } did := oldClient.Auth.Did - // connect to new host to discover service DID - newHostURL := "XXX" - newHandle := "XXX" - newPassword := "XXX" - newEmail := "XXX" - inviteCode := "XXX" + newHostURL := cctx.String("pds-host") + if !strings.Contains(newHostURL, "://") { + return fmt.Errorf("PDS host is not a url: %s", newHostURL) + } + newHandle := cctx.String("new-handle") + _, err = syntax.ParseHandle(newHandle) + if err != nil { + return err + } + newPassword := cctx.String("new-password") + plcToken := cctx.String("plc-token") + inviteCode := cctx.String("invite-code") + newEmail := cctx.String("new-email") + + newClient := xrpc.Client{ Host: newHostURL, } + + // connect to new host to discover service DID newHostDesc, err := comatproto.ServerDescribeServer(ctx, &newClient) if err != nil { return fmt.Errorf("failed connecting to new host: %w", err) @@ -64,10 +97,10 @@ func runAccountMigrate(cctx *cli.Context) error { if err != nil { return err } - slog.Info("new host", "did", newHostDID, "url", newHostURL) + slog.Info("new host", "serviceDID", newHostDID, "url", newHostURL) // 1. Create New Account - slog.Info("creating account on new host", "handle", newHandle) + slog.Info("creating account on new host", "handle", newHandle, "host", newHostURL) // get service auth token from old host // args: (ctx, client, aud string, exp int64, lxm string) @@ -78,20 +111,35 @@ func runAccountMigrate(cctx *cli.Context) error { } // then create the new account - // XXX: "Authorization Bearer " - _ = createAuthResp - createAccountResp, err := comatproto.ServerCreateAccount(ctx, &newClient, &comatproto.ServerCreateAccount_Input{ + createParams := comatproto.ServerCreateAccount_Input{ Did: &did, - Email: &newEmail, Handle: newHandle, - InviteCode: &inviteCode, Password: &newPassword, - }) + } + if newEmail != "" { + createParams.Email = &newEmail + } + if inviteCode != "" { + createParams.InviteCode = &inviteCode + } + + // use service auth for access token, temporarily + newClient.Auth = &xrpc.AuthInfo{ + Did: did, + Handle: newHandle, + AccessJwt: createAuthResp.Token, + RefreshJwt: createAuthResp.Token, + } + createAccountResp, err := comatproto.ServerCreateAccount(ctx, &newClient, &createParams) if err != nil { - return fmt.Errorf("failed getting service auth token from old host: %w", err) + return fmt.Errorf("failed creating new account: %w", err) + } + + if createAccountResp.Did != did { + return fmt.Errorf("new account DID not a match: %s != %s", createAccountResp.Did, did) } - // TODO: validate response? - _ = createAccountResp + newClient.Auth.AccessJwt = createAccountResp.AccessJwt + newClient.Auth.RefreshJwt = createAccountResp.RefreshJwt // login client on the new host sess, err := comatproto.ServerCreateSession(ctx, &newClient, &comatproto.ServerCreateSession_Input{ @@ -120,7 +168,7 @@ func runAccountMigrate(cctx *cli.Context) error { } slog.Info("migrating preferences") - // TODO: service proxy header + // TODO: service proxy header for app.bsky? prefResp, err := appbsky.ActorGetPreferences(ctx, oldClient) if err != nil { return fmt.Errorf("failed fetching old preferences: %w", err) @@ -169,32 +217,29 @@ func runAccountMigrate(cctx *cli.Context) error { // TODO: support did:web etc slog.Info("updating identity to new host") - credsResp, err := comatproto.IdentityGetRecommendedDidCredentials(ctx, &newClient) + credsResp, err := IdentityGetRecommendedDidCredentials(ctx, &newClient) if err != nil { return fmt.Errorf("failed fetching new credentials: %w", err) } - - // TODO: add some safety checks here around rotation key set intersection? - - err = comatproto.IdentityRequestPlcOperationSignature(ctx, oldClient) + credsBytes, err := json.Marshal(credsResp) if err != nil { - return fmt.Errorf("failed requesting PLC operation token: %w", err) + return nil } - plcAuthToken := "XXX" - signedPlcOpResp, err := comatproto.IdentitySignPlcOperation(ctx, oldClient, &comatproto.IdentitySignPlcOperation_Input{ - // XXX: not just pass-through - AlsoKnownAs: credsResp.AlsoKnownAs, - RotationKeys: credsResp.RotationKeys, - Services: credsResp.Services, - Token: &plcAuthToken, - VerificationMethods: credsResp.VerificationMethods, - }) + var unsignedOp IdentitySignPlcOperation_Input + if err = json.Unmarshal(credsBytes, &unsignedOp); err != nil { + return fmt.Errorf("failed parsing PLC op: %w", err) + } + unsignedOp.Token = &plcToken + + // TODO: add some safety checks here around rotation key set intersection? + + signedPlcOpResp, err := IdentitySignPlcOperation(ctx, oldClient, &unsignedOp) if err != nil { return fmt.Errorf("failed requesting PLC operation signature: %w", err) } - err = comatproto.IdentitySubmitPlcOperation(ctx, &newClient, &comatproto.IdentitySubmitPlcOperation_Input{ + err = IdentitySubmitPlcOperation(ctx, &newClient, &IdentitySubmitPlcOperation_Input{ Operation: signedPlcOpResp.Operation, }) if err != nil { @@ -208,7 +253,7 @@ func runAccountMigrate(cctx *cli.Context) error { if err != nil { return fmt.Errorf("failed activating new host: %w", err) } - err = comatproto.ServerDeactivateAccount(ctx, oldClient, nil) + err = comatproto.ServerDeactivateAccount(ctx, oldClient, &comatproto.ServerDeactivateAccount_Input{}) if err != nil { return fmt.Errorf("failed deactivating old host: %w", err) } From 19912f420fed38413643322a5dc8d76e0ca28043 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 1 Oct 2024 17:39:08 -0700 Subject: [PATCH 057/111] update warnings and comments from review --- cmd/goat/account_migrate.go | 28 +++++++++++++--------------- cmd/goat/bsky_prefs.go | 4 ++-- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cmd/goat/account_migrate.go b/cmd/goat/account_migrate.go index 6453de6f3..72c66e3fb 100644 --- a/cmd/goat/account_migrate.go +++ b/cmd/goat/account_migrate.go @@ -2,11 +2,11 @@ package main import ( "bytes" - "strings" "context" "encoding/json" "fmt" "log/slog" + "strings" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -58,7 +58,7 @@ var cmdAccountMigrate = &cli.Command{ } func runAccountMigrate(cctx *cli.Context) error { - // TODO: this could check rev / commit before and after + // NOTE: this could check rev / commit before and after and ensure last-minute content additions get lost ctx := context.Background() oldClient, err := loadAuthClient(ctx) @@ -83,7 +83,6 @@ func runAccountMigrate(cctx *cli.Context) error { inviteCode := cctx.String("invite-code") newEmail := cctx.String("new-email") - newClient := xrpc.Client{ Host: newHostURL, } @@ -112,9 +111,9 @@ func runAccountMigrate(cctx *cli.Context) error { // then create the new account createParams := comatproto.ServerCreateAccount_Input{ - Did: &did, - Handle: newHandle, - Password: &newPassword, + Did: &did, + Handle: newHandle, + Password: &newPassword, } if newEmail != "" { createParams.Email = &newEmail @@ -125,9 +124,9 @@ func runAccountMigrate(cctx *cli.Context) error { // use service auth for access token, temporarily newClient.Auth = &xrpc.AuthInfo{ - Did: did, - Handle: newHandle, - AccessJwt: createAuthResp.Token, + Did: did, + Handle: newHandle, + AccessJwt: createAuthResp.Token, RefreshJwt: createAuthResp.Token, } createAccountResp, err := comatproto.ServerCreateAccount(ctx, &newClient, &createParams) @@ -156,7 +155,6 @@ func runAccountMigrate(cctx *cli.Context) error { } // 2. Migrate Data - slog.Info("migrating repo") repoBytes, err := comatproto.SyncGetRepo(ctx, oldClient, did, "") if err != nil { @@ -168,7 +166,7 @@ func runAccountMigrate(cctx *cli.Context) error { } slog.Info("migrating preferences") - // TODO: service proxy header for app.bsky? + // TODO: service proxy header for AppView? prefResp, err := appbsky.ActorGetPreferences(ctx, oldClient) if err != nil { return fmt.Errorf("failed fetching old preferences: %w", err) @@ -197,7 +195,7 @@ func runAccountMigrate(cctx *cli.Context) error { if err != nil { slog.Warn("failed uploading blob", "cid", blobCID, "err", err, "size", len(blobBytes)) } - slog.Info("transfered blob", "cid", blobCID, "size", len(blobBytes)) + slog.Info("transferred blob", "cid", blobCID, "size", len(blobBytes)) } if listResp.Cursor == nil || *listResp.Cursor == "" { break @@ -206,7 +204,7 @@ func runAccountMigrate(cctx *cli.Context) error { } // display migration status - // TODO: should this loop? with a delay? + // NOTE: this could check between the old PDS and new PDS, polling in a loop showing progress until all records have been indexed statusResp, err := comatproto.ServerCheckAccountStatus(ctx, &newClient) if err != nil { return fmt.Errorf("failed checking account status: %w", err) @@ -214,7 +212,7 @@ func runAccountMigrate(cctx *cli.Context) error { slog.Info("account migration status", "status", statusResp) // 3. Migrate Identity - // TODO: support did:web etc + // NOTE: to work with did:web or non-PDS-managed did:plc, need to do manual migraiton process slog.Info("updating identity to new host") credsResp, err := IdentityGetRecommendedDidCredentials(ctx, &newClient) @@ -232,7 +230,7 @@ func runAccountMigrate(cctx *cli.Context) error { } unsignedOp.Token = &plcToken - // TODO: add some safety checks here around rotation key set intersection? + // NOTE: could add additional sanity checks here that any extra rotation keys were retained, and that old alsoKnownAs and service entries are retained? The stakes aren't super high for the later, as PLC has the full history. PLC and the new PDS already implement some basic sanity checks. signedPlcOpResp, err := IdentitySignPlcOperation(ctx, oldClient, &unsignedOp) if err != nil { diff --git a/cmd/goat/bsky_prefs.go b/cmd/goat/bsky_prefs.go index 1eba7012d..e24965491 100644 --- a/cmd/goat/bsky_prefs.go +++ b/cmd/goat/bsky_prefs.go @@ -40,7 +40,7 @@ func runBskyPrefsExport(cctx *cli.Context) error { return err } - // TODO: does this crash with unsupported preference types? + // TODO: does indigo API code crash with unsupported preference '$type'? Eg "Lexicon decoder" with unsupported type. resp, err := appbsky.ActorGetPreferences(ctx, xrpcc) if err != nil { return fmt.Errorf("failed fetching old preferences: %w", err) @@ -80,7 +80,7 @@ func runBskyPrefsImport(cctx *cli.Context) error { return err } - // TODO: does this clobber unsupported preference types? + // WARNING: might clobber off-Lexicon or new-Lexicon data fields (which don't round-trip deserialization) err = appbsky.ActorPutPreferences(ctx, xrpcc, &appbsky.ActorPutPreferences_Input{ Preferences: prefsArray, }) From 4af28baaa5b97a79a1f505d017b800bc3bf8764b Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 1 Oct 2024 17:51:19 -0700 Subject: [PATCH 058/111] backfill: checkout from relay, not entryway --- backfill/backfill.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backfill/backfill.go b/backfill/backfill.go index f84d97df9..6f199677c 100644 --- a/backfill/backfill.go +++ b/backfill/backfill.go @@ -119,7 +119,7 @@ func DefaultBackfillOptions() *BackfillOptions { ParallelRecordCreates: 100, NSIDFilter: "", SyncRequestsPerSecond: 2, - CheckoutPath: "https://bsky.social/xrpc/com.atproto.sync.getRepo", + CheckoutPath: "https://bsky.network/xrpc/com.atproto.sync.getRepo", } } From 9e64f973fdf8e2acbfe61246c8142c36f8f6de8a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 2 Oct 2024 20:32:05 -0700 Subject: [PATCH 059/111] fix typo in blob tests --- atproto/lexicon/testdata/record-data-invalid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json index 623ca2395..5675f1fac 100644 --- a/atproto/lexicon/testdata/record-data-invalid.json +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -37,7 +37,7 @@ "data": { "$type": "example.lexicon.record", "integer": 1, "blob": "green" } }, { "name": "invalid blob: wrong type", "rkey": "demo", - "data": { "$type": "example.lexicon.record", "integer": 1, "bytes": { + "data": { "$type": "example.lexicon.record", "integer": 1, "blob": { "type": "blob", "size": 123, "mimeType": false, From 5b33a818293c36ac9e479922f87f03143e37527d Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 2 Oct 2024 20:32:47 -0700 Subject: [PATCH 060/111] enforce 'unknown' must be an object --- atproto/lexicon/language.go | 4 ++++ .../lexicon/testdata/record-data-invalid.json | 19 +++++++++++++++++++ .../lexicon/testdata/record-data-valid.json | 13 +++++++++++++ 3 files changed, 36 insertions(+) diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 5f5325be9..25265abc4 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -914,5 +914,9 @@ func (s *SchemaUnknown) CheckSchema() error { } func (s *SchemaUnknown) Validate(d any) error { + _, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("'unknown' data must an object") + } return nil } diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json index 5675f1fac..f431cc2a1 100644 --- a/atproto/lexicon/testdata/record-data-invalid.json +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -180,5 +180,24 @@ { "name": "out of closed union", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "closedUnion": "other" } + }, + { "name": "unknown wrong type (bool)", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "unknown": false } + }, + { "name": "unknown wrong type (bytes)", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "unknown": { "$bytes": "123" } } + }, + { "name": "unknown wrong type (blob)", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "unknown": { + "$type": "blob", + "mimeType": "text/plain", + "size": 12345, + "ref": { + "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" + } + }} } ] diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json index bbdd89b3d..69aba3224 100644 --- a/atproto/lexicon/testdata/record-data-valid.json +++ b/atproto/lexicon/testdata/record-data-valid.json @@ -88,5 +88,18 @@ }, "closedUnion": "example.lexicon.record#demoToken" } + }, + { + "name": "unknown as a type", + "rkey": "demo", + "data": { + "$type": "example.lexicon.record", + "integer": 1, + "unknown": { + "$type": "example.lexicon.record#demoObject", + "a": 1, + "b": 2 + } + } } ] From 5d9eb21739a39da39418b4068573e0e6c078e8bc Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 16:31:54 -0700 Subject: [PATCH 061/111] more test data --- atproto/lexicon/testdata/catalog/record.json | 19 +++++++++++---- .../lexicon/testdata/record-data-invalid.json | 24 +++++++++++++++++-- .../lexicon/testdata/record-data-valid.json | 5 +++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/atproto/lexicon/testdata/catalog/record.json b/atproto/lexicon/testdata/catalog/record.json index cb32a07ee..b7ef7297d 100644 --- a/atproto/lexicon/testdata/catalog/record.json +++ b/atproto/lexicon/testdata/catalog/record.json @@ -69,8 +69,8 @@ "union": { "type": "union", "refs": [ - "example.lexicon.record#demoToken", - "example.lexicon.record#demoObject" + "example.lexicon.record#demoObject", + "example.lexicon.record#demoObjectTwo" ] }, "formats": { @@ -134,7 +134,6 @@ "closedUnion": { "type": "union", "refs": [ - "example.lexicon.record#demoToken", "example.lexicon.record#demoObject" ], "closed": true @@ -210,7 +209,7 @@ "demoObject": { "type": "object", "description": "smaller object schema for unions", - "parameters": { + "properties": { "a": { "type": "integer" }, @@ -218,6 +217,18 @@ "type": "integer" } } + }, + "demoObjectTwo": { + "type": "object", + "description": "smaller object schema for unions", + "properties": { + "c": { + "type": "integer" + }, + "d": { + "type": "integer" + } + } } } } diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json index f431cc2a1..58c227cf1 100644 --- a/atproto/lexicon/testdata/record-data-invalid.json +++ b/atproto/lexicon/testdata/record-data-invalid.json @@ -53,10 +53,14 @@ "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "array": [true, false] } }, - { "name": "invalid object", + { "name": "object wrong data type", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "object": 123 } }, + { "name": "object nested wrong data type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "object": {"a": "not-a-number" } } + }, { "name": "invalid token ref type", "rkey": "demo", "data": { "$type": "example.lexicon.record", "integer": 1, "ref": 123 } @@ -177,9 +181,25 @@ "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" } }}}, + { "name": "open union wrong data type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "union": 123 } + }, + { "name": "open union missing $type", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "union": {"a": 1, "b": 2 } } + }, { "name": "out of closed union", "rkey": "demo", - "data": { "$type": "example.lexicon.record", "integer": 1, "closedUnion": "other" } + "data": { "$type": "example.lexicon.record", "integer": 1, "closedUnion": { "$type": "example.unknown-lexicon.blah", "a": 1 } } + }, + { "name": "union inner invalid", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "closedUnion": { "$type": "example.lexicon.record#demoObjectTwo", "a": 1 } } + }, + { "name": "union inner invalid", + "rkey": "demo", + "data": { "$type": "example.lexicon.record", "integer": 1, "union": { "$type": "example.lexicon.record#demoObject", "a": "not-a-number" } } }, { "name": "unknown wrong type (bool)", "rkey": "demo", diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json index 69aba3224..a56489e7d 100644 --- a/atproto/lexicon/testdata/record-data-valid.json +++ b/atproto/lexicon/testdata/record-data-valid.json @@ -86,7 +86,10 @@ "$link": "bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq" } }, - "closedUnion": "example.lexicon.record#demoToken" + "closedUnion": { + "$type": "example.lexicon.record#demoObject", + "a": 1 + } } }, { From 3243c4a2225c00831e1402040e8433008944faf8 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 16:32:41 -0700 Subject: [PATCH 062/111] union validation --- atproto/lexicon/lexicon.go | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 71b47f8f2..207aa88ea 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -46,7 +46,6 @@ func validateRecordConfig(cat Catalog, recordData any, ref string, flags Validat } func validateData(cat Catalog, def any, d any, flags ValidateFlags) error { - // TODO: switch v := def.(type) { case SchemaNull: return v.Validate(d) @@ -127,19 +126,40 @@ func validateArray(cat Catalog, s SchemaArray, arr []any, flags ValidateFlags) e func validateUnion(cat Catalog, s SchemaUnion, d any, flags ValidateFlags) error { closed := s.Closed != nil && *s.Closed == true + + obj, ok := d.(map[string]any) + if !ok { + return fmt.Errorf("union data is not object type") + } + typeVal, ok := obj["$type"] + if !ok { + return fmt.Errorf("union data must have $type") + } + t, ok := typeVal.(string) + if !ok { + return fmt.Errorf("union data must have string $type") + } + for _, ref := range s.fullRefs { + if ref != t { + continue + } def, err := cat.Resolve(ref) if err != nil { - // TODO: how to actually handle unknown defs? - return err - } - if err = validateData(cat, def.Def, d, flags); nil == err { // if success - return nil + return fmt.Errorf("could not resolve known union variant $type: %s", ref) } + return validateData(cat, def.Def, d, flags) } if closed { - return fmt.Errorf("data did not match any variant of closed union") + return fmt.Errorf("data did not match any variant of closed union: %s", t) } - // TODO: anything matches if an open union? - return nil + + // eagerly attempt validation of the open union type + def, err := cat.Resolve(t) + if err != nil { + // NOTE: not currently failing on unknown $type. might add a flag to fail here in the future + return fmt.Errorf("could not resolve known union variant $type: %s", t) + //return nil + } + return validateData(cat, def.Def, d, flags) } From eda52d4bb3f69af5482e3dd943145e0150f5b51f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 17:05:40 -0700 Subject: [PATCH 063/111] add docs and comment --- atproto/lexicon/catalog.go | 11 ++++++--- atproto/lexicon/examples_test.go | 41 ++++++++++++++++++++++++++++++++ atproto/lexicon/language.go | 3 ++- atproto/lexicon/lexicon.go | 20 +++++++++++++--- 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 atproto/lexicon/examples_test.go diff --git a/atproto/lexicon/catalog.go b/atproto/lexicon/catalog.go index 22b4004e2..7797fad93 100644 --- a/atproto/lexicon/catalog.go +++ b/atproto/lexicon/catalog.go @@ -5,20 +5,24 @@ import ( "fmt" "io" "io/fs" + "log/slog" "os" "path/filepath" "strings" ) -// An aggregation of lexicon schemas, and methods for validating generic data against those schemas. +// Interface type for a resolver or container of lexicon schemas, and methods for validating generic data against those schemas. type Catalog interface { + // Looks up a schema refrence (NSID string with optional fragment) to a Schema object. Resolve(ref string) (*Schema, error) } +// Trivial in-memory Lexicon Catalog implementation. type BaseCatalog struct { schemas map[string]Schema } +// Creates a new empty BaseCatalog func NewBaseCatalog() BaseCatalog { return BaseCatalog{ schemas: make(map[string]Schema), @@ -40,6 +44,7 @@ func (c *BaseCatalog) Resolve(ref string) (*Schema, error) { return &s, nil } +// Inserts a schema loaded from a JSON file in to the catalog. func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error { base := sf.ID for frag, def := range sf.Defs { @@ -76,6 +81,7 @@ func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error { return nil } +// Recursively loads all '.json' files from a directory in to the catalog. func (c *BaseCatalog) LoadDirectory(dirPath string) error { return filepath.WalkDir(dirPath, func(p string, d fs.DirEntry, err error) error { if err != nil { @@ -87,8 +93,7 @@ func (c *BaseCatalog) LoadDirectory(dirPath string) error { if !strings.HasSuffix(p, ".json") { return nil } - // TODO: logging - fmt.Println(p) + slog.Debug("loading Lexicon schema file", "path", p) f, err := os.Open(p) if err != nil { return err diff --git a/atproto/lexicon/examples_test.go b/atproto/lexicon/examples_test.go new file mode 100644 index 000000000..66ea2e9ea --- /dev/null +++ b/atproto/lexicon/examples_test.go @@ -0,0 +1,41 @@ +package lexicon + +import ( + "fmt" + + atdata "github.com/bluesky-social/indigo/atproto/data" +) + +func ExampleRecordValidate() { + + // First load Lexicon schema JSON files from local disk. + cat := NewBaseCatalog() + if err := cat.LoadDirectory("testdata/catalog"); err != nil { + panic("failed to load lexicons") + } + + // Parse record JSON data using atproto/data helper + recordJSON := `{ + "$type": "example.lexicon.record", + "integer": 123, + "formats": { + "did": "did:web:example.com", + "aturi": "at://handle.example.com/com.example.nsid/asdf123", + "datetime": "2023-10-30T22:25:23Z", + "language": "en", + "tid": "3kznmn7xqxl22" + } + }` + + recordData, err := atdata.UnmarshalJSON([]byte(recordJSON)) + if err != nil { + panic("failed to parse record JSON") + } + + if err := ValidateRecord(&cat, recordData, "example.lexicon.record", 0); err != nil { + fmt.Printf("Schema validation failed: %v\n", err) + } else { + fmt.Println("Success!") + } + // Output: Success! +} diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go index 25265abc4..734d0031e 100644 --- a/atproto/lexicon/language.go +++ b/atproto/lexicon/language.go @@ -12,7 +12,7 @@ import ( "github.com/rivo/uniseg" ) -// Serialization helper for top-level Lexicon schema JSON objects (files) +// Serialization helper type for top-level Lexicon schema JSON objects (files) type SchemaFile struct { Lexicon int `json:"lexicon,const=1"` ID string `json:"id"` @@ -26,6 +26,7 @@ type SchemaDef struct { Inner any } +// Checks that the schema definition itself is valid (recursively). func (s *SchemaDef) CheckSchema() error { switch v := s.Inner.(type) { case SchemaRecord: diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/lexicon.go index 207aa88ea..840d2ae7a 100644 --- a/atproto/lexicon/lexicon.go +++ b/atproto/lexicon/lexicon.go @@ -5,22 +5,33 @@ import ( "reflect" ) +// Boolean flags tweaking how Lexicon validation rules are interpreted. type ValidateFlags int const ( + // Flag which allows legacy "blob" data to pass validation. AllowLegacyBlob = 1 << iota + // Flag which loosens "datetime" string syntax validation. String must still be an ISO datetime, but might be missing timezone (for example) AllowLenientDatetime + // Flag which requires validation of nested data in open unions. By default nested union types are only validated optimistically (if the type is known in catatalog) for unlisted types. This flag will result in a validation error if the Lexicon can't be resolved from the catalog. StrictRecursiveValidation ) +// Combination of agument flags for less formal validation. Recommended for, eg, working with old/legacy data from 2023. var LenientMode ValidateFlags = AllowLegacyBlob | AllowLenientDatetime +// Represents a Lexicon schema definition type Schema struct { ID string Revision *int Def any } +// Checks Lexicon schema (fetched from the catalog) for the given record, with optional flags tweaking default validation rules. +// +// 'recordData' is typed as 'any', but is expected to be 'map[string]any' +// 'ref' is a reference to the schema type, as an NSID with optional fragment. For records, the '$type' must match 'ref' +// 'flags' are parameters tweaking Lexicon validation rules. Zero value is default. func ValidateRecord(cat Catalog, recordData any, ref string, flags ValidateFlags) error { return validateRecordConfig(cat, recordData, ref, flags) } @@ -155,11 +166,14 @@ func validateUnion(cat Catalog, s SchemaUnion, d any, flags ValidateFlags) error } // eagerly attempt validation of the open union type + // TODO: validate reference as NSID with optional fragment def, err := cat.Resolve(t) if err != nil { - // NOTE: not currently failing on unknown $type. might add a flag to fail here in the future - return fmt.Errorf("could not resolve known union variant $type: %s", t) - //return nil + if flags&StrictRecursiveValidation != 0 { + return fmt.Errorf("could not strictly validate open union variant $type: %s", t) + } + // by default, ignore validation of unknown open union data + return nil } return validateData(cat, def.Def, d, flags) } From 2215e75dbef6f2eb956d15c77193415a14af8cdb Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 17:13:20 -0700 Subject: [PATCH 064/111] go vet --- atproto/lexicon/examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/lexicon/examples_test.go b/atproto/lexicon/examples_test.go index 66ea2e9ea..fe2cd0568 100644 --- a/atproto/lexicon/examples_test.go +++ b/atproto/lexicon/examples_test.go @@ -6,7 +6,7 @@ import ( atdata "github.com/bluesky-social/indigo/atproto/data" ) -func ExampleRecordValidate() { +func ExampleValidateRecord() { // First load Lexicon schema JSON files from local disk. cat := NewBaseCatalog() From 0b5371c144d5a9c9bc4176f9a269506789967a5a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 17:55:11 -0700 Subject: [PATCH 065/111] lexgen against atproto 600fea65d48266572e052f1dd61e41fe9872c943 --- api/atproto/repoapplyWrites.go | 87 +++++++++++++++++++-- api/atproto/repocreateRecord.go | 8 +- api/atproto/repodefs.go | 11 +++ api/atproto/repodeleteRecord.go | 14 +++- api/atproto/repoputRecord.go | 8 +- api/bsky/actordefs.go | 15 ++++ api/bsky/actorprofile.go | 3 +- api/bsky/cbor_gen.go | 45 ++++++++++- api/bsky/embedrecord.go | 1 + api/bsky/feeddefs.go | 32 ++++++++ api/bsky/feedgetAuthorFeed.go | 11 +-- api/bsky/feedgetPostThread.go | 3 +- api/bsky/feedgetQuotes.go | 39 +++++++++ api/bsky/graphgetSuggestedFollowsByActor.go | 2 + api/bsky/unspeccedgetSuggestionsSkeleton.go | 2 + api/ozone/communicationcreateTemplate.go | 2 + api/ozone/communicationdefs.go | 2 + api/ozone/communicationupdateTemplate.go | 2 + api/ozone/moderationdefs.go | 28 ++++--- api/ozone/moderationemitEvent.go | 8 ++ api/ozone/moderationgetRecords.go | 68 ++++++++++++++++ api/ozone/moderationgetRepos.go | 68 ++++++++++++++++ api/ozone/moderationqueryStatuses.go | 43 +++++----- api/ozone/signaturedefs.go | 11 +++ api/ozone/signaturefindCorrelation.go | 30 +++++++ api/ozone/signaturefindRelatedAccounts.go | 40 ++++++++++ api/ozone/signaturesearchAccounts.go | 34 ++++++++ 27 files changed, 564 insertions(+), 53 deletions(-) create mode 100644 api/atproto/repodefs.go create mode 100644 api/bsky/feedgetQuotes.go create mode 100644 api/ozone/moderationgetRecords.go create mode 100644 api/ozone/moderationgetRepos.go create mode 100644 api/ozone/signaturedefs.go create mode 100644 api/ozone/signaturefindCorrelation.go create mode 100644 api/ozone/signaturefindRelatedAccounts.go create mode 100644 api/ozone/signaturesearchAccounts.go diff --git a/api/atproto/repoapplyWrites.go b/api/atproto/repoapplyWrites.go index 939530c14..436a9f13d 100644 --- a/api/atproto/repoapplyWrites.go +++ b/api/atproto/repoapplyWrites.go @@ -25,6 +25,16 @@ type RepoApplyWrites_Create struct { Value *util.LexiconTypeDecoder `json:"value" cborgen:"value"` } +// RepoApplyWrites_CreateResult is a "createResult" in the com.atproto.repo.applyWrites schema. +// +// RECORDTYPE: RepoApplyWrites_CreateResult +type RepoApplyWrites_CreateResult struct { + LexiconTypeID string `json:"$type,const=com.atproto.repo.applyWrites#createResult" cborgen:"$type,const=com.atproto.repo.applyWrites#createResult"` + Cid string `json:"cid" cborgen:"cid"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` +} + // RepoApplyWrites_Delete is a "delete" in the com.atproto.repo.applyWrites schema. // // Operation which deletes an existing record. @@ -36,13 +46,20 @@ type RepoApplyWrites_Delete struct { Rkey string `json:"rkey" cborgen:"rkey"` } +// RepoApplyWrites_DeleteResult is a "deleteResult" in the com.atproto.repo.applyWrites schema. +// +// RECORDTYPE: RepoApplyWrites_DeleteResult +type RepoApplyWrites_DeleteResult struct { + LexiconTypeID string `json:"$type,const=com.atproto.repo.applyWrites#deleteResult" cborgen:"$type,const=com.atproto.repo.applyWrites#deleteResult"` +} + // RepoApplyWrites_Input is the input argument to a com.atproto.repo.applyWrites call. type RepoApplyWrites_Input struct { // repo: The handle or DID of the repo (aka, current account). Repo string `json:"repo" cborgen:"repo"` // swapCommit: If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. SwapCommit *string `json:"swapCommit,omitempty" cborgen:"swapCommit,omitempty"` - // validate: Can be set to 'false' to skip Lexicon schema validation of record data, for all operations. + // validate: Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. Validate *bool `json:"validate,omitempty" cborgen:"validate,omitempty"` Writes []*RepoApplyWrites_Input_Writes_Elem `json:"writes" cborgen:"writes"` } @@ -90,6 +107,55 @@ func (t *RepoApplyWrites_Input_Writes_Elem) UnmarshalJSON(b []byte) error { } } +// RepoApplyWrites_Output is the output of a com.atproto.repo.applyWrites call. +type RepoApplyWrites_Output struct { + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` + Results []*RepoApplyWrites_Output_Results_Elem `json:"results,omitempty" cborgen:"results,omitempty"` +} + +type RepoApplyWrites_Output_Results_Elem struct { + RepoApplyWrites_CreateResult *RepoApplyWrites_CreateResult + RepoApplyWrites_UpdateResult *RepoApplyWrites_UpdateResult + RepoApplyWrites_DeleteResult *RepoApplyWrites_DeleteResult +} + +func (t *RepoApplyWrites_Output_Results_Elem) MarshalJSON() ([]byte, error) { + if t.RepoApplyWrites_CreateResult != nil { + t.RepoApplyWrites_CreateResult.LexiconTypeID = "com.atproto.repo.applyWrites#createResult" + return json.Marshal(t.RepoApplyWrites_CreateResult) + } + if t.RepoApplyWrites_UpdateResult != nil { + t.RepoApplyWrites_UpdateResult.LexiconTypeID = "com.atproto.repo.applyWrites#updateResult" + return json.Marshal(t.RepoApplyWrites_UpdateResult) + } + if t.RepoApplyWrites_DeleteResult != nil { + t.RepoApplyWrites_DeleteResult.LexiconTypeID = "com.atproto.repo.applyWrites#deleteResult" + return json.Marshal(t.RepoApplyWrites_DeleteResult) + } + return nil, fmt.Errorf("cannot marshal empty enum") +} +func (t *RepoApplyWrites_Output_Results_Elem) UnmarshalJSON(b []byte) error { + typ, err := util.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "com.atproto.repo.applyWrites#createResult": + t.RepoApplyWrites_CreateResult = new(RepoApplyWrites_CreateResult) + return json.Unmarshal(b, t.RepoApplyWrites_CreateResult) + case "com.atproto.repo.applyWrites#updateResult": + t.RepoApplyWrites_UpdateResult = new(RepoApplyWrites_UpdateResult) + return json.Unmarshal(b, t.RepoApplyWrites_UpdateResult) + case "com.atproto.repo.applyWrites#deleteResult": + t.RepoApplyWrites_DeleteResult = new(RepoApplyWrites_DeleteResult) + return json.Unmarshal(b, t.RepoApplyWrites_DeleteResult) + + default: + return fmt.Errorf("closed enums must have a matching value") + } +} + // RepoApplyWrites_Update is a "update" in the com.atproto.repo.applyWrites schema. // // Operation which updates an existing record. @@ -102,11 +168,22 @@ type RepoApplyWrites_Update struct { Value *util.LexiconTypeDecoder `json:"value" cborgen:"value"` } +// RepoApplyWrites_UpdateResult is a "updateResult" in the com.atproto.repo.applyWrites schema. +// +// RECORDTYPE: RepoApplyWrites_UpdateResult +type RepoApplyWrites_UpdateResult struct { + LexiconTypeID string `json:"$type,const=com.atproto.repo.applyWrites#updateResult" cborgen:"$type,const=com.atproto.repo.applyWrites#updateResult"` + Cid string `json:"cid" cborgen:"cid"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` +} + // RepoApplyWrites calls the XRPC method "com.atproto.repo.applyWrites". -func RepoApplyWrites(ctx context.Context, c *xrpc.Client, input *RepoApplyWrites_Input) error { - if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.applyWrites", nil, input, nil); err != nil { - return err +func RepoApplyWrites(ctx context.Context, c *xrpc.Client, input *RepoApplyWrites_Input) (*RepoApplyWrites_Output, error) { + var out RepoApplyWrites_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.applyWrites", nil, input, &out); err != nil { + return nil, err } - return nil + return &out, nil } diff --git a/api/atproto/repocreateRecord.go b/api/atproto/repocreateRecord.go index fbc654800..4c3935ea3 100644 --- a/api/atproto/repocreateRecord.go +++ b/api/atproto/repocreateRecord.go @@ -23,14 +23,16 @@ type RepoCreateRecord_Input struct { Rkey *string `json:"rkey,omitempty" cborgen:"rkey,omitempty"` // swapCommit: Compare and swap with the previous commit by CID. SwapCommit *string `json:"swapCommit,omitempty" cborgen:"swapCommit,omitempty"` - // validate: Can be set to 'false' to skip Lexicon schema validation of record data. + // validate: Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. Validate *bool `json:"validate,omitempty" cborgen:"validate,omitempty"` } // RepoCreateRecord_Output is the output of a com.atproto.repo.createRecord call. type RepoCreateRecord_Output struct { - Cid string `json:"cid" cborgen:"cid"` - Uri string `json:"uri" cborgen:"uri"` + Cid string `json:"cid" cborgen:"cid"` + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` } // RepoCreateRecord calls the XRPC method "com.atproto.repo.createRecord". diff --git a/api/atproto/repodefs.go b/api/atproto/repodefs.go new file mode 100644 index 000000000..66ea2aa53 --- /dev/null +++ b/api/atproto/repodefs.go @@ -0,0 +1,11 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package atproto + +// schema: com.atproto.repo.defs + +// RepoDefs_CommitMeta is a "commitMeta" in the com.atproto.repo.defs schema. +type RepoDefs_CommitMeta struct { + Cid string `json:"cid" cborgen:"cid"` + Rev string `json:"rev" cborgen:"rev"` +} diff --git a/api/atproto/repodeleteRecord.go b/api/atproto/repodeleteRecord.go index f474f0254..5ce66c9c7 100644 --- a/api/atproto/repodeleteRecord.go +++ b/api/atproto/repodeleteRecord.go @@ -24,11 +24,17 @@ type RepoDeleteRecord_Input struct { SwapRecord *string `json:"swapRecord,omitempty" cborgen:"swapRecord,omitempty"` } +// RepoDeleteRecord_Output is the output of a com.atproto.repo.deleteRecord call. +type RepoDeleteRecord_Output struct { + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` +} + // RepoDeleteRecord calls the XRPC method "com.atproto.repo.deleteRecord". -func RepoDeleteRecord(ctx context.Context, c *xrpc.Client, input *RepoDeleteRecord_Input) error { - if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, nil); err != nil { - return err +func RepoDeleteRecord(ctx context.Context, c *xrpc.Client, input *RepoDeleteRecord_Input) (*RepoDeleteRecord_Output, error) { + var out RepoDeleteRecord_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil { + return nil, err } - return nil + return &out, nil } diff --git a/api/atproto/repoputRecord.go b/api/atproto/repoputRecord.go index 52bf1d4cf..5b8a77343 100644 --- a/api/atproto/repoputRecord.go +++ b/api/atproto/repoputRecord.go @@ -25,14 +25,16 @@ type RepoPutRecord_Input struct { SwapCommit *string `json:"swapCommit,omitempty" cborgen:"swapCommit,omitempty"` // swapRecord: Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation SwapRecord *string `json:"swapRecord" cborgen:"swapRecord"` - // validate: Can be set to 'false' to skip Lexicon schema validation of record data. + // validate: Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. Validate *bool `json:"validate,omitempty" cborgen:"validate,omitempty"` } // RepoPutRecord_Output is the output of a com.atproto.repo.putRecord call. type RepoPutRecord_Output struct { - Cid string `json:"cid" cborgen:"cid"` - Uri string `json:"uri" cborgen:"uri"` + Cid string `json:"cid" cborgen:"cid"` + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` } // RepoPutRecord calls the XRPC method "com.atproto.repo.putRecord". diff --git a/api/bsky/actordefs.go b/api/bsky/actordefs.go index feda2f128..ee9b9367e 100644 --- a/api/bsky/actordefs.go +++ b/api/bsky/actordefs.go @@ -35,6 +35,8 @@ type ActorDefs_BskyAppProgressGuide struct { type ActorDefs_BskyAppStatePref struct { LexiconTypeID string `json:"$type,const=app.bsky.actor.defs#bskyAppStatePref" cborgen:"$type,const=app.bsky.actor.defs#bskyAppStatePref"` ActiveProgressGuide *ActorDefs_BskyAppProgressGuide `json:"activeProgressGuide,omitempty" cborgen:"activeProgressGuide,omitempty"` + // nuxs: Storage for NUXs the user has encountered. + Nuxs []*ActorDefs_Nux `json:"nuxs,omitempty" cborgen:"nuxs,omitempty"` // queuedNudges: An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. QueuedNudges []string `json:"queuedNudges,omitempty" cborgen:"queuedNudges,omitempty"` } @@ -132,6 +134,18 @@ type ActorDefs_MutedWordsPref struct { Items []*ActorDefs_MutedWord `json:"items" cborgen:"items"` } +// ActorDefs_Nux is a "nux" in the app.bsky.actor.defs schema. +// +// A new user experiences (NUX) storage object +type ActorDefs_Nux struct { + Completed bool `json:"completed" cborgen:"completed"` + // data: Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. + Data *string `json:"data,omitempty" cborgen:"data,omitempty"` + // expiresAt: The date and time at which the NUX will expire and should be considered completed. + ExpiresAt *string `json:"expiresAt,omitempty" cborgen:"expiresAt,omitempty"` + Id string `json:"id" cborgen:"id"` +} + // ActorDefs_PersonalDetailsPref is a "personalDetailsPref" in the app.bsky.actor.defs schema. // // RECORDTYPE: ActorDefs_PersonalDetailsPref @@ -311,6 +325,7 @@ type ActorDefs_ProfileViewDetailed struct { IndexedAt *string `json:"indexedAt,omitempty" cborgen:"indexedAt,omitempty"` JoinedViaStarterPack *GraphDefs_StarterPackViewBasic `json:"joinedViaStarterPack,omitempty" cborgen:"joinedViaStarterPack,omitempty"` Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` + PinnedPost *comatprototypes.RepoStrongRef `json:"pinnedPost,omitempty" cborgen:"pinnedPost,omitempty"` PostsCount *int64 `json:"postsCount,omitempty" cborgen:"postsCount,omitempty"` Viewer *ActorDefs_ViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` } diff --git a/api/bsky/actorprofile.go b/api/bsky/actorprofile.go index b60ea27b1..e94a0e2d3 100644 --- a/api/bsky/actorprofile.go +++ b/api/bsky/actorprofile.go @@ -31,7 +31,8 @@ type ActorProfile struct { DisplayName *string `json:"displayName,omitempty" cborgen:"displayName,omitempty"` JoinedViaStarterPack *comatprototypes.RepoStrongRef `json:"joinedViaStarterPack,omitempty" cborgen:"joinedViaStarterPack,omitempty"` // labels: Self-label values, specific to the Bluesky application, on the overall account. - Labels *ActorProfile_Labels `json:"labels,omitempty" cborgen:"labels,omitempty"` + Labels *ActorProfile_Labels `json:"labels,omitempty" cborgen:"labels,omitempty"` + PinnedPost *comatprototypes.RepoStrongRef `json:"pinnedPost,omitempty" cborgen:"pinnedPost,omitempty"` } // Self-label values, specific to the Bluesky application, on the overall account. diff --git a/api/bsky/cbor_gen.go b/api/bsky/cbor_gen.go index 4287bcd51..bc4a94b78 100644 --- a/api/bsky/cbor_gen.go +++ b/api/bsky/cbor_gen.go @@ -2091,7 +2091,7 @@ func (t *ActorProfile) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 8 + fieldCount := 9 if t.Avatar == nil { fieldCount-- @@ -2121,6 +2121,10 @@ func (t *ActorProfile) MarshalCBOR(w io.Writer) error { fieldCount-- } + if t.PinnedPost == nil { + fieldCount-- + } + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { return err } @@ -2233,6 +2237,25 @@ func (t *ActorProfile) MarshalCBOR(w io.Writer) error { } } + // t.PinnedPost (atproto.RepoStrongRef) (struct) + if t.PinnedPost != nil { + + if len("pinnedPost") > 1000000 { + return xerrors.Errorf("Value in field \"pinnedPost\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedPost"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pinnedPost")); err != nil { + return err + } + + if err := t.PinnedPost.MarshalCBOR(cw); err != nil { + return err + } + } + // t.Description (string) (string) if t.Description != nil { @@ -2448,6 +2471,26 @@ func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { t.CreatedAt = (*string)(&sval) } } + // t.PinnedPost (atproto.RepoStrongRef) (struct) + case "pinnedPost": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.PinnedPost = new(atproto.RepoStrongRef) + if err := t.PinnedPost.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PinnedPost pointer: %w", err) + } + } + + } // t.Description (string) (string) case "description": diff --git a/api/bsky/embedrecord.go b/api/bsky/embedrecord.go index dbeff69c1..baddc252f 100644 --- a/api/bsky/embedrecord.go +++ b/api/bsky/embedrecord.go @@ -68,6 +68,7 @@ type EmbedRecord_ViewRecord struct { IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` + QuoteCount *int64 `json:"quoteCount,omitempty" cborgen:"quoteCount,omitempty"` ReplyCount *int64 `json:"replyCount,omitempty" cborgen:"replyCount,omitempty"` RepostCount *int64 `json:"repostCount,omitempty" cborgen:"repostCount,omitempty"` Uri string `json:"uri" cborgen:"uri"` diff --git a/api/bsky/feeddefs.go b/api/bsky/feeddefs.go index c2f0c1e91..d3c0d3784 100644 --- a/api/bsky/feeddefs.go +++ b/api/bsky/feeddefs.go @@ -39,6 +39,7 @@ type FeedDefs_FeedViewPost struct { type FeedDefs_FeedViewPost_Reason struct { FeedDefs_ReasonRepost *FeedDefs_ReasonRepost + FeedDefs_ReasonPin *FeedDefs_ReasonPin } func (t *FeedDefs_FeedViewPost_Reason) MarshalJSON() ([]byte, error) { @@ -46,6 +47,10 @@ func (t *FeedDefs_FeedViewPost_Reason) MarshalJSON() ([]byte, error) { t.FeedDefs_ReasonRepost.LexiconTypeID = "app.bsky.feed.defs#reasonRepost" return json.Marshal(t.FeedDefs_ReasonRepost) } + if t.FeedDefs_ReasonPin != nil { + t.FeedDefs_ReasonPin.LexiconTypeID = "app.bsky.feed.defs#reasonPin" + return json.Marshal(t.FeedDefs_ReasonPin) + } return nil, fmt.Errorf("cannot marshal empty enum") } func (t *FeedDefs_FeedViewPost_Reason) UnmarshalJSON(b []byte) error { @@ -58,6 +63,9 @@ func (t *FeedDefs_FeedViewPost_Reason) UnmarshalJSON(b []byte) error { case "app.bsky.feed.defs#reasonRepost": t.FeedDefs_ReasonRepost = new(FeedDefs_ReasonRepost) return json.Unmarshal(b, t.FeedDefs_ReasonRepost) + case "app.bsky.feed.defs#reasonPin": + t.FeedDefs_ReasonPin = new(FeedDefs_ReasonPin) + return json.Unmarshal(b, t.FeedDefs_ReasonPin) default: return nil @@ -117,6 +125,7 @@ type FeedDefs_PostView struct { IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` + QuoteCount *int64 `json:"quoteCount,omitempty" cborgen:"quoteCount,omitempty"` Record *util.LexiconTypeDecoder `json:"record" cborgen:"record"` ReplyCount *int64 `json:"replyCount,omitempty" cborgen:"replyCount,omitempty"` RepostCount *int64 `json:"repostCount,omitempty" cborgen:"repostCount,omitempty"` @@ -184,6 +193,13 @@ func (t *FeedDefs_PostView_Embed) UnmarshalJSON(b []byte) error { } } +// FeedDefs_ReasonPin is a "reasonPin" in the app.bsky.feed.defs schema. +// +// RECORDTYPE: FeedDefs_ReasonPin +type FeedDefs_ReasonPin struct { + LexiconTypeID string `json:"$type,const=app.bsky.feed.defs#reasonPin" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` +} + // FeedDefs_ReasonRepost is a "reasonRepost" in the app.bsky.feed.defs schema. // // RECORDTYPE: FeedDefs_ReasonRepost @@ -297,6 +313,7 @@ type FeedDefs_SkeletonFeedPost struct { type FeedDefs_SkeletonFeedPost_Reason struct { FeedDefs_SkeletonReasonRepost *FeedDefs_SkeletonReasonRepost + FeedDefs_SkeletonReasonPin *FeedDefs_SkeletonReasonPin } func (t *FeedDefs_SkeletonFeedPost_Reason) MarshalJSON() ([]byte, error) { @@ -304,6 +321,10 @@ func (t *FeedDefs_SkeletonFeedPost_Reason) MarshalJSON() ([]byte, error) { t.FeedDefs_SkeletonReasonRepost.LexiconTypeID = "app.bsky.feed.defs#skeletonReasonRepost" return json.Marshal(t.FeedDefs_SkeletonReasonRepost) } + if t.FeedDefs_SkeletonReasonPin != nil { + t.FeedDefs_SkeletonReasonPin.LexiconTypeID = "app.bsky.feed.defs#skeletonReasonPin" + return json.Marshal(t.FeedDefs_SkeletonReasonPin) + } return nil, fmt.Errorf("cannot marshal empty enum") } func (t *FeedDefs_SkeletonFeedPost_Reason) UnmarshalJSON(b []byte) error { @@ -316,12 +337,22 @@ func (t *FeedDefs_SkeletonFeedPost_Reason) UnmarshalJSON(b []byte) error { case "app.bsky.feed.defs#skeletonReasonRepost": t.FeedDefs_SkeletonReasonRepost = new(FeedDefs_SkeletonReasonRepost) return json.Unmarshal(b, t.FeedDefs_SkeletonReasonRepost) + case "app.bsky.feed.defs#skeletonReasonPin": + t.FeedDefs_SkeletonReasonPin = new(FeedDefs_SkeletonReasonPin) + return json.Unmarshal(b, t.FeedDefs_SkeletonReasonPin) default: return nil } } +// FeedDefs_SkeletonReasonPin is a "skeletonReasonPin" in the app.bsky.feed.defs schema. +// +// RECORDTYPE: FeedDefs_SkeletonReasonPin +type FeedDefs_SkeletonReasonPin struct { + LexiconTypeID string `json:"$type,const=app.bsky.feed.defs#skeletonReasonPin" cborgen:"$type,const=app.bsky.feed.defs#skeletonReasonPin"` +} + // FeedDefs_SkeletonReasonRepost is a "skeletonReasonRepost" in the app.bsky.feed.defs schema. // // RECORDTYPE: FeedDefs_SkeletonReasonRepost @@ -440,6 +471,7 @@ type FeedDefs_ThreadgateView struct { type FeedDefs_ViewerState struct { EmbeddingDisabled *bool `json:"embeddingDisabled,omitempty" cborgen:"embeddingDisabled,omitempty"` Like *string `json:"like,omitempty" cborgen:"like,omitempty"` + Pinned *bool `json:"pinned,omitempty" cborgen:"pinned,omitempty"` ReplyDisabled *bool `json:"replyDisabled,omitempty" cborgen:"replyDisabled,omitempty"` Repost *string `json:"repost,omitempty" cborgen:"repost,omitempty"` ThreadMuted *bool `json:"threadMuted,omitempty" cborgen:"threadMuted,omitempty"` diff --git a/api/bsky/feedgetAuthorFeed.go b/api/bsky/feedgetAuthorFeed.go index c582ea77a..32522d5b2 100644 --- a/api/bsky/feedgetAuthorFeed.go +++ b/api/bsky/feedgetAuthorFeed.go @@ -19,14 +19,15 @@ type FeedGetAuthorFeed_Output struct { // FeedGetAuthorFeed calls the XRPC method "app.bsky.feed.getAuthorFeed". // // filter: Combinations of post/repost types to include in response. -func FeedGetAuthorFeed(ctx context.Context, c *xrpc.Client, actor string, cursor string, filter string, limit int64) (*FeedGetAuthorFeed_Output, error) { +func FeedGetAuthorFeed(ctx context.Context, c *xrpc.Client, actor string, cursor string, filter string, includePins bool, limit int64) (*FeedGetAuthorFeed_Output, error) { var out FeedGetAuthorFeed_Output params := map[string]interface{}{ - "actor": actor, - "cursor": cursor, - "filter": filter, - "limit": limit, + "actor": actor, + "cursor": cursor, + "filter": filter, + "includePins": includePins, + "limit": limit, } if err := c.Do(ctx, xrpc.Query, "", "app.bsky.feed.getAuthorFeed", params, nil, &out); err != nil { return nil, err diff --git a/api/bsky/feedgetPostThread.go b/api/bsky/feedgetPostThread.go index aea994e1a..0cb7bc46d 100644 --- a/api/bsky/feedgetPostThread.go +++ b/api/bsky/feedgetPostThread.go @@ -15,7 +15,8 @@ import ( // FeedGetPostThread_Output is the output of a app.bsky.feed.getPostThread call. type FeedGetPostThread_Output struct { - Thread *FeedGetPostThread_Output_Thread `json:"thread" cborgen:"thread"` + Thread *FeedGetPostThread_Output_Thread `json:"thread" cborgen:"thread"` + Threadgate *FeedDefs_ThreadgateView `json:"threadgate,omitempty" cborgen:"threadgate,omitempty"` } type FeedGetPostThread_Output_Thread struct { diff --git a/api/bsky/feedgetQuotes.go b/api/bsky/feedgetQuotes.go new file mode 100644 index 000000000..d060d666d --- /dev/null +++ b/api/bsky/feedgetQuotes.go @@ -0,0 +1,39 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package bsky + +// schema: app.bsky.feed.getQuotes + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// FeedGetQuotes_Output is the output of a app.bsky.feed.getQuotes call. +type FeedGetQuotes_Output struct { + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Posts []*FeedDefs_PostView `json:"posts" cborgen:"posts"` + Uri string `json:"uri" cborgen:"uri"` +} + +// FeedGetQuotes calls the XRPC method "app.bsky.feed.getQuotes". +// +// cid: If supplied, filters to quotes of specific version (by CID) of the post record. +// uri: Reference (AT-URI) of post record +func FeedGetQuotes(ctx context.Context, c *xrpc.Client, cid string, cursor string, limit int64, uri string) (*FeedGetQuotes_Output, error) { + var out FeedGetQuotes_Output + + params := map[string]interface{}{ + "cid": cid, + "cursor": cursor, + "limit": limit, + "uri": uri, + } + if err := c.Do(ctx, xrpc.Query, "", "app.bsky.feed.getQuotes", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/bsky/graphgetSuggestedFollowsByActor.go b/api/bsky/graphgetSuggestedFollowsByActor.go index 58a334113..0dc57b929 100644 --- a/api/bsky/graphgetSuggestedFollowsByActor.go +++ b/api/bsky/graphgetSuggestedFollowsByActor.go @@ -12,6 +12,8 @@ import ( // GraphGetSuggestedFollowsByActor_Output is the output of a app.bsky.graph.getSuggestedFollowsByActor call. type GraphGetSuggestedFollowsByActor_Output struct { + // isFallback: If true, response has fallen-back to generic results, and is not scoped using relativeToDid + IsFallback *bool `json:"isFallback,omitempty" cborgen:"isFallback,omitempty"` Suggestions []*ActorDefs_ProfileView `json:"suggestions" cborgen:"suggestions"` } diff --git a/api/bsky/unspeccedgetSuggestionsSkeleton.go b/api/bsky/unspeccedgetSuggestionsSkeleton.go index 6a49b2cbe..f01ab8e96 100644 --- a/api/bsky/unspeccedgetSuggestionsSkeleton.go +++ b/api/bsky/unspeccedgetSuggestionsSkeleton.go @@ -14,6 +14,8 @@ import ( type UnspeccedGetSuggestionsSkeleton_Output struct { Actors []*UnspeccedDefs_SkeletonSearchActor `json:"actors" cborgen:"actors"` Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + // relativeToDid: DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. + RelativeToDid *string `json:"relativeToDid,omitempty" cborgen:"relativeToDid,omitempty"` } // UnspeccedGetSuggestionsSkeleton calls the XRPC method "app.bsky.unspecced.getSuggestionsSkeleton". diff --git a/api/ozone/communicationcreateTemplate.go b/api/ozone/communicationcreateTemplate.go index aef239863..a43c2bba0 100644 --- a/api/ozone/communicationcreateTemplate.go +++ b/api/ozone/communicationcreateTemplate.go @@ -16,6 +16,8 @@ type CommunicationCreateTemplate_Input struct { ContentMarkdown string `json:"contentMarkdown" cborgen:"contentMarkdown"` // createdBy: DID of the user who is creating the template. CreatedBy *string `json:"createdBy,omitempty" cborgen:"createdBy,omitempty"` + // lang: Message language. + Lang *string `json:"lang,omitempty" cborgen:"lang,omitempty"` // name: Name of the template. Name string `json:"name" cborgen:"name"` // subject: Subject of the message, used in emails. diff --git a/api/ozone/communicationdefs.go b/api/ozone/communicationdefs.go index 8184894c2..8c59f4a06 100644 --- a/api/ozone/communicationdefs.go +++ b/api/ozone/communicationdefs.go @@ -11,6 +11,8 @@ type CommunicationDefs_TemplateView struct { CreatedAt string `json:"createdAt" cborgen:"createdAt"` Disabled bool `json:"disabled" cborgen:"disabled"` Id string `json:"id" cborgen:"id"` + // lang: Message language. + Lang *string `json:"lang,omitempty" cborgen:"lang,omitempty"` // lastUpdatedBy: DID of the user who last updated the template. LastUpdatedBy string `json:"lastUpdatedBy" cborgen:"lastUpdatedBy"` // name: Name of the template. diff --git a/api/ozone/communicationupdateTemplate.go b/api/ozone/communicationupdateTemplate.go index 78b30a8ef..88b860847 100644 --- a/api/ozone/communicationupdateTemplate.go +++ b/api/ozone/communicationupdateTemplate.go @@ -17,6 +17,8 @@ type CommunicationUpdateTemplate_Input struct { Disabled *bool `json:"disabled,omitempty" cborgen:"disabled,omitempty"` // id: ID of the template to be updated. Id string `json:"id" cborgen:"id"` + // lang: Message language. + Lang *string `json:"lang,omitempty" cborgen:"lang,omitempty"` // name: Name of the template. Name *string `json:"name,omitempty" cborgen:"name,omitempty"` // subject: Subject of the message, used in emails. diff --git a/api/ozone/moderationdefs.go b/api/ozone/moderationdefs.go index a833fbda0..038bcbe7a 100644 --- a/api/ozone/moderationdefs.go +++ b/api/ozone/moderationdefs.go @@ -212,8 +212,10 @@ type ModerationDefs_ModEventTag struct { // // RECORDTYPE: ModerationDefs_ModEventTakedown type ModerationDefs_ModEventTakedown struct { - LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#modEventTakedown" cborgen:"$type,const=tools.ozone.moderation.defs#modEventTakedown"` - Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` + LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#modEventTakedown" cborgen:"$type,const=tools.ozone.moderation.defs#modEventTakedown"` + // acknowledgeAccountSubjects: If true, all other reports on content authored by this account will be resolved (acknowledged). + AcknowledgeAccountSubjects *bool `json:"acknowledgeAccountSubjects,omitempty" cborgen:"acknowledgeAccountSubjects,omitempty"` + Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"` // durationInHours: Indicates how long the takedown should be in effect before automatically expiring. DurationInHours *int64 `json:"durationInHours,omitempty" cborgen:"durationInHours,omitempty"` } @@ -659,15 +661,18 @@ type ModerationDefs_RecordView struct { } // ModerationDefs_RecordViewDetail is a "recordViewDetail" in the tools.ozone.moderation.defs schema. +// +// RECORDTYPE: ModerationDefs_RecordViewDetail type ModerationDefs_RecordViewDetail struct { - Blobs []*ModerationDefs_BlobView `json:"blobs" cborgen:"blobs"` - Cid string `json:"cid" cborgen:"cid"` - IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` - Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` - Moderation *ModerationDefs_ModerationDetail `json:"moderation" cborgen:"moderation"` - Repo *ModerationDefs_RepoView `json:"repo" cborgen:"repo"` - Uri string `json:"uri" cborgen:"uri"` - Value *util.LexiconTypeDecoder `json:"value" cborgen:"value"` + LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#recordViewDetail" cborgen:"$type,const=tools.ozone.moderation.defs#recordViewDetail"` + Blobs []*ModerationDefs_BlobView `json:"blobs" cborgen:"blobs"` + Cid string `json:"cid" cborgen:"cid"` + IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` + Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` + Moderation *ModerationDefs_ModerationDetail `json:"moderation" cborgen:"moderation"` + Repo *ModerationDefs_RepoView `json:"repo" cborgen:"repo"` + Uri string `json:"uri" cborgen:"uri"` + Value *util.LexiconTypeDecoder `json:"value" cborgen:"value"` } // ModerationDefs_RecordViewNotFound is a "recordViewNotFound" in the tools.ozone.moderation.defs schema. @@ -696,7 +701,10 @@ type ModerationDefs_RepoView struct { } // ModerationDefs_RepoViewDetail is a "repoViewDetail" in the tools.ozone.moderation.defs schema. +// +// RECORDTYPE: ModerationDefs_RepoViewDetail type ModerationDefs_RepoViewDetail struct { + LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#repoViewDetail" cborgen:"$type,const=tools.ozone.moderation.defs#repoViewDetail"` DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` Did string `json:"did" cborgen:"did"` Email *string `json:"email,omitempty" cborgen:"email,omitempty"` diff --git a/api/ozone/moderationemitEvent.go b/api/ozone/moderationemitEvent.go index ebd8662c8..064eef3a6 100644 --- a/api/ozone/moderationemitEvent.go +++ b/api/ozone/moderationemitEvent.go @@ -34,6 +34,7 @@ type ModerationEmitEvent_Input_Event struct { ModerationDefs_ModEventMuteReporter *ModerationDefs_ModEventMuteReporter ModerationDefs_ModEventUnmuteReporter *ModerationDefs_ModEventUnmuteReporter ModerationDefs_ModEventReverseTakedown *ModerationDefs_ModEventReverseTakedown + ModerationDefs_ModEventResolveAppeal *ModerationDefs_ModEventResolveAppeal ModerationDefs_ModEventEmail *ModerationDefs_ModEventEmail ModerationDefs_ModEventTag *ModerationDefs_ModEventTag } @@ -83,6 +84,10 @@ func (t *ModerationEmitEvent_Input_Event) MarshalJSON() ([]byte, error) { t.ModerationDefs_ModEventReverseTakedown.LexiconTypeID = "tools.ozone.moderation.defs#modEventReverseTakedown" return json.Marshal(t.ModerationDefs_ModEventReverseTakedown) } + if t.ModerationDefs_ModEventResolveAppeal != nil { + t.ModerationDefs_ModEventResolveAppeal.LexiconTypeID = "tools.ozone.moderation.defs#modEventResolveAppeal" + return json.Marshal(t.ModerationDefs_ModEventResolveAppeal) + } if t.ModerationDefs_ModEventEmail != nil { t.ModerationDefs_ModEventEmail.LexiconTypeID = "tools.ozone.moderation.defs#modEventEmail" return json.Marshal(t.ModerationDefs_ModEventEmail) @@ -133,6 +138,9 @@ func (t *ModerationEmitEvent_Input_Event) UnmarshalJSON(b []byte) error { case "tools.ozone.moderation.defs#modEventReverseTakedown": t.ModerationDefs_ModEventReverseTakedown = new(ModerationDefs_ModEventReverseTakedown) return json.Unmarshal(b, t.ModerationDefs_ModEventReverseTakedown) + case "tools.ozone.moderation.defs#modEventResolveAppeal": + t.ModerationDefs_ModEventResolveAppeal = new(ModerationDefs_ModEventResolveAppeal) + return json.Unmarshal(b, t.ModerationDefs_ModEventResolveAppeal) case "tools.ozone.moderation.defs#modEventEmail": t.ModerationDefs_ModEventEmail = new(ModerationDefs_ModEventEmail) return json.Unmarshal(b, t.ModerationDefs_ModEventEmail) diff --git a/api/ozone/moderationgetRecords.go b/api/ozone/moderationgetRecords.go new file mode 100644 index 000000000..86b14e2a6 --- /dev/null +++ b/api/ozone/moderationgetRecords.go @@ -0,0 +1,68 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.moderation.getRecords + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bluesky-social/indigo/lex/util" + "github.com/bluesky-social/indigo/xrpc" +) + +// ModerationGetRecords_Output is the output of a tools.ozone.moderation.getRecords call. +type ModerationGetRecords_Output struct { + Records []*ModerationGetRecords_Output_Records_Elem `json:"records" cborgen:"records"` +} + +type ModerationGetRecords_Output_Records_Elem struct { + ModerationDefs_RecordViewDetail *ModerationDefs_RecordViewDetail + ModerationDefs_RecordViewNotFound *ModerationDefs_RecordViewNotFound +} + +func (t *ModerationGetRecords_Output_Records_Elem) MarshalJSON() ([]byte, error) { + if t.ModerationDefs_RecordViewDetail != nil { + t.ModerationDefs_RecordViewDetail.LexiconTypeID = "tools.ozone.moderation.defs#recordViewDetail" + return json.Marshal(t.ModerationDefs_RecordViewDetail) + } + if t.ModerationDefs_RecordViewNotFound != nil { + t.ModerationDefs_RecordViewNotFound.LexiconTypeID = "tools.ozone.moderation.defs#recordViewNotFound" + return json.Marshal(t.ModerationDefs_RecordViewNotFound) + } + return nil, fmt.Errorf("cannot marshal empty enum") +} +func (t *ModerationGetRecords_Output_Records_Elem) UnmarshalJSON(b []byte) error { + typ, err := util.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "tools.ozone.moderation.defs#recordViewDetail": + t.ModerationDefs_RecordViewDetail = new(ModerationDefs_RecordViewDetail) + return json.Unmarshal(b, t.ModerationDefs_RecordViewDetail) + case "tools.ozone.moderation.defs#recordViewNotFound": + t.ModerationDefs_RecordViewNotFound = new(ModerationDefs_RecordViewNotFound) + return json.Unmarshal(b, t.ModerationDefs_RecordViewNotFound) + + default: + return nil + } +} + +// ModerationGetRecords calls the XRPC method "tools.ozone.moderation.getRecords". +func ModerationGetRecords(ctx context.Context, c *xrpc.Client, uris []string) (*ModerationGetRecords_Output, error) { + var out ModerationGetRecords_Output + + params := map[string]interface{}{ + "uris": uris, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.moderation.getRecords", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/moderationgetRepos.go b/api/ozone/moderationgetRepos.go new file mode 100644 index 000000000..af51a97f3 --- /dev/null +++ b/api/ozone/moderationgetRepos.go @@ -0,0 +1,68 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.moderation.getRepos + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bluesky-social/indigo/lex/util" + "github.com/bluesky-social/indigo/xrpc" +) + +// ModerationGetRepos_Output is the output of a tools.ozone.moderation.getRepos call. +type ModerationGetRepos_Output struct { + Repos []*ModerationGetRepos_Output_Repos_Elem `json:"repos" cborgen:"repos"` +} + +type ModerationGetRepos_Output_Repos_Elem struct { + ModerationDefs_RepoViewDetail *ModerationDefs_RepoViewDetail + ModerationDefs_RepoViewNotFound *ModerationDefs_RepoViewNotFound +} + +func (t *ModerationGetRepos_Output_Repos_Elem) MarshalJSON() ([]byte, error) { + if t.ModerationDefs_RepoViewDetail != nil { + t.ModerationDefs_RepoViewDetail.LexiconTypeID = "tools.ozone.moderation.defs#repoViewDetail" + return json.Marshal(t.ModerationDefs_RepoViewDetail) + } + if t.ModerationDefs_RepoViewNotFound != nil { + t.ModerationDefs_RepoViewNotFound.LexiconTypeID = "tools.ozone.moderation.defs#repoViewNotFound" + return json.Marshal(t.ModerationDefs_RepoViewNotFound) + } + return nil, fmt.Errorf("cannot marshal empty enum") +} +func (t *ModerationGetRepos_Output_Repos_Elem) UnmarshalJSON(b []byte) error { + typ, err := util.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "tools.ozone.moderation.defs#repoViewDetail": + t.ModerationDefs_RepoViewDetail = new(ModerationDefs_RepoViewDetail) + return json.Unmarshal(b, t.ModerationDefs_RepoViewDetail) + case "tools.ozone.moderation.defs#repoViewNotFound": + t.ModerationDefs_RepoViewNotFound = new(ModerationDefs_RepoViewNotFound) + return json.Unmarshal(b, t.ModerationDefs_RepoViewNotFound) + + default: + return nil + } +} + +// ModerationGetRepos calls the XRPC method "tools.ozone.moderation.getRepos". +func ModerationGetRepos(ctx context.Context, c *xrpc.Client, dids []string) (*ModerationGetRepos_Output, error) { + var out ModerationGetRepos_Output + + params := map[string]interface{}{ + "dids": dids, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.moderation.getRepos", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/moderationqueryStatuses.go b/api/ozone/moderationqueryStatuses.go index fe00aad86..68dac122b 100644 --- a/api/ozone/moderationqueryStatuses.go +++ b/api/ozone/moderationqueryStatuses.go @@ -20,6 +20,7 @@ type ModerationQueryStatuses_Output struct { // // appealed: Get subjects in unresolved appealed status // comment: Search subjects by keyword from comments +// includeAllUserRecords: All subjects belonging to the account specified in the 'subject' param will be returned. // includeMuted: By default, we don't include muted subjects in the results. Set this to true to include them. // lastReviewedBy: Get all subject statuses that were reviewed by a specific moderator // onlyMuted: When set to true, only muted subjects and reporters will be returned. @@ -28,30 +29,32 @@ type ModerationQueryStatuses_Output struct { // reviewState: Specify when fetching subjects in a certain state // reviewedAfter: Search subjects reviewed after a given timestamp // reviewedBefore: Search subjects reviewed before a given timestamp +// subject: The subject to get the status for. // takendown: Get subjects that were taken down -func ModerationQueryStatuses(ctx context.Context, c *xrpc.Client, appealed bool, comment string, cursor string, excludeTags []string, ignoreSubjects []string, includeMuted bool, lastReviewedBy string, limit int64, onlyMuted bool, reportedAfter string, reportedBefore string, reviewState string, reviewedAfter string, reviewedBefore string, sortDirection string, sortField string, subject string, tags []string, takendown bool) (*ModerationQueryStatuses_Output, error) { +func ModerationQueryStatuses(ctx context.Context, c *xrpc.Client, appealed bool, comment string, cursor string, excludeTags []string, ignoreSubjects []string, includeAllUserRecords bool, includeMuted bool, lastReviewedBy string, limit int64, onlyMuted bool, reportedAfter string, reportedBefore string, reviewState string, reviewedAfter string, reviewedBefore string, sortDirection string, sortField string, subject string, tags []string, takendown bool) (*ModerationQueryStatuses_Output, error) { var out ModerationQueryStatuses_Output params := map[string]interface{}{ - "appealed": appealed, - "comment": comment, - "cursor": cursor, - "excludeTags": excludeTags, - "ignoreSubjects": ignoreSubjects, - "includeMuted": includeMuted, - "lastReviewedBy": lastReviewedBy, - "limit": limit, - "onlyMuted": onlyMuted, - "reportedAfter": reportedAfter, - "reportedBefore": reportedBefore, - "reviewState": reviewState, - "reviewedAfter": reviewedAfter, - "reviewedBefore": reviewedBefore, - "sortDirection": sortDirection, - "sortField": sortField, - "subject": subject, - "tags": tags, - "takendown": takendown, + "appealed": appealed, + "comment": comment, + "cursor": cursor, + "excludeTags": excludeTags, + "ignoreSubjects": ignoreSubjects, + "includeAllUserRecords": includeAllUserRecords, + "includeMuted": includeMuted, + "lastReviewedBy": lastReviewedBy, + "limit": limit, + "onlyMuted": onlyMuted, + "reportedAfter": reportedAfter, + "reportedBefore": reportedBefore, + "reviewState": reviewState, + "reviewedAfter": reviewedAfter, + "reviewedBefore": reviewedBefore, + "sortDirection": sortDirection, + "sortField": sortField, + "subject": subject, + "tags": tags, + "takendown": takendown, } if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.moderation.queryStatuses", params, nil, &out); err != nil { return nil, err diff --git a/api/ozone/signaturedefs.go b/api/ozone/signaturedefs.go new file mode 100644 index 000000000..7336836a6 --- /dev/null +++ b/api/ozone/signaturedefs.go @@ -0,0 +1,11 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.signature.defs + +// SignatureDefs_SigDetail is a "sigDetail" in the tools.ozone.signature.defs schema. +type SignatureDefs_SigDetail struct { + Property string `json:"property" cborgen:"property"` + Value string `json:"value" cborgen:"value"` +} diff --git a/api/ozone/signaturefindCorrelation.go b/api/ozone/signaturefindCorrelation.go new file mode 100644 index 000000000..e07e67fe4 --- /dev/null +++ b/api/ozone/signaturefindCorrelation.go @@ -0,0 +1,30 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.signature.findCorrelation + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SignatureFindCorrelation_Output is the output of a tools.ozone.signature.findCorrelation call. +type SignatureFindCorrelation_Output struct { + Details []*SignatureDefs_SigDetail `json:"details" cborgen:"details"` +} + +// SignatureFindCorrelation calls the XRPC method "tools.ozone.signature.findCorrelation". +func SignatureFindCorrelation(ctx context.Context, c *xrpc.Client, dids []string) (*SignatureFindCorrelation_Output, error) { + var out SignatureFindCorrelation_Output + + params := map[string]interface{}{ + "dids": dids, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.signature.findCorrelation", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/signaturefindRelatedAccounts.go b/api/ozone/signaturefindRelatedAccounts.go new file mode 100644 index 000000000..b32bba8e3 --- /dev/null +++ b/api/ozone/signaturefindRelatedAccounts.go @@ -0,0 +1,40 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.signature.findRelatedAccounts + +import ( + "context" + + comatprototypes "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/xrpc" +) + +// SignatureFindRelatedAccounts_Output is the output of a tools.ozone.signature.findRelatedAccounts call. +type SignatureFindRelatedAccounts_Output struct { + Accounts []*SignatureFindRelatedAccounts_RelatedAccount `json:"accounts" cborgen:"accounts"` + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` +} + +// SignatureFindRelatedAccounts_RelatedAccount is a "relatedAccount" in the tools.ozone.signature.findRelatedAccounts schema. +type SignatureFindRelatedAccounts_RelatedAccount struct { + Account *comatprototypes.AdminDefs_AccountView `json:"account" cborgen:"account"` + Similarities []*SignatureDefs_SigDetail `json:"similarities,omitempty" cborgen:"similarities,omitempty"` +} + +// SignatureFindRelatedAccounts calls the XRPC method "tools.ozone.signature.findRelatedAccounts". +func SignatureFindRelatedAccounts(ctx context.Context, c *xrpc.Client, cursor string, did string, limit int64) (*SignatureFindRelatedAccounts_Output, error) { + var out SignatureFindRelatedAccounts_Output + + params := map[string]interface{}{ + "cursor": cursor, + "did": did, + "limit": limit, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.signature.findRelatedAccounts", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/signaturesearchAccounts.go b/api/ozone/signaturesearchAccounts.go new file mode 100644 index 000000000..08da3eec9 --- /dev/null +++ b/api/ozone/signaturesearchAccounts.go @@ -0,0 +1,34 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.signature.searchAccounts + +import ( + "context" + + comatprototypes "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/xrpc" +) + +// SignatureSearchAccounts_Output is the output of a tools.ozone.signature.searchAccounts call. +type SignatureSearchAccounts_Output struct { + Accounts []*comatprototypes.AdminDefs_AccountView `json:"accounts" cborgen:"accounts"` + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` +} + +// SignatureSearchAccounts calls the XRPC method "tools.ozone.signature.searchAccounts". +func SignatureSearchAccounts(ctx context.Context, c *xrpc.Client, cursor string, limit int64, values []string) (*SignatureSearchAccounts_Output, error) { + var out SignatureSearchAccounts_Output + + params := map[string]interface{}{ + "cursor": cursor, + "limit": limit, + "values": values, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.signature.searchAccounts", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} From f02937b6925c5e7011a73473e119e544ef47bb22 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 3 Oct 2024 17:58:26 -0700 Subject: [PATCH 066/111] get things building after lexgen changes --- cmd/athome/handlers.go | 4 ++-- cmd/goat/record.go | 2 +- cmd/gosky/bsky.go | 5 +++-- fakedata/generators.go | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/athome/handlers.go b/cmd/athome/handlers.go index 6c13d9b0d..229cf1eb3 100644 --- a/cmd/athome/handlers.go +++ b/cmd/athome/handlers.go @@ -85,7 +85,7 @@ func (srv *Server) WebProfile(c echo.Context) error { did := pv.Did data["did"] = did - af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle.String(), "", "posts_no_replies", 100) + af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle.String(), "", "posts_no_replies", false, 100) if err != nil { slog.Warn("failed to fetch author feed", "handle", handle, "err", err) // TODO: show some error? @@ -126,7 +126,7 @@ func (srv *Server) WebRepoRSS(c echo.Context) error { //return err } - af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle.String(), "", "posts_no_replies", 30) + af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle.String(), "", "posts_no_replies", false, 30) if err != nil { slog.Warn("failed to fetch author feed", "handle", handle, "err", err) return err diff --git a/cmd/goat/record.go b/cmd/goat/record.go index aced5a737..7dfebc8e1 100644 --- a/cmd/goat/record.go +++ b/cmd/goat/record.go @@ -343,7 +343,7 @@ func runRecordDelete(cctx *cli.Context) error { return err } - err = comatproto.RepoDeleteRecord(ctx, xrpcc, &comatproto.RepoDeleteRecord_Input{ + _, err = comatproto.RepoDeleteRecord(ctx, xrpcc, &comatproto.RepoDeleteRecord_Input{ Collection: collection.String(), Repo: xrpcc.Auth.Did, Rkey: rkey.String(), diff --git a/cmd/gosky/bsky.go b/cmd/gosky/bsky.go index 6a0769e38..afc7cb9df 100644 --- a/cmd/gosky/bsky.go +++ b/cmd/gosky/bsky.go @@ -178,7 +178,7 @@ var bskyGetFeedCmd = &cli.Command{ author = xrpcc.Auth.Did } - tl, err := appbsky.FeedGetAuthorFeed(ctx, xrpcc, author, "", "", 99) + tl, err := appbsky.FeedGetAuthorFeed(ctx, xrpcc, author, "", "", false, 99) if err != nil { return err } @@ -314,11 +314,12 @@ var bskyDeletePostCmd = &cli.Command{ rkey = parts[1] } - return comatproto.RepoDeleteRecord(context.TODO(), xrpcc, &comatproto.RepoDeleteRecord_Input{ + _, err = comatproto.RepoDeleteRecord(context.TODO(), xrpcc, &comatproto.RepoDeleteRecord_Input{ Repo: xrpcc.Auth.Did, Collection: schema, Rkey: rkey, }) + return err }, } diff --git a/fakedata/generators.go b/fakedata/generators.go index 9fd99507d..8b13af173 100644 --- a/fakedata/generators.go +++ b/fakedata/generators.go @@ -405,7 +405,7 @@ func BrowseAccount(xrpcc *xrpc.Client, acc *AccountContext) error { if err != nil { return err } - _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, notif.Author.Did, "", "", 50) + _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, notif.Author.Did, "", "", false, 50) if err != nil { return err } @@ -447,7 +447,7 @@ func BrowseAccount(xrpcc *xrpc.Client, acc *AccountContext) error { if err != nil { return err } - _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, post.Post.Author.Did, "", "", 50) + _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, post.Post.Author.Did, "", "", false, 50) if err != nil { return err } From 336bdeac143ec38f2a6008ca473877a175757d37 Mon Sep 17 00:00:00 2001 From: Jaz Volpert Date: Fri, 4 Oct 2024 16:52:38 +0000 Subject: [PATCH 067/111] List repos where we have no upstream status stores --- bgs/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bgs/handlers.go b/bgs/handlers.go index d81ba7c4a..da87c9521 100644 --- a/bgs/handlers.go +++ b/bgs/handlers.go @@ -195,7 +195,7 @@ func (s *BGS) handleComAtprotoSyncNotifyOfUpdate(ctx context.Context, body *coma func (s *BGS) handleComAtprotoSyncListRepos(ctx context.Context, cursor int64, limit int) (*comatprototypes.SyncListRepos_Output, error) { // Filter out tombstoned, taken down, and deactivated accounts - q := fmt.Sprintf("id > ? AND NOT tombstoned AND NOT taken_down AND upstream_status != '%s' AND upstream_status != '%s' AND upstream_status != '%s'", + q := fmt.Sprintf("id > ? AND NOT tombstoned AND NOT taken_down AND (upstream_status is NULL OR (upstream_status != '%s' AND upstream_status != '%s' AND upstream_status != '%s'))", events.AccountStatusDeactivated, events.AccountStatusSuspended, events.AccountStatusTakendown) // Load the users From c47920c113530c9ea3d04229b88fa522f38ad29a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 7 Oct 2024 18:05:02 -0700 Subject: [PATCH 068/111] refactor automod consumers (firehose+ozone) to package --- automod/consumer/doc.go | 2 + automod/consumer/firehose.go | 312 +++++++++++++++++++++++++++++++++++ automod/consumer/ozone.go | 186 +++++++++++++++++++++ automod/consumer/util.go | 25 +++ 4 files changed, 525 insertions(+) create mode 100644 automod/consumer/doc.go create mode 100644 automod/consumer/firehose.go create mode 100644 automod/consumer/ozone.go create mode 100644 automod/consumer/util.go diff --git a/automod/consumer/doc.go b/automod/consumer/doc.go new file mode 100644 index 000000000..fa8ccdcb2 --- /dev/null +++ b/automod/consumer/doc.go @@ -0,0 +1,2 @@ +// Code for consuming from atproto firehose and ozone event stream, pushing events in to automod engine. +package consumer diff --git a/automod/consumer/firehose.go b/automod/consumer/firehose.go new file mode 100644 index 000000000..df6d8f91b --- /dev/null +++ b/automod/consumer/firehose.go @@ -0,0 +1,312 @@ +package consumer + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "net/http" + "net/url" + "sync/atomic" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/events/schedulers/autoscaling" + "github.com/bluesky-social/indigo/events/schedulers/parallel" + lexutil "github.com/bluesky-social/indigo/lex/util" + + "github.com/bluesky-social/indigo/events" + "github.com/bluesky-social/indigo/repo" + "github.com/bluesky-social/indigo/repomgr" + "github.com/carlmjohnson/versioninfo" + "github.com/gorilla/websocket" + "github.com/redis/go-redis/v9" +) + +// TODO: should probably make this not hepa-specific; or even configurable +var firehoseCursorKey = "hepa/seq" + +type FirehoseConsumer struct { + Parallelism int + Logger *slog.Logger + RedisClient *redis.Client + Engine *automod.Engine + Host string + + // TODO: prefilter record collections; or predicate function? + // TODO: enable/disable event types; or predicate function? + + // lastSeq is the most recent event sequence number we've received and begun to handle. + // This number is periodically persisted to redis, if redis is present. + // The value is best-effort (the stream handling itself is concurrent, so event numbers may not be monotonic), + // but nonetheless, you must use atomics when updating or reading this (to avoid data races). + lastSeq int64 +} + +func (fc *FirehoseConsumer) Run(ctx context.Context) error { + + if fc.Engine == nil { + return fmt.Errorf("nil engine") + } + + cur, err := fc.ReadLastCursor(ctx) + if err != nil { + return err + } + + dialer := websocket.DefaultDialer + u, err := url.Parse(fc.Host) + if err != nil { + return fmt.Errorf("invalid Host URI: %w", err) + } + u.Path = "xrpc/com.atproto.sync.subscribeRepos" + if cur != 0 { + u.RawQuery = fmt.Sprintf("cursor=%d", cur) + } + fc.Logger.Info("subscribing to repo event stream", "upstream", fc.Host, "cursor", cur) + con, _, err := dialer.Dial(u.String(), http.Header{ + "User-Agent": []string{fmt.Sprintf("hepa/%s", versioninfo.Short())}, + }) + if err != nil { + return fmt.Errorf("subscribing to firehose failed (dialing): %w", err) + } + + rsc := &events.RepoStreamCallbacks{ + RepoCommit: func(evt *comatproto.SyncSubscribeRepos_Commit) error { + atomic.StoreInt64(&fc.lastSeq, evt.Seq) + return fc.HandleRepoCommit(ctx, evt) + }, + RepoIdentity: func(evt *comatproto.SyncSubscribeRepos_Identity) error { + atomic.StoreInt64(&fc.lastSeq, evt.Seq) + did, err := syntax.ParseDID(evt.Did) + if err != nil { + fc.Logger.Error("bad DID in RepoIdentity event", "did", evt.Did, "seq", evt.Seq, "err", err) + return nil + } + if err := fc.Engine.ProcessIdentityEvent(ctx, "identity", did); err != nil { + fc.Logger.Error("processing repo identity failed", "did", evt.Did, "seq", evt.Seq, "err", err) + } + return nil + }, + RepoAccount: func(evt *comatproto.SyncSubscribeRepos_Account) error { + atomic.StoreInt64(&fc.lastSeq, evt.Seq) + did, err := syntax.ParseDID(evt.Did) + if err != nil { + fc.Logger.Error("bad DID in RepoAccount event", "did", evt.Did, "seq", evt.Seq, "err", err) + return nil + } + if err := fc.Engine.ProcessIdentityEvent(ctx, "account", did); err != nil { + fc.Logger.Error("processing repo account failed", "did", evt.Did, "seq", evt.Seq, "err", err) + } + return nil + }, + // TODO: deprecated + RepoHandle: func(evt *comatproto.SyncSubscribeRepos_Handle) error { + atomic.StoreInt64(&fc.lastSeq, evt.Seq) + did, err := syntax.ParseDID(evt.Did) + if err != nil { + fc.Logger.Error("bad DID in RepoHandle event", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) + return nil + } + if err := fc.Engine.ProcessIdentityEvent(ctx, "handle", did); err != nil { + fc.Logger.Error("processing handle update failed", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) + } + return nil + }, + // TODO: deprecated + RepoTombstone: func(evt *comatproto.SyncSubscribeRepos_Tombstone) error { + atomic.StoreInt64(&fc.lastSeq, evt.Seq) + did, err := syntax.ParseDID(evt.Did) + if err != nil { + fc.Logger.Error("bad DID in RepoTombstone event", "did", evt.Did, "seq", evt.Seq, "err", err) + return nil + } + if err := fc.Engine.ProcessIdentityEvent(ctx, "tombstone", did); err != nil { + fc.Logger.Error("processing repo tombstone failed", "did", evt.Did, "seq", evt.Seq, "err", err) + } + return nil + }, + } + + var scheduler events.Scheduler + if fc.Parallelism > 0 { + // use a fixed-parallelism scheduler if configured + scheduler = parallel.NewScheduler( + fc.Parallelism, + 1000, + fc.Host, + rsc.EventHandler, + ) + fc.Logger.Info("hepa scheduler configured", "scheduler", "parallel", "initial", fc.Parallelism) + } else { + // otherwise use auto-scaling scheduler + scaleSettings := autoscaling.DefaultAutoscaleSettings() + // start at higher parallelism (somewhat arbitrary) + scaleSettings.Concurrency = 4 + scaleSettings.MaxConcurrency = 200 + scheduler = autoscaling.NewScheduler(scaleSettings, fc.Host, rsc.EventHandler) + fc.Logger.Info("hepa scheduler configured", "scheduler", "autoscaling", "initial", scaleSettings.Concurrency, "max", scaleSettings.MaxConcurrency) + } + + return events.HandleRepoStream(ctx, con, scheduler) +} + +// NOTE: for now, this function basically never errors, just logs and returns nil. Should think through error processing better. +func (fc *FirehoseConsumer) HandleRepoCommit(ctx context.Context, evt *comatproto.SyncSubscribeRepos_Commit) error { + + logger := fc.Logger.With("event", "commit", "did", evt.Repo, "rev", evt.Rev, "seq", evt.Seq) + logger.Debug("received commit event") + + if evt.TooBig { + logger.Warn("skipping tooBig events for now") + return nil + } + + did, err := syntax.ParseDID(evt.Repo) + if err != nil { + logger.Error("bad DID syntax in event", "err", err) + return nil + } + + rr, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(evt.Blocks)) + if err != nil { + logger.Error("failed to read repo from car", "err", err) + return nil + } + + // empty commit is a special case, temporarily, basically indicates "new account" + if len(evt.Ops) == 0 { + if err := fc.Engine.ProcessIdentityEvent(ctx, "create", did); err != nil { + fc.Logger.Error("processing handle update failed", "did", evt.Repo, "rev", evt.Rev, "seq", evt.Seq, "err", err) + } + } + + for _, op := range evt.Ops { + logger = logger.With("eventKind", op.Action, "path", op.Path) + collection, rkey, err := splitRepoPath(op.Path) + if err != nil { + logger.Error("invalid path in repo op") + return nil + } + + ek := repomgr.EventKind(op.Action) + switch ek { + case repomgr.EvtKindCreateRecord, repomgr.EvtKindUpdateRecord: + // read the record bytes from blocks, and verify CID + rc, recCBOR, err := rr.GetRecordBytes(ctx, op.Path) + if err != nil { + logger.Error("reading record from event blocks (CAR)", "err", err) + break + } + if op.Cid == nil || lexutil.LexLink(rc) != *op.Cid { + logger.Error("mismatch between commit op CID and record block", "recordCID", rc, "opCID", op.Cid) + break + } + var action string + switch ek { + case repomgr.EvtKindCreateRecord: + action = automod.CreateOp + case repomgr.EvtKindUpdateRecord: + action = automod.UpdateOp + default: + logger.Error("impossible event kind", "kind", ek) + break + } + recCID := syntax.CID(op.Cid.String()) + err = fc.Engine.ProcessRecordOp(ctx, automod.RecordOp{ + Action: action, + DID: did, + Collection: collection, + RecordKey: rkey, + CID: &recCID, + RecordCBOR: *recCBOR, + }) + if err != nil { + logger.Error("engine failed to process record", "err", err) + continue + } + case repomgr.EvtKindDeleteRecord: + err = fc.Engine.ProcessRecordOp(ctx, automod.RecordOp{ + Action: automod.DeleteOp, + DID: did, + Collection: collection, + RecordKey: rkey, + CID: nil, + RecordCBOR: nil, + }) + if err != nil { + logger.Error("engine failed to process record", "err", err) + continue + } + default: + // TODO: should this be an error? + } + } + + return nil +} + +func (fc *FirehoseConsumer) ReadLastCursor(ctx context.Context) (int64, error) { + // if redis isn't configured, just skip + if fc.RedisClient == nil { + fc.Logger.Info("redis not configured, skipping cursor read") + return 0, nil + } + + val, err := fc.RedisClient.Get(ctx, firehoseCursorKey).Int64() + if err == redis.Nil { + fc.Logger.Info("no pre-existing cursor in redis") + return 0, nil + } else if err != nil { + return 0, err + } + fc.Logger.Info("successfully found prior subscription cursor seq in redis", "seq", val) + return val, nil +} + +func (fc *FirehoseConsumer) PersistCursor(ctx context.Context) error { + // if redis isn't configured, just skip + if fc.RedisClient == nil { + return nil + } + lastSeq := atomic.LoadInt64(&fc.lastSeq) + if lastSeq <= 0 { + return nil + } + err := fc.RedisClient.Set(ctx, firehoseCursorKey, lastSeq, 14*24*time.Hour).Err() + return err +} + +// this method runs in a loop, persisting the current cursor state every 5 seconds +func (fc *FirehoseConsumer) RunPersistCursor(ctx context.Context) error { + + // if redis isn't configured, just skip + if fc.RedisClient == nil { + return nil + } + ticker := time.NewTicker(5 * time.Second) + for { + select { + case <-ctx.Done(): + lastSeq := atomic.LoadInt64(&fc.lastSeq) + if lastSeq >= 1 { + fc.Logger.Info("persisting final cursor seq value", "seq", lastSeq) + err := fc.PersistCursor(ctx) + if err != nil { + fc.Logger.Error("failed to persist cursor", "err", err, "seq", lastSeq) + } + } + return nil + case <-ticker.C: + lastSeq := atomic.LoadInt64(&fc.lastSeq) + if lastSeq >= 1 { + err := fc.PersistCursor(ctx) + if err != nil { + fc.Logger.Error("failed to persist cursor", "err", err, "seq", lastSeq) + } + } + } + } +} diff --git a/automod/consumer/ozone.go b/automod/consumer/ozone.go new file mode 100644 index 000000000..0692ac393 --- /dev/null +++ b/automod/consumer/ozone.go @@ -0,0 +1,186 @@ +package consumer + +import ( + "context" + "fmt" + "log/slog" + "sync/atomic" + "time" + + toolsozone "github.com/bluesky-social/indigo/api/ozone" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/xrpc" + + "github.com/redis/go-redis/v9" +) + +// TODO: should probably make this not hepa-specific; or even configurable +var ozoneCursorKey = "hepa/ozoneTimestamp" + +type OzoneConsumer struct { + Logger *slog.Logger + RedisClient *redis.Client + OzoneClient *xrpc.Client + Engine *automod.Engine + + // same as lastSeq, but for Ozone timestamp cursor. the value is a string. + lastCursor atomic.Value +} + +func (oc *OzoneConsumer) Run(ctx context.Context) error { + + if oc.Engine == nil { + return fmt.Errorf("nil engine") + } + if oc.OzoneClient == nil { + return fmt.Errorf("nil ozoneclient") + } + + cur, err := oc.ReadLastCursor(ctx) + if err != nil { + return err + } + + if cur == "" { + cur = syntax.DatetimeNow().String() + } + since, err := syntax.ParseDatetime(cur) + if err != nil { + return err + } + + oc.Logger.Info("subscribing to ozone event log", "upstream", oc.OzoneClient.Host, "cursor", cur, "since", since) + var limit int64 = 50 + period := time.Second * 5 + + for { + //func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []string, addedTags []string, comment string, createdAfter string, createdBefore string, createdBy string, cursor string, hasComment bool, includeAllUserRecords bool, limit int64, removedLabels []string, removedTags []string, reportTypes []string, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) { + me, err := toolsozone.ModerationQueryEvents( + ctx, + oc.OzoneClient, + nil, // addedLabels: If specified, only events where all of these labels were added are returned + nil, // addedTags: If specified, only events where all of these tags were added are returned + "", // comment: If specified, only events with comments containing the keyword are returned + since.String(), // createdAfter: Retrieve events created after a given timestamp + "", // createdBefore: Retrieve events created before a given timestamp + "", // createdBy + "", // cursor + false, // hasComment: If true, only events with comments are returned + true, // includeAllUserRecords: If true, events on all record types (posts, lists, profile etc.) owned by the did are returned + limit, + nil, // removedLabels: If specified, only events where all of these labels were removed are returned + nil, // removedTags + nil, // reportTypes + "asc", // sortDirection: Sort direction for the events. Defaults to descending order of created at timestamp. + "", // subject + nil, // types: The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent) to filter by. If not specified, all events are returned. + ) + if err != nil { + oc.Logger.Warn("ozone query events failed; sleeping then will retrying", "err", err, "period", period.String()) + time.Sleep(period) + continue + } + + // track if the response contained anything new + anyNewEvents := false + for _, evt := range me.Events { + createdAt, err := syntax.ParseDatetime(evt.CreatedAt) + if err != nil { + return fmt.Errorf("invalid time format for ozone 'createdAt': %w", err) + } + // skip if the timestamp is the exact same + if createdAt == since { + continue + } + anyNewEvents = true + // TODO: is there a race condition here? + if !createdAt.Time().After(since.Time()) { + oc.Logger.Error("out of order ozone event", "createdAt", createdAt, "since", since) + return fmt.Errorf("out of order ozone event") + } + if err = oc.HandleOzoneEvent(ctx, evt); err != nil { + oc.Logger.Error("failed to process ozone event", "event", evt) + } + since = createdAt + oc.lastCursor.Store(since.String()) + } + if !anyNewEvents { + oc.Logger.Debug("... ozone poller sleeping", "period", period.String()) + time.Sleep(period) + } + } +} + +func (oc *OzoneConsumer) HandleOzoneEvent(ctx context.Context, eventView *toolsozone.ModerationDefs_ModEventView) error { + + oc.Logger.Debug("received ozone event", "eventID", eventView.Id, "createdAt", eventView.CreatedAt) + + if err := oc.Engine.ProcessOzoneEvent(ctx, eventView); err != nil { + oc.Logger.Error("engine failed to process ozone event", "err", err) + } + return nil +} + +func (oc *OzoneConsumer) ReadLastCursor(ctx context.Context) (string, error) { + // if redis isn't configured, just skip + if oc.RedisClient == nil { + oc.Logger.Info("redis not configured, skipping ozone cursor read") + return "", nil + } + + val, err := oc.RedisClient.Get(ctx, ozoneCursorKey).Result() + if err == redis.Nil || val == "" { + oc.Logger.Info("no pre-existing ozone cursor in redis") + return "", nil + } else if err != nil { + return "", err + } + oc.Logger.Info("successfully found prior ozone offset timestamp in redis", "cursor", val) + return val, nil +} + +func (oc *OzoneConsumer) PersistCursor(ctx context.Context) error { + // if redis isn't configured, just skip + if oc.RedisClient == nil { + return nil + } + lastCursor := oc.lastCursor.Load() + if lastCursor == nil || lastCursor == "" { + return nil + } + err := oc.RedisClient.Set(ctx, ozoneCursorKey, lastCursor, 14*24*time.Hour).Err() + return err +} + +// this method runs in a loop, persisting the current cursor state every 5 seconds +func (oc *OzoneConsumer) RunPersistCursor(ctx context.Context) error { + + // if redis isn't configured, just skip + if oc.RedisClient == nil { + return nil + } + ticker := time.NewTicker(5 * time.Second) + for { + select { + case <-ctx.Done(): + lastCursor := oc.lastCursor.Load() + if lastCursor != nil && lastCursor != "" { + oc.Logger.Info("persisting final ozone cursor timestamp", "cursor", lastCursor) + err := oc.PersistCursor(ctx) + if err != nil { + oc.Logger.Error("failed to persist ozone cursor", "err", err, "cursor", lastCursor) + } + } + return nil + case <-ticker.C: + lastCursor := oc.lastCursor.Load() + if lastCursor != nil && lastCursor != "" { + err := oc.PersistCursor(ctx) + if err != nil { + oc.Logger.Error("failed to persist ozone cursor", "err", err, "cursor", lastCursor) + } + } + } + } +} diff --git a/automod/consumer/util.go b/automod/consumer/util.go new file mode 100644 index 000000000..b1c34ebaf --- /dev/null +++ b/automod/consumer/util.go @@ -0,0 +1,25 @@ +package consumer + +import ( + "fmt" + "strings" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +// TODO: move this to a "ParsePath" helper in syntax package? +func splitRepoPath(path string) (syntax.NSID, syntax.RecordKey, error) { + parts := strings.SplitN(path, "/", 3) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid record path: %s", path) + } + collection, err := syntax.ParseNSID(parts[0]) + if err != nil { + return "", "", err + } + rkey, err := syntax.ParseRecordKey(parts[1]) + if err != nil { + return "", "", err + } + return collection, rkey, nil +} From 13c772a83933d703fcf427764054228b4948b025 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 7 Oct 2024 18:05:36 -0700 Subject: [PATCH 069/111] update hepa (automod) to use refactored consumers --- cmd/hepa/consumer.go | 240 ------------------------------------- cmd/hepa/consumer_ozone.go | 97 --------------- cmd/hepa/main.go | 67 +++++++---- cmd/hepa/server.go | 156 ++---------------------- 4 files changed, 52 insertions(+), 508 deletions(-) delete mode 100644 cmd/hepa/consumer.go delete mode 100644 cmd/hepa/consumer_ozone.go diff --git a/cmd/hepa/consumer.go b/cmd/hepa/consumer.go deleted file mode 100644 index e9d789baa..000000000 --- a/cmd/hepa/consumer.go +++ /dev/null @@ -1,240 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "net/http" - "net/url" - "strings" - "sync/atomic" - - comatproto "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/automod" - "github.com/bluesky-social/indigo/events/schedulers/autoscaling" - "github.com/bluesky-social/indigo/events/schedulers/parallel" - lexutil "github.com/bluesky-social/indigo/lex/util" - - "github.com/bluesky-social/indigo/events" - "github.com/bluesky-social/indigo/repo" - "github.com/bluesky-social/indigo/repomgr" - "github.com/carlmjohnson/versioninfo" - "github.com/gorilla/websocket" -) - -func (s *Server) RunConsumer(ctx context.Context) error { - - cur, err := s.ReadLastCursor(ctx) - if err != nil { - return err - } - - dialer := websocket.DefaultDialer - u, err := url.Parse(s.relayHost) - if err != nil { - return fmt.Errorf("invalid relayHost URI: %w", err) - } - u.Path = "xrpc/com.atproto.sync.subscribeRepos" - if cur != 0 { - u.RawQuery = fmt.Sprintf("cursor=%d", cur) - } - s.logger.Info("subscribing to repo event stream", "upstream", s.relayHost, "cursor", cur) - con, _, err := dialer.Dial(u.String(), http.Header{ - "User-Agent": []string{fmt.Sprintf("hepa/%s", versioninfo.Short())}, - }) - if err != nil { - return fmt.Errorf("subscribing to firehose failed (dialing): %w", err) - } - - rsc := &events.RepoStreamCallbacks{ - RepoCommit: func(evt *comatproto.SyncSubscribeRepos_Commit) error { - atomic.StoreInt64(&s.lastSeq, evt.Seq) - return s.HandleRepoCommit(ctx, evt) - }, - RepoIdentity: func(evt *comatproto.SyncSubscribeRepos_Identity) error { - atomic.StoreInt64(&s.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - s.logger.Error("bad DID in RepoIdentity event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := s.engine.ProcessIdentityEvent(ctx, "identity", did); err != nil { - s.logger.Error("processing repo identity failed", "did", evt.Did, "seq", evt.Seq, "err", err) - } - return nil - }, - RepoAccount: func(evt *comatproto.SyncSubscribeRepos_Account) error { - atomic.StoreInt64(&s.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - s.logger.Error("bad DID in RepoAccount event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := s.engine.ProcessIdentityEvent(ctx, "account", did); err != nil { - s.logger.Error("processing repo account failed", "did", evt.Did, "seq", evt.Seq, "err", err) - } - return nil - }, - // TODO: deprecated - RepoHandle: func(evt *comatproto.SyncSubscribeRepos_Handle) error { - atomic.StoreInt64(&s.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - s.logger.Error("bad DID in RepoHandle event", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) - return nil - } - if err := s.engine.ProcessIdentityEvent(ctx, "handle", did); err != nil { - s.logger.Error("processing handle update failed", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) - } - return nil - }, - // TODO: deprecated - RepoTombstone: func(evt *comatproto.SyncSubscribeRepos_Tombstone) error { - atomic.StoreInt64(&s.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - s.logger.Error("bad DID in RepoTombstone event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := s.engine.ProcessIdentityEvent(ctx, "tombstone", did); err != nil { - s.logger.Error("processing repo tombstone failed", "did", evt.Did, "seq", evt.Seq, "err", err) - } - return nil - }, - } - - var scheduler events.Scheduler - if s.firehoseParallelism > 0 { - // use a fixed-parallelism scheduler if configured - scheduler = parallel.NewScheduler( - s.firehoseParallelism, - 1000, - s.relayHost, - rsc.EventHandler, - ) - s.logger.Info("hepa scheduler configured", "scheduler", "parallel", "initial", s.firehoseParallelism) - } else { - // otherwise use auto-scaling scheduler - scaleSettings := autoscaling.DefaultAutoscaleSettings() - // start at higher parallelism (somewhat arbitrary) - scaleSettings.Concurrency = 4 - scaleSettings.MaxConcurrency = 200 - scheduler = autoscaling.NewScheduler(scaleSettings, s.relayHost, rsc.EventHandler) - s.logger.Info("hepa scheduler configured", "scheduler", "autoscaling", "initial", scaleSettings.Concurrency, "max", scaleSettings.MaxConcurrency) - } - - return events.HandleRepoStream(ctx, con, scheduler) -} - -// TODO: move this to a "ParsePath" helper in syntax package? -func splitRepoPath(path string) (syntax.NSID, syntax.RecordKey, error) { - parts := strings.SplitN(path, "/", 3) - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid record path: %s", path) - } - collection, err := syntax.ParseNSID(parts[0]) - if err != nil { - return "", "", err - } - rkey, err := syntax.ParseRecordKey(parts[1]) - if err != nil { - return "", "", err - } - return collection, rkey, nil -} - -// NOTE: for now, this function basically never errors, just logs and returns nil. Should think through error processing better. -func (s *Server) HandleRepoCommit(ctx context.Context, evt *comatproto.SyncSubscribeRepos_Commit) error { - - logger := s.logger.With("event", "commit", "did", evt.Repo, "rev", evt.Rev, "seq", evt.Seq) - logger.Debug("received commit event") - - if evt.TooBig { - logger.Warn("skipping tooBig events for now") - return nil - } - - did, err := syntax.ParseDID(evt.Repo) - if err != nil { - logger.Error("bad DID syntax in event", "err", err) - return nil - } - - rr, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(evt.Blocks)) - if err != nil { - logger.Error("failed to read repo from car", "err", err) - return nil - } - - // empty commit is a special case, temporarily, basically indicates "new account" - if len(evt.Ops) == 0 { - if err := s.engine.ProcessIdentityEvent(ctx, "create", did); err != nil { - s.logger.Error("processing handle update failed", "did", evt.Repo, "rev", evt.Rev, "seq", evt.Seq, "err", err) - } - } - - for _, op := range evt.Ops { - logger = logger.With("eventKind", op.Action, "path", op.Path) - collection, rkey, err := splitRepoPath(op.Path) - if err != nil { - logger.Error("invalid path in repo op") - return nil - } - - ek := repomgr.EventKind(op.Action) - switch ek { - case repomgr.EvtKindCreateRecord, repomgr.EvtKindUpdateRecord: - // read the record bytes from blocks, and verify CID - rc, recCBOR, err := rr.GetRecordBytes(ctx, op.Path) - if err != nil { - logger.Error("reading record from event blocks (CAR)", "err", err) - break - } - if op.Cid == nil || lexutil.LexLink(rc) != *op.Cid { - logger.Error("mismatch between commit op CID and record block", "recordCID", rc, "opCID", op.Cid) - break - } - var action string - switch ek { - case repomgr.EvtKindCreateRecord: - action = automod.CreateOp - case repomgr.EvtKindUpdateRecord: - action = automod.UpdateOp - default: - logger.Error("impossible event kind", "kind", ek) - break - } - recCID := syntax.CID(op.Cid.String()) - err = s.engine.ProcessRecordOp(ctx, automod.RecordOp{ - Action: action, - DID: did, - Collection: collection, - RecordKey: rkey, - CID: &recCID, - RecordCBOR: *recCBOR, - }) - if err != nil { - logger.Error("engine failed to process record", "err", err) - continue - } - case repomgr.EvtKindDeleteRecord: - err = s.engine.ProcessRecordOp(ctx, automod.RecordOp{ - Action: automod.DeleteOp, - DID: did, - Collection: collection, - RecordKey: rkey, - CID: nil, - RecordCBOR: nil, - }) - if err != nil { - logger.Error("engine failed to process record", "err", err) - continue - } - default: - // TODO: should this be an error? - } - } - - return nil -} diff --git a/cmd/hepa/consumer_ozone.go b/cmd/hepa/consumer_ozone.go deleted file mode 100644 index 406a34a4c..000000000 --- a/cmd/hepa/consumer_ozone.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - toolsozone "github.com/bluesky-social/indigo/api/ozone" - "github.com/bluesky-social/indigo/atproto/syntax" -) - -func (s *Server) RunOzoneConsumer(ctx context.Context) error { - - cur, err := s.ReadLastOzoneCursor(ctx) - if err != nil { - return err - } - - if cur == "" { - cur = syntax.DatetimeNow().String() - } - since, err := syntax.ParseDatetime(cur) - if err != nil { - return err - } - - s.logger.Info("subscribing to ozone event log", "upstream", s.engine.OzoneClient.Host, "cursor", cur, "since", since) - var limit int64 = 50 - period := time.Second * 5 - - for { - //func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []string, addedTags []string, comment string, createdAfter string, createdBefore string, createdBy string, cursor string, hasComment bool, includeAllUserRecords bool, limit int64, removedLabels []string, removedTags []string, reportTypes []string, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) { - me, err := toolsozone.ModerationQueryEvents( - ctx, - s.engine.OzoneClient, - nil, // addedLabels: If specified, only events where all of these labels were added are returned - nil, // addedTags: If specified, only events where all of these tags were added are returned - "", // comment: If specified, only events with comments containing the keyword are returned - since.String(), // createdAfter: Retrieve events created after a given timestamp - "", // createdBefore: Retrieve events created before a given timestamp - "", // createdBy - "", // cursor - false, // hasComment: If true, only events with comments are returned - true, // includeAllUserRecords: If true, events on all record types (posts, lists, profile etc.) owned by the did are returned - limit, - nil, // removedLabels: If specified, only events where all of these labels were removed are returned - nil, // removedTags - nil, // reportTypes - "asc", // sortDirection: Sort direction for the events. Defaults to descending order of created at timestamp. - "", // subject - nil, // types: The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent) to filter by. If not specified, all events are returned. - ) - if err != nil { - s.logger.Warn("ozone query events failed; sleeping then will retrying", "err", err, "period", period.String()) - time.Sleep(period) - continue - } - - // track if the response contained anything new - anyNewEvents := false - for _, evt := range me.Events { - createdAt, err := syntax.ParseDatetime(evt.CreatedAt) - if err != nil { - return fmt.Errorf("invalid time format for ozone 'createdAt': %w", err) - } - // skip if the timestamp is the exact same - if createdAt == since { - continue - } - anyNewEvents = true - // TODO: is there a race condition here? - if !createdAt.Time().After(since.Time()) { - s.logger.Error("out of order ozone event", "createdAt", createdAt, "since", since) - return fmt.Errorf("out of order ozone event") - } - if err = s.HandleOzoneEvent(ctx, evt); err != nil { - s.logger.Error("failed to process ozone event", "event", evt) - } - since = createdAt - s.lastOzoneCursor.Store(since.String()) - } - if !anyNewEvents { - s.logger.Debug("... ozone poller sleeping", "period", period.String()) - time.Sleep(period) - } - } -} - -func (s *Server) HandleOzoneEvent(ctx context.Context, eventView *toolsozone.ModerationDefs_ModEventView) error { - - s.logger.Debug("received ozone event", "eventID", eventView.Id, "createdAt", eventView.CreatedAt) - - if err := s.engine.ProcessOzoneEvent(ctx, eventView); err != nil { - s.logger.Error("engine failed to process ozone event", "err", err) - } - return nil -} diff --git a/cmd/hepa/main.go b/cmd/hepa/main.go index dbef6c488..bceaaa189 100644 --- a/cmd/hepa/main.go +++ b/cmd/hepa/main.go @@ -17,6 +17,7 @@ import ( "github.com/bluesky-social/indigo/atproto/identity/redisdir" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod/capture" + "github.com/bluesky-social/indigo/automod/consumer" "github.com/carlmjohnson/versioninfo" _ "github.com/joho/godotenv/autoload" @@ -236,7 +237,7 @@ var runCmd = &cli.Command{ dir, Config{ Logger: logger, - RelayHost: cctx.String("atp-relay-host"), + RelayHost: cctx.String("atp-relay-host"), // DEPRECATED BskyHost: cctx.String("atp-bsky-host"), OzoneHost: cctx.String("atp-ozone-host"), OzoneDID: cctx.String("ozone-did"), @@ -251,7 +252,7 @@ var runCmd = &cli.Command{ AbyssPassword: cctx.String("abyss-password"), RatelimitBypass: cctx.String("ratelimit-bypass"), RulesetName: cctx.String("ruleset"), - FirehoseParallelism: cctx.Int("firehose-parallelism"), + FirehoseParallelism: cctx.Int("firehose-parallelism"), // DEPRECATED PreScreenHost: cctx.String("prescreen-host"), PreScreenToken: cctx.String("prescreen-token"), }, @@ -260,41 +261,59 @@ var runCmd = &cli.Command{ return fmt.Errorf("failed to construct server: %v", err) } - // prometheus HTTP endpoint: /metrics - go func() { - runtime.SetBlockProfileRate(10) - runtime.SetMutexProfileFraction(10) - if err := srv.RunMetrics(cctx.String("metrics-listen")); err != nil { - slog.Error("failed to start metrics endpoint", "error", err) - panic(fmt.Errorf("failed to start metrics endpoint: %w", err)) + // firehose event consumer + relayHost := cctx.String("atp-relay-host") + if relayHost != "" { + fc := consumer.FirehoseConsumer{ + Engine: srv.Engine, + Logger: logger.With("subsystem", "firehose-consumer"), + Host: cctx.String("atp-relay-host"), + Parallelism: cctx.Int("firehose-parallelism"), + RedisClient: srv.RedisClient, } - }() - go func() { - if err := srv.RunPersistCursor(ctx); err != nil { - slog.Error("cursor routine failed", "err", err) + go func() { + if err := fc.RunPersistCursor(ctx); err != nil { + slog.Error("cursor routine failed", "err", err) + } + }() + + if err := fc.Run(ctx); err != nil { + return fmt.Errorf("failure consuming and processing firehose: %w", err) } - }() + } // ozone event consumer (if configured) - if srv.engine.OzoneClient != nil { + if srv.Engine.OzoneClient != nil { + oc := consumer.OzoneConsumer{ + Engine: srv.Engine, + Logger: logger.With("subsystem", "ozone-consumer"), + RedisClient: srv.RedisClient, + } + go func() { - if err := srv.RunOzoneConsumer(ctx); err != nil { + if err := oc.Run(ctx); err != nil { slog.Error("ozone consumer failed", "err", err) } }() go func() { - if err := srv.RunPersistOzoneCursor(ctx); err != nil { + if err := oc.RunPersistCursor(ctx); err != nil { slog.Error("ozone cursor routine failed", "err", err) } }() } - // firehose event consumer (main processor) - if err := srv.RunConsumer(ctx); err != nil { - return fmt.Errorf("failure consuming and processing firehose: %w", err) - } + // prometheus HTTP endpoint: /metrics + go func() { + runtime.SetBlockProfileRate(10) + runtime.SetMutexProfileFraction(10) + if err := srv.RunMetrics(cctx.String("metrics-listen")); err != nil { + slog.Error("failed to start metrics endpoint", "error", err) + panic(fmt.Errorf("failed to start metrics endpoint: %w", err)) + } + }() + return nil }, } @@ -355,7 +374,7 @@ var processRecordCmd = &cli.Command{ return err } - return capture.FetchAndProcessRecord(ctx, srv.engine, aturi) + return capture.FetchAndProcessRecord(ctx, srv.Engine, aturi) }, } @@ -386,7 +405,7 @@ var processRecentCmd = &cli.Command{ return err } - return capture.FetchAndProcessRecent(ctx, srv.engine, *atid, cctx.Int("limit")) + return capture.FetchAndProcessRecent(ctx, srv.Engine, *atid, cctx.Int("limit")) }, } @@ -417,7 +436,7 @@ var captureRecentCmd = &cli.Command{ return err } - cap, err := capture.CaptureRecent(ctx, srv.engine, *atid, cctx.Int("limit")) + cap, err := capture.CaptureRecent(ctx, srv.Engine, *atid, cctx.Int("limit")) if err != nil { return err } diff --git a/cmd/hepa/server.go b/cmd/hepa/server.go index 55ebf49f2..9fe08f98e 100644 --- a/cmd/hepa/server.go +++ b/cmd/hepa/server.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "strings" - "sync/atomic" "time" "github.com/bluesky-social/indigo/atproto/identity" @@ -27,25 +26,17 @@ import ( ) type Server struct { - relayHost string - firehoseParallelism int - logger *slog.Logger - engine *automod.Engine - rdb *redis.Client - - // lastSeq is the most recent event sequence number we've received and begun to handle. - // This number is periodically persisted to redis, if redis is present. - // The value is best-effort (the stream handling itself is concurrent, so event numbers may not be monotonic), - // but nonetheless, you must use atomics when updating or reading this (to avoid data races). - lastSeq int64 + Engine *automod.Engine + RedisClient *redis.Client - // same as lastSeq, but for Ozone timestamp cursor. the value is a string. - lastOzoneCursor atomic.Value + relayHost string // DEPRECATED + firehoseParallelism int // DEPRECATED + logger *slog.Logger } type Config struct { Logger *slog.Logger - RelayHost string + RelayHost string // DEPRECATED BskyHost string OzoneHost string OzoneDID string @@ -60,7 +51,7 @@ type Config struct { AbyssPassword string RulesetName string RatelimitBypass string - FirehoseParallelism int + FirehoseParallelism int // DEPRECATED PreScreenHost string PreScreenToken string } @@ -234,8 +225,8 @@ func NewServer(dir identity.Directory, config Config) (*Server, error) { relayHost: config.RelayHost, firehoseParallelism: config.FirehoseParallelism, logger: logger, - engine: &engine, - rdb: rdb, + Engine: &engine, + RedisClient: rdb, } return s, nil @@ -245,132 +236,3 @@ func (s *Server) RunMetrics(listen string) error { http.Handle("/metrics", promhttp.Handler()) return http.ListenAndServe(listen, nil) } - -var cursorKey = "hepa/seq" -var ozoneCursorKey = "hepa/ozoneTimestamp" - -func (s *Server) ReadLastCursor(ctx context.Context) (int64, error) { - // if redis isn't configured, just skip - if s.rdb == nil { - s.logger.Info("redis not configured, skipping cursor read") - return 0, nil - } - - val, err := s.rdb.Get(ctx, cursorKey).Int64() - if err == redis.Nil { - s.logger.Info("no pre-existing cursor in redis") - return 0, nil - } else if err != nil { - return 0, err - } - s.logger.Info("successfully found prior subscription cursor seq in redis", "seq", val) - return val, nil -} - -func (s *Server) ReadLastOzoneCursor(ctx context.Context) (string, error) { - // if redis isn't configured, just skip - if s.rdb == nil { - s.logger.Info("redis not configured, skipping ozone cursor read") - return "", nil - } - - val, err := s.rdb.Get(ctx, ozoneCursorKey).Result() - if err == redis.Nil || val == "" { - s.logger.Info("no pre-existing ozone cursor in redis") - return "", nil - } else if err != nil { - return "", err - } - s.logger.Info("successfully found prior ozone offset timestamp in redis", "cursor", val) - return val, nil -} - -func (s *Server) PersistCursor(ctx context.Context) error { - // if redis isn't configured, just skip - if s.rdb == nil { - return nil - } - lastSeq := atomic.LoadInt64(&s.lastSeq) - if lastSeq <= 0 { - return nil - } - err := s.rdb.Set(ctx, cursorKey, lastSeq, 14*24*time.Hour).Err() - return err -} - -func (s *Server) PersistOzoneCursor(ctx context.Context) error { - // if redis isn't configured, just skip - if s.rdb == nil { - return nil - } - lastCursor := s.lastOzoneCursor.Load() - if lastCursor == nil || lastCursor == "" { - return nil - } - err := s.rdb.Set(ctx, ozoneCursorKey, lastCursor, 14*24*time.Hour).Err() - return err -} - -// this method runs in a loop, persisting the current cursor state every 5 seconds -func (s *Server) RunPersistCursor(ctx context.Context) error { - - // if redis isn't configured, just skip - if s.rdb == nil { - return nil - } - ticker := time.NewTicker(5 * time.Second) - for { - select { - case <-ctx.Done(): - lastSeq := atomic.LoadInt64(&s.lastSeq) - if lastSeq >= 1 { - s.logger.Info("persisting final cursor seq value", "seq", lastSeq) - err := s.PersistCursor(ctx) - if err != nil { - s.logger.Error("failed to persist cursor", "err", err, "seq", lastSeq) - } - } - return nil - case <-ticker.C: - lastSeq := atomic.LoadInt64(&s.lastSeq) - if lastSeq >= 1 { - err := s.PersistCursor(ctx) - if err != nil { - s.logger.Error("failed to persist cursor", "err", err, "seq", lastSeq) - } - } - } - } -} - -// this method runs in a loop, persisting the current cursor state every 5 seconds -func (s *Server) RunPersistOzoneCursor(ctx context.Context) error { - - // if redis isn't configured, just skip - if s.rdb == nil { - return nil - } - ticker := time.NewTicker(5 * time.Second) - for { - select { - case <-ctx.Done(): - lastCursor := s.lastOzoneCursor.Load() - if lastCursor != nil && lastCursor != "" { - s.logger.Info("persisting final ozone cursor timestamp", "cursor", lastCursor) - err := s.PersistOzoneCursor(ctx) - if err != nil { - s.logger.Error("failed to persist ozone cursor", "err", err, "cursor", lastCursor) - } - } - return nil - case <-ticker.C: - lastCursor := s.lastOzoneCursor.Load() - if lastCursor != nil && lastCursor != "" { - err := s.PersistOzoneCursor(ctx) - if err != nil { - s.logger.Error("failed to persist ozone cursor", "err", err, "cursor", lastCursor) - } - } - } - } -} From f49cfdb05a3bc5f2125177044e87e025a06e797b Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 22 Oct 2024 11:44:56 -0700 Subject: [PATCH 070/111] automod verbosity --- automod/engine/fetch_account_meta.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index 16dae3f2d..a20503c98 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -24,7 +24,7 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( // fallback in case client wasn't configured (eg, testing) if e.BskyClient == nil { - logger.Warn("skipping account meta hydration") + logger.Debug("skipping account meta hydration") am := AccountMeta{ Identity: ident, Profile: ProfileSummary{}, @@ -64,7 +64,7 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( // most common cause of this is a race between automod and ozone/appview for new accounts. just sleep a couple seconds and retry! var xrpcError *xrpc.Error if err != nil && errors.As(err, &xrpcError) && (xrpcError.StatusCode == 400 || xrpcError.StatusCode == 404) { - logger.Info("account profile lookup initially failed (from bsky appview), will retry", "err", err, "sleepDuration", newAccountRetryDuration) + logger.Debug("account profile lookup initially failed (from bsky appview), will retry", "err", err, "sleepDuration", newAccountRetryDuration) time.Sleep(newAccountRetryDuration) pv, err = appbsky.ActorGetProfile(ctx, e.BskyClient, ident.DID.String()) } From 1b2d84c83f3b5aecff95e11b49b44ef3de671bb7 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 22 Oct 2024 12:20:11 -0700 Subject: [PATCH 071/111] automod: add helper to get access to underlying engine --- automod/engine/context.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/automod/engine/context.go b/automod/engine/context.go index cc5b6a5e8..f88b8c9cb 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -169,6 +169,13 @@ func (c *BaseContext) InSet(name, val string) bool { return out } +// Returns a pointer to the underlying automod engine. This usually should NOT be used in rules. +// +// This is an escape hatch for hacking on the system before features get fully integerated in to the content API surface. The Engine API is not stable. +func (c *BaseContext) InternalEngine() *Engine { + return c.engine +} + func NewAccountContext(ctx context.Context, eng *Engine, meta AccountMeta) AccountContext { return AccountContext{ BaseContext: BaseContext{ From d52ce4d59cc81f31c67f8c47205de64c147c9e46 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 23 Oct 2024 09:49:40 -0700 Subject: [PATCH 072/111] automod: make account meta fetching optional --- automod/engine/engine.go | 40 ++++++++++++++++++++++++++++++++-------- automod/pkg.go | 1 + 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/automod/engine/engine.go b/automod/engine/engine.go index ae80db3ee..4ad71ba15 100644 --- a/automod/engine/engine.go +++ b/automod/engine/engine.go @@ -43,6 +43,14 @@ type Engine struct { AdminClient *xrpc.Client // used to fetch blobs from upstream PDS instances BlobClient *http.Client + + // internal configuration + Config EngineConfig +} + +type EngineConfig struct { + // if enabled, account metadata is not hydrated for every event by default + SkipAccountMeta bool } // Entrypoint for external code pushing arbitrary identity events in to the engine. @@ -80,10 +88,18 @@ func (eng *Engine) ProcessIdentityEvent(ctx context.Context, typ string, did syn return fmt.Errorf("identity not found for DID: %s", did.String()) } - am, err := eng.GetAccountMeta(ctx, ident) - if err != nil { - eventErrorCount.WithLabelValues("identity").Inc() - return fmt.Errorf("failed to fetch account metadata: %w", err) + var am *AccountMeta + if !eng.Config.SkipAccountMeta { + am, err = eng.GetAccountMeta(ctx, ident) + if err != nil { + eventErrorCount.WithLabelValues("identity").Inc() + return fmt.Errorf("failed to fetch account metadata: %w", err) + } + } else { + am = &AccountMeta{ + Identity: ident, + Profile: ProfileSummary{}, + } } ac := NewAccountContext(ctx, eng, *am) if err := eng.Rules.CallIdentityRules(&ac); err != nil { @@ -136,10 +152,18 @@ func (eng *Engine) ProcessRecordOp(ctx context.Context, op RecordOp) error { return fmt.Errorf("identity not found for DID: %s", op.DID) } - am, err := eng.GetAccountMeta(ctx, ident) - if err != nil { - eventErrorCount.WithLabelValues("record").Inc() - return fmt.Errorf("failed to fetch account metadata: %w", err) + var am *AccountMeta + if !eng.Config.SkipAccountMeta { + am, err = eng.GetAccountMeta(ctx, ident) + if err != nil { + eventErrorCount.WithLabelValues("identity").Inc() + return fmt.Errorf("failed to fetch account metadata: %w", err) + } + } else { + am = &AccountMeta{ + Identity: ident, + Profile: ProfileSummary{}, + } } rc := NewRecordContext(ctx, eng, *am, op) rc.Logger.Debug("processing record") diff --git a/automod/pkg.go b/automod/pkg.go index ebdca9811..e04589698 100644 --- a/automod/pkg.go +++ b/automod/pkg.go @@ -6,6 +6,7 @@ import ( ) type Engine = engine.Engine +type EngineConfig = engine.EngineConfig type AccountMeta = engine.AccountMeta type ProfileSummary = engine.ProfileSummary type AccountPrivate = engine.AccountPrivate From cc1ca95b0a78299b369327f3d64b768c73a26525 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 29 Oct 2024 00:57:25 -0700 Subject: [PATCH 073/111] add some helpers to automod --- automod/rules/helpers.go | 57 +++++++++++++++ automod/rules/helpers_test.go | 134 ++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/automod/rules/helpers.go b/automod/rules/helpers.go index 993b3913c..4a44b690c 100644 --- a/automod/rules/helpers.go +++ b/automod/rules/helpers.go @@ -283,3 +283,60 @@ func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { } return false } + +func ParentOrRootIsDid(post *appbsky.FeedPost, did string) bool { + if post.Reply == nil { + return false + } + + rootUri, err := syntax.ParseATURI(post.Reply.Root.Uri) + if err != nil || !rootUri.Authority().IsDID() { + return false + } + + parentUri, err := syntax.ParseATURI(post.Reply.Parent.Uri) + if err != nil || !parentUri.Authority().IsDID() { + return false + } + + return rootUri.Authority().String() == did || parentUri.Authority().String() == did +} + +func ParentOrRootIsAnyDid(post *appbsky.FeedPost, dids []string) bool { + if post.Reply == nil { + return false + } + + for _, did := range dids { + if ParentOrRootIsDid(post, did) { + return true + } + } + + return false +} + +func PostMentionsDid(post *appbsky.FeedPost, did string) bool { + facets, err := ExtractFacets(post) + if err != nil { + return false + } + + for _, facet := range facets { + if facet.DID != nil && *facet.DID == did { + return true + } + } + + return false +} + +func PostMentionsAnyDid(post *appbsky.FeedPost, dids []string) bool { + for _, did := range dids { + if PostMentionsDid(post, did) { + return true + } + } + + return false +} diff --git a/automod/rules/helpers_test.go b/automod/rules/helpers_test.go index 0d5e11ef2..e6d18f47e 100644 --- a/automod/rules/helpers_test.go +++ b/automod/rules/helpers_test.go @@ -1,6 +1,8 @@ package rules import ( + comatproto "github.com/bluesky-social/indigo/api/atproto" + appbsky "github.com/bluesky-social/indigo/api/bsky" "testing" "time" @@ -115,3 +117,135 @@ func TestAccountIsYoungerThan(t *testing.T) { assert.True(AccountIsOlderThan(&ac, time.Hour)) assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) } + +func TestPostMentionsDid(t *testing.T) { + assert := assert.New(t) + + post := &appbsky.FeedPost{ + Text: "@hailey.at what is upppp also hello to @darthbluesky.bsky.social", + Facets: []*appbsky.RichtextFacet{ + { + Features: []*appbsky.RichtextFacet_Features_Elem{ + { + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ + Did: "did:plc:abc123", + }, + }, + }, + Index: &appbsky.RichtextFacet_ByteSlice{ + ByteStart: 0, + ByteEnd: 9, + }, + }, + { + Features: []*appbsky.RichtextFacet_Features_Elem{ + { + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ + Did: "did:plc:abc456", + }, + }, + }, + Index: &appbsky.RichtextFacet_ByteSlice{ + ByteStart: 39, + ByteEnd: 63, + }, + }, + }, + } + assert.True(PostMentionsDid(post, "did:plc:abc123")) + assert.False(PostMentionsDid(post, "did:plc:cba321")) + + didList1 := []string{ + "did:plc:cba321", + "did:web:bsky.app", + "did:plc:abc456", + } + + didList2 := []string{ + "did:plc:321cba", + "did:web:bsky.app", + "did:plc:123abc", + } + + assert.True(PostMentionsAnyDid(post, didList1)) + assert.False(PostMentionsAnyDid(post, didList2)) +} + +func TestParentOrRootIsDid(t *testing.T) { + assert := assert.New(t) + + post1 := &appbsky.FeedPost{ + Text: "some random post that i dreamt up last night, idk", + Reply: &appbsky.FeedPost_ReplyRef{ + Root: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", + }, + Parent: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", + }, + }, + } + + post2 := &appbsky.FeedPost{ + Text: "some random post that i dreamt up last night, idk", + Reply: &appbsky.FeedPost_ReplyRef{ + Root: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", + }, + Parent: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", + }, + }, + } + + post3 := &appbsky.FeedPost{ + Text: "some random post that i dreamt up last night, idk", + Reply: &appbsky.FeedPost_ReplyRef{ + Root: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", + }, + Parent: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", + }, + }, + } + + post4 := &appbsky.FeedPost{ + Text: "some random post that i dreamt up last night, idk", + Reply: &appbsky.FeedPost_ReplyRef{ + Root: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", + }, + Parent: &comatproto.RepoStrongRef{ + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", + }, + }, + } + + assert.True(ParentOrRootIsDid(post1, "did:plc:abc123")) + assert.False(ParentOrRootIsDid(post1, "did:plc:321abc")) + + assert.True(ParentOrRootIsDid(post2, "did:plc:abc123")) + assert.True(ParentOrRootIsDid(post2, "did:plc:321abc")) + + assert.True(ParentOrRootIsDid(post3, "did:plc:abc123")) + assert.True(ParentOrRootIsDid(post3, "did:plc:321abc")) + + assert.False(ParentOrRootIsDid(post4, "did:plc:abc123")) + assert.True(ParentOrRootIsDid(post4, "did:plc:321abc")) + + didList1 := []string{ + "did:plc:cba321", + "did:web:bsky.app", + "did:plc:abc123", + } + + didList2 := []string{ + "did:plc:321cba", + "did:web:bsky.app", + "did:plc:123abc", + } + + assert.True(ParentOrRootIsAnyDid(post1, didList1)) + assert.False(ParentOrRootIsAnyDid(post1, didList2)) +} From 18a1d462123dffc80626ca25e6bc19a0ee0c6da1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 29 Oct 2024 00:58:22 -0700 Subject: [PATCH 074/111] nit --- automod/rules/helpers_test.go | 106 +++++++++++++++++----------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/automod/rules/helpers_test.go b/automod/rules/helpers_test.go index e6d18f47e..dd47d0b7c 100644 --- a/automod/rules/helpers_test.go +++ b/automod/rules/helpers_test.go @@ -118,59 +118,6 @@ func TestAccountIsYoungerThan(t *testing.T) { assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) } -func TestPostMentionsDid(t *testing.T) { - assert := assert.New(t) - - post := &appbsky.FeedPost{ - Text: "@hailey.at what is upppp also hello to @darthbluesky.bsky.social", - Facets: []*appbsky.RichtextFacet{ - { - Features: []*appbsky.RichtextFacet_Features_Elem{ - { - RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ - Did: "did:plc:abc123", - }, - }, - }, - Index: &appbsky.RichtextFacet_ByteSlice{ - ByteStart: 0, - ByteEnd: 9, - }, - }, - { - Features: []*appbsky.RichtextFacet_Features_Elem{ - { - RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ - Did: "did:plc:abc456", - }, - }, - }, - Index: &appbsky.RichtextFacet_ByteSlice{ - ByteStart: 39, - ByteEnd: 63, - }, - }, - }, - } - assert.True(PostMentionsDid(post, "did:plc:abc123")) - assert.False(PostMentionsDid(post, "did:plc:cba321")) - - didList1 := []string{ - "did:plc:cba321", - "did:web:bsky.app", - "did:plc:abc456", - } - - didList2 := []string{ - "did:plc:321cba", - "did:web:bsky.app", - "did:plc:123abc", - } - - assert.True(PostMentionsAnyDid(post, didList1)) - assert.False(PostMentionsAnyDid(post, didList2)) -} - func TestParentOrRootIsDid(t *testing.T) { assert := assert.New(t) @@ -249,3 +196,56 @@ func TestParentOrRootIsDid(t *testing.T) { assert.True(ParentOrRootIsAnyDid(post1, didList1)) assert.False(ParentOrRootIsAnyDid(post1, didList2)) } + +func TestPostMentionsDid(t *testing.T) { + assert := assert.New(t) + + post := &appbsky.FeedPost{ + Text: "@hailey.at what is upppp also hello to @darthbluesky.bsky.social", + Facets: []*appbsky.RichtextFacet{ + { + Features: []*appbsky.RichtextFacet_Features_Elem{ + { + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ + Did: "did:plc:abc123", + }, + }, + }, + Index: &appbsky.RichtextFacet_ByteSlice{ + ByteStart: 0, + ByteEnd: 9, + }, + }, + { + Features: []*appbsky.RichtextFacet_Features_Elem{ + { + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ + Did: "did:plc:abc456", + }, + }, + }, + Index: &appbsky.RichtextFacet_ByteSlice{ + ByteStart: 39, + ByteEnd: 63, + }, + }, + }, + } + assert.True(PostMentionsDid(post, "did:plc:abc123")) + assert.False(PostMentionsDid(post, "did:plc:cba321")) + + didList1 := []string{ + "did:plc:cba321", + "did:web:bsky.app", + "did:plc:abc456", + } + + didList2 := []string{ + "did:plc:321cba", + "did:web:bsky.app", + "did:plc:123abc", + } + + assert.True(PostMentionsAnyDid(post, didList1)) + assert.False(PostMentionsAnyDid(post, didList2)) +} From 04400b8ad199344357f547252f626d4b2fb86b15 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 29 Oct 2024 01:01:29 -0700 Subject: [PATCH 075/111] add avatar to accountmeta --- automod/engine/account_meta.go | 1 + automod/engine/fetch_account_meta.go | 1 + 2 files changed, 2 insertions(+) diff --git a/automod/engine/account_meta.go b/automod/engine/account_meta.go index 5a5a178a2..94dfe67c9 100644 --- a/automod/engine/account_meta.go +++ b/automod/engine/account_meta.go @@ -25,6 +25,7 @@ type AccountMeta struct { type ProfileSummary struct { HasAvatar bool + Avatar *string Description *string DisplayName *string } diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index 16dae3f2d..f501b92f0 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -75,6 +75,7 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( am.Profile = ProfileSummary{ HasAvatar: pv.Avatar != nil, + Avatar: pv.Avatar, Description: pv.Description, DisplayName: pv.DisplayName, } From 0b12c1c86d4f6bc0490f70b35b70a794e1a25b60 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 29 Oct 2024 01:05:18 -0700 Subject: [PATCH 076/111] nits --- automod/rules/helpers.go | 6 +++--- automod/rules/helpers_test.go | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/automod/rules/helpers.go b/automod/rules/helpers.go index 4a44b690c..e5bdcb2dd 100644 --- a/automod/rules/helpers.go +++ b/automod/rules/helpers.go @@ -284,7 +284,7 @@ func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { return false } -func ParentOrRootIsDid(post *appbsky.FeedPost, did string) bool { +func PostParentOrRootIsDid(post *appbsky.FeedPost, did string) bool { if post.Reply == nil { return false } @@ -302,13 +302,13 @@ func ParentOrRootIsDid(post *appbsky.FeedPost, did string) bool { return rootUri.Authority().String() == did || parentUri.Authority().String() == did } -func ParentOrRootIsAnyDid(post *appbsky.FeedPost, dids []string) bool { +func PostParentOrRootIsAnyDid(post *appbsky.FeedPost, dids []string) bool { if post.Reply == nil { return false } for _, did := range dids { - if ParentOrRootIsDid(post, did) { + if PostParentOrRootIsDid(post, did) { return true } } diff --git a/automod/rules/helpers_test.go b/automod/rules/helpers_test.go index dd47d0b7c..bba200cb0 100644 --- a/automod/rules/helpers_test.go +++ b/automod/rules/helpers_test.go @@ -169,17 +169,17 @@ func TestParentOrRootIsDid(t *testing.T) { }, } - assert.True(ParentOrRootIsDid(post1, "did:plc:abc123")) - assert.False(ParentOrRootIsDid(post1, "did:plc:321abc")) + assert.True(PostParentOrRootIsDid(post1, "did:plc:abc123")) + assert.False(PostParentOrRootIsDid(post1, "did:plc:321abc")) - assert.True(ParentOrRootIsDid(post2, "did:plc:abc123")) - assert.True(ParentOrRootIsDid(post2, "did:plc:321abc")) + assert.True(PostParentOrRootIsDid(post2, "did:plc:abc123")) + assert.True(PostParentOrRootIsDid(post2, "did:plc:321abc")) - assert.True(ParentOrRootIsDid(post3, "did:plc:abc123")) - assert.True(ParentOrRootIsDid(post3, "did:plc:321abc")) + assert.True(PostParentOrRootIsDid(post3, "did:plc:abc123")) + assert.True(PostParentOrRootIsDid(post3, "did:plc:321abc")) - assert.False(ParentOrRootIsDid(post4, "did:plc:abc123")) - assert.True(ParentOrRootIsDid(post4, "did:plc:321abc")) + assert.False(PostParentOrRootIsDid(post4, "did:plc:abc123")) + assert.True(PostParentOrRootIsDid(post4, "did:plc:321abc")) didList1 := []string{ "did:plc:cba321", @@ -193,8 +193,8 @@ func TestParentOrRootIsDid(t *testing.T) { "did:plc:123abc", } - assert.True(ParentOrRootIsAnyDid(post1, didList1)) - assert.False(ParentOrRootIsAnyDid(post1, didList2)) + assert.True(PostParentOrRootIsAnyDid(post1, didList1)) + assert.False(PostParentOrRootIsAnyDid(post1, didList2)) } func TestPostMentionsDid(t *testing.T) { From dd8ad60bbb3d38a10207e13dd70e0435af42fbff Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 29 Oct 2024 15:53:21 -0700 Subject: [PATCH 077/111] get cid from url --- automod/engine/account_meta.go | 3 +- automod/engine/cid_from_cdn_test.go | 42 ++++++++++++++++++++++++++++ automod/engine/fetch_account_meta.go | 24 +++++++++++++++- automod/engine/util.go | 24 ++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 automod/engine/cid_from_cdn_test.go diff --git a/automod/engine/account_meta.go b/automod/engine/account_meta.go index 94dfe67c9..76d16755d 100644 --- a/automod/engine/account_meta.go +++ b/automod/engine/account_meta.go @@ -25,7 +25,8 @@ type AccountMeta struct { type ProfileSummary struct { HasAvatar bool - Avatar *string + AvatarCid *string + BannerCid *string Description *string DisplayName *string } diff --git a/automod/engine/cid_from_cdn_test.go b/automod/engine/cid_from_cdn_test.go new file mode 100644 index 000000000..cc7553cb8 --- /dev/null +++ b/automod/engine/cid_from_cdn_test.go @@ -0,0 +1,42 @@ +package engine + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCidFromCdnUrl(t *testing.T) { + assert := assert.New(t) + + fixCid := "abcdefghijk" + + fixtures := []struct { + url string + cid *string + }{ + { + url: "https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/abcdefghijk@jpeg", + cid: &fixCid, + }, + { + url: "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:abc123/abcdefghijk@jpeg", + cid: &fixCid, + }, + { + url: "https://cdn.bsky.app/img/feed_fullsize", + cid: nil, + }, + { + url: "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:abc123/abcdefghijk", + cid: &fixCid, + }, + { + url: "https://cdn.asky.app/img/feed_fullsize/plain/did:plc:abc123/abcdefghijk@jpeg", + cid: nil, + }, + } + + for _, fix := range fixtures { + assert.Equal(fix.cid, CidFromCdnUrl(&fix.url)) + } +} diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index f501b92f0..b634c856a 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "net/url" + "strings" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -17,6 +19,25 @@ import ( var newAccountRetryDuration = 3 * 1000 * time.Millisecond +// get the cid from a bluesky cdn url +func CidFromCdnUrl(str *string) *string { + if str == nil { + return nil + } + + u, err := url.Parse(*str) + if err != nil || u.Host != "cdn.bsky.app" { + return nil + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 6 { + return nil + } + + return &strings.Split(parts[5], "@")[0] +} + // Helper to hydrate metadata about an account from several sources: PDS (if access), mod service (if access), public identity resolution func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) (*AccountMeta, error) { @@ -75,7 +96,8 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( am.Profile = ProfileSummary{ HasAvatar: pv.Avatar != nil, - Avatar: pv.Avatar, + AvatarCid: cidFromCdnUrl(pv.Avatar), + BannerCid: cidFromCdnUrl(pv.Banner), Description: pv.Description, DisplayName: pv.DisplayName, } diff --git a/automod/engine/util.go b/automod/engine/util.go index 195454c1b..e96c411d5 100644 --- a/automod/engine/util.go +++ b/automod/engine/util.go @@ -1,5 +1,10 @@ package engine +import ( + "net/url" + "strings" +) + func dedupeStrings(in []string) []string { var out []string seen := make(map[string]bool) @@ -11,3 +16,22 @@ func dedupeStrings(in []string) []string { } return out } + +// get the cid from a bluesky cdn url +func cidFromCdnUrl(str *string) *string { + if str == nil { + return nil + } + + u, err := url.Parse(*str) + if err != nil || u.Host != "cdn.bsky.app" { + return nil + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 6 { + return nil + } + + return &strings.Split(parts[5], "@")[0] +} From 5fdfd70573000ea42b746a03b94a4f338a28be80 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 16:48:42 -0700 Subject: [PATCH 078/111] automod: refactor identity and account event processing --- automod/capture/testing.go | 10 ++++- automod/consumer/firehose.go | 59 +++++-------------------- automod/engine/engine.go | 84 ++++++++++++++++++++++++++++++++++-- automod/engine/ruleset.go | 12 ++++++ automod/engine/ruletypes.go | 1 + 5 files changed, 113 insertions(+), 53 deletions(-) diff --git a/automod/capture/testing.go b/automod/capture/testing.go index 998aaef48..fbe00d6cb 100644 --- a/automod/capture/testing.go +++ b/automod/capture/testing.go @@ -7,6 +7,7 @@ import ( "io" "os" + comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" @@ -38,12 +39,19 @@ func ProcessCaptureRules(eng *automod.Engine, capture AccountCapture) error { ctx := context.Background() did := capture.AccountMeta.Identity.DID + handle := capture.AccountMeta.Identity.Handle.String() dir := identity.NewMockDirectory() dir.Insert(*capture.AccountMeta.Identity) eng.Directory = &dir // initial identity rules - eng.ProcessIdentityEvent(ctx, "new", did) + identEvent := comatproto.SyncSubscribeRepos_Identity{ + Did: did.String(), + Handle: &handle, + Seq: 12345, + Time: syntax.DatetimeNow().String(), + } + eng.ProcessIdentityEvent(ctx, identEvent) // all the post rules for _, pr := range capture.PostRecords { diff --git a/automod/consumer/firehose.go b/automod/consumer/firehose.go index df6d8f91b..f210b3055 100644 --- a/automod/consumer/firehose.go +++ b/automod/consumer/firehose.go @@ -80,54 +80,20 @@ func (fc *FirehoseConsumer) Run(ctx context.Context) error { }, RepoIdentity: func(evt *comatproto.SyncSubscribeRepos_Identity) error { atomic.StoreInt64(&fc.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - fc.Logger.Error("bad DID in RepoIdentity event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := fc.Engine.ProcessIdentityEvent(ctx, "identity", did); err != nil { + if err := fc.Engine.ProcessIdentityEvent(ctx, *evt); err != nil { fc.Logger.Error("processing repo identity failed", "did", evt.Did, "seq", evt.Seq, "err", err) } return nil }, RepoAccount: func(evt *comatproto.SyncSubscribeRepos_Account) error { atomic.StoreInt64(&fc.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - fc.Logger.Error("bad DID in RepoAccount event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := fc.Engine.ProcessIdentityEvent(ctx, "account", did); err != nil { + if err := fc.Engine.ProcessAccountEvent(ctx, *evt); err != nil { fc.Logger.Error("processing repo account failed", "did", evt.Did, "seq", evt.Seq, "err", err) } return nil }, - // TODO: deprecated - RepoHandle: func(evt *comatproto.SyncSubscribeRepos_Handle) error { - atomic.StoreInt64(&fc.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - fc.Logger.Error("bad DID in RepoHandle event", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) - return nil - } - if err := fc.Engine.ProcessIdentityEvent(ctx, "handle", did); err != nil { - fc.Logger.Error("processing handle update failed", "did", evt.Did, "handle", evt.Handle, "seq", evt.Seq, "err", err) - } - return nil - }, - // TODO: deprecated - RepoTombstone: func(evt *comatproto.SyncSubscribeRepos_Tombstone) error { - atomic.StoreInt64(&fc.lastSeq, evt.Seq) - did, err := syntax.ParseDID(evt.Did) - if err != nil { - fc.Logger.Error("bad DID in RepoTombstone event", "did", evt.Did, "seq", evt.Seq, "err", err) - return nil - } - if err := fc.Engine.ProcessIdentityEvent(ctx, "tombstone", did); err != nil { - fc.Logger.Error("processing repo tombstone failed", "did", evt.Did, "seq", evt.Seq, "err", err) - } - return nil - }, + // NOTE: no longer process #handle events + // NOTE: no longer process #tombstone events } var scheduler events.Scheduler @@ -176,13 +142,6 @@ func (fc *FirehoseConsumer) HandleRepoCommit(ctx context.Context, evt *comatprot return nil } - // empty commit is a special case, temporarily, basically indicates "new account" - if len(evt.Ops) == 0 { - if err := fc.Engine.ProcessIdentityEvent(ctx, "create", did); err != nil { - fc.Logger.Error("processing handle update failed", "did", evt.Repo, "rev", evt.Rev, "seq", evt.Seq, "err", err) - } - } - for _, op := range evt.Ops { logger = logger.With("eventKind", op.Action, "path", op.Path) collection, rkey, err := splitRepoPath(op.Path) @@ -215,27 +174,29 @@ func (fc *FirehoseConsumer) HandleRepoCommit(ctx context.Context, evt *comatprot break } recCID := syntax.CID(op.Cid.String()) - err = fc.Engine.ProcessRecordOp(ctx, automod.RecordOp{ + op := automod.RecordOp{ Action: action, DID: did, Collection: collection, RecordKey: rkey, CID: &recCID, RecordCBOR: *recCBOR, - }) + } + err = fc.Engine.ProcessRecordOp(ctx, op) if err != nil { logger.Error("engine failed to process record", "err", err) continue } case repomgr.EvtKindDeleteRecord: - err = fc.Engine.ProcessRecordOp(ctx, automod.RecordOp{ + op := automod.RecordOp{ Action: automod.DeleteOp, DID: did, Collection: collection, RecordKey: rkey, CID: nil, RecordCBOR: nil, - }) + } + err = fc.Engine.ProcessRecordOp(ctx, op) if err != nil { logger.Error("engine failed to process record", "err", err) continue diff --git a/automod/engine/engine.go b/automod/engine/engine.go index 4ad71ba15..8ed864371 100644 --- a/automod/engine/engine.go +++ b/automod/engine/engine.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod/cachestore" @@ -53,10 +54,10 @@ type EngineConfig struct { SkipAccountMeta bool } -// Entrypoint for external code pushing arbitrary identity events in to the engine. +// Entrypoint for external code pushing #identity events in to the engine. // // This method can be called concurrently, though cached state may end up inconsistent if multiple events for the same account (DID) are processed in parallel. -func (eng *Engine) ProcessIdentityEvent(ctx context.Context, typ string, did syntax.DID) error { +func (eng *Engine) ProcessIdentityEvent(ctx context.Context, evt comatproto.SyncSubscribeRepos_Identity) error { eventProcessCount.WithLabelValues("identity").Inc() start := time.Now() defer func() { @@ -64,10 +65,15 @@ func (eng *Engine) ProcessIdentityEvent(ctx context.Context, typ string, did syn eventProcessDuration.WithLabelValues("identity").Observe(duration.Seconds()) }() + did, err := syntax.ParseDID(evt.Did) + if err != nil { + return fmt.Errorf("bad DID in repo #identity event (%s): %w", evt.Did, err) + } + // similar to an HTTP server, we want to recover any panics from rule execution defer func() { if r := recover(); r != nil { - eng.Logger.Error("automod event execution exception", "err", r, "did", did, "type", typ) + eng.Logger.Error("automod event execution exception", "err", r, "did", did, "type", "identity") eventErrorCount.WithLabelValues("identity").Inc() } }() @@ -78,6 +84,7 @@ func (eng *Engine) ProcessIdentityEvent(ctx context.Context, typ string, did syn if err := eng.PurgeAccountCaches(ctx, did); err != nil { eng.Logger.Error("failed to purge identity cache; identity rule may not run correctly", "err", err) } + // TODO(bnewbold): if it was a tombstone, this might fail ident, err := eng.Directory.LookupDID(ctx, did) if err != nil { eventErrorCount.WithLabelValues("identity").Inc() @@ -118,6 +125,77 @@ func (eng *Engine) ProcessIdentityEvent(ctx context.Context, typ string, did syn return nil } +// Entrypoint for external code pushing #account events in to the engine. +// +// This method can be called concurrently, though cached state may end up inconsistent if multiple events for the same account (DID) are processed in parallel. +func (eng *Engine) ProcessAccountEvent(ctx context.Context, evt comatproto.SyncSubscribeRepos_Account) error { + eventProcessCount.WithLabelValues("account").Inc() + start := time.Now() + defer func() { + duration := time.Since(start) + eventProcessDuration.WithLabelValues("account").Observe(duration.Seconds()) + }() + + did, err := syntax.ParseDID(evt.Did) + if err != nil { + return fmt.Errorf("bad DID in repo #account event (%s): %w", evt.Did, err) + } + + // similar to an HTTP server, we want to recover any panics from rule execution + defer func() { + if r := recover(); r != nil { + eng.Logger.Error("automod event execution exception", "err", r, "did", did, "type", "account") + eventErrorCount.WithLabelValues("account").Inc() + } + }() + ctx, cancel := context.WithTimeout(ctx, identityEventTimeout) + defer cancel() + + // first purge any caches; we need to re-resolve from scratch on account updates + if err := eng.PurgeAccountCaches(ctx, did); err != nil { + eng.Logger.Error("failed to purge account cache; account rule may not run correctly", "err", err) + } + // TODO(bnewbold): if it was a tombstone, this might fail + ident, err := eng.Directory.LookupDID(ctx, did) + if err != nil { + eventErrorCount.WithLabelValues("account").Inc() + return fmt.Errorf("resolving identity: %w", err) + } + if ident == nil { + eventErrorCount.WithLabelValues("account").Inc() + return fmt.Errorf("identity not found for DID: %s", did.String()) + } + + var am *AccountMeta + if !eng.Config.SkipAccountMeta { + am, err = eng.GetAccountMeta(ctx, ident) + if err != nil { + eventErrorCount.WithLabelValues("identity").Inc() + return fmt.Errorf("failed to fetch account metadata: %w", err) + } + } else { + am = &AccountMeta{ + Identity: ident, + Profile: ProfileSummary{}, + } + } + ac := NewAccountContext(ctx, eng, *am) + if err := eng.Rules.CallAccountRules(&ac); err != nil { + eventErrorCount.WithLabelValues("account").Inc() + return fmt.Errorf("rule execution failed: %w", err) + } + eng.CanonicalLogLineAccount(&ac) + if err := eng.persistAccountModActions(&ac); err != nil { + eventErrorCount.WithLabelValues("account").Inc() + return fmt.Errorf("failed to persist actions for account event: %w", err) + } + if err := eng.persistCounters(ctx, ac.effects); err != nil { + eventErrorCount.WithLabelValues("account").Inc() + return fmt.Errorf("failed to persist counters for account event: %w", err) + } + return nil +} + // Entrypoint for external code pushing repository updates. A simple repo commit results in multiple calls. // // This method can be called concurrently, though cached state may end up inconsistent if multiple events for the same account (DID) are processed in parallel. diff --git a/automod/engine/ruleset.go b/automod/engine/ruleset.go index 0b7d90cc4..4c72ef8f9 100644 --- a/automod/engine/ruleset.go +++ b/automod/engine/ruleset.go @@ -16,6 +16,7 @@ type RuleSet struct { RecordRules []RecordRuleFunc RecordDeleteRules []RecordRuleFunc IdentityRules []IdentityRuleFunc + AccountRules []AccountRuleFunc BlobRules []BlobRuleFunc NotificationRules []NotificationRuleFunc OzoneEventRules []OzoneEventRuleFunc @@ -89,6 +90,17 @@ func (r *RuleSet) CallIdentityRules(c *AccountContext) error { return nil } +// Executes rules for account update events. +func (r *RuleSet) CallAccountRules(c *AccountContext) error { + for _, f := range r.AccountRules { + err := f(c) + if err != nil { + c.Logger.Error("account rule execution failed", "err", err) + } + } + return nil +} + func (r *RuleSet) CallNotificationRules(c *NotificationContext) error { for _, f := range r.NotificationRules { err := f(c) diff --git a/automod/engine/ruletypes.go b/automod/engine/ruletypes.go index a86567ead..27d4a149f 100644 --- a/automod/engine/ruletypes.go +++ b/automod/engine/ruletypes.go @@ -6,6 +6,7 @@ import ( ) type IdentityRuleFunc = func(c *AccountContext) error +type AccountRuleFunc = func(c *AccountContext) error type RecordRuleFunc = func(c *RecordContext) error type PostRuleFunc = func(c *RecordContext, post *appbsky.FeedPost) error type ProfileRuleFunc = func(c *RecordContext, profile *appbsky.ActorProfile) error From 7b51c02882b0348463c6d38b3a4ccf25dc829e6d Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 5 Sep 2024 19:58:59 -0700 Subject: [PATCH 079/111] fetch and cache account review state and appeal state --- automod/engine/account_meta.go | 3 +++ automod/engine/fetch_account_meta.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/automod/engine/account_meta.go b/automod/engine/account_meta.go index 5a5a178a2..1ab25568e 100644 --- a/automod/engine/account_meta.go +++ b/automod/engine/account_meta.go @@ -34,4 +34,7 @@ type AccountPrivate struct { EmailConfirmed bool IndexedAt *time.Time AccountTags []string + // ReviewState will be one of "open", "escalated", "closed", "none", or "" (unknown) + ReviewState string + Appealed bool } diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index a20503c98..55fb4bc4d 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -131,7 +131,22 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( if rd.Moderation.SubjectStatus.Takendown != nil && *rd.Moderation.SubjectStatus.Takendown == true { am.Takendown = true } + if rd.Moderation.SubjectStatus.Appealed != nil && *rd.Moderation.SubjectStatus.Appealed == true { + ap.Appealed = true + } ap.AccountTags = dedupeStrings(rd.Moderation.SubjectStatus.Tags) + if rd.Moderation.SubjectStatus.ReviewState != nil { + switch *rd.Moderation.SubjectStatus.ReviewState { + case "#reviewOpen": + ap.ReviewState = "open" + case "#reviewEscalated": + ap.ReviewState = "escalated" + case "#reviewClosed": + ap.ReviewState = "closed" + case "#reviewNonde": + ap.ReviewState = "none" + } + } } am.Private = &ap } From 293a3806751ce95b07d074221d34b0e60945281b Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 5 Sep 2024 20:28:49 -0700 Subject: [PATCH 080/111] constants for review states --- automod/engine/account_meta.go | 9 ++++++++- automod/engine/fetch_account_meta.go | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/automod/engine/account_meta.go b/automod/engine/account_meta.go index 1ab25568e..e7d4e86ba 100644 --- a/automod/engine/account_meta.go +++ b/automod/engine/account_meta.go @@ -6,6 +6,13 @@ import ( "github.com/bluesky-social/indigo/atproto/identity" ) +var ( + ReviewStateEscalated = "escalated" + ReviewStateOpen = "open" + ReviewStateClosed = "closed" + ReviewStateNone = "none" +) + // information about a repo/account/identity, always pre-populated and relevant to many rules type AccountMeta struct { Identity *identity.Identity @@ -34,7 +41,7 @@ type AccountPrivate struct { EmailConfirmed bool IndexedAt *time.Time AccountTags []string - // ReviewState will be one of "open", "escalated", "closed", "none", or "" (unknown) + // ReviewState will be one of ReviewStateEscalated, ReviewStateOpen, ReviewStateClosed, ReviewStateNone, or "" (unknown) ReviewState string Appealed bool } diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index 55fb4bc4d..5bc5c3637 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -138,13 +138,13 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( if rd.Moderation.SubjectStatus.ReviewState != nil { switch *rd.Moderation.SubjectStatus.ReviewState { case "#reviewOpen": - ap.ReviewState = "open" + ap.ReviewState = ReviewStateOpen case "#reviewEscalated": - ap.ReviewState = "escalated" + ap.ReviewState = ReviewStateEscalated case "#reviewClosed": - ap.ReviewState = "closed" + ap.ReviewState = ReviewStateClosed case "#reviewNonde": - ap.ReviewState = "none" + ap.ReviewState = ReviewStateNone } } } From 66b2bdbcd011ea8f9c912a017a064135251511a7 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 5 Sep 2024 20:29:09 -0700 Subject: [PATCH 081/111] refactors --- automod/engine/context.go | 8 ++++ automod/engine/effects.go | 18 +++++++- automod/engine/metrics.go | 12 +++++- automod/engine/persist.go | 73 +++++++++++++++++++++++++++++++- automod/engine/persisthelpers.go | 20 +++++++++ 5 files changed, 127 insertions(+), 4 deletions(-) diff --git a/automod/engine/context.go b/automod/engine/context.go index f88b8c9cb..b015b3ac0 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -279,6 +279,14 @@ func (c *AccountContext) TakedownAccount() { c.effects.TakedownAccount() } +func (c *AccountContext) EscalateAccount() { + c.effects.EscalateAccount() +} + +func (c *AccountContext) AcknowledgeAccount() { + c.effects.AcknowledgeAccount() +} + func (c *RecordContext) AddRecordFlag(val string) { c.effects.AddRecordFlag(val) } diff --git a/automod/engine/effects.go b/automod/engine/effects.go index ed5ac2998..7f2e91d8f 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -12,6 +12,8 @@ var ( QuotaModReportDay = 2000 // number of takedowns automod can action per day, for all subjects combined (circuit breaker) QuotaModTakedownDay = 200 + // number of misc actions automod can do per day, for all subjects combined (circuit breaker) + QuotaModActionDay = 1000 ) type CounterRef struct { @@ -42,8 +44,12 @@ type Effects struct { AccountFlags []string // Reports which should be filed against this account, as a result of rule execution. AccountReports []ModReport - // If "true", indicates that a rule indicates that the entire account should have a takedown. + // If "true", a rule decided that the entire account should have a takedown. AccountTakedown bool + // If "true", a rule decided that the reported account should be escalated. + AccountEscalate bool + // If "true", a rule decided that the reports on account should be resolved as acknowledged. + AccountAcknowledge bool // Same as "AccountLabels", but at record-level RecordLabels []string // Same as "AccountFlags", but at record-level @@ -128,6 +134,16 @@ func (e *Effects) TakedownAccount() { e.AccountTakedown = true } +// Enqueues the account to be "escalated" for mod review at the end of rule processing. +func (e *Effects) EscalateAccount() { + e.AccountEscalate = true +} + +// Enqueues reports on account to be "acknowledged" (closed) at the end of rule processing. +func (e *Effects) AcknowledgeAccount() { + e.AccountAcknowledge = true +} + // Enqueues the provided label (string value) to be added to the record at the end of rule processing. func (e *Effects) AddRecordLabel(val string) { e.mu.Lock() diff --git a/automod/engine/metrics.go b/automod/engine/metrics.go index bf71197d4..1a08944e7 100644 --- a/automod/engine/metrics.go +++ b/automod/engine/metrics.go @@ -37,7 +37,17 @@ var actionNewReportCount = promauto.NewCounterVec(prometheus.CounterOpts{ var actionNewTakedownCount = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "automod_new_action_takedowns", - Help: "Number of new flags persisted", + Help: "Number of new takedowns", +}, []string{"type"}) + +var actionNewEscalationCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "automod_new_action_escalations", + Help: "Number of new subject escalations", +}, []string{"type"}) + +var actionNewAcknowledgeCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "automod_new_action_acknowledges", + Help: "Number of new subjects acknowledged", }, []string{"type"}) var accountMetaFetches = promauto.NewCounter(prometheus.CounterOpts{ diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 7d6675bbf..4864b3538 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -57,8 +57,28 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { if err != nil { return fmt.Errorf("circuit-breaking takedowns: %w", err) } + newEscalation := c.effects.AccountEscalate + if c.Account.Private != nil && c.Account.Private.ReviewState == ReviewStateEscalated { + // de-dupe account escalation + newEscalation = false + } else { + newEscalation, err = eng.circuitBreakModAction(ctx, newEscalation) + if err != nil { + return fmt.Errorf("circuit-breaking escalation: %w", err) + } + } + newAcknowledge := c.effects.AccountAcknowledge + if c.Account.Private != nil && (c.Account.Private.ReviewState == "closed" || c.Account.Private.ReviewState == "none") { + // de-dupe account escalation + newAcknowledge = false + } else { + newAcknowledge, err = eng.circuitBreakModAction(ctx, newAcknowledge) + if err != nil { + return fmt.Errorf("circuit-breaking acknowledge: %w", err) + } + } - anyModActions := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 + anyModActions := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 if anyModActions && eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendAccount(ctx, srv, c); err != nil { @@ -145,9 +165,56 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { if err != nil { c.Logger.Error("failed to execute account takedown", "err", err) } + + // we don't want to escalate if there is a takedown + newEscalation = false + } + + if newEscalation { + c.Logger.Warn("account-escalate") + actionNewEscalationCount.WithLabelValues("account").Inc() + comment := "[automod]: auto account-escalation" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventEscalate: &toolsozone.ModerationDefs_ModEventEscalate{ + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + AdminDefs_RepoRef: &comatproto.AdminDefs_RepoRef{ + Did: c.Account.Identity.DID.String(), + }, + }, + }) + if err != nil { + c.Logger.Error("failed to execute account escalation", "err", err) + } + } + + if newAcknowledge { + c.Logger.Warn("account-acknowledge") + actionNewAcknowledgeCount.WithLabelValues("account").Inc() + comment := "[automod]: auto account-acknowledge" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventAcknowledge: &toolsozone.ModerationDefs_ModEventAcknowledge{ + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + AdminDefs_RepoRef: &comatproto.AdminDefs_RepoRef{ + Did: c.Account.Identity.DID.String(), + }, + }, + }) + if err != nil { + c.Logger.Error("failed to execute account acknowledge", "err", err) + } } - needCachePurge := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || createdReports + needCachePurge := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || createdReports if needCachePurge { return eng.PurgeAccountCaches(ctx, c.Account.Identity.DID) } @@ -210,6 +277,8 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { if err != nil { return fmt.Errorf("failed to circuit break takedowns: %w", err) } + // TODO: record escalation + // TODO: record acknowledge if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { if eng.Notifier != nil { diff --git a/automod/engine/persisthelpers.go b/automod/engine/persisthelpers.go index c224cc4ab..f1dd2b039 100644 --- a/automod/engine/persisthelpers.go +++ b/automod/engine/persisthelpers.go @@ -111,6 +111,26 @@ func (eng *Engine) circuitBreakTakedown(ctx context.Context, takedown bool) (boo return takedown, nil } +// Combined circuit breaker for miscellaneous mod actions like: escalate, acknowledge +func (eng *Engine) circuitBreakModAction(ctx context.Context, action bool) (bool, error) { + if !action { + return false, nil + } + c, err := eng.Counters.GetCount(ctx, "automod-quota", "mod-action", countstore.PeriodDay) + if err != nil { + return false, fmt.Errorf("checking mod action quota: %w", err) + } + if c >= QuotaModActionDay { + eng.Logger.Warn("CIRCUIT BREAKER: automod action") + return false, nil + } + err = eng.Counters.Increment(ctx, "automod-quota", "mod-action") + if err != nil { + return false, fmt.Errorf("incrementing mod action quota: %w", err) + } + return action, nil +} + // Creates a moderation report, but checks first if there was a similar recent one, and skips if so. // // Returns a bool indicating if a new report was created. From 6ed407ea5debaf402e3ba9c3aa9b7a32658a5980 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 15 Oct 2024 12:16:41 +0200 Subject: [PATCH 082/111] :sparkles: Add record ack and escalation --- automod/engine/persist.go | 56 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 4864b3538..03cd63705 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -277,10 +277,18 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { if err != nil { return fmt.Errorf("failed to circuit break takedowns: %w", err) } - // TODO: record escalation - // TODO: record acknowledge + // @TODO: should we check for existing escalation? there doesn't seem to be an existing flag for this at record level + newEscalation, err := eng.circuitBreakModAction(ctx, c.effects.RecordEscalate) + if err != nil { + return fmt.Errorf("circuit-breaking escalation: %w", err) + } + // @TODO: should we check if the subject is already acked? there doesn't seem to be an existing flag for this at record level + newAcknowledge, err := eng.circuitBreakModAction(ctx, c.effects.RecordAcknowledge) + if err != nil { + return fmt.Errorf("circuit-breaking acknowledge: %w", err) + } - if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { + if newEscalation || newAcknowledge || newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { if eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendRecord(ctx, srv, c); err != nil { @@ -300,7 +308,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } // exit early - if !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { + if !newAcknowledge && !newEscalation && !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { return nil } @@ -372,5 +380,45 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { c.Logger.Error("failed to execute record takedown", "err", err) } } + + if newEscalation { + c.Logger.Warn("record-escalation") + actionNewEscalationCount.WithLabelValues("record").Inc() + comment := "[automod]: automated record-escalation" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventEscalate: &toolsozone.ModerationDefs_ModEventEscalate{ + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + RepoStrongRef: &strongRef, + }, + }) + if err != nil { + c.Logger.Error("failed to execute record escalation", "err", err) + } + } + + if newAcknowledge { + c.Logger.Warn("record-acknowledge") + actionNewAcknowledgeCount.WithLabelValues("record").Inc() + comment := "[automod]: automated record-acknowledge" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventAcknowledge: &toolsozone.ModerationDefs_ModEventAcknowledge{ + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + RepoStrongRef: &strongRef, + }, + }) + if err != nil { + c.Logger.Error("failed to execute record acknowledge", "err", err) + } + } return nil } From f4ce81ede806511cdd93af79608fc114d47d9c14 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 17:28:17 -0700 Subject: [PATCH 083/111] remove record-level ask/esc (for now) --- automod/engine/persist.go | 53 ++------------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 03cd63705..d6e7f7554 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -277,18 +277,8 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { if err != nil { return fmt.Errorf("failed to circuit break takedowns: %w", err) } - // @TODO: should we check for existing escalation? there doesn't seem to be an existing flag for this at record level - newEscalation, err := eng.circuitBreakModAction(ctx, c.effects.RecordEscalate) - if err != nil { - return fmt.Errorf("circuit-breaking escalation: %w", err) - } - // @TODO: should we check if the subject is already acked? there doesn't seem to be an existing flag for this at record level - newAcknowledge, err := eng.circuitBreakModAction(ctx, c.effects.RecordAcknowledge) - if err != nil { - return fmt.Errorf("circuit-breaking acknowledge: %w", err) - } - if newEscalation || newAcknowledge || newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { + if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { if eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendRecord(ctx, srv, c); err != nil { @@ -308,7 +298,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } // exit early - if !newAcknowledge && !newEscalation && !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { + if !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { return nil } @@ -381,44 +371,5 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } } - if newEscalation { - c.Logger.Warn("record-escalation") - actionNewEscalationCount.WithLabelValues("record").Inc() - comment := "[automod]: automated record-escalation" - _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ - CreatedBy: xrpcc.Auth.Did, - Event: &toolsozone.ModerationEmitEvent_Input_Event{ - ModerationDefs_ModEventEscalate: &toolsozone.ModerationDefs_ModEventEscalate{ - Comment: &comment, - }, - }, - Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ - RepoStrongRef: &strongRef, - }, - }) - if err != nil { - c.Logger.Error("failed to execute record escalation", "err", err) - } - } - - if newAcknowledge { - c.Logger.Warn("record-acknowledge") - actionNewAcknowledgeCount.WithLabelValues("record").Inc() - comment := "[automod]: automated record-acknowledge" - _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ - CreatedBy: xrpcc.Auth.Did, - Event: &toolsozone.ModerationEmitEvent_Input_Event{ - ModerationDefs_ModEventAcknowledge: &toolsozone.ModerationDefs_ModEventAcknowledge{ - Comment: &comment, - }, - }, - Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ - RepoStrongRef: &strongRef, - }, - }) - if err != nil { - c.Logger.Error("failed to execute record acknowledge", "err", err) - } - } return nil } From c728f2ec99eb388249db7259afbd03aa334ec971 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 5 Sep 2024 18:49:52 -0700 Subject: [PATCH 084/111] automod: engine support for adding tags --- automod/engine/effects.go | 30 +++++++++++- automod/engine/metrics.go | 5 ++ automod/engine/persist.go | 78 ++++++++++++++++++++++++++++---- automod/engine/persisthelpers.go | 17 +++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/automod/engine/effects.go b/automod/engine/effects.go index 7f2e91d8f..a318615cc 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -40,7 +40,9 @@ type Effects struct { CounterDistinctIncrements []CounterDistinctRef // TODO: better variable names // Label values which should be applied to the overall account, as a result of rule execution. AccountLabels []string - // Moderation flags (similar to labels, but private) which should be applied to the overall account, as a result of rule execution. + // Moderation tags (similar to labels, but private) which should be applied to the overall account, as a result of rule execution. + AccountTags []string + // automod flags (metadata) which should be applied to the account as a result of rule execution. AccountFlags []string // Reports which should be filed against this account, as a result of rule execution. AccountReports []ModReport @@ -52,6 +54,8 @@ type Effects struct { AccountAcknowledge bool // Same as "AccountLabels", but at record-level RecordLabels []string + // Same as "AccountTags", but at record-level + RecordTags []string // Same as "AccountFlags", but at record-level RecordFlags []string // Same as "AccountReports", but at record-level @@ -102,6 +106,18 @@ func (e *Effects) AddAccountLabel(val string) { e.AccountLabels = append(e.AccountLabels, val) } +// Enqueues the provided label (string value) to be added to the account at the end of rule processing. +func (e *Effects) AddAccountTag(val string) { + e.mu.Lock() + defer e.mu.Unlock() + for _, v := range e.AccountTags { + if v == val { + return + } + } + e.AccountTags = append(e.AccountTags, val) +} + // Enqueues the provided flag (string value) to be recorded (in the Engine's flagstore) at the end of rule processing. func (e *Effects) AddAccountFlag(val string) { e.mu.Lock() @@ -156,6 +172,18 @@ func (e *Effects) AddRecordLabel(val string) { e.RecordLabels = append(e.RecordLabels, val) } +// Enqueues the provided tag (string value) to be added to the record at the end of rule processing. +func (e *Effects) AddRecordTag(val string) { + e.mu.Lock() + defer e.mu.Unlock() + for _, v := range e.RecordTags { + if v == val { + return + } + } + e.RecordTags = append(e.RecordTags, val) +} + // Enqueues the provided flag (string value) to be recorded (in the Engine's flagstore) at the end of rule processing. func (e *Effects) AddRecordFlag(val string) { e.mu.Lock() diff --git a/automod/engine/metrics.go b/automod/engine/metrics.go index 1a08944e7..bc32b8e54 100644 --- a/automod/engine/metrics.go +++ b/automod/engine/metrics.go @@ -25,6 +25,11 @@ var actionNewLabelCount = promauto.NewCounterVec(prometheus.CounterOpts{ Help: "Number of new labels persisted", }, []string{"type", "val"}) +var actionNewTagCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "automod_new_action_tags", + Help: "Number of new tags persisted", +}, []string{"type", "val"}) + var actionNewFlagCount = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "automod_new_action_flags", Help: "Number of new flags persisted", diff --git a/automod/engine/persist.go b/automod/engine/persist.go index d6e7f7554..e289a64ef 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -32,7 +32,7 @@ func (eng *Engine) persistCounters(ctx context.Context, eff *Effects) error { return nil } -// Persists account-level moderation actions: new labels, new flags, new takedowns, and reports. +// Persists account-level moderation actions: new labels, new tags, new flags, new takedowns, and reports. // // If necessary, will "purge" identity and account caches, so that state updates will be picked up for subsequent events. // @@ -42,6 +42,11 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { // de-dupe actions newLabels := dedupeLabelActions(c.effects.AccountLabels, c.Account.AccountLabels, c.Account.AccountNegatedLabels) + existingTags := []string{} + if c.Account.Private != nil { + existingTags = c.Account.Private.AccountTags + } + newTags := dedupeTagActions(c.effects.AccountTags, existingTags) newFlags := dedupeFlagActions(c.effects.AccountFlags, c.Account.AccountFlags) // don't report the same account multiple times on the same day for the same reason. this is a quick check; we also query the mod service API just before creating the report. @@ -78,7 +83,7 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { } } - anyModActions := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 + anyModActions := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newTags) > 0 || len(newFlags) > 0 || len(newReports) > 0 if anyModActions && eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendAccount(ctx, srv, c); err != nil { @@ -107,7 +112,7 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { xrpcc := eng.OzoneClient if len(newLabels) > 0 { - c.Logger.Info("labeling record", "newLabels", newLabels) + c.Logger.Info("labeling account", "newLabels", newLabels) for _, val := range newLabels { // note: WithLabelValues is a prometheus label, not an atproto label actionNewLabelCount.WithLabelValues("account", val).Inc() @@ -133,6 +138,33 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { } } + if len(newTags) > 0 { + c.Logger.Info("tagging account", "newTags", newTags) + for _, val := range newTags { + // note: WithLabelValues is a prometheus label, not an atproto label + actionNewTagCount.WithLabelValues("account", val).Inc() + } + comment := "[automod]: auto-tagging account" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventTag: &toolsozone.ModerationDefs_ModEventTag{ + Add: newTags, + Remove: []string{}, + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + AdminDefs_RepoRef: &comatproto.AdminDefs_RepoRef{ + Did: c.Account.Identity.DID.String(), + }, + }, + }) + if err != nil { + c.Logger.Error("failed to create account tags", "err", err) + } + } + // reports are additionally de-duped when persisting the action, so track with a flag createdReports := false for _, mr := range newReports { @@ -214,7 +246,7 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { } } - needCachePurge := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || createdReports + needCachePurge := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newTags) > 0 || len(newFlags) > 0 || createdReports if needCachePurge { return eng.PurgeAccountCaches(ctx, c.Account.Identity.DID) } @@ -222,7 +254,7 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error { return nil } -// Persists some record-level state: labels, takedowns, reports. +// Persists some record-level state: labels, tags, takedowns, reports. // // NOTE: this method currently does *not* persist record-level flags to any storage, and does not de-dupe most actions, on the assumption that the record is new (from firehose) and has no existing mod state. func (eng *Engine) persistRecordModActions(c *RecordContext) error { @@ -233,7 +265,9 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { atURI := c.RecordOp.ATURI().String() newLabels := dedupeStrings(c.effects.RecordLabels) - if len(newLabels) > 0 && eng.OzoneClient != nil { + newTags := dedupeStrings(c.effects.RecordTags) + if (len(newLabels) > 0 || len(newTags) > 0) && eng.OzoneClient != nil { + // fetch existing record labels, tags, etc rv, err := toolsozone.ModerationGetRecord(ctx, eng.OzoneClient, c.RecordOp.CID.String(), c.RecordOp.ATURI().String()) if err != nil { // NOTE: there is a frequent 4xx error here from Ozone because this record has not been indexed yet @@ -250,10 +284,11 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } existingLabels = dedupeStrings(existingLabels) negLabels = dedupeStrings(negLabels) - // fetch existing record labels newLabels = dedupeLabelActions(newLabels, existingLabels, negLabels) + newTags = dedupeTagActions(newTags, rv.Moderation.SubjectStatus.Tags) } } + newFlags := dedupeStrings(c.effects.RecordFlags) if len(newFlags) > 0 { // fetch existing flags, and de-dupe @@ -278,7 +313,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { return fmt.Errorf("failed to circuit break takedowns: %w", err) } - if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { + if newTakedown || len(newLabels) > 0 || len(newTags) > 0 || len(newFlags) > 0 || len(newReports) > 0 { if eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendRecord(ctx, srv, c); err != nil { @@ -298,7 +333,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } // exit early - if !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { + if !newTakedown && len(newLabels) == 0 && len(newTags) == 0 && len(newReports) == 0 { return nil } @@ -343,6 +378,31 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } } + if len(newTags) > 0 { + c.Logger.Info("tagging record", "newTags", newTags) + for _, val := range newTags { + // note: WithLabelValues is a prometheus label, not an atproto label + actionNewTagCount.WithLabelValues("record", val).Inc() + } + comment := "[automod]: auto-tagging record" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventTag: &toolsozone.ModerationDefs_ModEventTag{ + Add: newLabels, + Remove: []string{}, + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + RepoStrongRef: &strongRef, + }, + }) + if err != nil { + c.Logger.Error("failed to create record tag", "err", err) + } + } + for _, mr := range newReports { _, err := eng.createRecordReportIfFresh(ctx, xrpcc, c.RecordOp.ATURI(), c.RecordOp.CID, mr) if err != nil { diff --git a/automod/engine/persisthelpers.go b/automod/engine/persisthelpers.go index f1dd2b039..491a86fa4 100644 --- a/automod/engine/persisthelpers.go +++ b/automod/engine/persisthelpers.go @@ -35,6 +35,23 @@ func dedupeLabelActions(labels, existing, existingNegated []string) []string { return newLabels } +func dedupeTagActions(tags, existing []string) []string { + newTags := []string{} + for _, val := range dedupeStrings(tags) { + exists := false + for _, e := range existing { + if val == e { + exists = true + break + } + } + if !exists { + newTags = append(newTags, val) + } + } + return newTags +} + func dedupeFlagActions(flags, existing []string) []string { newFlags := []string{} for _, val := range dedupeStrings(flags) { From 5da2027f1292287c56715fd2f40f47cf8ef3ce25 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 5 Sep 2024 19:26:37 -0700 Subject: [PATCH 085/111] add context helpers --- automod/engine/context.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/automod/engine/context.go b/automod/engine/context.go index b015b3ac0..2e447bebd 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -271,6 +271,10 @@ func (c *AccountContext) AddAccountLabel(val string) { c.effects.AddAccountLabel(val) } +func (c *AccountContext) AddAccountTag(val string) { + c.effects.AddAccountTag(val) +} + func (c *AccountContext) ReportAccount(reason, comment string) { c.effects.ReportAccount(reason, comment) } @@ -295,6 +299,10 @@ func (c *RecordContext) AddRecordLabel(val string) { c.effects.AddRecordLabel(val) } +func (c *RecordContext) AddRecordTag(val string) { + c.effects.AddRecordTag(val) +} + func (c *RecordContext) ReportRecord(reason, comment string) { c.effects.ReportRecord(reason, comment) } From 0ed95120d0188d19a83dce1e2c562e8aba9ab2c9 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 17:37:54 -0700 Subject: [PATCH 086/111] add gtube tags for testing --- automod/rules/gtube.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/automod/rules/gtube.go b/automod/rules/gtube.go index 4684541a4..71922a528 100644 --- a/automod/rules/gtube.go +++ b/automod/rules/gtube.go @@ -16,6 +16,7 @@ func GtubePostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { if strings.Contains(post.Text, gtubeString) { c.AddRecordLabel("spam") c.Notify("slack") + c.AddRecordTag("gtube-record") } return nil } @@ -26,6 +27,7 @@ func GtubeProfileRule(c *automod.RecordContext, profile *appbsky.ActorProfile) e if profile.Description != nil && strings.Contains(*profile.Description, gtubeString) { c.AddRecordLabel("spam") c.Notify("slack") + c.AddAccountTag("gtuber-account") } return nil } From ca4d0bab572569b85e196e1650d78e3a65b16f38 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 17:50:59 -0700 Subject: [PATCH 087/111] move automod rule helpers to package --- automod/{rules => helpers}/helpers.go | 10 +++++----- automod/{rules => helpers}/helpers_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename automod/{rules => helpers}/helpers.go (98%) rename automod/{rules => helpers}/helpers_test.go (99%) diff --git a/automod/rules/helpers.go b/automod/helpers/helpers.go similarity index 98% rename from automod/rules/helpers.go rename to automod/helpers/helpers.go index e5bdcb2dd..7aa615a30 100644 --- a/automod/rules/helpers.go +++ b/automod/helpers/helpers.go @@ -1,4 +1,4 @@ -package rules +package helpers import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/spaolacci/murmur3" ) -func dedupeStrings(in []string) []string { +func DedupeStrings(in []string) []string { var out []string seen := make(map[string]bool) for _, v := range in { @@ -37,7 +37,7 @@ func ExtractHashtagsPost(post *appbsky.FeedPost) []string { } } } - return dedupeStrings(tags) + return DedupeStrings(tags) } func NormalizeHashtag(raw string) string { @@ -103,7 +103,7 @@ func ExtractPostBlobCIDsPost(post *appbsky.FeedPost) []string { } } } - return dedupeStrings(out) + return DedupeStrings(out) } func ExtractBlobCIDsProfile(profile *appbsky.ActorProfile) []string { @@ -114,7 +114,7 @@ func ExtractBlobCIDsProfile(profile *appbsky.ActorProfile) []string { if profile.Banner != nil { out = append(out, profile.Banner.Ref.String()) } - return dedupeStrings(out) + return DedupeStrings(out) } func ExtractTextTokensPost(post *appbsky.FeedPost) []string { diff --git a/automod/rules/helpers_test.go b/automod/helpers/helpers_test.go similarity index 99% rename from automod/rules/helpers_test.go rename to automod/helpers/helpers_test.go index bba200cb0..9b04a041c 100644 --- a/automod/rules/helpers_test.go +++ b/automod/helpers/helpers_test.go @@ -1,4 +1,4 @@ -package rules +package helpers import ( comatproto "github.com/bluesky-social/indigo/api/atproto" From e1b57bf8a281f5c68293a23b533eae854f71de7a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 17:51:36 -0700 Subject: [PATCH 088/111] refactor rules to use new helpers package --- automod/rules/harassment.go | 11 ++++++----- automod/rules/hashtags.go | 7 ++++--- automod/rules/identity.go | 3 ++- automod/rules/keyword.go | 9 +++++---- automod/rules/mentions.go | 3 ++- automod/rules/misleading.go | 7 ++++--- automod/rules/misleading_test.go | 21 +++++++++++---------- automod/rules/nostr.go | 3 ++- automod/rules/promo.go | 9 +++++---- automod/rules/quick.go | 5 +++-- automod/rules/replies.go | 25 +++++++++++++------------ automod/rules/reposts.go | 3 ++- automod/visual/hiveai_rule.go | 4 ++-- 13 files changed, 61 insertions(+), 49 deletions(-) diff --git a/automod/rules/harassment.go b/automod/rules/harassment.go index 5212b69e1..2cf7ce194 100644 --- a/automod/rules/harassment.go +++ b/automod/rules/harassment.go @@ -8,18 +8,19 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) var _ automod.PostRuleFunc = HarassmentTargetInteractionPostRule // looks for new accounts, which interact with frequently-harassed accounts, and report them for review func HarassmentTargetInteractionPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 24*time.Hour) { return nil } var interactionDIDs []string - facets, err := ExtractFacets(post) + facets, err := helpers.ExtractFacets(post) if err != nil { return err } @@ -28,7 +29,7 @@ func HarassmentTargetInteractionPostRule(c *automod.RecordContext, post *appbsky interactionDIDs = append(interactionDIDs, *pf.DID) } } - if post.Reply != nil && !IsSelfThread(c, post) { + if post.Reply != nil && !helpers.IsSelfThread(c, post) { parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri) if err != nil { return err @@ -57,7 +58,7 @@ func HarassmentTargetInteractionPostRule(c *automod.RecordContext, post *appbsky return nil } - interactionDIDs = dedupeStrings(interactionDIDs) + interactionDIDs = helpers.DedupeStrings(interactionDIDs) for _, d := range interactionDIDs { did, err := syntax.ParseDID(d) if err != nil { @@ -114,7 +115,7 @@ var _ automod.PostRuleFunc = HarassmentTrivialPostRule // looks for new accounts, which frequently post the same type of content func HarassmentTrivialPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { return nil } diff --git a/automod/rules/hashtags.go b/automod/rules/hashtags.go index c6d734807..682ce746a 100644 --- a/automod/rules/hashtags.go +++ b/automod/rules/hashtags.go @@ -5,13 +5,14 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/helpers" "github.com/bluesky-social/indigo/automod/keyword" ) // looks for specific hashtags from known lists func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - for _, tag := range ExtractHashtagsPost(post) { - tag = NormalizeHashtag(tag) + for _, tag := range helpers.ExtractHashtagsPost(post) { + tag = helpers.NormalizeHashtag(tag) // skip some bad-word hashtags which frequently false-positive if tag == "nazi" || tag == "hitler" { continue @@ -35,7 +36,7 @@ var _ automod.PostRuleFunc = BadHashtagsPostRule // if a post is "almost all" hashtags, it might be a form of search spam func TooManyHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - tags := ExtractHashtagsPost(post) + tags := helpers.ExtractHashtagsPost(post) tagChars := 0 for _, tag := range tags { tagChars += len(tag) diff --git a/automod/rules/identity.go b/automod/rules/identity.go index 365d63f95..e74991233 100644 --- a/automod/rules/identity.go +++ b/automod/rules/identity.go @@ -7,11 +7,12 @@ import ( "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) // triggers on first identity event for an account (DID) func NewAccountRule(c *automod.AccountContext) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(c, 4*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(c, 4*time.Hour) { return nil } diff --git a/automod/rules/keyword.go b/automod/rules/keyword.go index abb202600..8d5caa395 100644 --- a/automod/rules/keyword.go +++ b/automod/rules/keyword.go @@ -7,6 +7,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/helpers" "github.com/bluesky-social/indigo/automod/keyword" ) @@ -17,7 +18,7 @@ func BadWordPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { isJapanese = true } } - for _, tok := range ExtractTextTokensPost(post) { + for _, tok := range helpers.ExtractTextTokensPost(post) { word := keyword.SlugIsExplicitSlur(tok) // used very frequently in a reclaimed context if word != "" && word != "faggot" && word != "tranny" && word != "coon" && !(word == "kike" && isJapanese) { @@ -54,7 +55,7 @@ func BadWordProfileRule(c *automod.RecordContext, profile *appbsky.ActorProfile) //c.Notify("slack") } } - for _, tok := range ExtractTextTokensProfile(profile) { + for _, tok := range helpers.ExtractTextTokensProfile(profile) { // de-pluralize tok = strings.TrimSuffix(tok, "s") if c.InSet("worst-words", tok) { @@ -71,8 +72,8 @@ var _ automod.ProfileRuleFunc = BadWordProfileRule // looks for the specific harassment situation of a replay to another user with only a single word func ReplySingleBadWordPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if post.Reply != nil && !IsSelfThread(c, post) { - tokens := ExtractTextTokensPost(post) + if post.Reply != nil && !helpers.IsSelfThread(c, post) { + tokens := helpers.ExtractTextTokensPost(post) if len(tokens) != 1 { return nil } diff --git a/automod/rules/mentions.go b/automod/rules/mentions.go index 8155b4a4a..98d419d09 100644 --- a/automod/rules/mentions.go +++ b/automod/rules/mentions.go @@ -8,6 +8,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) var _ automod.PostRuleFunc = DistinctMentionsRule @@ -47,7 +48,7 @@ var youngMentionAccountLimit = 12 var _ automod.PostRuleFunc = YoungAccountDistinctMentionsRule func YoungAccountDistinctMentionsRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 14*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 14*24*time.Hour) { return nil } diff --git a/automod/rules/misleading.go b/automod/rules/misleading.go index df4525cfc..31822ccce 100644 --- a/automod/rules/misleading.go +++ b/automod/rules/misleading.go @@ -9,9 +9,10 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/helpers" ) -func isMisleadingURLFacet(facet PostFacet, logger *slog.Logger) bool { +func isMisleadingURLFacet(facet helpers.PostFacet, logger *slog.Logger) bool { linkURL, err := url.Parse(*facet.URL) if err != nil { logger.Warn("invalid link metadata URL", "url", facet.URL) @@ -84,7 +85,7 @@ func MisleadingURLPostRule(c *automod.RecordContext, post *appbsky.FeedPost) err if c.Account.Identity.Handle == "nowbreezing.ntw.app" { return nil } - facets, err := ExtractFacets(post) + facets, err := helpers.ExtractFacets(post) if err != nil { c.Logger.Warn("invalid facets", "err", err) // TODO: or some other "this record is corrupt" indicator? @@ -105,7 +106,7 @@ func MisleadingURLPostRule(c *automod.RecordContext, post *appbsky.FeedPost) err var _ automod.PostRuleFunc = MisleadingMentionPostRule func MisleadingMentionPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - facets, err := ExtractFacets(post) + facets, err := helpers.ExtractFacets(post) if err != nil { c.Logger.Warn("invalid facets", "err", err) // TODO: or some other "this record is corrupt" indicator? diff --git a/automod/rules/misleading_test.go b/automod/rules/misleading_test.go index cf8e814af..2e47883a6 100644 --- a/automod/rules/misleading_test.go +++ b/automod/rules/misleading_test.go @@ -11,6 +11,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/engine" + "github.com/bluesky-social/indigo/automod/helpers" "github.com/stretchr/testify/assert" ) @@ -118,67 +119,67 @@ func TestIsMisleadingURL(t *testing.T) { logger := slog.Default() fixtures := []struct { - facet PostFacet + facet helpers.PostFacet out bool }{ { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "https://atproto.com", URL: pstr("https://atproto.com"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "https://atproto.com", URL: pstr("https://evil.com"), }, out: true, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "https://www.atproto.com", URL: pstr("https://atproto.com"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "https://atproto.com", URL: pstr("https://www.atproto.com"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "[example.com]", URL: pstr("https://www.example.com"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "example.com...", URL: pstr("https://example.com.evil.com"), }, out: true, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "ATPROTO.com...", URL: pstr("https://atproto.com"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "1234.5678", URL: pstr("https://arxiv.org/abs/1234.5678"), }, out: false, }, { - facet: PostFacet{ + facet: helpers.PostFacet{ Text: "www.techdirt.com…", URL: pstr("https://www.techdirt.com/"), }, diff --git a/automod/rules/nostr.go b/automod/rules/nostr.go index 0291d0668..5f91e7ee6 100644 --- a/automod/rules/nostr.go +++ b/automod/rules/nostr.go @@ -7,13 +7,14 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/helpers" ) var _ automod.PostRuleFunc = NostrSpamPostRule // looks for new accounts, which frequently post the same type of content func NostrSpamPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { return nil } diff --git a/automod/rules/promo.go b/automod/rules/promo.go index 0dad7aaf4..f6fe23a24 100644 --- a/automod/rules/promo.go +++ b/automod/rules/promo.go @@ -9,6 +9,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) var _ automod.PostRuleFunc = AggressivePromotionRule @@ -17,16 +18,16 @@ var _ automod.PostRuleFunc = AggressivePromotionRule // // this rule depends on ReplyCountPostRule() to set counts func AggressivePromotionRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { return nil } - if post.Reply == nil || IsSelfThread(c, post) { + if post.Reply == nil || helpers.IsSelfThread(c, post) { return nil } - allURLs := ExtractTextURLs(post.Text) + allURLs := helpers.ExtractTextURLs(post.Text) if c.Account.Profile.Description != nil { - profileURLs := ExtractTextURLs(*c.Account.Profile.Description) + profileURLs := helpers.ExtractTextURLs(*c.Account.Profile.Description) allURLs = append(allURLs, profileURLs...) } hasPromo := false diff --git a/automod/rules/quick.go b/automod/rules/quick.go index 77075d94a..ea6a69e36 100644 --- a/automod/rules/quick.go +++ b/automod/rules/quick.go @@ -7,6 +7,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/helpers" ) var botLinkStrings = []string{"ainna13762491", "LINK押して", "→ https://tiny", "⇒ http://tiny"} @@ -54,7 +55,7 @@ func SimpleBotPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { var _ automod.IdentityRuleFunc = NewAccountBotEmailRule func NewAccountBotEmailRule(c *automod.AccountContext) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(c, 1*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(c, 1*time.Hour) { return nil } @@ -73,7 +74,7 @@ var _ automod.PostRuleFunc = TrivialSpamPostRule // looks for new accounts, which frequently post the same type of content func TrivialSpamPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 8*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 8*24*time.Hour) { return nil } diff --git a/automod/rules/replies.go b/automod/rules/replies.go index aed986737..e03e9de53 100644 --- a/automod/rules/replies.go +++ b/automod/rules/replies.go @@ -9,13 +9,14 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) var _ automod.PostRuleFunc = ReplyCountPostRule // does not count "self-replies" (direct to self, or in own post thread) func ReplyCountPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if post.Reply == nil || IsSelfThread(c, post) { + if post.Reply == nil || helpers.IsSelfThread(c, post) { return nil } @@ -47,7 +48,7 @@ var _ automod.PostRuleFunc = IdenticalReplyPostRule // // There can be legitimate situations that trigger this rule, so in most situations should be a "report" not "label" action. func IdenticalReplyPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if post.Reply == nil || IsSelfThread(c, post) { + if post.Reply == nil || helpers.IsSelfThread(c, post) { return nil } @@ -55,18 +56,18 @@ func IdenticalReplyPostRule(c *automod.RecordContext, post *appbsky.FeedPost) er if utf8.RuneCountInString(post.Text) <= 10 { return nil } - if AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { + if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { return nil } // don't count if there is a follow-back relationship - if ParentOrRootIsFollower(c, post) { + if helpers.ParentOrRootIsFollower(c, post) { return nil } // increment before read. use a specific period (IncrementPeriod()) to reduce the number of counters (one per unique post text) period := countstore.PeriodDay - bucket := c.Account.Identity.DID.String() + "/" + HashOfString(post.Text) + bucket := c.Account.Identity.DID.String() + "/" + helpers.HashOfString(post.Text) c.IncrementPeriod("reply-text", bucket, period) count := c.GetCount("reply-text", bucket, period) @@ -91,21 +92,21 @@ var identicalReplySameParentMaxPosts int64 = 50 var _ automod.PostRuleFunc = IdenticalReplyPostSameParentRule func IdenticalReplyPostSameParentRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - if post.Reply == nil || IsSelfThread(c, post) { + if post.Reply == nil || helpers.IsSelfThread(c, post) { return nil } - if ParentOrRootIsFollower(c, post) { + if helpers.ParentOrRootIsFollower(c, post) { return nil } postCount := c.Account.PostsCount - if AccountIsOlderThan(&c.AccountContext, identicalReplySameParentMaxAge) || postCount >= identicalReplySameParentMaxPosts { + if helpers.AccountIsOlderThan(&c.AccountContext, identicalReplySameParentMaxAge) || postCount >= identicalReplySameParentMaxPosts { return nil } period := countstore.PeriodHour - bucket := c.Account.Identity.DID.String() + "/" + post.Reply.Parent.Uri + "/" + HashOfString(post.Text) + bucket := c.Account.Identity.DID.String() + "/" + post.Reply.Parent.Uri + "/" + helpers.HashOfString(post.Text) c.IncrementPeriod("reply-text-same-post", bucket, period) count := c.GetCount("reply-text-same-post", bucket, period) @@ -126,7 +127,7 @@ var _ automod.PostRuleFunc = YoungAccountDistinctRepliesRule func YoungAccountDistinctRepliesRule(c *automod.RecordContext, post *appbsky.FeedPost) error { // only replies, and skip self-replies (eg, threads) - if post.Reply == nil || IsSelfThread(c, post) { + if post.Reply == nil || helpers.IsSelfThread(c, post) { return nil } @@ -134,12 +135,12 @@ func YoungAccountDistinctRepliesRule(c *automod.RecordContext, post *appbsky.Fee if utf8.RuneCountInString(post.Text) <= 10 { return nil } - if AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { + if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { return nil } // don't count if there is a follow-back relationship - if ParentOrRootIsFollower(c, post) { + if helpers.ParentOrRootIsFollower(c, post) { return nil } diff --git a/automod/rules/reposts.go b/automod/rules/reposts.go index 75b248461..573146558 100644 --- a/automod/rules/reposts.go +++ b/automod/rules/reposts.go @@ -7,6 +7,7 @@ import ( "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/countstore" + "github.com/bluesky-social/indigo/automod/helpers" ) var dailyRepostThresholdWithoutPost = 30 @@ -18,7 +19,7 @@ var _ automod.RecordRuleFunc = TooManyRepostRule // looks for accounts which do frequent reposts func TooManyRepostRule(c *automod.RecordContext) error { // Don't bother checking reposts from accounts older than 30 days - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 30*24*time.Hour) { + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 30*24*time.Hour) { return nil } diff --git a/automod/visual/hiveai_rule.go b/automod/visual/hiveai_rule.go index 32bcf6a9a..850ee83b1 100644 --- a/automod/visual/hiveai_rule.go +++ b/automod/visual/hiveai_rule.go @@ -5,7 +5,7 @@ import ( "time" "github.com/bluesky-social/indigo/automod" - "github.com/bluesky-social/indigo/automod/rules" + "github.com/bluesky-social/indigo/automod/helpers" lexutil "github.com/bluesky-social/indigo/lex/util" ) @@ -43,7 +43,7 @@ func (hal *HiveAIClient) HiveLabelBlobRule(c *automod.RecordContext, blob lexuti for _, l := range labels { // NOTE: experimenting with profile reporting for new accounts - if l == "sexual" && c.RecordOp.Collection.String() == "app.bsky.actor.profile" && rules.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { + if l == "sexual" && c.RecordOp.Collection.String() == "app.bsky.actor.profile" && helpers.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { c.ReportRecord(automod.ReportReasonSexual, "possible sexual profile (not labeled yet)") c.Logger.Info("skipping record label", "label", l, "reason", "sexual-profile-experiment") } else { From 867d96f71bfcad53b76b4183a0a86bd44362a36f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 29 Oct 2024 18:00:07 -0700 Subject: [PATCH 089/111] refactor helpers in to separate files --- automod/helpers/account.go | 49 ++++++++ automod/helpers/account_test.go | 61 ++++++++++ automod/helpers/{helpers.go => bsky.go} | 73 ------------ .../helpers/{helpers_test.go => bsky_test.go} | 110 ------------------ automod/helpers/text.go | 35 ++++++ automod/helpers/text_test.go | 64 ++++++++++ 6 files changed, 209 insertions(+), 183 deletions(-) create mode 100644 automod/helpers/account.go create mode 100644 automod/helpers/account_test.go rename automod/helpers/{helpers.go => bsky.go} (71%) rename automod/helpers/{helpers_test.go => bsky_test.go} (53%) create mode 100644 automod/helpers/text.go create mode 100644 automod/helpers/text_test.go diff --git a/automod/helpers/account.go b/automod/helpers/account.go new file mode 100644 index 000000000..2a1a275cb --- /dev/null +++ b/automod/helpers/account.go @@ -0,0 +1,49 @@ +package helpers + +import ( + "time" + + "github.com/bluesky-social/indigo/automod" +) + +// no accounts exist before this time +var atprotoAccountEpoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + +// returns true if account creation timestamp is plausible: not-nil, not in distant past, not in the future +func plausibleAccountCreation(when *time.Time) bool { + if when == nil { + return false + } + // this is mostly to check for misconfigurations or null values (eg, UNIX epoch zero means "unknown" not actually 1970) + if !when.After(atprotoAccountEpoch) { + return false + } + // a timestamp in the future would also indicate some misconfiguration + if when.After(time.Now().Add(time.Hour)) { + return false + } + return true +} + +// checks if account was created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' +func AccountIsYoungerThan(c *automod.AccountContext, age time.Duration) bool { + // TODO: consider swapping priority order here (and below) + if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { + return time.Since(*c.Account.CreatedAt) < age + } + if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { + return time.Since(*c.Account.Private.IndexedAt) < age + } + return false +} + +// checks if account was *not* created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' +func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { + if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { + return time.Since(*c.Account.CreatedAt) >= age + } + if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { + return time.Since(*c.Account.Private.IndexedAt) >= age + } + return false +} diff --git a/automod/helpers/account_test.go b/automod/helpers/account_test.go new file mode 100644 index 000000000..c949eb77c --- /dev/null +++ b/automod/helpers/account_test.go @@ -0,0 +1,61 @@ +package helpers + +import ( + "testing" + "time" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod" + "github.com/stretchr/testify/assert" +) + +func TestAccountIsYoungerThan(t *testing.T) { + assert := assert.New(t) + + am := automod.AccountMeta{ + Identity: &identity.Identity{ + DID: syntax.DID("did:plc:abc111"), + Handle: syntax.Handle("handle.example.com"), + }, + Profile: automod.ProfileSummary{}, + Private: nil, + } + now := time.Now() + ac := automod.AccountContext{ + Account: am, + } + assert.False(AccountIsYoungerThan(&ac, time.Hour)) + assert.False(AccountIsOlderThan(&ac, time.Hour)) + + ac.Account.CreatedAt = &now + assert.True(AccountIsYoungerThan(&ac, time.Hour)) + assert.False(AccountIsOlderThan(&ac, time.Hour)) + + yesterday := time.Now().Add(-1 * time.Hour * 24) + ac.Account.CreatedAt = &yesterday + assert.False(AccountIsYoungerThan(&ac, time.Hour)) + assert.True(AccountIsOlderThan(&ac, time.Hour)) + + old := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) + ac.Account.CreatedAt = &old + assert.False(AccountIsYoungerThan(&ac, time.Hour)) + assert.False(AccountIsYoungerThan(&ac, time.Hour*24*365*100)) + assert.False(AccountIsOlderThan(&ac, time.Hour)) + assert.False(AccountIsOlderThan(&ac, time.Hour*24*365*100)) + + future := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC) + ac.Account.CreatedAt = &future + assert.False(AccountIsYoungerThan(&ac, time.Hour)) + assert.False(AccountIsOlderThan(&ac, time.Hour)) + + ac.Account.CreatedAt = nil + ac.Account.Private = &automod.AccountPrivate{ + Email: "account@example.com", + IndexedAt: &yesterday, + } + assert.True(AccountIsYoungerThan(&ac, 48*time.Hour)) + assert.False(AccountIsYoungerThan(&ac, time.Hour)) + assert.True(AccountIsOlderThan(&ac, time.Hour)) + assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) +} diff --git a/automod/helpers/helpers.go b/automod/helpers/bsky.go similarity index 71% rename from automod/helpers/helpers.go rename to automod/helpers/bsky.go index 7aa615a30..c7416f2dd 100644 --- a/automod/helpers/helpers.go +++ b/automod/helpers/bsky.go @@ -2,29 +2,13 @@ package helpers import ( "fmt" - "regexp" - "time" appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/automod" "github.com/bluesky-social/indigo/automod/keyword" - - "github.com/spaolacci/murmur3" ) -func DedupeStrings(in []string) []string { - var out []string - seen := make(map[string]bool) - for _, v := range in { - if !seen[v] { - out = append(out, v) - seen[v] = true - } - } - return out -} - func ExtractHashtagsPost(post *appbsky.FeedPost) []string { var tags []string for _, tag := range post.Tags { @@ -152,13 +136,6 @@ func ExtractTextTokensProfile(profile *appbsky.ActorProfile) []string { return keyword.TokenizeText(s) } -// based on: https://stackoverflow.com/a/48769624, with no trailing period allowed -var urlRegex = regexp.MustCompile(`(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-&?=%.]*[\w/\-&?=%]+`) - -func ExtractTextURLs(raw string) []string { - return urlRegex.FindAllString(raw, -1) -} - func ExtractTextURLsProfile(profile *appbsky.ActorProfile) []string { s := "" if profile.Description != nil { @@ -191,14 +168,6 @@ func IsSelfThread(c *automod.RecordContext, post *appbsky.FeedPost) bool { return false } -// returns a fast, compact hash of a string -// -// current implementation uses murmur3, default seed, and hex encoding -func HashOfString(s string) string { - val := murmur3.Sum64([]byte(s)) - return fmt.Sprintf("%016x", val) -} - func ParentOrRootIsFollower(c *automod.RecordContext, post *appbsky.FeedPost) bool { if post.Reply == nil || IsSelfThread(c, post) { return false @@ -242,48 +211,6 @@ func ParentOrRootIsFollower(c *automod.RecordContext, post *appbsky.FeedPost) bo return false } -// no accounts exist before this time -var atprotoAccountEpoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - -// returns true if account creation timestamp is plausible: not-nil, not in distant past, not in the future -func plausibleAccountCreation(when *time.Time) bool { - if when == nil { - return false - } - // this is mostly to check for misconfigurations or null values (eg, UNIX epoch zero means "unknown" not actually 1970) - if !when.After(atprotoAccountEpoch) { - return false - } - // a timestamp in the future would also indicate some misconfiguration - if when.After(time.Now().Add(time.Hour)) { - return false - } - return true -} - -// checks if account was created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' -func AccountIsYoungerThan(c *automod.AccountContext, age time.Duration) bool { - // TODO: consider swapping priority order here (and below) - if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { - return time.Since(*c.Account.CreatedAt) < age - } - if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { - return time.Since(*c.Account.Private.IndexedAt) < age - } - return false -} - -// checks if account was *not* created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' -func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { - if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { - return time.Since(*c.Account.CreatedAt) >= age - } - if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { - return time.Since(*c.Account.Private.IndexedAt) >= age - } - return false -} - func PostParentOrRootIsDid(post *appbsky.FeedPost, did string) bool { if post.Reply == nil { return false diff --git a/automod/helpers/helpers_test.go b/automod/helpers/bsky_test.go similarity index 53% rename from automod/helpers/helpers_test.go rename to automod/helpers/bsky_test.go index 9b04a041c..b5d6cb242 100644 --- a/automod/helpers/helpers_test.go +++ b/automod/helpers/bsky_test.go @@ -4,120 +4,10 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" appbsky "github.com/bluesky-social/indigo/api/bsky" "testing" - "time" - "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/automod" - "github.com/bluesky-social/indigo/automod/keyword" "github.com/stretchr/testify/assert" ) -func TestTokenizeText(t *testing.T) { - assert := assert.New(t) - - fixtures := []struct { - s string - out []string - }{ - { - s: "1 'Two' three!", - out: []string{"1", "two", "three"}, - }, - { - s: " foo1;bar2,baz3...", - out: []string{"foo1", "bar2", "baz3"}, - }, - { - s: "https://example.com/index.html", - out: []string{"https", "example", "com", "index", "html"}, - }, - } - - for _, fix := range fixtures { - assert.Equal(fix.out, keyword.TokenizeText(fix.s)) - } -} - -func TestExtractURL(t *testing.T) { - assert := assert.New(t) - - fixtures := []struct { - s string - out []string - }{ - { - s: "this is a description with example.com mentioned in the middle", - out: []string{"example.com"}, - }, - { - s: "this is another example with https://en.wikipedia.org/index.html: and archive.org, and https://eff.org/... and bsky.app.", - out: []string{"https://en.wikipedia.org/index.html", "archive.org", "https://eff.org/", "bsky.app"}, - }, - } - - for _, fix := range fixtures { - assert.Equal(fix.out, ExtractTextURLs(fix.s)) - } -} - -func TestHashOfString(t *testing.T) { - assert := assert.New(t) - - // hashing function should be consistent over time - assert.Equal("4e6f69c0e3d10992", HashOfString("dummy-value")) -} - -func TestAccountIsYoungerThan(t *testing.T) { - assert := assert.New(t) - - am := automod.AccountMeta{ - Identity: &identity.Identity{ - DID: syntax.DID("did:plc:abc111"), - Handle: syntax.Handle("handle.example.com"), - }, - Profile: automod.ProfileSummary{}, - Private: nil, - } - now := time.Now() - ac := automod.AccountContext{ - Account: am, - } - assert.False(AccountIsYoungerThan(&ac, time.Hour)) - assert.False(AccountIsOlderThan(&ac, time.Hour)) - - ac.Account.CreatedAt = &now - assert.True(AccountIsYoungerThan(&ac, time.Hour)) - assert.False(AccountIsOlderThan(&ac, time.Hour)) - - yesterday := time.Now().Add(-1 * time.Hour * 24) - ac.Account.CreatedAt = &yesterday - assert.False(AccountIsYoungerThan(&ac, time.Hour)) - assert.True(AccountIsOlderThan(&ac, time.Hour)) - - old := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) - ac.Account.CreatedAt = &old - assert.False(AccountIsYoungerThan(&ac, time.Hour)) - assert.False(AccountIsYoungerThan(&ac, time.Hour*24*365*100)) - assert.False(AccountIsOlderThan(&ac, time.Hour)) - assert.False(AccountIsOlderThan(&ac, time.Hour*24*365*100)) - - future := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC) - ac.Account.CreatedAt = &future - assert.False(AccountIsYoungerThan(&ac, time.Hour)) - assert.False(AccountIsOlderThan(&ac, time.Hour)) - - ac.Account.CreatedAt = nil - ac.Account.Private = &automod.AccountPrivate{ - Email: "account@example.com", - IndexedAt: &yesterday, - } - assert.True(AccountIsYoungerThan(&ac, 48*time.Hour)) - assert.False(AccountIsYoungerThan(&ac, time.Hour)) - assert.True(AccountIsOlderThan(&ac, time.Hour)) - assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) -} - func TestParentOrRootIsDid(t *testing.T) { assert := assert.New(t) diff --git a/automod/helpers/text.go b/automod/helpers/text.go new file mode 100644 index 000000000..412eb9c8c --- /dev/null +++ b/automod/helpers/text.go @@ -0,0 +1,35 @@ +package helpers + +import ( + "fmt" + "regexp" + + "github.com/spaolacci/murmur3" +) + +func DedupeStrings(in []string) []string { + var out []string + seen := make(map[string]bool) + for _, v := range in { + if !seen[v] { + out = append(out, v) + seen[v] = true + } + } + return out +} + +// returns a fast, compact hash of a string +// +// current implementation uses murmur3, default seed, and hex encoding +func HashOfString(s string) string { + val := murmur3.Sum64([]byte(s)) + return fmt.Sprintf("%016x", val) +} + +// based on: https://stackoverflow.com/a/48769624, with no trailing period allowed +var urlRegex = regexp.MustCompile(`(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-&?=%.]*[\w/\-&?=%]+`) + +func ExtractTextURLs(raw string) []string { + return urlRegex.FindAllString(raw, -1) +} diff --git a/automod/helpers/text_test.go b/automod/helpers/text_test.go new file mode 100644 index 000000000..ef219155e --- /dev/null +++ b/automod/helpers/text_test.go @@ -0,0 +1,64 @@ +package helpers + +import ( + "testing" + + "github.com/bluesky-social/indigo/automod/keyword" + + "github.com/stretchr/testify/assert" +) + +func TestTokenizeText(t *testing.T) { + assert := assert.New(t) + + fixtures := []struct { + s string + out []string + }{ + { + s: "1 'Two' three!", + out: []string{"1", "two", "three"}, + }, + { + s: " foo1;bar2,baz3...", + out: []string{"foo1", "bar2", "baz3"}, + }, + { + s: "https://example.com/index.html", + out: []string{"https", "example", "com", "index", "html"}, + }, + } + + for _, fix := range fixtures { + assert.Equal(fix.out, keyword.TokenizeText(fix.s)) + } +} + +func TestExtractURL(t *testing.T) { + assert := assert.New(t) + + fixtures := []struct { + s string + out []string + }{ + { + s: "this is a description with example.com mentioned in the middle", + out: []string{"example.com"}, + }, + { + s: "this is another example with https://en.wikipedia.org/index.html: and archive.org, and https://eff.org/... and bsky.app.", + out: []string{"https://en.wikipedia.org/index.html", "archive.org", "https://eff.org/", "bsky.app"}, + }, + } + + for _, fix := range fixtures { + assert.Equal(fix.out, ExtractTextURLs(fix.s)) + } +} + +func TestHashOfString(t *testing.T) { + assert := assert.New(t) + + // hashing function should be consistent over time + assert.Equal("4e6f69c0e3d10992", HashOfString("dummy-value")) +} From 976ce81842082ebbbb7aed67458fcf99ceb8df79 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 30 Oct 2024 15:54:22 -0700 Subject: [PATCH 090/111] ozone lexicon updates --- api/ozone/moderationqueryEvents.go | 8 ++++++-- api/ozone/moderationqueryStatuses.go | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/api/ozone/moderationqueryEvents.go b/api/ozone/moderationqueryEvents.go index fee7adc39..1be68412f 100644 --- a/api/ozone/moderationqueryEvents.go +++ b/api/ozone/moderationqueryEvents.go @@ -20,21 +20,24 @@ type ModerationQueryEvents_Output struct { // // addedLabels: If specified, only events where all of these labels were added are returned // addedTags: If specified, only events where all of these tags were added are returned +// collections: If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored. // comment: If specified, only events with comments containing the keyword are returned // createdAfter: Retrieve events created after a given timestamp // createdBefore: Retrieve events created before a given timestamp // hasComment: If true, only events with comments are returned -// includeAllUserRecords: If true, events on all record types (posts, lists, profile etc.) owned by the did are returned +// includeAllUserRecords: If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned. // removedLabels: If specified, only events where all of these labels were removed are returned // removedTags: If specified, only events where all of these tags were removed are returned // sortDirection: Sort direction for the events. Defaults to descending order of created at timestamp. +// subjectType: If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. // types: The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent) to filter by. If not specified, all events are returned. -func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []string, addedTags []string, comment string, createdAfter string, createdBefore string, createdBy string, cursor string, hasComment bool, includeAllUserRecords bool, limit int64, removedLabels []string, removedTags []string, reportTypes []string, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) { +func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []string, addedTags []string, collections []string, comment string, createdAfter string, createdBefore string, createdBy string, cursor string, hasComment bool, includeAllUserRecords bool, limit int64, removedLabels []string, removedTags []string, reportTypes []string, sortDirection string, subject string, subjectType string, types []string) (*ModerationQueryEvents_Output, error) { var out ModerationQueryEvents_Output params := map[string]interface{}{ "addedLabels": addedLabels, "addedTags": addedTags, + "collections": collections, "comment": comment, "createdAfter": createdAfter, "createdBefore": createdBefore, @@ -48,6 +51,7 @@ func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []st "reportTypes": reportTypes, "sortDirection": sortDirection, "subject": subject, + "subjectType": subjectType, "types": types, } if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.moderation.queryEvents", params, nil, &out); err != nil { diff --git a/api/ozone/moderationqueryStatuses.go b/api/ozone/moderationqueryStatuses.go index 68dac122b..969d77e9d 100644 --- a/api/ozone/moderationqueryStatuses.go +++ b/api/ozone/moderationqueryStatuses.go @@ -19,8 +19,9 @@ type ModerationQueryStatuses_Output struct { // ModerationQueryStatuses calls the XRPC method "tools.ozone.moderation.queryStatuses". // // appealed: Get subjects in unresolved appealed status +// collections: If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored. // comment: Search subjects by keyword from comments -// includeAllUserRecords: All subjects belonging to the account specified in the 'subject' param will be returned. +// includeAllUserRecords: All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned. // includeMuted: By default, we don't include muted subjects in the results. Set this to true to include them. // lastReviewedBy: Get all subject statuses that were reviewed by a specific moderator // onlyMuted: When set to true, only muted subjects and reporters will be returned. @@ -30,12 +31,14 @@ type ModerationQueryStatuses_Output struct { // reviewedAfter: Search subjects reviewed after a given timestamp // reviewedBefore: Search subjects reviewed before a given timestamp // subject: The subject to get the status for. +// subjectType: If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. // takendown: Get subjects that were taken down -func ModerationQueryStatuses(ctx context.Context, c *xrpc.Client, appealed bool, comment string, cursor string, excludeTags []string, ignoreSubjects []string, includeAllUserRecords bool, includeMuted bool, lastReviewedBy string, limit int64, onlyMuted bool, reportedAfter string, reportedBefore string, reviewState string, reviewedAfter string, reviewedBefore string, sortDirection string, sortField string, subject string, tags []string, takendown bool) (*ModerationQueryStatuses_Output, error) { +func ModerationQueryStatuses(ctx context.Context, c *xrpc.Client, appealed bool, collections []string, comment string, cursor string, excludeTags []string, ignoreSubjects []string, includeAllUserRecords bool, includeMuted bool, lastReviewedBy string, limit int64, onlyMuted bool, reportedAfter string, reportedBefore string, reviewState string, reviewedAfter string, reviewedBefore string, sortDirection string, sortField string, subject string, subjectType string, tags []string, takendown bool) (*ModerationQueryStatuses_Output, error) { var out ModerationQueryStatuses_Output params := map[string]interface{}{ "appealed": appealed, + "collections": collections, "comment": comment, "cursor": cursor, "excludeTags": excludeTags, @@ -53,6 +56,7 @@ func ModerationQueryStatuses(ctx context.Context, c *xrpc.Client, appealed bool, "sortDirection": sortDirection, "sortField": sortField, "subject": subject, + "subjectType": subjectType, "tags": tags, "takendown": takendown, } From 5116f9a3b1673b9a44ec1de1df155eff6c31670c Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 30 Oct 2024 15:54:37 -0700 Subject: [PATCH 091/111] ozone sets API --- api/ozone/setaddValues.go | 28 +++++++++++++++++++++++++++ api/ozone/setdefs.go | 20 +++++++++++++++++++ api/ozone/setdeleteSet.go | 31 ++++++++++++++++++++++++++++++ api/ozone/setdeleteValues.go | 28 +++++++++++++++++++++++++++ api/ozone/setgetValues.go | 34 +++++++++++++++++++++++++++++++++ api/ozone/setquerySets.go | 37 ++++++++++++++++++++++++++++++++++++ api/ozone/setupsertSet.go | 21 ++++++++++++++++++++ 7 files changed, 199 insertions(+) create mode 100644 api/ozone/setaddValues.go create mode 100644 api/ozone/setdefs.go create mode 100644 api/ozone/setdeleteSet.go create mode 100644 api/ozone/setdeleteValues.go create mode 100644 api/ozone/setgetValues.go create mode 100644 api/ozone/setquerySets.go create mode 100644 api/ozone/setupsertSet.go diff --git a/api/ozone/setaddValues.go b/api/ozone/setaddValues.go new file mode 100644 index 000000000..1835d5e92 --- /dev/null +++ b/api/ozone/setaddValues.go @@ -0,0 +1,28 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.addValues + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetAddValues_Input is the input argument to a tools.ozone.set.addValues call. +type SetAddValues_Input struct { + // name: Name of the set to add values to + Name string `json:"name" cborgen:"name"` + // values: Array of string values to add to the set + Values []string `json:"values" cborgen:"values"` +} + +// SetAddValues calls the XRPC method "tools.ozone.set.addValues". +func SetAddValues(ctx context.Context, c *xrpc.Client, input *SetAddValues_Input) error { + if err := c.Do(ctx, xrpc.Procedure, "application/json", "tools.ozone.set.addValues", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/api/ozone/setdefs.go b/api/ozone/setdefs.go new file mode 100644 index 000000000..2181b12fd --- /dev/null +++ b/api/ozone/setdefs.go @@ -0,0 +1,20 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.defs + +// SetDefs_Set is a "set" in the tools.ozone.set.defs schema. +type SetDefs_Set struct { + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + Name string `json:"name" cborgen:"name"` +} + +// SetDefs_SetView is a "setView" in the tools.ozone.set.defs schema. +type SetDefs_SetView struct { + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + Name string `json:"name" cborgen:"name"` + SetSize int64 `json:"setSize" cborgen:"setSize"` + UpdatedAt string `json:"updatedAt" cborgen:"updatedAt"` +} diff --git a/api/ozone/setdeleteSet.go b/api/ozone/setdeleteSet.go new file mode 100644 index 000000000..b5d192364 --- /dev/null +++ b/api/ozone/setdeleteSet.go @@ -0,0 +1,31 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.deleteSet + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetDeleteSet_Input is the input argument to a tools.ozone.set.deleteSet call. +type SetDeleteSet_Input struct { + // name: Name of the set to delete + Name string `json:"name" cborgen:"name"` +} + +// SetDeleteSet_Output is the output of a tools.ozone.set.deleteSet call. +type SetDeleteSet_Output struct { +} + +// SetDeleteSet calls the XRPC method "tools.ozone.set.deleteSet". +func SetDeleteSet(ctx context.Context, c *xrpc.Client, input *SetDeleteSet_Input) (*SetDeleteSet_Output, error) { + var out SetDeleteSet_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "tools.ozone.set.deleteSet", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/setdeleteValues.go b/api/ozone/setdeleteValues.go new file mode 100644 index 000000000..34b62898a --- /dev/null +++ b/api/ozone/setdeleteValues.go @@ -0,0 +1,28 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.deleteValues + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetDeleteValues_Input is the input argument to a tools.ozone.set.deleteValues call. +type SetDeleteValues_Input struct { + // name: Name of the set to delete values from + Name string `json:"name" cborgen:"name"` + // values: Array of string values to delete from the set + Values []string `json:"values" cborgen:"values"` +} + +// SetDeleteValues calls the XRPC method "tools.ozone.set.deleteValues". +func SetDeleteValues(ctx context.Context, c *xrpc.Client, input *SetDeleteValues_Input) error { + if err := c.Do(ctx, xrpc.Procedure, "application/json", "tools.ozone.set.deleteValues", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/api/ozone/setgetValues.go b/api/ozone/setgetValues.go new file mode 100644 index 000000000..50e77fdda --- /dev/null +++ b/api/ozone/setgetValues.go @@ -0,0 +1,34 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.getValues + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetGetValues_Output is the output of a tools.ozone.set.getValues call. +type SetGetValues_Output struct { + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Set *SetDefs_SetView `json:"set" cborgen:"set"` + Values []string `json:"values" cborgen:"values"` +} + +// SetGetValues calls the XRPC method "tools.ozone.set.getValues". +func SetGetValues(ctx context.Context, c *xrpc.Client, cursor string, limit int64, name string) (*SetGetValues_Output, error) { + var out SetGetValues_Output + + params := map[string]interface{}{ + "cursor": cursor, + "limit": limit, + "name": name, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.set.getValues", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/setquerySets.go b/api/ozone/setquerySets.go new file mode 100644 index 000000000..f2f31effb --- /dev/null +++ b/api/ozone/setquerySets.go @@ -0,0 +1,37 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.querySets + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetQuerySets_Output is the output of a tools.ozone.set.querySets call. +type SetQuerySets_Output struct { + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Sets []*SetDefs_SetView `json:"sets" cborgen:"sets"` +} + +// SetQuerySets calls the XRPC method "tools.ozone.set.querySets". +// +// sortDirection: Defaults to ascending order of name field. +func SetQuerySets(ctx context.Context, c *xrpc.Client, cursor string, limit int64, namePrefix string, sortBy string, sortDirection string) (*SetQuerySets_Output, error) { + var out SetQuerySets_Output + + params := map[string]interface{}{ + "cursor": cursor, + "limit": limit, + "namePrefix": namePrefix, + "sortBy": sortBy, + "sortDirection": sortDirection, + } + if err := c.Do(ctx, xrpc.Query, "", "tools.ozone.set.querySets", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/setupsertSet.go b/api/ozone/setupsertSet.go new file mode 100644 index 000000000..acc266c4c --- /dev/null +++ b/api/ozone/setupsertSet.go @@ -0,0 +1,21 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package ozone + +// schema: tools.ozone.set.upsertSet + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// SetUpsertSet calls the XRPC method "tools.ozone.set.upsertSet". +func SetUpsertSet(ctx context.Context, c *xrpc.Client, input *SetUpsertSet_Input) (*SetDefs_SetView, error) { + var out SetDefs_SetView + if err := c.Do(ctx, xrpc.Procedure, "application/json", "tools.ozone.set.upsertSet", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} From 672b10e976ea81dd95759711a99b3dffb656d85e Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 30 Oct 2024 15:56:42 -0700 Subject: [PATCH 092/111] hack: tools.ozone.set.upsertSet input is via ref --- api/ozone/setupsertSet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/ozone/setupsertSet.go b/api/ozone/setupsertSet.go index acc266c4c..9f6cc5376 100644 --- a/api/ozone/setupsertSet.go +++ b/api/ozone/setupsertSet.go @@ -11,7 +11,7 @@ import ( ) // SetUpsertSet calls the XRPC method "tools.ozone.set.upsertSet". -func SetUpsertSet(ctx context.Context, c *xrpc.Client, input *SetUpsertSet_Input) (*SetDefs_SetView, error) { +func SetUpsertSet(ctx context.Context, c *xrpc.Client, input *SetDefs_Set) (*SetDefs_SetView, error) { var out SetDefs_SetView if err := c.Do(ctx, xrpc.Procedure, "application/json", "tools.ozone.set.upsertSet", nil, input, &out); err != nil { return nil, err From 466e6ec79cefeef6b9fc5543b4515e86d6ee2921 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 30 Oct 2024 16:13:45 -0700 Subject: [PATCH 093/111] updates for QueryModerationEvents calls --- automod/consumer/ozone.go | 35 ++++++++-------- automod/engine/persisthelpers.go | 70 ++++++++++++++++---------------- cmd/beemo/notify_reports.go | 35 ++++++++-------- cmd/gosky/admin.go | 69 ++++++++++++++++--------------- 4 files changed, 108 insertions(+), 101 deletions(-) diff --git a/automod/consumer/ozone.go b/automod/consumer/ozone.go index 0692ac393..2211cf21a 100644 --- a/automod/consumer/ozone.go +++ b/automod/consumer/ozone.go @@ -55,26 +55,27 @@ func (oc *OzoneConsumer) Run(ctx context.Context) error { period := time.Second * 5 for { - //func ModerationQueryEvents(ctx context.Context, c *xrpc.Client, addedLabels []string, addedTags []string, comment string, createdAfter string, createdBefore string, createdBy string, cursor string, hasComment bool, includeAllUserRecords bool, limit int64, removedLabels []string, removedTags []string, reportTypes []string, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) { me, err := toolsozone.ModerationQueryEvents( ctx, oc.OzoneClient, - nil, // addedLabels: If specified, only events where all of these labels were added are returned - nil, // addedTags: If specified, only events where all of these tags were added are returned - "", // comment: If specified, only events with comments containing the keyword are returned - since.String(), // createdAfter: Retrieve events created after a given timestamp - "", // createdBefore: Retrieve events created before a given timestamp - "", // createdBy - "", // cursor - false, // hasComment: If true, only events with comments are returned - true, // includeAllUserRecords: If true, events on all record types (posts, lists, profile etc.) owned by the did are returned - limit, - nil, // removedLabels: If specified, only events where all of these labels were removed are returned - nil, // removedTags - nil, // reportTypes - "asc", // sortDirection: Sort direction for the events. Defaults to descending order of created at timestamp. - "", // subject - nil, // types: The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent) to filter by. If not specified, all events are returned. + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + since.String(), // createdAfter string + "", // createdBefore string + "", // createdBy string + "", // cursor string + false, // hasComment bool + true, // includeAllUserRecords bool + limit, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "asc", // sortDirection string + "", // subject string + "", // subjectType string + nil, // types []string ) if err != nil { oc.Logger.Warn("ozone query events failed; sleeping then will retrying", "err", err, "period", period.String()) diff --git a/automod/engine/persisthelpers.go b/automod/engine/persisthelpers.go index 491a86fa4..42ba8934e 100644 --- a/automod/engine/persisthelpers.go +++ b/automod/engine/persisthelpers.go @@ -155,26 +155,27 @@ func (eng *Engine) createReportIfFresh(ctx context.Context, xrpcc *xrpc.Client, // before creating a report, query to see if automod has already reported this account in the past week for the same reason // NOTE: this is running in an inner loop (if there are multiple reports), which is a bit inefficient, but seems acceptable - // ModerationQueryEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, inc ludeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) resp, err := toolsozone.ModerationQueryEvents( ctx, xrpcc, - nil, - nil, - "", - "", - "", - xrpcc.Auth.Did, - "", - false, - false, - 5, - nil, - nil, - nil, - "", - did.String(), - []string{"tools.ozone.moderation.defs#modEventReport"}, + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + "", // createdAfter string + "", // createdBefore string + xrpcc.Auth.Did, // createdBy string + "", // cursor string + false, // hasComment bool + false, // includeAllUserRecords bool + 5, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "", // sortDirection string + did.String(), // subject string + "", // subjectType string + []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string ) if err != nil { @@ -231,26 +232,27 @@ func (eng *Engine) createRecordReportIfFresh(ctx context.Context, xrpcc *xrpc.Cl // before creating a report, query to see if automod has already reported this account in the past week for the same reason // NOTE: this is running in an inner loop (if there are multiple reports), which is a bit inefficient, but seems acceptable - // ModerationQueryEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, inc ludeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) resp, err := toolsozone.ModerationQueryEvents( ctx, xrpcc, - nil, - nil, - "", - "", - "", - xrpcc.Auth.Did, - "", - false, - false, - 5, - nil, - nil, - nil, - "", - uri.String(), - []string{"tools.ozone.moderation.defs#modEventReport"}, + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + "", // createdAfter string + "", // createdBefore string + xrpcc.Auth.Did, // createdBy string + "", // cursor string + false, // hasComment bool + false, // includeAllUserRecords bool + 5, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "", // sortDirection string + uri.String(), // subject string + "", // subjectType string + []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string ) if err != nil { return false, err diff --git a/cmd/beemo/notify_reports.go b/cmd/beemo/notify_reports.go index 5404619bc..7593bd87c 100644 --- a/cmd/beemo/notify_reports.go +++ b/cmd/beemo/notify_reports.go @@ -68,27 +68,28 @@ func pollNewReports(cctx *cli.Context) error { xrpcc.Auth.RefreshJwt = refresh.RefreshJwt // query just new reports (regardless of resolution state) - // ModerationQueryEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, includeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) var limit int64 = 50 me, err := toolsozone.ModerationQueryEvents( cctx.Context, xrpcc, - nil, - nil, - "", - "", - "", - "", - "", - false, - true, - limit, - nil, - nil, - nil, - "", - "", - []string{"tools.ozone.moderation.defs#modEventReport"}, + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + "", // createdAfter string + "", // createdBefore string + "", // createdBy string + "", // cursor string + false, // hasComment bool + true, // includeAllUserRecords bool + limit, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "", // sortDirection string + "", // subject string + "", // subjectType string + []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string ) if err != nil { return err diff --git a/cmd/gosky/admin.go b/cmd/gosky/admin.go index 769577e5e..9467975da 100644 --- a/cmd/gosky/admin.go +++ b/cmd/gosky/admin.go @@ -389,26 +389,27 @@ var listReportsCmd = &cli.Command{ xrpcc.AdminToken = &adminKey // fetch recent moderation reports - // AdminQueryModerationEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, includeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) (*AdminQueryModerationEvents_Output, error) resp, err := toolsozone.ModerationQueryEvents( ctx, xrpcc, - nil, - nil, - "", - "", - "", - "", - "", - false, - false, - 100, - nil, - nil, - nil, - "", - "", - []string{"tools.ozone.moderation.defs#modEventReport"}, + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + "", // createdAfter string + "", // createdBefore string + "", // createdBy string + "", // cursor string + false, // hasComment bool + false, // includeAllUserRecords bool + 100, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "", // sortDirection string + "", // subject string + "", // subjectType string + []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string ) if err != nil { return err @@ -705,22 +706,24 @@ var queryModerationStatusesCmd = &cli.Command{ resp, err := toolsozone.ModerationQueryEvents( ctx, xrpcc, - nil, - nil, - "", - "", - "", - "", - "", - false, - false, - 100, - nil, - nil, - nil, - "", - "", - []string{"tools.ozone.moderation.defs#modEventReport"}, + nil, // addedLabels []string + nil, // addedTags []string + nil, // collections []string + "", // comment string + "", // createdAfter string + "", // createdBefore string + "", // createdBy string + "", // cursor string + false, // hasComment bool + false, // includeAllUserRecords bool + 100, // limit int64 + nil, // removedLabels []string + nil, // removedTags []string + nil, // reportTypes []string + "", // sortDirection string + "", // subject string + "", // subjectType string + []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string ) if err != nil { return err From bdb4c396b081319cf3c5bcc348b4749c7940316a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 30 Oct 2024 23:24:33 -0700 Subject: [PATCH 094/111] more lexgen --- api/atproto/admindefs.go | 29 ++++++++++++------- api/bsky/unspeccedgetConfig.go | 26 +++++++++++++++++ api/ozone/moderationdefs.go | 52 ++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 api/bsky/unspeccedgetConfig.go diff --git a/api/atproto/admindefs.go b/api/atproto/admindefs.go index f674117d6..c6e424236 100644 --- a/api/atproto/admindefs.go +++ b/api/atproto/admindefs.go @@ -10,17 +10,18 @@ import ( // AdminDefs_AccountView is a "accountView" in the com.atproto.admin.defs schema. type AdminDefs_AccountView struct { - DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` - Did string `json:"did" cborgen:"did"` - Email *string `json:"email,omitempty" cborgen:"email,omitempty"` - EmailConfirmedAt *string `json:"emailConfirmedAt,omitempty" cborgen:"emailConfirmedAt,omitempty"` - Handle string `json:"handle" cborgen:"handle"` - IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` - InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` - InvitedBy *ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` - Invites []*ServerDefs_InviteCode `json:"invites,omitempty" cborgen:"invites,omitempty"` - InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` - RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords,omitempty" cborgen:"relatedRecords,omitempty"` + DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` + Did string `json:"did" cborgen:"did"` + Email *string `json:"email,omitempty" cborgen:"email,omitempty"` + EmailConfirmedAt *string `json:"emailConfirmedAt,omitempty" cborgen:"emailConfirmedAt,omitempty"` + Handle string `json:"handle" cborgen:"handle"` + IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` + InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` + InvitedBy *ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` + Invites []*ServerDefs_InviteCode `json:"invites,omitempty" cborgen:"invites,omitempty"` + InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` + RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords,omitempty" cborgen:"relatedRecords,omitempty"` + ThreatSignatures []*AdminDefs_ThreatSignature `json:"threatSignatures,omitempty" cborgen:"threatSignatures,omitempty"` } // AdminDefs_RepoBlobRef is a "repoBlobRef" in the com.atproto.admin.defs schema. @@ -46,3 +47,9 @@ type AdminDefs_StatusAttr struct { Applied bool `json:"applied" cborgen:"applied"` Ref *string `json:"ref,omitempty" cborgen:"ref,omitempty"` } + +// AdminDefs_ThreatSignature is a "threatSignature" in the com.atproto.admin.defs schema. +type AdminDefs_ThreatSignature struct { + Property string `json:"property" cborgen:"property"` + Value string `json:"value" cborgen:"value"` +} diff --git a/api/bsky/unspeccedgetConfig.go b/api/bsky/unspeccedgetConfig.go new file mode 100644 index 000000000..7bc728341 --- /dev/null +++ b/api/bsky/unspeccedgetConfig.go @@ -0,0 +1,26 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package bsky + +// schema: app.bsky.unspecced.getConfig + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// UnspeccedGetConfig_Output is the output of a app.bsky.unspecced.getConfig call. +type UnspeccedGetConfig_Output struct { + CheckEmailConfirmed *bool `json:"checkEmailConfirmed,omitempty" cborgen:"checkEmailConfirmed,omitempty"` +} + +// UnspeccedGetConfig calls the XRPC method "app.bsky.unspecced.getConfig". +func UnspeccedGetConfig(ctx context.Context, c *xrpc.Client) (*UnspeccedGetConfig_Output, error) { + var out UnspeccedGetConfig_Output + if err := c.Do(ctx, xrpc.Query, "", "app.bsky.unspecced.getConfig", nil, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/api/ozone/moderationdefs.go b/api/ozone/moderationdefs.go index 038bcbe7a..f419676ec 100644 --- a/api/ozone/moderationdefs.go +++ b/api/ozone/moderationdefs.go @@ -687,37 +687,39 @@ type ModerationDefs_RecordViewNotFound struct { // // RECORDTYPE: ModerationDefs_RepoView type ModerationDefs_RepoView struct { - LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#repoView" cborgen:"$type,const=tools.ozone.moderation.defs#repoView"` - DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` - Did string `json:"did" cborgen:"did"` - Email *string `json:"email,omitempty" cborgen:"email,omitempty"` - Handle string `json:"handle" cborgen:"handle"` - IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` - InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` - InvitedBy *comatprototypes.ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` - InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` - Moderation *ModerationDefs_Moderation `json:"moderation" cborgen:"moderation"` - RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords" cborgen:"relatedRecords"` + LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#repoView" cborgen:"$type,const=tools.ozone.moderation.defs#repoView"` + DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` + Did string `json:"did" cborgen:"did"` + Email *string `json:"email,omitempty" cborgen:"email,omitempty"` + Handle string `json:"handle" cborgen:"handle"` + IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` + InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` + InvitedBy *comatprototypes.ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` + InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` + Moderation *ModerationDefs_Moderation `json:"moderation" cborgen:"moderation"` + RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords" cborgen:"relatedRecords"` + ThreatSignatures []*comatprototypes.AdminDefs_ThreatSignature `json:"threatSignatures,omitempty" cborgen:"threatSignatures,omitempty"` } // ModerationDefs_RepoViewDetail is a "repoViewDetail" in the tools.ozone.moderation.defs schema. // // RECORDTYPE: ModerationDefs_RepoViewDetail type ModerationDefs_RepoViewDetail struct { - LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#repoViewDetail" cborgen:"$type,const=tools.ozone.moderation.defs#repoViewDetail"` - DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` - Did string `json:"did" cborgen:"did"` - Email *string `json:"email,omitempty" cborgen:"email,omitempty"` - EmailConfirmedAt *string `json:"emailConfirmedAt,omitempty" cborgen:"emailConfirmedAt,omitempty"` - Handle string `json:"handle" cborgen:"handle"` - IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` - InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` - InvitedBy *comatprototypes.ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` - Invites []*comatprototypes.ServerDefs_InviteCode `json:"invites,omitempty" cborgen:"invites,omitempty"` - InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` - Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` - Moderation *ModerationDefs_ModerationDetail `json:"moderation" cborgen:"moderation"` - RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords" cborgen:"relatedRecords"` + LexiconTypeID string `json:"$type,const=tools.ozone.moderation.defs#repoViewDetail" cborgen:"$type,const=tools.ozone.moderation.defs#repoViewDetail"` + DeactivatedAt *string `json:"deactivatedAt,omitempty" cborgen:"deactivatedAt,omitempty"` + Did string `json:"did" cborgen:"did"` + Email *string `json:"email,omitempty" cborgen:"email,omitempty"` + EmailConfirmedAt *string `json:"emailConfirmedAt,omitempty" cborgen:"emailConfirmedAt,omitempty"` + Handle string `json:"handle" cborgen:"handle"` + IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` + InviteNote *string `json:"inviteNote,omitempty" cborgen:"inviteNote,omitempty"` + InvitedBy *comatprototypes.ServerDefs_InviteCode `json:"invitedBy,omitempty" cborgen:"invitedBy,omitempty"` + Invites []*comatprototypes.ServerDefs_InviteCode `json:"invites,omitempty" cborgen:"invites,omitempty"` + InvitesDisabled *bool `json:"invitesDisabled,omitempty" cborgen:"invitesDisabled,omitempty"` + Labels []*comatprototypes.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` + Moderation *ModerationDefs_ModerationDetail `json:"moderation" cborgen:"moderation"` + RelatedRecords []*util.LexiconTypeDecoder `json:"relatedRecords" cborgen:"relatedRecords"` + ThreatSignatures []*comatprototypes.AdminDefs_ThreatSignature `json:"threatSignatures,omitempty" cborgen:"threatSignatures,omitempty"` } // ModerationDefs_RepoViewNotFound is a "repoViewNotFound" in the tools.ozone.moderation.defs schema. From d0cf4e6bb1affc09e588ef8afbd22b705fa2e5b1 Mon Sep 17 00:00:00 2001 From: Jaz Volpert Date: Thu, 31 Oct 2024 23:06:27 +0000 Subject: [PATCH 095/111] Update usage --- cmd/bigsky/main.go | 11 ++++++++++- events/diskpersist.go | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/bigsky/main.go b/cmd/bigsky/main.go index 5fb9f73da..c4d7d7b28 100644 --- a/cmd/bigsky/main.go +++ b/cmd/bigsky/main.go @@ -188,6 +188,12 @@ func run(args []string) error { EnvVars: []string{"RELAY_DID_CACHE_SIZE"}, Value: 5_000_000, }, + &cli.DurationFlag{ + Name: "event-playback-ttl", + Usage: "time to live for event playback buffering (only applies to disk persister)", + EnvVars: []string{"RELAY_EVENT_PLAYBACK_TTL"}, + Value: 72 * time.Hour, + }, } app.Action = runBigsky @@ -327,7 +333,10 @@ func runBigsky(cctx *cli.Context) error { if dpd := cctx.String("disk-persister-dir"); dpd != "" { log.Infow("setting up disk persister") - dp, err := events.NewDiskPersistence(dpd, "", db, events.DefaultDiskPersistOptions()) + + pOpts := events.DefaultDiskPersistOptions() + pOpts.Retention = cctx.Duration("event-playback-ttl") + dp, err := events.NewDiskPersistence(dpd, "", db, pOpts) if err != nil { return fmt.Errorf("setting up disk persister: %w", err) } diff --git a/events/diskpersist.go b/events/diskpersist.go index b793ee843..25eb989af 100644 --- a/events/diskpersist.go +++ b/events/diskpersist.go @@ -81,8 +81,8 @@ type DiskPersistOptions struct { func DefaultDiskPersistOptions() *DiskPersistOptions { return &DiskPersistOptions{ EventsPerFile: 10_000, - UIDCacheSize: 100_000, - DIDCacheSize: 100_000, + UIDCacheSize: 1_000_000, + DIDCacheSize: 1_000_000, WriteBufferSize: 50, Retention: time.Hour * 24 * 3, // 3 days } From 7b818d1eb06026e591da9b85bb8320418e5d1dd8 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 4 Nov 2024 17:04:37 -0800 Subject: [PATCH 096/111] lexgen: handle 'ref' as procedure input type --- lex/gen.go | 11 +++++++++-- lex/type_schema.go | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lex/gen.go b/lex/gen.go index af8dab70d..29447bb53 100644 --- a/lex/gen.go +++ b/lex/gen.go @@ -232,9 +232,16 @@ func writeMethods(typename string, ts *TypeSchema, w io.Writer) error { case "record": return nil case "query": - return ts.WriteRPC(w, typename) + return ts.WriteRPC(w, typename, fmt.Sprintf("%s_Input", typename)) case "procedure": - return ts.WriteRPC(w, typename) + if ts.Input == nil || ts.Input.Schema == nil || ts.Input.Schema.Type == "object" { + return ts.WriteRPC(w, typename, fmt.Sprintf("%s_Input", typename)) + } else if ts.Input.Schema.Type == "ref" { + inputname, _ := ts.namesFromRef(ts.Input.Schema.Ref) + return ts.WriteRPC(w, typename, inputname) + } else { + return fmt.Errorf("unhandled input type: %s", ts.Input.Schema.Type) + } case "object", "string": return nil case "subscription": diff --git a/lex/type_schema.go b/lex/type_schema.go index aeeb7389d..fcec3575a 100644 --- a/lex/type_schema.go +++ b/lex/type_schema.go @@ -50,7 +50,7 @@ type TypeSchema struct { Maximum any `json:"maximum"` } -func (s *TypeSchema) WriteRPC(w io.Writer, typename string) error { +func (s *TypeSchema) WriteRPC(w io.Writer, typename, inputname string) error { pf := printerf(w) fname := typename @@ -65,7 +65,7 @@ func (s *TypeSchema) WriteRPC(w io.Writer, typename string) error { case EncodingCBOR, EncodingCAR, EncodingANY, EncodingMP4: params = fmt.Sprintf("%s, input io.Reader", params) case EncodingJSON: - params = fmt.Sprintf("%s, input *%s_Input", params, fname) + params = fmt.Sprintf("%s, input *%s", params, inputname) default: return fmt.Errorf("unsupported input encoding (RPC input): %q", s.Input.Encoding) From 0739b895fc89817068274d2fef051c2af71878ff Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 4 Nov 2024 17:22:58 -0800 Subject: [PATCH 097/111] wire up abuse metadata from ozone to automod --- automod/engine/account_meta.go | 11 +++++++++-- automod/engine/fetch_account_meta.go | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/automod/engine/account_meta.go b/automod/engine/account_meta.go index e7d4e86ba..6270908f3 100644 --- a/automod/engine/account_meta.go +++ b/automod/engine/account_meta.go @@ -36,12 +36,19 @@ type ProfileSummary struct { DisplayName *string } +// opaque fingerprints for correlating abusive accounts +type AbuseSignature struct { + Property string + Value string +} + type AccountPrivate struct { Email string EmailConfirmed bool IndexedAt *time.Time AccountTags []string // ReviewState will be one of ReviewStateEscalated, ReviewStateOpen, ReviewStateClosed, ReviewStateNone, or "" (unknown) - ReviewState string - Appealed bool + ReviewState string + Appealed bool + AbuseSignatures []AbuseSignature } diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index 5bc5c3637..fd4fc11ed 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -148,6 +148,13 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( } } } + if rd.ThreatSignatures != nil || len(rd.ThreatSignatures) > 0 { + asigs := make([]AbuseSignature, len(rd.ThreatSignatures)) + for i, sig := range rd.ThreatSignatures { + asigs[i] = AbuseSignature{Property: sig.Property, Value: sig.Value} + } + am.Private.AbuseSignatures = asigs + } am.Private = &ap } } From 73bb35e35c25bca2b5626691148a958a5401e69e Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 4 Nov 2024 17:27:52 -0800 Subject: [PATCH 098/111] update --- automod/engine/cid_from_cdn_test.go | 2 +- automod/engine/fetch_account_meta.go | 21 --------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/automod/engine/cid_from_cdn_test.go b/automod/engine/cid_from_cdn_test.go index cc7553cb8..0780403ca 100644 --- a/automod/engine/cid_from_cdn_test.go +++ b/automod/engine/cid_from_cdn_test.go @@ -37,6 +37,6 @@ func TestCidFromCdnUrl(t *testing.T) { } for _, fix := range fixtures { - assert.Equal(fix.cid, CidFromCdnUrl(&fix.url)) + assert.Equal(fix.cid, cidFromCdnUrl(&fix.url)) } } diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index b634c856a..2b51930c6 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" - "strings" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -19,25 +17,6 @@ import ( var newAccountRetryDuration = 3 * 1000 * time.Millisecond -// get the cid from a bluesky cdn url -func CidFromCdnUrl(str *string) *string { - if str == nil { - return nil - } - - u, err := url.Parse(*str) - if err != nil || u.Host != "cdn.bsky.app" { - return nil - } - - parts := strings.Split(u.Path, "/") - if len(parts) != 6 { - return nil - } - - return &strings.Split(parts[5], "@")[0] -} - // Helper to hydrate metadata about an account from several sources: PDS (if access), mod service (if access), public identity resolution func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) (*AccountMeta, error) { From aa48298d153754a6a91a6ccfbf1d043ee14b1d9c Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Mon, 4 Nov 2024 21:43:56 -0800 Subject: [PATCH 099/111] fix segfaults --- automod/engine/engine.go | 4 ++++ automod/engine/fetch_account_meta.go | 2 +- automod/engine/persist.go | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/automod/engine/engine.go b/automod/engine/engine.go index 8ed864371..b313a4492 100644 --- a/automod/engine/engine.go +++ b/automod/engine/engine.go @@ -351,6 +351,7 @@ func (e *Engine) CanonicalLogLineAccount(c *AccountContext) { c.Logger.Info("canonical-event-line", "accountLabels", c.effects.AccountLabels, "accountFlags", c.effects.AccountFlags, + "accountTags", c.effects.AccountTags, "accountTakedown", c.effects.AccountTakedown, "accountReports", len(c.effects.AccountReports), ) @@ -360,10 +361,12 @@ func (e *Engine) CanonicalLogLineRecord(c *RecordContext) { c.Logger.Info("canonical-event-line", "accountLabels", c.effects.AccountLabels, "accountFlags", c.effects.AccountFlags, + "accountTags", c.effects.AccountTags, "accountTakedown", c.effects.AccountTakedown, "accountReports", len(c.effects.AccountReports), "recordLabels", c.effects.RecordLabels, "recordFlags", c.effects.RecordFlags, + "recordTags", c.effects.RecordTags, "recordTakedown", c.effects.RecordTakedown, "recordReports", len(c.effects.RecordReports), ) @@ -373,6 +376,7 @@ func (e *Engine) CanonicalLogLineNotification(c *NotificationContext) { c.Logger.Info("canonical-event-line", "accountLabels", c.effects.AccountLabels, "accountFlags", c.effects.AccountFlags, + "accountTags", c.effects.AccountTags, "accountTakedown", c.effects.AccountTakedown, "accountReports", len(c.effects.AccountReports), "reject", c.effects.RejectEvent, diff --git a/automod/engine/fetch_account_meta.go b/automod/engine/fetch_account_meta.go index fd4fc11ed..775e92eca 100644 --- a/automod/engine/fetch_account_meta.go +++ b/automod/engine/fetch_account_meta.go @@ -153,7 +153,7 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) ( for i, sig := range rd.ThreatSignatures { asigs[i] = AbuseSignature{Property: sig.Property, Value: sig.Value} } - am.Private.AbuseSignatures = asigs + ap.AbuseSignatures = asigs } am.Private = &ap } diff --git a/automod/engine/persist.go b/automod/engine/persist.go index e289a64ef..3c4b36fd0 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -285,7 +285,11 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { existingLabels = dedupeStrings(existingLabels) negLabels = dedupeStrings(negLabels) newLabels = dedupeLabelActions(newLabels, existingLabels, negLabels) - newTags = dedupeTagActions(newTags, rv.Moderation.SubjectStatus.Tags) + existingTags := []string{} + if rv.Moderation != nil && rv.Moderation.SubjectStatus != nil && rv.Moderation.SubjectStatus.Tags != nil { + existingTags = rv.Moderation.SubjectStatus.Tags + } + newTags = dedupeTagActions(newTags, existingTags) } } @@ -389,7 +393,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { CreatedBy: xrpcc.Auth.Did, Event: &toolsozone.ModerationEmitEvent_Input_Event{ ModerationDefs_ModEventTag: &toolsozone.ModerationDefs_ModEventTag{ - Add: newLabels, + Add: newTags, Remove: []string{}, Comment: &comment, }, From e5a1ac546b3b76e8022b26867f6f02a9c45e7d6f Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 5 Nov 2024 15:05:17 -0800 Subject: [PATCH 100/111] hepa: fix routine order --- cmd/hepa/main.go | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/cmd/hepa/main.go b/cmd/hepa/main.go index bceaaa189..92560f944 100644 --- a/cmd/hepa/main.go +++ b/cmd/hepa/main.go @@ -261,28 +261,6 @@ var runCmd = &cli.Command{ return fmt.Errorf("failed to construct server: %v", err) } - // firehose event consumer - relayHost := cctx.String("atp-relay-host") - if relayHost != "" { - fc := consumer.FirehoseConsumer{ - Engine: srv.Engine, - Logger: logger.With("subsystem", "firehose-consumer"), - Host: cctx.String("atp-relay-host"), - Parallelism: cctx.Int("firehose-parallelism"), - RedisClient: srv.RedisClient, - } - - go func() { - if err := fc.RunPersistCursor(ctx); err != nil { - slog.Error("cursor routine failed", "err", err) - } - }() - - if err := fc.Run(ctx); err != nil { - return fmt.Errorf("failure consuming and processing firehose: %w", err) - } - } - // ozone event consumer (if configured) if srv.Engine.OzoneClient != nil { oc := consumer.OzoneConsumer{ @@ -314,6 +292,28 @@ var runCmd = &cli.Command{ } }() + // firehose event consumer (note this is actually mandatory) + relayHost := cctx.String("atp-relay-host") + if relayHost != "" { + fc := consumer.FirehoseConsumer{ + Engine: srv.Engine, + Logger: logger.With("subsystem", "firehose-consumer"), + Host: cctx.String("atp-relay-host"), + Parallelism: cctx.Int("firehose-parallelism"), + RedisClient: srv.RedisClient, + } + + go func() { + if err := fc.RunPersistCursor(ctx); err != nil { + slog.Error("cursor routine failed", "err", err) + } + }() + + if err := fc.Run(ctx); err != nil { + return fmt.Errorf("failure consuming and processing firehose: %w", err) + } + } + return nil }, } From 716b3d5db9d85f1f6099f0c7aac383c59f64ecd8 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 5 Nov 2024 20:31:34 -0800 Subject: [PATCH 101/111] make goat record creation work w/o schema defs --- cmd/goat/record.go | 19 ++++++-------- cmd/goat/repocreateRecord.go | 51 ++++++++++++++++++++++++++++++++++++ cmd/goat/repoputRecord.go | 47 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 cmd/goat/repocreateRecord.go create mode 100644 cmd/goat/repoputRecord.go diff --git a/cmd/goat/record.go b/cmd/goat/record.go index 7dfebc8e1..013913aa2 100644 --- a/cmd/goat/record.go +++ b/cmd/goat/record.go @@ -10,7 +10,6 @@ import ( "github.com/bluesky-social/indigo/atproto/data" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" - lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/bluesky-social/indigo/xrpc" "github.com/urfave/cli/v2" @@ -231,9 +230,8 @@ func runRecordCreate(cctx *cli.Context) error { return err } - // TODO: replace this with something that allows arbitrary Lexicons, instead of needing registered types - var recordVal lexutil.LexiconTypeDecoder - if err = recordVal.UnmarshalJSON(recordBytes); err != nil { + recordVal, err := data.UnmarshalJSON(recordBytes) + if err != nil { return err } @@ -248,10 +246,10 @@ func runRecordCreate(cctx *cli.Context) error { } validate := !cctx.Bool("no-validate") - resp, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ + resp, err := RepoCreateRecord(ctx, xrpcc, &RepoCreateRecord_Input{ Collection: nsid, Repo: xrpcc.Auth.Did, - Record: &recordVal, + Record: recordVal, Rkey: rkey, Validate: &validate, }) @@ -300,18 +298,17 @@ func runRecordUpdate(cctx *cli.Context) error { return err } - // TODO: replace this with something that allows arbitrary Lexicons, instead of needing registered types - var recordVal lexutil.LexiconTypeDecoder - if err = recordVal.UnmarshalJSON(recordBytes); err != nil { + recordVal, err := data.UnmarshalJSON(recordBytes) + if err != nil { return err } validate := !cctx.Bool("no-validate") - resp, err := comatproto.RepoPutRecord(ctx, xrpcc, &comatproto.RepoPutRecord_Input{ + resp, err := RepoPutRecord(ctx, xrpcc, &RepoPutRecord_Input{ Collection: nsid, Repo: xrpcc.Auth.Did, - Record: &recordVal, + Record: recordVal, Rkey: rkey, Validate: &validate, SwapRecord: existing.Cid, diff --git a/cmd/goat/repocreateRecord.go b/cmd/goat/repocreateRecord.go new file mode 100644 index 000000000..c1fa67a39 --- /dev/null +++ b/cmd/goat/repocreateRecord.go @@ -0,0 +1,51 @@ +// Copied from indigo:api/atproto/repocreateRecords.go + +package main + +// schema: com.atproto.repo.createRecord + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// RepoDefs_CommitMeta is a "commitMeta" in the com.atproto.repo.defs schema. +type RepoDefs_CommitMeta struct { + Cid string `json:"cid" cborgen:"cid"` + Rev string `json:"rev" cborgen:"rev"` +} + +// RepoCreateRecord_Input is the input argument to a com.atproto.repo.createRecord call. +type RepoCreateRecord_Input struct { + // collection: The NSID of the record collection. + Collection string `json:"collection" cborgen:"collection"` + // record: The record itself. Must contain a $type field. + Record map[string]any `json:"record" cborgen:"record"` + // repo: The handle or DID of the repo (aka, current account). + Repo string `json:"repo" cborgen:"repo"` + // rkey: The Record Key. + Rkey *string `json:"rkey,omitempty" cborgen:"rkey,omitempty"` + // swapCommit: Compare and swap with the previous commit by CID. + SwapCommit *string `json:"swapCommit,omitempty" cborgen:"swapCommit,omitempty"` + // validate: Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. + Validate *bool `json:"validate,omitempty" cborgen:"validate,omitempty"` +} + +// RepoCreateRecord_Output is the output of a com.atproto.repo.createRecord call. +type RepoCreateRecord_Output struct { + Cid string `json:"cid" cborgen:"cid"` + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` +} + +// RepoCreateRecord calls the XRPC method "com.atproto.repo.createRecord". +func RepoCreateRecord(ctx context.Context, c *xrpc.Client, input *RepoCreateRecord_Input) (*RepoCreateRecord_Output, error) { + var out RepoCreateRecord_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/cmd/goat/repoputRecord.go b/cmd/goat/repoputRecord.go new file mode 100644 index 000000000..34011797b --- /dev/null +++ b/cmd/goat/repoputRecord.go @@ -0,0 +1,47 @@ +// Copied from indigo:api/atproto/repoputRecords.go + +package main + +// schema: com.atproto.repo.putRecord + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// RepoPutRecord_Input is the input argument to a com.atproto.repo.putRecord call. +type RepoPutRecord_Input struct { + // collection: The NSID of the record collection. + Collection string `json:"collection" cborgen:"collection"` + // record: The record to write. + Record map[string]any `json:"record" cborgen:"record"` + // repo: The handle or DID of the repo (aka, current account). + Repo string `json:"repo" cborgen:"repo"` + // rkey: The Record Key. + Rkey string `json:"rkey" cborgen:"rkey"` + // swapCommit: Compare and swap with the previous commit by CID. + SwapCommit *string `json:"swapCommit,omitempty" cborgen:"swapCommit,omitempty"` + // swapRecord: Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation + SwapRecord *string `json:"swapRecord" cborgen:"swapRecord"` + // validate: Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. + Validate *bool `json:"validate,omitempty" cborgen:"validate,omitempty"` +} + +// RepoPutRecord_Output is the output of a com.atproto.repo.putRecord call. +type RepoPutRecord_Output struct { + Cid string `json:"cid" cborgen:"cid"` + Commit *RepoDefs_CommitMeta `json:"commit,omitempty" cborgen:"commit,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + ValidationStatus *string `json:"validationStatus,omitempty" cborgen:"validationStatus,omitempty"` +} + +// RepoPutRecord calls the XRPC method "com.atproto.repo.putRecord". +func RepoPutRecord(ctx context.Context, c *xrpc.Client, input *RepoPutRecord_Input) (*RepoPutRecord_Output, error) { + var out RepoPutRecord_Output + if err := c.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} From 29a16d0dc6de2396c2eb20c7390f7a72c0c6d594 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 5 Nov 2024 20:39:10 -0800 Subject: [PATCH 102/111] make goat pref handling schema-agnostic --- cmd/goat/account_migrate.go | 5 ++--- cmd/goat/actorgetPreferences.go | 28 ++++++++++++++++++++++++++++ cmd/goat/actorputPreferences.go | 25 +++++++++++++++++++++++++ cmd/goat/bsky_prefs.go | 12 ++++-------- 4 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 cmd/goat/actorgetPreferences.go create mode 100644 cmd/goat/actorputPreferences.go diff --git a/cmd/goat/account_migrate.go b/cmd/goat/account_migrate.go index 72c66e3fb..fa3d60535 100644 --- a/cmd/goat/account_migrate.go +++ b/cmd/goat/account_migrate.go @@ -10,7 +10,6 @@ import ( "time" comatproto "github.com/bluesky-social/indigo/api/atproto" - appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" @@ -167,11 +166,11 @@ func runAccountMigrate(cctx *cli.Context) error { slog.Info("migrating preferences") // TODO: service proxy header for AppView? - prefResp, err := appbsky.ActorGetPreferences(ctx, oldClient) + prefResp, err := ActorGetPreferences(ctx, oldClient) if err != nil { return fmt.Errorf("failed fetching old preferences: %w", err) } - err = appbsky.ActorPutPreferences(ctx, &newClient, &appbsky.ActorPutPreferences_Input{ + err = ActorPutPreferences(ctx, &newClient, &ActorPutPreferences_Input{ Preferences: prefResp.Preferences, }) if err != nil { diff --git a/cmd/goat/actorgetPreferences.go b/cmd/goat/actorgetPreferences.go new file mode 100644 index 000000000..bd6e8a18c --- /dev/null +++ b/cmd/goat/actorgetPreferences.go @@ -0,0 +1,28 @@ +// Copied from indigo:api/atproto/actorgetPreferences.go + +package main + +// schema: app.bsky.actor.getPreferences + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// ActorGetPreferences_Output is the output of a app.bsky.actor.getPreferences call. +type ActorGetPreferences_Output struct { + Preferences []map[string]any `json:"preferences" cborgen:"preferences"` +} + +// ActorGetPreferences calls the XRPC method "app.bsky.actor.getPreferences". +func ActorGetPreferences(ctx context.Context, c *xrpc.Client) (*ActorGetPreferences_Output, error) { + var out ActorGetPreferences_Output + + params := map[string]interface{}{} + if err := c.Do(ctx, xrpc.Query, "", "app.bsky.actor.getPreferences", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/cmd/goat/actorputPreferences.go b/cmd/goat/actorputPreferences.go new file mode 100644 index 000000000..24042236d --- /dev/null +++ b/cmd/goat/actorputPreferences.go @@ -0,0 +1,25 @@ +// Copied from indigo:api/atproto/actorputPreferences.go + +package main + +// schema: app.bsky.actor.putPreferences + +import ( + "context" + + "github.com/bluesky-social/indigo/xrpc" +) + +// ActorPutPreferences_Input is the input argument to a app.bsky.actor.putPreferences call. +type ActorPutPreferences_Input struct { + Preferences []map[string]any `json:"preferences" cborgen:"preferences"` +} + +// ActorPutPreferences calls the XRPC method "app.bsky.actor.putPreferences". +func ActorPutPreferences(ctx context.Context, c *xrpc.Client, input *ActorPutPreferences_Input) error { + if err := c.Do(ctx, xrpc.Procedure, "application/json", "app.bsky.actor.putPreferences", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/cmd/goat/bsky_prefs.go b/cmd/goat/bsky_prefs.go index e24965491..725072344 100644 --- a/cmd/goat/bsky_prefs.go +++ b/cmd/goat/bsky_prefs.go @@ -6,8 +6,6 @@ import ( "fmt" "os" - appbsky "github.com/bluesky-social/indigo/api/bsky" - "github.com/urfave/cli/v2" ) @@ -41,7 +39,7 @@ func runBskyPrefsExport(cctx *cli.Context) error { } // TODO: does indigo API code crash with unsupported preference '$type'? Eg "Lexicon decoder" with unsupported type. - resp, err := appbsky.ActorGetPreferences(ctx, xrpcc) + resp, err := ActorGetPreferences(ctx, xrpcc) if err != nil { return fmt.Errorf("failed fetching old preferences: %w", err) } @@ -74,14 +72,12 @@ func runBskyPrefsImport(cctx *cli.Context) error { return err } - var prefsArray []appbsky.ActorDefs_Preferences_Elem - err = json.Unmarshal(prefsBytes, &prefsArray) - if err != nil { + var prefsArray []map[string]any + if err = json.Unmarshal(prefsBytes, &prefsArray); err != nil { return err } - // WARNING: might clobber off-Lexicon or new-Lexicon data fields (which don't round-trip deserialization) - err = appbsky.ActorPutPreferences(ctx, xrpcc, &appbsky.ActorPutPreferences_Input{ + err = ActorPutPreferences(ctx, xrpcc, &ActorPutPreferences_Input{ Preferences: prefsArray, }) if err != nil { From 83930c910b127b56fba52292907c16b2e5d23b3a Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 7 Nov 2024 16:27:38 -0800 Subject: [PATCH 103/111] bigsky: show something helpful when you hit home route --- bgs/bgs.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bgs/bgs.go b/bgs/bgs.go index 192f4ee3d..6ac5be78a 100644 --- a/bgs/bgs.go +++ b/bgs/bgs.go @@ -349,6 +349,7 @@ func (bgs *BGS) StartWithListener(listen net.Listener) error { e.GET("/xrpc/com.atproto.sync.notifyOfUpdate", bgs.HandleComAtprotoSyncNotifyOfUpdate) e.GET("/xrpc/_health", bgs.HandleHealthCheck) e.GET("/_health", bgs.HandleHealthCheck) + e.GET("/", bgs.HandleHomeMessage) admin := e.Group("/admin", bgs.checkAdminAuth) @@ -420,6 +421,23 @@ func (bgs *BGS) HandleHealthCheck(c echo.Context) error { } } +var homeMessage string = ` +d8888b. d888888b d888b .d8888. db dD db db +88 '8D '88' 88' Y8b 88' YP 88 ,8P' '8b d8' +88oooY' 88 88 '8bo. 88,8P '8bd8' +88~~~b. 88 88 ooo 'Y8b. 88'8b 88 +88 8D .88. 88. ~8~ db 8D 88 '88. 88 +Y8888P' Y888888P Y888P '8888Y' YP YD YP + +This is an atproto [https://atproto.com] relay instance, running the 'bigsky' codebase [https://github.com/bluesky-social/indigo] + +The firehose WebSocket path is at: /xrpc/com.atproto.sync.subscribeRepos +` + +func (bgs *BGS) HandleHomeMessage(c echo.Context) error { + return c.String(http.StatusOK, homeMessage) +} + type AuthToken struct { gorm.Model Token string `gorm:"index"` From 951384a7135020b24d7a0ea351e7ec84aeb5ea66 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 8 Nov 2024 10:05:06 -0800 Subject: [PATCH 104/111] relay: env var for persist dir, and alt env var for data dir --- cmd/bigsky/main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/bigsky/main.go b/cmd/bigsky/main.go index c4d7d7b28..3009db3db 100644 --- a/cmd/bigsky/main.go +++ b/cmd/bigsky/main.go @@ -85,7 +85,7 @@ func run(args []string) error { Name: "data-dir", Usage: "path of directory for CAR files and other data", Value: "data/bigsky", - EnvVars: []string{"DATA_DIR"}, + EnvVars: []string{"RELAY_DATA_DIR", "DATA_DIR"}, }, &cli.StringFlag{ Name: "plc-host", @@ -112,8 +112,9 @@ func run(args []string) error { EnvVars: []string{"RELAY_METRICS_LISTEN", "BGS_METRICS_LISTEN"}, }, &cli.StringFlag{ - Name: "disk-persister-dir", - Usage: "set directory for disk persister (implicitly enables disk persister)", + Name: "disk-persister-dir", + Usage: "set directory for disk persister (implicitly enables disk persister)", + EnvVars: []string{"RELAY_PERSISTER_DIR"}, }, &cli.StringFlag{ Name: "admin-key", From bf2ec9e41c78c6ea0e0f58b9ce770ab0611885b5 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 8 Nov 2024 13:52:05 -0800 Subject: [PATCH 105/111] hepa: wire up ozone consumption correctly --- cmd/hepa/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/hepa/main.go b/cmd/hepa/main.go index 92560f944..7883d9c29 100644 --- a/cmd/hepa/main.go +++ b/cmd/hepa/main.go @@ -264,9 +264,10 @@ var runCmd = &cli.Command{ // ozone event consumer (if configured) if srv.Engine.OzoneClient != nil { oc := consumer.OzoneConsumer{ - Engine: srv.Engine, Logger: logger.With("subsystem", "ozone-consumer"), RedisClient: srv.RedisClient, + OzoneClient: srv.Engine.OzoneClient, + Engine: srv.Engine, } go func() { From 1f22a51acc79097b4c1d8fdce70a148c5efb37f2 Mon Sep 17 00:00:00 2001 From: Brian Olson Date: Tue, 12 Nov 2024 21:07:20 -0500 Subject: [PATCH 106/111] simple http client for web did --- did/web.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/did/web.go b/did/web.go index 322520fdc..baea42e6c 100644 --- a/did/web.go +++ b/did/web.go @@ -6,18 +6,26 @@ import ( "fmt" "net/http" "strings" + "time" "unicode" "github.com/whyrusleeping/go-did" "go.opentelemetry.io/otel" ) +var webDidDefaultTimeout = 5 * time.Second + type WebResolver struct { Insecure bool // TODO: cache? maybe at a different layer + + client http.Client } func (wr *WebResolver) GetDocument(ctx context.Context, didstr string) (*Document, error) { + if wr.client.Timeout == 0 { + wr.client.Timeout = webDidDefaultTimeout + } ctx, span := otel.Tracer("did").Start(ctx, "didWebGetDocument") defer span.End() @@ -36,7 +44,7 @@ func (wr *WebResolver) GetDocument(ctx context.Context, didstr string) (*Documen proto = "http" } - resp, err := http.Get(proto + "://" + val + "/.well-known/did.json") + resp, err := wr.client.Get(proto + "://" + val + "/.well-known/did.json") if err != nil { return nil, err } From 8b7dba258df12d35a10a0fb8b79851e71120d1ba Mon Sep 17 00:00:00 2001 From: whyrusleeping Date: Wed, 13 Nov 2024 10:59:55 -0800 Subject: [PATCH 107/111] make compaction worker count configurable --- bgs/bgs.go | 26 +++++++++++++++----------- cmd/bigsky/main.go | 6 ++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/bgs/bgs.go b/bgs/bgs.go index 6ac5be78a..35dfab9d9 100644 --- a/bgs/bgs.go +++ b/bgs/bgs.go @@ -107,20 +107,22 @@ type SocketConsumer struct { } type BGSConfig struct { - SSL bool - CompactInterval time.Duration - DefaultRepoLimit int64 - ConcurrencyPerPDS int64 - MaxQueuePerPDS int64 + SSL bool + CompactInterval time.Duration + DefaultRepoLimit int64 + ConcurrencyPerPDS int64 + MaxQueuePerPDS int64 + NumCompactionWorkers int } func DefaultBGSConfig() *BGSConfig { return &BGSConfig{ - SSL: true, - CompactInterval: 4 * time.Hour, - DefaultRepoLimit: 100, - ConcurrencyPerPDS: 100, - MaxQueuePerPDS: 1_000, + SSL: true, + CompactInterval: 4 * time.Hour, + DefaultRepoLimit: 100, + ConcurrencyPerPDS: 100, + MaxQueuePerPDS: 1_000, + NumCompactionWorkers: 2, } } @@ -168,7 +170,9 @@ func NewBGS(db *gorm.DB, ix *indexer.Indexer, repoman *repomgr.RepoManager, evtm return nil, err } - compactor := NewCompactor(nil) + cOpts := DefaultCompactorOptions() + cOpts.NumWorkers = config.NumCompactionWorkers + compactor := NewCompactor(cOpts) compactor.requeueInterval = config.CompactInterval compactor.Start(bgs) bgs.compactor = compactor diff --git a/cmd/bigsky/main.go b/cmd/bigsky/main.go index 3009db3db..540796f51 100644 --- a/cmd/bigsky/main.go +++ b/cmd/bigsky/main.go @@ -195,6 +195,11 @@ func run(args []string) error { EnvVars: []string{"RELAY_EVENT_PLAYBACK_TTL"}, Value: 72 * time.Hour, }, + &cli.IntFlag{ + Name: "num-compaction-workers", + EnvVars: []string{"RELAY_NUM_COMPACTION_WORKERS"}, + Value: 2, + }, } app.Action = runBigsky @@ -413,6 +418,7 @@ func runBigsky(cctx *cli.Context) error { bgsConfig.ConcurrencyPerPDS = cctx.Int64("concurrency-per-pds") bgsConfig.MaxQueuePerPDS = cctx.Int64("max-queue-per-pds") bgsConfig.DefaultRepoLimit = cctx.Int64("default-repo-limit") + bgsConfig.NumCompactionWorkers = cctx.Int("num-compaction-workers") bgs, err := libbgs.NewBGS(db, ix, repoman, evtman, cachedidr, rf, hr, bgsConfig) if err != nil { return err From 11a0f1c68d301481439e7e773e9d22f14776a47f Mon Sep 17 00:00:00 2001 From: whyrusleeping Date: Wed, 13 Nov 2024 11:19:13 -0800 Subject: [PATCH 108/111] update to more optimized cborgen --- api/atproto/cbor_gen.go | 336 ++++++++++------ api/bsky/cbor_gen.go | 798 +++++++++++++++++++++++--------------- api/cbor_gen.go | 21 +- api/chat/cbor_gen.go | 21 +- atproto/data/cbor_gen.go | 63 +-- events/cbor_gen.go | 42 +- go.mod | 6 +- go.sum | 16 +- lex/util/cbor_gen.go | 63 +-- lex/util/cbor_gen_test.go | 126 +++--- mst/cbor_gen.go | 42 +- repo/cbor_gen.go | 42 +- util/labels/cbor_gen.go | 21 +- 13 files changed, 994 insertions(+), 603 deletions(-) diff --git a/api/atproto/cbor_gen.go b/api/atproto/cbor_gen.go index c1f325ebc..d0b57ccbb 100644 --- a/api/atproto/cbor_gen.go +++ b/api/atproto/cbor_gen.go @@ -129,21 +129,24 @@ func (t *RepoStrongRef) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RepoStrongRef: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (string) (string) case "cid": @@ -180,7 +183,9 @@ func (t *RepoStrongRef) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -492,21 +497,24 @@ func (t *SyncSubscribeRepos_Commit) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Commit: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Ops ([]*atproto.SyncSubscribeRepos_RepoOp) (slice) case "ops": @@ -767,7 +775,9 @@ func (t *SyncSubscribeRepos_Commit) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -901,21 +911,24 @@ func (t *SyncSubscribeRepos_Handle) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Handle: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -978,7 +991,9 @@ func (t *SyncSubscribeRepos_Handle) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1126,21 +1141,24 @@ func (t *SyncSubscribeRepos_Identity) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Identity: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -1213,7 +1231,9 @@ func (t *SyncSubscribeRepos_Identity) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1377,21 +1397,24 @@ func (t *SyncSubscribeRepos_Account) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Account: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -1482,7 +1505,9 @@ func (t *SyncSubscribeRepos_Account) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1585,21 +1610,24 @@ func (t *SyncSubscribeRepos_Info) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Info: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Name (string) (string) case "name": @@ -1635,7 +1663,9 @@ func (t *SyncSubscribeRepos_Info) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1775,21 +1805,24 @@ func (t *SyncSubscribeRepos_Migrate) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Migrate: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -1862,7 +1895,9 @@ func (t *SyncSubscribeRepos_Migrate) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1967,21 +2002,24 @@ func (t *SyncSubscribeRepos_RepoOp) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_RepoOp: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (util.LexLink) (struct) case "cid": @@ -2027,7 +2065,9 @@ func (t *SyncSubscribeRepos_RepoOp) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2138,21 +2178,24 @@ func (t *SyncSubscribeRepos_Tombstone) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SyncSubscribeRepos_Tombstone: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 4) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -2204,7 +2247,9 @@ func (t *SyncSubscribeRepos_Tombstone) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2301,21 +2346,24 @@ func (t *LabelDefs_SelfLabels) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelDefs_SelfLabels: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -2390,7 +2438,9 @@ func (t *LabelDefs_SelfLabels) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2456,21 +2506,24 @@ func (t *LabelDefs_SelfLabel) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelDefs_SelfLabel: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 3) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Val (string) (string) case "val": @@ -2485,7 +2538,9 @@ func (t *LabelDefs_SelfLabel) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2790,21 +2845,24 @@ func (t *LabelDefs_Label) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelDefs_Label: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 3) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (string) (string) case "cid": @@ -2986,7 +3044,9 @@ func (t *LabelDefs_Label) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3077,21 +3137,24 @@ func (t *LabelSubscribeLabels_Labels) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelSubscribeLabels_Labels: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Seq (int64) (int64) case "seq": { @@ -3170,7 +3233,9 @@ func (t *LabelSubscribeLabels_Labels) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3273,21 +3338,24 @@ func (t *LabelSubscribeLabels_Info) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelSubscribeLabels_Info: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Name (string) (string) case "name": @@ -3323,7 +3391,9 @@ func (t *LabelSubscribeLabels_Info) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3527,21 +3597,24 @@ func (t *LabelDefs_LabelValueDefinition) UnmarshalCBOR(r io.Reader) (err error) return fmt.Errorf("LabelDefs_LabelValueDefinition: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 14) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Blurs (string) (string) case "blurs": @@ -3681,7 +3754,9 @@ func (t *LabelDefs_LabelValueDefinition) UnmarshalCBOR(r io.Reader) (err error) default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3793,21 +3868,24 @@ func (t *LabelDefs_LabelValueDefinitionStrings) UnmarshalCBOR(r io.Reader) (err return fmt.Errorf("LabelDefs_LabelValueDefinitionStrings: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 11) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Lang (string) (string) case "lang": @@ -3844,7 +3922,9 @@ func (t *LabelDefs_LabelValueDefinitionStrings) UnmarshalCBOR(r io.Reader) (err default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/api/bsky/cbor_gen.go b/api/bsky/cbor_gen.go index bc4a94b78..383265fda 100644 --- a/api/bsky/cbor_gen.go +++ b/api/bsky/cbor_gen.go @@ -338,21 +338,24 @@ func (t *FeedPost) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPost: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Tags ([]string) (slice) case "tags": @@ -627,7 +630,9 @@ func (t *FeedPost) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -728,21 +733,24 @@ func (t *FeedRepost) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedRepost: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -788,7 +796,9 @@ func (t *FeedRepost) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -893,21 +903,24 @@ func (t *FeedPost_Entity) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPost_Entity: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Type (string) (string) case "type": @@ -953,7 +966,9 @@ func (t *FeedPost_Entity) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1028,21 +1043,24 @@ func (t *FeedPost_ReplyRef) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPost_ReplyRef: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Root (atproto.RepoStrongRef) (struct) case "root": @@ -1086,7 +1104,9 @@ func (t *FeedPost_ReplyRef) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1174,21 +1194,24 @@ func (t *FeedPost_TextSlice) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPost_TextSlice: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.End (int64) (int64) case "end": { @@ -1244,7 +1267,9 @@ func (t *FeedPost_TextSlice) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1332,21 +1357,24 @@ func (t *EmbedImages) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedImages: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -1410,7 +1438,9 @@ func (t *EmbedImages) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1488,21 +1518,24 @@ func (t *EmbedExternal) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedExternal: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -1537,7 +1570,9 @@ func (t *EmbedExternal) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1673,21 +1708,24 @@ func (t *EmbedExternal_External) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedExternal_External: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 11) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Uri (string) (string) case "uri": @@ -1744,7 +1782,9 @@ func (t *EmbedExternal_External) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1850,21 +1890,24 @@ func (t *EmbedImages_Image) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedImages_Image: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 11) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Alt (string) (string) case "alt": @@ -1919,7 +1962,9 @@ func (t *EmbedImages_Image) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2027,21 +2072,24 @@ func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphFollow: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -2078,7 +2126,9 @@ func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2364,21 +2414,24 @@ func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("ActorProfile: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 20) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -2556,7 +2609,9 @@ func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2634,21 +2689,24 @@ func (t *EmbedRecord) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedRecord: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -2683,7 +2741,9 @@ func (t *EmbedRecord) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2784,21 +2844,24 @@ func (t *FeedLike) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedLike: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -2844,7 +2907,9 @@ func (t *FeedLike) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -2929,21 +2994,24 @@ func (t *RichtextFacet) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RichtextFacet: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Index (bsky.RichtextFacet_ByteSlice) (struct) case "index": @@ -3016,7 +3084,9 @@ func (t *RichtextFacet) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3104,21 +3174,24 @@ func (t *RichtextFacet_ByteSlice) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RichtextFacet_ByteSlice: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.ByteEnd (int64) (int64) case "byteEnd": { @@ -3174,7 +3247,9 @@ func (t *RichtextFacet_ByteSlice) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3259,21 +3334,24 @@ func (t *RichtextFacet_Link) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RichtextFacet_Link: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Uri (string) (string) case "uri": @@ -3299,7 +3377,9 @@ func (t *RichtextFacet_Link) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3384,21 +3464,24 @@ func (t *RichtextFacet_Mention) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RichtextFacet_Mention: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -3424,7 +3507,9 @@ func (t *RichtextFacet_Mention) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3509,21 +3594,24 @@ func (t *RichtextFacet_Tag) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("RichtextFacet_Tag: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Tag (string) (string) case "tag": @@ -3549,7 +3637,9 @@ func (t *RichtextFacet_Tag) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3643,21 +3733,24 @@ func (t *EmbedRecordWithMedia) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedRecordWithMedia: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -3712,7 +3805,9 @@ func (t *EmbedRecordWithMedia) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3813,21 +3908,24 @@ func (t *FeedDefs_NotFoundPost) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedDefs_NotFoundPost: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Uri (string) (string) case "uri": @@ -3871,7 +3969,9 @@ func (t *FeedDefs_NotFoundPost) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -3979,21 +4079,24 @@ func (t *GraphBlock) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphBlock: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -4030,7 +4133,9 @@ func (t *GraphBlock) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -4283,21 +4388,24 @@ func (t *GraphList) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphList: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 17) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Name (string) (string) case "name": @@ -4465,7 +4573,9 @@ func (t *GraphList) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -4596,21 +4706,24 @@ func (t *GraphListitem) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphListitem: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.List (string) (string) case "list": @@ -4658,7 +4771,9 @@ func (t *GraphListitem) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -4934,21 +5049,24 @@ func (t *FeedGenerator) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedGenerator: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 19) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -5139,7 +5257,9 @@ func (t *FeedGenerator) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -5247,21 +5367,24 @@ func (t *GraphListblock) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphListblock: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -5298,7 +5421,9 @@ func (t *GraphListblock) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -5386,21 +5511,24 @@ func (t *EmbedDefs_AspectRatio) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedDefs_AspectRatio: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Width (int64) (int64) case "width": { @@ -5456,7 +5584,9 @@ func (t *EmbedDefs_AspectRatio) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -5638,21 +5768,24 @@ func (t *FeedThreadgate) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedThreadgate: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 13) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Post (string) (string) case "post": @@ -5778,7 +5911,9 @@ func (t *FeedThreadgate) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -5863,21 +5998,24 @@ func (t *FeedThreadgate_ListRule) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedThreadgate_ListRule: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.List (string) (string) case "list": @@ -5903,7 +6041,9 @@ func (t *FeedThreadgate_ListRule) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -5965,21 +6105,24 @@ func (t *FeedThreadgate_MentionRule) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedThreadgate_MentionRule: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -5994,7 +6137,9 @@ func (t *FeedThreadgate_MentionRule) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -6056,21 +6201,24 @@ func (t *FeedThreadgate_FollowingRule) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedThreadgate_FollowingRule: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -6085,7 +6233,9 @@ func (t *FeedThreadgate_FollowingRule) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -6151,21 +6301,24 @@ func (t *GraphStarterpack_FeedItem) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphStarterpack_FeedItem: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 3) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Uri (string) (string) case "uri": @@ -6180,7 +6333,9 @@ func (t *GraphStarterpack_FeedItem) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -6414,21 +6569,24 @@ func (t *GraphStarterpack) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GraphStarterpack: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 17) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.List (string) (string) case "list": @@ -6595,7 +6753,9 @@ func (t *GraphStarterpack) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -6720,21 +6880,24 @@ func (t *LabelerService) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelerService: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 9) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -6800,7 +6963,9 @@ func (t *LabelerService) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -6916,21 +7081,24 @@ func (t *LabelerDefs_LabelerPolicies) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LabelerDefs_LabelerPolicies: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 21) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LabelValues ([]*string) (slice) case "labelValues": @@ -7033,7 +7201,9 @@ func (t *LabelerDefs_LabelerPolicies) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -7204,21 +7374,24 @@ func (t *EmbedVideo) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedVideo: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 11) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Alt (string) (string) case "alt": @@ -7343,7 +7516,9 @@ func (t *EmbedVideo) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -7425,21 +7600,24 @@ func (t *EmbedVideo_Caption) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EmbedVideo_Caption: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 4) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.File (util.LexBlob) (struct) case "file": @@ -7474,7 +7652,9 @@ func (t *EmbedVideo_Caption) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -7656,21 +7836,24 @@ func (t *FeedPostgate) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPostgate: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 21) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Post (string) (string) case "post": @@ -7796,7 +7979,9 @@ func (t *FeedPostgate) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -7858,21 +8043,24 @@ func (t *FeedPostgate_DisableRule) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("FeedPostgate_DisableRule: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -7887,7 +8075,9 @@ func (t *FeedPostgate_DisableRule) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/api/cbor_gen.go b/api/cbor_gen.go index 66b989efa..766bc3180 100644 --- a/api/cbor_gen.go +++ b/api/cbor_gen.go @@ -230,21 +230,24 @@ func (t *CreateOp) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("CreateOp: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 11) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Sig (string) (string) case "sig": @@ -335,7 +338,9 @@ func (t *CreateOp) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/api/chat/cbor_gen.go b/api/chat/cbor_gen.go index 0cb37eaac..44d077e75 100644 --- a/api/chat/cbor_gen.go +++ b/api/chat/cbor_gen.go @@ -97,21 +97,24 @@ func (t *ActorDeclaration) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("ActorDeclaration: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 13) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.LexiconTypeID (string) (string) case "$type": @@ -137,7 +140,9 @@ func (t *ActorDeclaration) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/atproto/data/cbor_gen.go b/atproto/data/cbor_gen.go index 89c5e0a5c..18280b707 100644 --- a/atproto/data/cbor_gen.go +++ b/atproto/data/cbor_gen.go @@ -78,21 +78,24 @@ func (t *GenericRecord) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("GenericRecord: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Type (string) (string) case "$type": @@ -107,7 +110,9 @@ func (t *GenericRecord) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -196,21 +201,24 @@ func (t *LegacyBlobSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LegacyBlobSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (string) (string) case "cid": @@ -236,7 +244,9 @@ func (t *LegacyBlobSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -359,21 +369,24 @@ func (t *BlobSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("BlobSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Ref (data.CIDLink) (struct) case "ref": @@ -435,7 +448,9 @@ func (t *BlobSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/events/cbor_gen.go b/events/cbor_gen.go index efb873446..8e13f8339 100644 --- a/events/cbor_gen.go +++ b/events/cbor_gen.go @@ -101,21 +101,24 @@ func (t *EventHeader) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("EventHeader: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 2) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.MsgType (string) (string) case "t": @@ -156,7 +159,9 @@ func (t *EventHeader) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -245,21 +250,24 @@ func (t *ErrorFrame) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("ErrorFrame: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Error (string) (string) case "error": @@ -285,7 +293,9 @@ func (t *ErrorFrame) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/go.mod b/go.mod index 4b942517b..66391db61 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/flosch/pongo2/v6 v6.0.0 github.com/go-redis/cache/v9 v9.0.0 github.com/goccy/go-json v0.10.2 + github.com/gocql/gocql v1.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/websocket v1.5.1 github.com/hashicorp/go-retryablehttp v0.7.5 @@ -53,7 +54,7 @@ require ( github.com/samber/slog-echo v1.8.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.25.7 - github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 @@ -79,6 +80,8 @@ require ( require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-redis/redis v6.15.9+incompatible // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/klauspost/compress v1.17.3 // indirect @@ -91,6 +94,7 @@ require ( github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + gopkg.in/inf.v0 v0.9.1 // indirect ) require ( diff --git a/go.sum b/go.sum index dfc251448..8cd2edd60 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk= github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -152,6 +156,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= +github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -189,6 +195,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -231,6 +239,8 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -616,8 +626,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= -github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c h1:UsxJNcLPfyLyVaA4iusIrsLAqJn/xh36Qgb8emqtXzk= -github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= +github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic= @@ -1061,6 +1071,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lex/util/cbor_gen.go b/lex/util/cbor_gen.go index dd057f1fd..84e78775e 100644 --- a/lex/util/cbor_gen.go +++ b/lex/util/cbor_gen.go @@ -78,21 +78,24 @@ func (t *CborChecker) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("CborChecker: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 5) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Type (string) (string) case "$type": @@ -107,7 +110,9 @@ func (t *CborChecker) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -196,21 +201,24 @@ func (t *LegacyBlob) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("LegacyBlob: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (string) (string) case "cid": @@ -236,7 +244,9 @@ func (t *LegacyBlob) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -359,21 +369,24 @@ func (t *BlobSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("BlobSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 8) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Ref (util.LexLink) (struct) case "ref": @@ -435,7 +448,9 @@ func (t *BlobSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/lex/util/cbor_gen_test.go b/lex/util/cbor_gen_test.go index 76bc90ee3..175f2cb00 100644 --- a/lex/util/cbor_gen_test.go +++ b/lex/util/cbor_gen_test.go @@ -254,21 +254,24 @@ func (t *basicSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("basicSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Bool (bool) (bool) case "bool": @@ -430,7 +433,9 @@ func (t *basicSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -567,21 +572,24 @@ func (t *basicSchemaInner) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("basicSchemaInner: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 6) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Arr ([]string) (slice) case "arr": @@ -680,7 +688,9 @@ func (t *basicSchemaInner) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -779,21 +789,24 @@ func (t *ipldSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("ipldSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.A (util.LexLink) (struct) case "a": @@ -840,7 +853,9 @@ func (t *ipldSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1059,21 +1074,24 @@ func (t *basicOldSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("basicOldSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.A (string) (string) case "a": @@ -1224,7 +1242,9 @@ func (t *basicOldSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1361,21 +1381,24 @@ func (t *basicOldSchemaInner) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("basicOldSchemaInner: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.H (string) (string) case "h": @@ -1474,7 +1497,9 @@ func (t *basicOldSchemaInner) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -1558,21 +1583,24 @@ func (t *ipldOldSchema) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("ipldOldSchema: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 8192) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.A (util.LexLink) (struct) case "a": @@ -1608,7 +1636,9 @@ func (t *ipldOldSchema) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/mst/cbor_gen.go b/mst/cbor_gen.go index 19dd2f75d..8f7e7dcc9 100644 --- a/mst/cbor_gen.go +++ b/mst/cbor_gen.go @@ -104,21 +104,24 @@ func (t *nodeData) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("nodeData: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Entries ([]mst.treeEntry) (slice) case "e": @@ -184,7 +187,9 @@ func (t *nodeData) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -312,21 +317,24 @@ func (t *treeEntry) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("treeEntry: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 1) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.KeySuffix ([]uint8) (slice) case "k": @@ -415,7 +423,9 @@ func (t *treeEntry) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/repo/cbor_gen.go b/repo/cbor_gen.go index 96bfb2e0d..02594508b 100644 --- a/repo/cbor_gen.go +++ b/repo/cbor_gen.go @@ -194,21 +194,24 @@ func (t *SignedCommit) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("SignedCommit: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -319,7 +322,9 @@ func (t *SignedCommit) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } @@ -477,21 +482,24 @@ func (t *UnsignedCommit) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("UnsignedCommit: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 7) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Did (string) (string) case "did": @@ -579,7 +587,9 @@ func (t *UnsignedCommit) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } diff --git a/util/labels/cbor_gen.go b/util/labels/cbor_gen.go index e777d7e45..b70d76aef 100644 --- a/util/labels/cbor_gen.go +++ b/util/labels/cbor_gen.go @@ -285,21 +285,24 @@ func (t *UnsignedLabel) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("UnsignedLabel: map struct too large (%d)", extra) } - var name string n := extra + nameBuf := make([]byte, 3) for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } - { - sval, err := cbg.ReadStringWithMax(cr, 1000000) - if err != nil { + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { return err } - - name = string(sval) + continue } - switch name { + switch string(nameBuf[:nameLen]) { // t.Cid (string) (string) case "cid": @@ -458,7 +461,9 @@ func (t *UnsignedLabel) UnmarshalCBOR(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it - cbg.ScanForLinks(r, func(cid.Cid) {}) + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } } } From e8c9d2e788367b4716c9eb4c82fc5488c348a83c Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 15 Nov 2024 14:04:34 +0000 Subject: [PATCH 109/111] identity: default dir with 100 max idle conns, and 1sec idle --- atproto/identity/identity.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/atproto/identity/identity.go b/atproto/identity/identity.go index 02e66f22c..2e67e0a5c 100644 --- a/atproto/identity/identity.go +++ b/atproto/identity/identity.go @@ -66,6 +66,11 @@ func DefaultDirectory() Directory { PLCURL: DefaultPLCURL, HTTPClient: http.Client{ Timeout: time.Second * 15, + Transport: &http.Transport{ + // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad. + IdleConnTimeout: time.Millisecond * 1000, + MaxIdleConns: 100, + }, }, Resolver: net.Resolver{ Dial: func(ctx context.Context, network, address string) (net.Conn, error) { From ed0a5c6480a725d6fb1153723c49810f25ebad3d Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 15 Nov 2024 14:05:05 +0000 Subject: [PATCH 110/111] identity: drop default HTTP timeout from 15s to 10s --- atproto/identity/identity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/identity/identity.go b/atproto/identity/identity.go index 2e67e0a5c..c8192e6d4 100644 --- a/atproto/identity/identity.go +++ b/atproto/identity/identity.go @@ -65,7 +65,7 @@ func DefaultDirectory() Directory { base := BaseDirectory{ PLCURL: DefaultPLCURL, HTTPClient: http.Client{ - Timeout: time.Second * 15, + Timeout: time.Second * 10, Transport: &http.Transport{ // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad. IdleConnTimeout: time.Millisecond * 1000, From d88346ab5df72a2a1809e27d9994403ced006c71 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Fri, 15 Nov 2024 14:05:20 +0000 Subject: [PATCH 111/111] identity: drop default DNS timeout from 5s to 3s --- atproto/identity/identity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atproto/identity/identity.go b/atproto/identity/identity.go index c8192e6d4..c0453b2af 100644 --- a/atproto/identity/identity.go +++ b/atproto/identity/identity.go @@ -74,7 +74,7 @@ func DefaultDirectory() Directory { }, Resolver: net.Resolver{ Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{Timeout: time.Second * 5} + d := net.Dialer{Timeout: time.Second * 3} return d.DialContext(ctx, network, address) }, },