refactored, added some tests, still a lot to do

This commit is contained in:
@s.roertgen 2025-03-31 23:14:50 +02:00
parent fec3e8e2a5
commit bf07264e17
7 changed files with 1152 additions and 181 deletions

4
go.mod
View file

@ -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
)

1
go.sum
View file

@ -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=

457
typesense30142/nostr_amb.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -0,0 +1 @@
// "hello learningResourceType.prefLabel:bla"

View file

@ -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
}