From bf07264e17ecf057d62cd57076b0e1a1010e01ed Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Mon, 31 Mar 2025 23:14:50 +0200 Subject: [PATCH] refactored, added some tests, still a lot to do --- go.mod | 4 + go.sum | 1 + typesense30142/nostr_amb.go | 457 ++++++++++++++++ typesense30142/nostr_amb_test.go | 637 +++++++++++++++++++++++ typesense30142/query.go | 84 +-- typesense30142/query_test.go | 1 + typesense30142/{nostramb.go => types.go} | 149 +----- 7 files changed, 1152 insertions(+), 181 deletions(-) create mode 100644 typesense30142/nostr_amb.go create mode 100644 typesense30142/nostr_amb_test.go create mode 100644 typesense30142/query_test.go rename typesense30142/{nostramb.go => types.go} (65%) diff --git a/go.mod b/go.mod index 160be26..9638567 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.1 require ( github.com/fiatjaf/eventstore v0.16.2 github.com/nbd-wtf/go-nostr v0.51.8 + github.com/stretchr/testify v1.10.0 ) require ( @@ -15,6 +16,7 @@ require ( github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.12 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -23,6 +25,7 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -31,4 +34,5 @@ require ( golang.org/x/arch v0.15.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/sys v0.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9d0ad4b..bd6d4ec 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,7 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/typesense30142/nostr_amb.go b/typesense30142/nostr_amb.go new file mode 100644 index 0000000..2a9d481 --- /dev/null +++ b/typesense30142/nostr_amb.go @@ -0,0 +1,457 @@ +package typesense30142 + +import ( + "encoding/json" + "fmt" + + "github.com/nbd-wtf/go-nostr" +) + +// converts a nostr event to stringified JSON +func eventToStringifiedJSON(event *nostr.Event) (string, error) { + jsonData, err := json.Marshal(event) + if err != nil { + return "", err + } + + jsonString := string(jsonData) + return jsonString, err +} + +// NostrToAMB converts a Nostr event of kind 30142 to AMB metadata +func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { + if event == nil { + return nil, fmt.Errorf("cannot convert nil event") + } + + eventRaw, err := eventToStringifiedJSON(event) + if err != nil { + return nil, fmt.Errorf("error converting event to JSON: %w", err) + } + amb := &AMBMetadata{ + Type: []string{"LearningResource"}, + NostrMetadata: NostrMetadata{ + EventID: event.ID, + EventPubKey: event.PubKey, + EventContent: event.Content, + EventCreatedAt: event.CreatedAt, + EventKind: event.Kind, + EventSig: event.Sig, + EventRaw: eventRaw, + }, + About: []*About{}, + Keywords: []string{}, + InLanguage: []string{}, + Creator: []*Creator{}, + Contributor: []*Contributor{}, + Publisher: []*Publisher{}, + Funder: []*Funder{}, + LearningResourceType: []*LearningResourceType{}, + Audience: []*Audience{}, + Teaches: []*Teaches{}, + Assesses: []*Assesses{}, + CompetencyRequired: []*CompetencyRequired{}, + EducationalLevel: []*EducationalLevel{}, + IsBasedOn: []*IsBasedOn{}, + IsPartOf: []*IsPartOf{}, + HasPart: []*HasPart{}, + Trailer: []*Trailer{}, + } + + for _, tag := range event.Tags { + if len(tag) < 2 { + continue + } + + switch tag[0] { + case "d": + if len(tag) >= 2 { + amb.D = tag[1] + amb.ID = event.ID + } + case "type": + if len(tag) >= 2 { + amb.Type = tag[1:] + } + + case "name": + if len(tag) >= 2 { + amb.Name = tag[1] + } + case "description": + if len(tag) >= 2 { + amb.Description = tag[1] + } + case "creator": + if len(tag) >= 3 { + creator := &Creator{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + }, + Affiliation: &Affiliation{}, + } + if len(tag) >= 4 { + creator.Type = tag[3] + } + if len(tag) >= 5 { + creator.Affiliation.Name = tag[4] + } + if len(tag) >= 6 { + creator.Affiliation.Type = tag[5] + } + if len(tag) >= 7 { + creator.Affiliation.ID = tag[6] + } + amb.Creator = append(amb.Creator, creator) + } + case "image": + if len(tag) >= 2 { + amb.Image = tag[1] + } + case "about": + if len(tag) >= 4 { + subject := &About{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + if len(tag) >= 5 { + subject.Type = tag[4] + } + amb.About = append(amb.About, subject) + } + case "learningResourceType": + if len(tag) >= 4 { + lrt := &LearningResourceType{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.LearningResourceType = append(amb.LearningResourceType, lrt) + } + case "inLanguage": + if len(tag) >= 2 { + amb.InLanguage = append(amb.InLanguage, tag[1]) + } + case "keywords": + if len(tag) >= 2 { + amb.Keywords = tag[1:] + } + case "license": + if len(tag) >= 3 { + amb.License = &License{ + ID: tag[1], + Name: tag[2], + } + } + case "datePublished": + if len(tag) >= 2 { + amb.DatePublished = tag[1] + } + case "dateCreated": + if len(tag) >= 2 { + amb.DateCreated = tag[1] + } + case "dateModified": + if len(tag) >= 2 { + amb.DateModified = tag[1] + } + case "publisher": + if len(tag) >= 3 { + publisher := &Publisher{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + }, + } + if len(tag) >= 4 { + publisher.Type = tag[3] + } + amb.Publisher = append(amb.Publisher, publisher) + } + case "contributor": + if len(tag) >= 4 { + contributor := &Contributor{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + Type: tag[3], + }, + Affiliation: &Affiliation{}, + } + if len(tag) >= 5 { + contributor.Affiliation.Name = tag[4] + } + if len(tag) >= 6 { + contributor.Affiliation.Type = tag[5] + } + if len(tag) >= 7 { + contributor.Affiliation.ID = tag[6] + } + amb.Contributor = append(amb.Contributor, contributor) + } + case "funder": + if len(tag) >= 3 { + funder := &Funder{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + }, + } + if len(tag) >= 4 { + funder.Type = tag[3] + } + amb.Funder = append(amb.Funder, funder) + } + case "isAccessibleForFree": + if len(tag) >= 2 && (tag[1] == "true" || tag[1] == "1") { + amb.IsAccessibleForFree = true + } + case "audience": + if len(tag) >= 4 { + audience := &Audience{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.Audience = append(amb.Audience, audience) + } + case "duration": + if len(tag) >= 2 { + amb.Duration = tag[1] + } + case "conditionsOfAccess": + if len(tag) == 4 { + conditionsOfAccess := &ConditionsOfAccess{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.ConditionsOfAccess = conditionsOfAccess + } + case "teaches": + if len(tag) >= 3 { + teaches := &Teaches{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.Teaches = append(amb.Teaches, teaches) + } + case "assesses": + if len(tag) >= 3 { + assesses := &Assesses{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.Assesses = append(amb.Assesses, assesses) + } + case "competencyRequired": + if len(tag) >= 3 { + competencyRequired := &CompetencyRequired{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.CompetencyRequired = append(amb.CompetencyRequired, competencyRequired) + } + case "educationalLevel": + if len(tag) >= 3 { + educationalLevel := &EducationalLevel{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.EducationalLevel = append(amb.EducationalLevel, educationalLevel) + } + case "interactivityType": + if len(tag) >= 3 { + interactivityType := &InteractivityType{ + ControlledVocabulary: ControlledVocabulary{ + ID: tag[1], + PrefLabel: tag[2], + InLanguage: tag[3], + }, + } + amb.InteractivityType = interactivityType + } + case "isBasedOn": + if len(tag) >= 3 { + isBasedOn := &IsBasedOn{ + ID: tag[1], + Name: tag[2], + } + amb.IsBasedOn = append(amb.IsBasedOn, isBasedOn) + } + case "isPartOf": + if len(tag) >= 4 { + isPartOf := &IsPartOf{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + Type: tag[3], + }, + } + amb.IsPartOf = append(amb.IsPartOf, isPartOf) + } + case "hasPart": + if len(tag) >= 4 { + hasPart := &HasPart{ + BaseEntity: BaseEntity{ + ID: tag[1], + Name: tag[2], + Type: tag[3], + }, + } + amb.HasPart = append(amb.HasPart, hasPart) + } + case "trailer": + if len(tag) >= 8 { + trailer := &Trailer{ + ContentUrl: tag[1], + Type: tag[2], + EncodingFormat: tag[3], + ContentSize: tag[4], + Sha256: tag[5], + EmbedUrl: tag[6], + Bitrate: tag[7], + } + amb.Trailer = append(amb.Trailer, trailer) + } + } + } + + return amb, nil +} + +// // NostrToAMB converts a Nostr event of kind 30142 to AMB metadata +// func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { +// eventRaw, _ := eventToStringifiedJSON(event) + +// amb := &AMBMetadata{ +// Type: "LearningResource", +// NostrMetadata: NostrMetadata{ +// EventID: event.ID, +// EventPubKey: event.PubKey, +// EventContent: event.Content, +// EventCreatedAt: event.CreatedAt, +// EventKind: event.Kind, +// EventSig: event.Sig, +// EventRaw: eventRaw, +// }, +// } + +// for _, tag := range event.Tags { +// if len(tag) < 2 { +// continue +// } + +// // TODO alle Attribute durchgehen für das parsen +// switch tag[0] { +// case "d": +// if len(tag) >= 2 { +// amb.ID = event.ID +// amb.D = tag[1] +// } +// case "name": +// if len(tag) >= 2 { +// amb.Name = tag[1] +// } +// case "description": +// if len(tag) >= 2 { +// amb.Description = tag[1] +// } +// case "creator": +// if len(tag) >= 2 { +// creator := &Creator{} +// creator.Name = tag[1] +// if len(tag) >= 3 { +// creator.ID = tag[2] +// } +// if len(tag) >= 4 { +// creator.Type = tag[3] +// } + +// amb.Creator = append(amb.Creator, creator) +// } +// case "image": +// if len(tag) >= 2 { +// amb.Image = tag[1] +// } +// case "about": +// if len(tag) >= 3 { +// subject := &About{} +// subject.PrefLabel = tag[1] +// subject.InLanguage = tag[2] +// if len(tag) >= 4 { +// subject.ID = tag[3] +// } +// amb.About = append(amb.About, subject) +// } +// case "learningResourceType": +// if len(tag) >= 3 { +// lrt := &LearningResourceType{} +// lrt.PrefLabel = tag[1] +// lrt.InLanguage = tag[2] +// if len(tag) >= 4 { +// lrt.ID = tag[3] +// } +// amb.LearningResourceType = append(amb.LearningResourceType, lrt) +// } +// case "inLanguage": +// if len(tag) >= 2 { +// amb.InLanguage = append(amb.InLanguage, tag[1]) +// } +// case "keywords": +// if len(tag) >= 2 { +// amb.Keywords = tag[1:] +// } +// case "license": +// if len(tag) >= 3 { +// amb.License = &License{} +// amb.License.ID = tag[1] +// amb.License.Name = tag[2] + +// } +// case "datePublished": +// if len(tag) >= 2 { +// amb.DatePublished = tag[1] +// } +// } +// } + +// return amb, nil +// } + +// converts a stringified JSON event to a nostr.Event +func StringifiedJSONToNostrEvent(jsonString string) (nostr.Event, error) { + var event nostr.Event + err := json.Unmarshal([]byte(jsonString), &event) + if err != nil { + return nostr.Event{}, err + } + return event, nil +} + +func Hello(name string) (string, error) { + resp := "Hello " + name + " nice to meet you" + return resp, nil +} diff --git a/typesense30142/nostr_amb_test.go b/typesense30142/nostr_amb_test.go new file mode 100644 index 0000000..4a01b99 --- /dev/null +++ b/typesense30142/nostr_amb_test.go @@ -0,0 +1,637 @@ +package typesense30142 + +import ( + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" +) + + +func TestNostrToAmbEvent(t *testing.T) { + assert := assert.New(t) + sk := nostr.GeneratePrivateKey() + event := &nostr.Event{ + Content: "", + CreatedAt: nostr.Now(), + Kind: 30142, + Tags: nostr.Tags{ + {"d", "test-resource-id"}, + {"name", "Test Resource"}, + {"description", "This is a test resource"}, + }, + } + + event.Sign(sk) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(event.ID, amb.EventID) + assert.Equal(event.PubKey, amb.EventPubKey) + assert.Equal(event.Content, amb.EventContent) + assert.Equal(event.Kind, amb.EventKind) + assert.Equal(event.Sig, amb.EventSig) + assert.Equal(event.CreatedAt, amb.EventCreatedAt) + +} + + +// Helper function to create a test event with a specific tag +func createTestEvent(tags nostr.Tags) *nostr.Event { + sk := nostr.GeneratePrivateKey() + event := &nostr.Event{ + Content: "", + CreatedAt: nostr.Now(), + Kind: 30142, + Tags: tags, + } + event.Sign(sk) + return event +} + +func TestNostrToAMB_BasicFields(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"name", "Test Resource"}, + {"description", "This is a test resource"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(event.ID, amb.ID) + assert.Equal("test-resource-id", amb.D) + assert.Equal("Test Resource", amb.Name) + assert.Equal("This is a test resource", amb.Description) + + assert.Equal(event.ID, amb.EventID) + assert.Equal(event.PubKey, amb.EventPubKey) + assert.Equal(event.Content, amb.EventContent) + assert.Equal(event.CreatedAt, amb.EventCreatedAt) + assert.Equal(event.Kind, amb.EventKind) + assert.Equal(event.Sig, amb.EventSig) +} + +func TestNostrToAMB_About(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"about", "http://w3id.org/kim/schulfaecher/s1009", "Französisch", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.NotEmpty(amb.About) + assert.Equal(1, len(amb.About)) + assert.Equal("http://w3id.org/kim/schulfaecher/s1009", amb.About[0].ID) + assert.Equal("Französisch", amb.About[0].PrefLabel) + assert.Equal("de", amb.About[0].InLanguage) +} + +func TestNostrToAMB_Keywords(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"keywords", "Französisch", "Niveau A2", "Sprache"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(3, len(amb.Keywords)) + assert.Contains(amb.Keywords, "Französisch") + assert.Contains(amb.Keywords, "Niveau A2") + assert.Contains(amb.Keywords, "Sprache") +} + +func TestNostrToAMB_InLanguage(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"inLanguage", "fr"}, + {"inLanguage", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.InLanguage)) + assert.Contains(amb.InLanguage, "fr") + assert.Contains(amb.InLanguage, "de") +} + +func TestNostrToAMB_Image(t *testing.T) { + assert := assert.New(t) + + imageURL := "https://www.tutory.de/worksheet/fbbadf1a-145a-463d-9a43-1ae9965c86b9.jpg?width=1000" + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"image", imageURL}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(imageURL, amb.Image) +} + +func TestNostrToAMB_Creator(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"creator", "http://author1.org", "Autorin 1", "Person"}, + {"creator", "http://author2.org", "Autorin 2", "Person"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Creator)) + + assert.Equal("http://author1.org", amb.Creator[0].ID) + assert.Equal("Autorin 1", amb.Creator[0].Name) + assert.Equal("Person", amb.Creator[0].Type) + + assert.Equal("http://author2.org", amb.Creator[1].ID) + assert.Equal("Autorin 2", amb.Creator[1].Name) + assert.Equal("Person", amb.Creator[1].Type) +} + +func TestNostrToAMB_Contributor(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"contributor", "http://author1.org", "Autorin 1", "Person"}, + {"contributor", "http://author2.org", "Autorin 2", "Person"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Contributor)) + + assert.Equal("http://author1.org", amb.Contributor[0].ID) + assert.Equal("Autorin 1", amb.Contributor[0].Name) + assert.Equal("Person", amb.Contributor[0].Type) + + assert.Equal("http://author2.org", amb.Contributor[1].ID) + assert.Equal("Autorin 2", amb.Contributor[1].Name) + assert.Equal("Person", amb.Contributor[1].Type) +} + +func TestNostrToAMB_Dates(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"dateCreated", "2019-07-02"}, + {"datePublished", "2019-07-03"}, + {"dateModified", "2019-07-04"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal("2019-07-02", amb.DateCreated) + assert.Equal("2019-07-03", amb.DatePublished) + assert.Equal("2019-07-04", amb.DateModified) +} + +func TestNostrToAMB_Publisher(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"publisher", "http://publisher1.org", "Publisher 1", "Person"}, + {"publisher", "http://publisher2.org", "Publisher 2", "Organization"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Publisher)) + + assert.Equal("http://publisher1.org", amb.Publisher[0].ID) + assert.Equal("Publisher 1", amb.Publisher[0].Name) + assert.Equal("Person", amb.Publisher[0].Type) + + assert.Equal("http://publisher2.org", amb.Publisher[1].ID) + assert.Equal("Publisher 2", amb.Publisher[1].Name) + assert.Equal("Organization", amb.Publisher[1].Type) +} + +func TestNostrToAMB_Funder(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"funder", "http://funder1.org", "Funder 1", "Person"}, + {"funder", "http://funder2.org", "Funder 2", "Organization"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Funder)) + + assert.Equal("http://funder1.org", amb.Funder[0].ID) + assert.Equal("Funder 1", amb.Funder[0].Name) + assert.Equal("Person", amb.Funder[0].Type) + + assert.Equal("http://funder2.org", amb.Funder[1].ID) + assert.Equal("Funder 2", amb.Funder[1].Name) + assert.Equal("Organization", amb.Funder[1].Type) +} + +func TestNostrToAMB_IsAccessibleForFree(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"isAccessibleForFree", "true"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.True(amb.IsAccessibleForFree) + + // Test with "false" value + tags = nostr.Tags{ + {"d", "test-resource-id"}, + {"isAccessibleForFree", "false"}, + } + event = createTestEvent(tags) + + amb, err = NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.False(amb.IsAccessibleForFree) +} + +func TestNostrToAMB_License(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"license", "https://creativecommons.org/publicdomain/zero/1.0/", "CC-0"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.NotNil(amb.License) + assert.Equal("https://creativecommons.org/publicdomain/zero/1.0/", amb.License.ID) + assert.Equal("CC-0", amb.License.Name) +} + +func TestNostrToAMB_ConditionsOfAccess(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"conditionsOfAccess", "http://w3id.org/kim/conditionsOfAccess/no_login", "Kein Login", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.NotNil(amb.ConditionsOfAccess) + assert.Equal("http://w3id.org/kim/conditionsOfAccess/no_login", amb.ConditionsOfAccess.ID) + assert.Equal("Kein Login", amb.ConditionsOfAccess.PrefLabel) + assert.Equal("de", amb.ConditionsOfAccess.InLanguage) +} + +func TestNostrToAMB_LearningResourceType(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"learningResourceType", "http://w3id.org/openeduhub/vocabs/new_lrt/video", "Video", "de"}, + {"learningResourceType", "http://w3id.org/openeduhub/vocabs/new_lrt/tutorial", "Tutorial", "en"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.LearningResourceType)) + + assert.Equal("http://w3id.org/openeduhub/vocabs/new_lrt/video", amb.LearningResourceType[0].ID) + assert.Equal("Video", amb.LearningResourceType[0].PrefLabel) + assert.Equal("de", amb.LearningResourceType[0].InLanguage) + + assert.Equal("http://w3id.org/openeduhub/vocabs/new_lrt/tutorial", amb.LearningResourceType[1].ID) + assert.Equal("Tutorial", amb.LearningResourceType[1].PrefLabel) + assert.Equal("en", amb.LearningResourceType[1].InLanguage) +} + +func TestNostrToAMB_Audience(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"audience", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student", "Schüler:in", "de"}, + {"audience", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/teacher", "Lehrer:in", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Audience)) + + assert.Equal("http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student", amb.Audience[0].ID) + assert.Equal("Schüler:in", amb.Audience[0].PrefLabel) + assert.Equal("de", amb.Audience[0].InLanguage) + + assert.Equal("http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/teacher", amb.Audience[1].ID) + assert.Equal("Lehrer:in", amb.Audience[1].PrefLabel) + assert.Equal("de", amb.Audience[1].InLanguage) +} + +func TestNostrToAMB_Teaches(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"teaches", "http://awesome-skills.org/1", "Zuhören", "de"}, + {"teaches", "http://awesome-skills.org/2", "Sprechen", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Teaches)) + + assert.Equal("http://awesome-skills.org/1", amb.Teaches[0].ID) + assert.Equal("Zuhören", amb.Teaches[0].PrefLabel) + assert.Equal("de", amb.Teaches[0].InLanguage) + + assert.Equal("http://awesome-skills.org/2", amb.Teaches[1].ID) + assert.Equal("Sprechen", amb.Teaches[1].PrefLabel) + assert.Equal("de", amb.Teaches[1].InLanguage) +} + +func TestNostrToAMB_Assesses(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"assesses", "http://awesome-skills.org/1", "Hörverständnis", "de"}, + {"assesses", "http://awesome-skills.org/2", "Grammatik", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.Assesses)) + + assert.Equal("http://awesome-skills.org/1", amb.Assesses[0].ID) + assert.Equal("Hörverständnis", amb.Assesses[0].PrefLabel) + assert.Equal("de", amb.Assesses[0].InLanguage) + + assert.Equal("http://awesome-skills.org/2", amb.Assesses[1].ID) + assert.Equal("Grammatik", amb.Assesses[1].PrefLabel) + assert.Equal("de", amb.Assesses[1].InLanguage) +} + +func TestNostrToAMB_CompetencyRequired(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"competencyRequired", "http://awesome-skills.org/1", "Basisvokabular", "de"}, + {"competencyRequired", "http://awesome-skills.org/2", "Grundkenntnisse", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.CompetencyRequired)) + + assert.Equal("http://awesome-skills.org/1", amb.CompetencyRequired[0].ID) + assert.Equal("Basisvokabular", amb.CompetencyRequired[0].PrefLabel) + assert.Equal("de", amb.CompetencyRequired[0].InLanguage) + + assert.Equal("http://awesome-skills.org/2", amb.CompetencyRequired[1].ID) + assert.Equal("Grundkenntnisse", amb.CompetencyRequired[1].PrefLabel) + assert.Equal("de", amb.CompetencyRequired[1].InLanguage) +} + +func TestNostrToAMB_EducationalLevel(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"educationalLevel", "https://w3id.org/kim/educationalLevel/level_2", "Sekundarstufe 1", "de"}, + {"educationalLevel", "https://w3id.org/kim/educationalLevel/level_3", "Sekundarstufe 2", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.EducationalLevel)) + + assert.Equal("https://w3id.org/kim/educationalLevel/level_2", amb.EducationalLevel[0].ID) + assert.Equal("Sekundarstufe 1", amb.EducationalLevel[0].PrefLabel) + assert.Equal("de", amb.EducationalLevel[0].InLanguage) + + assert.Equal("https://w3id.org/kim/educationalLevel/level_3", amb.EducationalLevel[1].ID) + assert.Equal("Sekundarstufe 2", amb.EducationalLevel[1].PrefLabel) + assert.Equal("de", amb.EducationalLevel[1].InLanguage) +} + +func TestNostrToAMB_InteractivityType(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"interactivityType", "http://purl.org/dcx/lrmi-vocabs/interactivityType/active", "aktiv", "de"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.NotNil(amb.InteractivityType) + assert.Equal("http://purl.org/dcx/lrmi-vocabs/interactivityType/active", amb.InteractivityType.ID) + assert.Equal("aktiv", amb.InteractivityType.PrefLabel) + assert.Equal("de", amb.InteractivityType.InLanguage) +} + +func TestNostrToAMB_IsBasedOn(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"isBasedOn", "http://an-awesome-resource.org", "Französisch I"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(1, len(amb.IsBasedOn)) + assert.Equal("http://an-awesome-resource.org", amb.IsBasedOn[0].ID) + assert.Equal("Französisch I", amb.IsBasedOn[0].Name) +} + +func TestNostrToAMB_IsPartOf(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"isPartOf", "http://whole.org", "Whole", "PresentationDigitalDocument"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(1, len(amb.IsPartOf)) + assert.Equal("http://whole.org", amb.IsPartOf[0].ID) + assert.Equal("Whole", amb.IsPartOf[0].Name) + assert.Equal("PresentationDigitalDocument", amb.IsPartOf[0].Type) +} + +func TestNostrToAMB_HasPart(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"hasPart", "http://part1.org", "Part 1", "LearningResource"}, + {"hasPart", "http://part2.org", "Part 2", "LearningResource"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(2, len(amb.HasPart)) + assert.Equal("http://part1.org", amb.HasPart[0].ID) + assert.Equal("Part 1", amb.HasPart[0].Name) + assert.Equal("LearningResource", amb.HasPart[0].Type) + + assert.Equal("http://part2.org", amb.HasPart[1].ID) + assert.Equal("Part 2", amb.HasPart[1].Name) + assert.Equal("LearningResource", amb.HasPart[1].Type) +} + +func TestNostrToAMB_Duration(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"duration", "PT30M"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal("PT30M", amb.Duration) +} + +func TestNostrToAMB_Trailer(t *testing.T) { + assert := assert.New(t) + + tags := nostr.Tags{ + {"d", "test-resource-id"}, + {"trailer", "https://example.com/video.mp4", "Video", "video/mp4", "10MB", "abc123", "https://example.com/embed", "1Mbps"}, + } + event := createTestEvent(tags) + + amb, err := NostrToAMB(event) + + assert.NoError(err) + assert.NotNil(amb) + + assert.Equal(1, len(amb.Trailer)) + assert.Equal("https://example.com/video.mp4", amb.Trailer[0].ContentUrl) + assert.Equal("Video", amb.Trailer[0].Type) + assert.Equal("video/mp4", amb.Trailer[0].EncodingFormat) + assert.Equal("10MB", amb.Trailer[0].ContentSize) + assert.Equal("abc123", amb.Trailer[0].Sha256) + assert.Equal("https://example.com/embed", amb.Trailer[0].EmbedUrl) + assert.Equal("1Mbps", amb.Trailer[0].Bitrate) +} + diff --git a/typesense30142/query.go b/typesense30142/query.go index ce226df..6c0d7d9 100644 --- a/typesense30142/query.go +++ b/typesense30142/query.go @@ -32,6 +32,48 @@ func (ts *TSBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan return ch, nil } +// SearchResources searches for resources and returns both the AMB metadata and converted Nostr events +func (ts *TSBackend) SearchResources(searchStr string) ([]nostr.Event, error) { + parsedQuery := ParseSearchQuery(searchStr) + + mainQuery, params, err := BuildTypesenseQuery(parsedQuery) + if err != nil { + return nil, fmt.Errorf("error building Typesense query: %v", err) + } + + // URL encode the main query + encodedQuery := url.QueryEscape(mainQuery) + + // Default fields to search in + queryBy := "name,description" + + // Start building the search URL + searchURL := fmt.Sprintf("%s/collections/%s/documents/search?q=%s&query_by=%s", + ts.Host, ts.CollectionName, encodedQuery, queryBy) + + // Add additional parameters + for key, value := range params { + searchURL += fmt.Sprintf("&%s=%s", key, url.QueryEscape(value)) + } + + // Debug information + fmt.Printf("Search URL: %s\n", searchURL) + + resp, err := ts.makehttpRequest(searchURL, http.MethodGet, nil) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + // Check for errors + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("search failed with status code %d: %s", resp.StatusCode, string(body)) + } + + return parseSearchResponse(body) +} + // SearchQuery represents a parsed search query with raw terms and field filters type SearchQuery struct { RawTerms []string @@ -109,48 +151,6 @@ func BuildTypesenseQuery(query SearchQuery) (string, map[string]string, error) { return mainQuery, params, nil } -// SearchResources searches for resources and returns both the AMB metadata and converted Nostr events -func (ts *TSBackend) SearchResources(searchStr string) ([]nostr.Event, error) { - parsedQuery := ParseSearchQuery(searchStr) - - mainQuery, params, err := BuildTypesenseQuery(parsedQuery) - if err != nil { - return nil, fmt.Errorf("error building Typesense query: %v", err) - } - - // URL encode the main query - encodedQuery := url.QueryEscape(mainQuery) - - // Default fields to search in - queryBy := "name,description" - - // Start building the search URL - searchURL := fmt.Sprintf("%s/collections/%s/documents/search?q=%s&query_by=%s", - ts.Host, ts.CollectionName, encodedQuery, queryBy) - - // Add additional parameters - for key, value := range params { - searchURL += fmt.Sprintf("&%s=%s", key, url.QueryEscape(value)) - } - - // Debug information - fmt.Printf("Search URL: %s\n", searchURL) - - resp, err := ts.makehttpRequest(searchURL, http.MethodGet, nil) - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error reading response body: %v", err) - } - - // Check for errors - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("search failed with status code %d: %s", resp.StatusCode, string(body)) - } - - return parseSearchResponse(body) -} - func parseSearchResponse(responseBody []byte) ([]nostr.Event, error) { var searchResponse SearchResponse if err := json.Unmarshal(responseBody, &searchResponse); err != nil { diff --git a/typesense30142/query_test.go b/typesense30142/query_test.go new file mode 100644 index 0000000..87187d6 --- /dev/null +++ b/typesense30142/query_test.go @@ -0,0 +1 @@ +// "hello learningResourceType.prefLabel:bla" diff --git a/typesense30142/nostramb.go b/typesense30142/types.go similarity index 65% rename from typesense30142/nostramb.go rename to typesense30142/types.go index 0dabdf9..5a7b45a 100644 --- a/typesense30142/nostramb.go +++ b/typesense30142/types.go @@ -1,10 +1,6 @@ package typesense30142 -import ( - "encoding/json" - - "github.com/nbd-wtf/go-nostr" -) +import "github.com/nbd-wtf/go-nostr" // BaseEntity contains common fields used across many entity types type BaseEntity struct { @@ -45,7 +41,8 @@ type Creator struct { // Contributor represents someone who contributed to the content type Contributor struct { BaseEntity - HonoricPrefix string `json:"honoricPrefix,omitempty"` + Affiliation *Affiliation `json:"affiliation,omitempty"` + HonoricPrefix string `json:"honoricPrefix,omitempty"` } // Publisher represents the publisher of the content @@ -80,23 +77,17 @@ type Audience struct { // Teaches represents what the content teaches type Teaches struct { - ID string `json:"id"` - LabeledEntity - LanguageEntity + ControlledVocabulary } // Assesses represents what the content assesses type Assesses struct { - ID string `json:"id"` - LabeledEntity - LanguageEntity + ControlledVocabulary } // CompetencyRequired represents required competencies type CompetencyRequired struct { - ID string `json:"id"` - LabeledEntity - LanguageEntity + ControlledVocabulary } // EducationalLevel represents the educational level @@ -111,6 +102,7 @@ type InteractivityType struct { // IsBasedOn represents a reference to source material type IsBasedOn struct { + ID string `json:"id"` Type string `json:"type,omitempty"` Name string `json:"name"` Creator *Creator `json:"creator,omitempty"` @@ -156,11 +148,11 @@ type NostrMetadata struct { // AMBMetadata represents the full metadata structure type AMBMetadata struct { - // Event ID - ID string `json:"id"` + // Event ID + ID string `json:"id"` // Document ID D string `json:"d"` - Type string `json:"type"` + Type []string `json:"type"` Name string `json:"name"` Description string `json:"description,omitempty"` About []*About `json:"about,omitempty"` @@ -205,124 +197,3 @@ type AMBMetadata struct { // Nostr integration NostrMetadata `json:",inline"` } - -// converts a nostr event to stringified JSON -func eventToStringifiedJSON(event *nostr.Event) (string, error) { - jsonData, err := json.Marshal(event) - if err != nil { - return "", err - } - - jsonString := string(jsonData) - return jsonString, err -} - -// NostrToAMB converts a Nostr event of kind 30142 to AMB metadata -func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { - eventRaw, _ := eventToStringifiedJSON(event) - - amb := &AMBMetadata{ - Type: "LearningResource", - NostrMetadata: NostrMetadata{ - EventID: event.ID, - EventPubKey: event.PubKey, - EventContent: event.Content, - EventCreatedAt: event.CreatedAt, - EventKind: event.Kind, - EventSig: event.Sig, - EventRaw: eventRaw, - }, - } - - for _, tag := range event.Tags { - if len(tag) < 2 { - continue - } - - // TODO alle Attribute durchgehen für das parsen - switch tag[0] { - case "d": - if len(tag) >= 2 { - amb.ID = event.ID - amb.D = tag[1] - } - case "name": - if len(tag) >= 2 { - amb.Name = tag[1] - } - case "description": - if len(tag) >= 2 { - amb.Description = tag[1] - } - case "creator": - if len(tag) >= 2 { - creator := &Creator{} - creator.Name = tag[1] - if len(tag) >= 3 { - creator.ID = tag[2] - } - if len(tag) >= 4 { - creator.Type = tag[3] - } - - amb.Creator = append(amb.Creator, creator) - } - case "image": - if len(tag) >= 2 { - amb.Image = tag[1] - } - case "about": - if len(tag) >= 3 { - subject := &About{} - subject.PrefLabel = tag[1] - subject.InLanguage = tag[2] - if len(tag) >= 4 { - subject.ID = tag[3] - } - amb.About = append(amb.About, subject) - } - case "learningResourceType": - if len(tag) >= 3 { - lrt := &LearningResourceType{} - lrt.PrefLabel = tag[1] - lrt.InLanguage = tag[2] - if len(tag) >= 4 { - lrt.ID = tag[3] - } - amb.LearningResourceType = append(amb.LearningResourceType, lrt) - } - case "inLanguage": - if len(tag) >= 2 { - amb.InLanguage = append(amb.InLanguage, tag[1]) - } - case "keywords": - if len(tag) >= 2 { - amb.Keywords = tag[1:] - } - case "license": - if len(tag) >= 3 { - amb.License = &License{} - amb.License.ID = tag[1] - amb.License.Name = tag[2] - - } - case "datePublished": - if len(tag) >= 2 { - amb.DatePublished = tag[1] - } - } - } - - return amb, nil -} - -// converts a stringified JSON event to a nostr.Event -func StringifiedJSONToNostrEvent(jsonString string) (nostr.Event, error) { - var event nostr.Event - err := json.Unmarshal([]byte(jsonString), &event) - if err != nil { - return nostr.Event{}, err - } - return event, nil -} -