mirror of
https://github.com/edufeed-org/eventstore.git
synced 2025-12-10 00:34:32 +00:00
Basic Eventstore for 30142 seems to work
This commit is contained in:
commit
fec3e8e2a5
8 changed files with 956 additions and 0 deletions
34
go.mod
Normal file
34
go.mod
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
module github.com/sroertgen/eventstore
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fiatjaf/eventstore v0.16.2
|
||||||
|
github.com/nbd-wtf/go-nostr v0.51.8
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.13.1 // indirect
|
||||||
|
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/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
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
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/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
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
|
||||||
|
)
|
||||||
81
go.sum
Normal file
81
go.sum
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||||
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
|
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||||
|
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
|
github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
|
||||||
|
github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
|
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/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=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
34
typesense30142/delete.go
Normal file
34
typesense30142/delete.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete a nostr event from the index
|
||||||
|
func (ts *TSBackend) DeleteEvent(ctx context.Context, event *nostr.Event) error {
|
||||||
|
fmt.Println("deleting event")
|
||||||
|
d := event.Tags.GetD()
|
||||||
|
|
||||||
|
url := fmt.Sprintf(
|
||||||
|
"%s/collections/%s/documents?filter_by=d:=%s&&eventPubKey:=%s",
|
||||||
|
ts.Host, ts.CollectionName, d, event.PubKey)
|
||||||
|
|
||||||
|
resp, err := ts.makehttpRequest(url, http.MethodDelete, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any status code other than 200 is an error
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
30
typesense30142/lib.go
Normal file
30
typesense30142/lib.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/eventstore"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ eventstore.Store = (*TSBackend)(nil)
|
||||||
|
|
||||||
|
type TSBackend struct {
|
||||||
|
ApiKey string
|
||||||
|
Host string
|
||||||
|
CollectionName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TSBackend) Init() error {
|
||||||
|
err := ts.CheckOrCreateCollection()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to check/create collection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TSBackend) Close() {}
|
||||||
|
|
||||||
|
func (ts *TSBackend) SaveEvent(ctx context.Context, event *nostr.Event) error {return nil}
|
||||||
328
typesense30142/nostramb.go
Normal file
328
typesense30142/nostramb.go
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseEntity contains common fields used across many entity types
|
||||||
|
type BaseEntity struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlledVocabulary struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
PrefLabel string `json:"prefLabel"`
|
||||||
|
InLanguage string `json:"inLanguage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LanguageEntity adds language support to entities
|
||||||
|
type LanguageEntity struct {
|
||||||
|
InLanguage string `json:"inLanguage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabeledEntity adds prefLabel to entities
|
||||||
|
type LabeledEntity struct {
|
||||||
|
PrefLabel string `json:"prefLabel,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// About represents a topic or subject
|
||||||
|
type About struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creator represents the creator of content
|
||||||
|
type Creator struct {
|
||||||
|
BaseEntity
|
||||||
|
Affiliation *Affiliation `json:"affiliation,omitempty"`
|
||||||
|
HonoricPrefix string `json:"honoricPrefix,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contributor represents someone who contributed to the content
|
||||||
|
type Contributor struct {
|
||||||
|
BaseEntity
|
||||||
|
HonoricPrefix string `json:"honoricPrefix,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publisher represents the publisher of the content
|
||||||
|
type Publisher struct {
|
||||||
|
BaseEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funder represents an entity that funded the content
|
||||||
|
type Funder struct {
|
||||||
|
BaseEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affiliation represents an organization affiliation
|
||||||
|
type Affiliation struct {
|
||||||
|
BaseEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConditionsOfAccess represents access conditions
|
||||||
|
type ConditionsOfAccess struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// LearningResourceType categorizes the learning resource
|
||||||
|
type LearningResourceType struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience represents the target audience
|
||||||
|
type Audience struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teaches represents what the content teaches
|
||||||
|
type Teaches struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
LabeledEntity
|
||||||
|
LanguageEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assesses represents what the content assesses
|
||||||
|
type Assesses struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
LabeledEntity
|
||||||
|
LanguageEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompetencyRequired represents required competencies
|
||||||
|
type CompetencyRequired struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
LabeledEntity
|
||||||
|
LanguageEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// EducationalLevel represents the educational level
|
||||||
|
type EducationalLevel struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// InteractivityType represents the type of interactivity
|
||||||
|
type InteractivityType struct {
|
||||||
|
ControlledVocabulary
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBasedOn represents a reference to source material
|
||||||
|
type IsBasedOn struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Creator *Creator `json:"creator,omitempty"`
|
||||||
|
License *License `json:"license,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPartOf represents a parent relationship
|
||||||
|
type IsPartOf struct {
|
||||||
|
BaseEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
type HasPart struct {
|
||||||
|
BaseEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailer represents a media trailer
|
||||||
|
type Trailer struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ContentUrl string `json:"contentUrl"`
|
||||||
|
EncodingFormat string `json:"encodingFormat"`
|
||||||
|
ContentSize string `json:"contentSize,omitempty"`
|
||||||
|
Sha256 string `json:"sha256,omitempty"`
|
||||||
|
EmbedUrl string `json:"embedUrl,omitempty"`
|
||||||
|
Bitrate string `json:"bitrate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// License represents the content license
|
||||||
|
type License struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NostrMetadata contains Nostr-specific metadata
|
||||||
|
type NostrMetadata struct {
|
||||||
|
EventID string `json:"eventID"`
|
||||||
|
EventKind int `json:"eventKind"`
|
||||||
|
EventPubKey string `json:"eventPubKey"`
|
||||||
|
EventSig string `json:"eventSignature"`
|
||||||
|
EventCreatedAt nostr.Timestamp `json:"eventCreatedAt"`
|
||||||
|
EventContent string `json:"eventContent"`
|
||||||
|
EventRaw string `json:"eventRaw"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMBMetadata represents the full metadata structure
|
||||||
|
type AMBMetadata struct {
|
||||||
|
// Event ID
|
||||||
|
ID string `json:"id"`
|
||||||
|
// Document ID
|
||||||
|
D string `json:"d"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
About []*About `json:"about,omitempty"`
|
||||||
|
Keywords []string `json:"keywords,omitempty"`
|
||||||
|
InLanguage []string `json:"inLanguage,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
Trailer []*Trailer `json:"trailer,omitempty"`
|
||||||
|
|
||||||
|
// Provenience
|
||||||
|
Creator []*Creator `json:"creator,omitempty"`
|
||||||
|
Contributor []*Contributor `json:"contributor,omitempty"`
|
||||||
|
DateCreated string `json:"dateCreated,omitempty"`
|
||||||
|
DatePublished string `json:"datePublished,omitempty"`
|
||||||
|
DateModified string `json:"dateModified,omitempty"`
|
||||||
|
Publisher []*Publisher `json:"publisher,omitempty"`
|
||||||
|
Funder []*Funder `json:"funder,omitempty"`
|
||||||
|
|
||||||
|
// Costs and Rights
|
||||||
|
IsAccessibleForFree bool `json:"isAccessibleForFree,omitempty"`
|
||||||
|
License *License `json:"license,omitempty"`
|
||||||
|
ConditionsOfAccess *ConditionsOfAccess `json:"conditionsOfAccess,omitempty"`
|
||||||
|
|
||||||
|
// Educational metadata
|
||||||
|
LearningResourceType []*LearningResourceType `json:"learningResourceType,omitempty"`
|
||||||
|
Audience []*Audience `json:"audience,omitempty"`
|
||||||
|
Teaches []*Teaches `json:"teaches,omitempty"`
|
||||||
|
Assesses []*Assesses `json:"assesses,omitempty"`
|
||||||
|
CompetencyRequired []*CompetencyRequired `json:"competencyRequired,omitempty"`
|
||||||
|
EducationalLevel []*EducationalLevel `json:"educationalLevel,omitempty"`
|
||||||
|
InteractivityType *InteractivityType `json:"interactivityType,omitempty"`
|
||||||
|
|
||||||
|
// Relation
|
||||||
|
IsBasedOn []*IsBasedOn `json:"isBasedOn,omitempty"`
|
||||||
|
IsPartOf []*IsPartOf `json:"isPartOf,omitempty"`
|
||||||
|
HasPart []*HasPart `json:"hasPart,omitempty"`
|
||||||
|
|
||||||
|
// Technical
|
||||||
|
Duration string `json:"duration,omitempty"`
|
||||||
|
// TODO Encoding ``
|
||||||
|
// TODO Caption
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
195
typesense30142/query.go
Normal file
195
typesense30142/query.go
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ts *TSBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
||||||
|
ch := make(chan *nostr.Event)
|
||||||
|
|
||||||
|
nostrs, err := ts.SearchResources(filter.Search)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Search failed: %v", err)
|
||||||
|
return ch, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for _, evt := range nostrs {
|
||||||
|
ch <- &evt
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchQuery represents a parsed search query with raw terms and field filters
|
||||||
|
type SearchQuery struct {
|
||||||
|
RawTerms []string
|
||||||
|
FieldFilters map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSearchQuery parses a search string with support for quoted terms and field:value pairs
|
||||||
|
func ParseSearchQuery(searchStr string) SearchQuery {
|
||||||
|
var query SearchQuery
|
||||||
|
query.RawTerms = []string{}
|
||||||
|
query.FieldFilters = make(map[string]string)
|
||||||
|
|
||||||
|
// Regular expression to match quoted strings and field:value pairs
|
||||||
|
// This regex handles:
|
||||||
|
// 1. Quoted strings (preserving spaces and everything inside)
|
||||||
|
// 2. Field:value pairs
|
||||||
|
// 3. Regular words
|
||||||
|
re := regexp.MustCompile(`"([^"]+)"|(\S+\.\S+):(\S+)|(\S+)`)
|
||||||
|
matches := re.FindAllStringSubmatch(searchStr, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
if match[1] != "" {
|
||||||
|
// This is a quoted string, add it to raw terms
|
||||||
|
query.RawTerms = append(query.RawTerms, match[1])
|
||||||
|
} else if match[2] != "" && match[3] != "" {
|
||||||
|
// This is a field:value pair
|
||||||
|
fieldName := match[2]
|
||||||
|
fieldValue := match[3]
|
||||||
|
query.FieldFilters[fieldName] = fieldValue
|
||||||
|
} else if match[4] != "" {
|
||||||
|
// This is a regular word, check if it's a simple field:value
|
||||||
|
parts := strings.SplitN(match[4], ":", 2)
|
||||||
|
if len(parts) == 2 && !strings.Contains(parts[0], ".") {
|
||||||
|
// Simple field:value without dot notation
|
||||||
|
query.FieldFilters[parts[0]] = parts[1]
|
||||||
|
} else {
|
||||||
|
// Regular search term
|
||||||
|
query.RawTerms = append(query.RawTerms, match[4])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTypesenseQuery builds a Typesense search query from a parsed SearchQuery
|
||||||
|
func BuildTypesenseQuery(query SearchQuery) (string, map[string]string, error) {
|
||||||
|
// Join raw terms for the main query
|
||||||
|
mainQuery := strings.Join(query.RawTerms, " ")
|
||||||
|
|
||||||
|
// Parameters for filter_by and other Typesense parameters
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
// Build filter expressions for field filters
|
||||||
|
var filterExpressions []string
|
||||||
|
|
||||||
|
for field, value := range query.FieldFilters {
|
||||||
|
// Handle special fields with dot notation
|
||||||
|
if strings.Contains(field, ".") {
|
||||||
|
parts := strings.SplitN(field, ".", 2)
|
||||||
|
fieldName := parts[0]
|
||||||
|
subField := parts[1]
|
||||||
|
|
||||||
|
filterExpressions = append(filterExpressions, fmt.Sprintf("%s.%s:%s", fieldName, subField, value))
|
||||||
|
} else {
|
||||||
|
filterExpressions = append(filterExpressions, fmt.Sprintf("%s:%s", field, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all filter expressions
|
||||||
|
if len(filterExpressions) > 0 {
|
||||||
|
params["filter_by"] = strings.Join(filterExpressions, " && ")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, fmt.Errorf("error parsing search response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nostrResults := make([]nostr.Event, 0, len(searchResponse.Hits))
|
||||||
|
|
||||||
|
for _, hit := range searchResponse.Hits {
|
||||||
|
// Extract the document from the hit
|
||||||
|
docMap, ok := hit["document"].(map[string]AMBMetadata)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid document format in search results")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the map to AMB metadata
|
||||||
|
docJSON, err := json.Marshal(docMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling document: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ambData AMBMetadata
|
||||||
|
if err := json.Unmarshal(docJSON, &ambData); err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling to AMBMetadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the AMB metadata to a Nostr event
|
||||||
|
nostrEvent, err := StringifiedJSONToNostrEvent(ambData.EventRaw)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to convert AMB to Nostr: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nostrResults = append(nostrResults, nostrEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the number of results for logging
|
||||||
|
fmt.Printf("Found %d results\n",
|
||||||
|
len(nostrResults))
|
||||||
|
|
||||||
|
return nostrResults, nil
|
||||||
|
}
|
||||||
72
typesense30142/replace.go
Normal file
72
typesense30142/replace.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IndexNostrEvent converts a Nostr event to AMB metadata and indexes it in Typesense
|
||||||
|
func (ts *TSBackend) ReplaceEvent(ctx context.Context, event *nostr.Event) error {
|
||||||
|
ambData, err := NostrToAMB(event)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting Nostr event to AMB metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if event is already there, if so replace it, else index it
|
||||||
|
alreadyIndexed, err := ts.eventAlreadyIndexed(ambData)
|
||||||
|
return ts.indexDocument(ctx, ambData, alreadyIndexed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TSBackend) eventAlreadyIndexed(doc *AMBMetadata) (*nostr.Event, error) {
|
||||||
|
url := fmt.Sprintf(
|
||||||
|
"%s/collections/%s/documents/search?filter_by=d:=%s&&eventPubKey:=%s&q=&query_by=d,eventPubKey",
|
||||||
|
ts.Host, ts.CollectionName, doc.D, doc.EventPubKey)
|
||||||
|
|
||||||
|
resp, err := ts.makehttpRequest(url, http.MethodGet, nil)
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("Search for event failed, status: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := parseSearchResponse(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error while parsing search response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we found any events
|
||||||
|
if len(events) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &events[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index a document in Typesense
|
||||||
|
func (ts *TSBackend) indexDocument(ctx context.Context, doc *AMBMetadata, alreadyIndexedEvent *nostr.Event) error {
|
||||||
|
if alreadyIndexedEvent != nil {
|
||||||
|
fmt.Println("deleting old event for new one")
|
||||||
|
ts.DeleteEvent(ctx, alreadyIndexedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/collections/%s/documents", ts.Host, ts.CollectionName)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(doc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := ts.makehttpRequest(url, http.MethodPost, jsonData)
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
return fmt.Errorf("failed to index document, status: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
182
typesense30142/typesense.go
Normal file
182
typesense30142/typesense.go
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
package typesense30142
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CollectionSchema struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Fields []Field `json:"fields"`
|
||||||
|
DefaultSortingField string `json:"default_sorting_field"`
|
||||||
|
EnableNestedFields bool `json:"enable_nested_fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Facet bool `json:"facet,omitempty"`
|
||||||
|
Optional bool `json:"optional,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResponse struct {
|
||||||
|
Found int `json:"found"`
|
||||||
|
Hits []map[string]any `json:"hits"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Request map[string]any `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckOrCreateCollection checks if a collection exists and creates it if it doesn't
|
||||||
|
func (ts *TSBackend) CheckOrCreateCollection() error {
|
||||||
|
exists, err := ts.collectionExists()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error checking collection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
log.Printf("Collection %s does not exist. Creating...\n", ts.CollectionName)
|
||||||
|
if err := ts.createCollection(ts.CollectionName); err != nil {
|
||||||
|
log.Fatalf("Error creating collection: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Collection %s created successfully\n", ts.CollectionName)
|
||||||
|
} else {
|
||||||
|
log.Printf("Collection %s already exists\n", ts.CollectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TSBackend) collectionExists() (bool, error) {
|
||||||
|
url := fmt.Sprintf("%s/collections/%s", ts.Host, ts.CollectionName)
|
||||||
|
|
||||||
|
resp, err := ts.makehttpRequest(url, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// 404 means collection doesn't exist
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any status code other than 200 is an error
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return false, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a typesense collection
|
||||||
|
func (ts *TSBackend) createCollection(name string) error {
|
||||||
|
schema := CollectionSchema{
|
||||||
|
Name: name,
|
||||||
|
Fields: []Field{
|
||||||
|
// Base information
|
||||||
|
{Name: "id", Type: "string"},
|
||||||
|
{Name: "d", Type: "string"},
|
||||||
|
{Name: "type", Type: "string"},
|
||||||
|
{Name: "name", Type: "string"},
|
||||||
|
{Name: "description", Type: "string", Optional: true},
|
||||||
|
{Name: "about", Type: "object[]", Optional: true},
|
||||||
|
{Name: "keywords", Type: "string[]", Optional: true},
|
||||||
|
{Name: "inLanguage", Type: "string[]", Optional: true},
|
||||||
|
{Name: "image", Type: "string", Optional: true},
|
||||||
|
{Name: "trailer", Type: "object[]", Optional: true},
|
||||||
|
|
||||||
|
// Provenience
|
||||||
|
{Name: "creator", Type: "object[]", Optional: true},
|
||||||
|
{Name: "contributor", Type: "object[]", Optional: true},
|
||||||
|
{Name: "dateCreated", Type: "string", Optional: true},
|
||||||
|
{Name: "datePublished", Type: "string", Optional: true},
|
||||||
|
{Name: "dateModified", Type: "string", Optional: true},
|
||||||
|
{Name: "publisher", Type: "object[]", Optional: true},
|
||||||
|
{Name: "funder", Type: "object[]", Optional: true},
|
||||||
|
|
||||||
|
// Costs and Rights
|
||||||
|
{Name: "isAccessibleForFree", Type: "bool", Optional: true},
|
||||||
|
{Name: "license", Type: "object", Optional: true},
|
||||||
|
{Name: "conditionsOfAccess", Type: "object", Optional: true},
|
||||||
|
|
||||||
|
// Educational Metadata
|
||||||
|
{Name: "learningResourceType", Type: "object[]", Optional: true},
|
||||||
|
{Name: "audience", Type: "object[]", Optional: true},
|
||||||
|
{Name: "teaches", Type: "object[]", Optional: true},
|
||||||
|
{Name: "assesses", Type: "object[]", Optional: true},
|
||||||
|
{Name: "competencyRequired", Type: "object[]", Optional: true},
|
||||||
|
{Name: "educationalLevel", Type: "object[]", Optional: true},
|
||||||
|
{Name: "interactivityType", Type: "object", Optional: true},
|
||||||
|
|
||||||
|
// Relation
|
||||||
|
{Name: "isBasedOn", Type: "object[]", Optional: true},
|
||||||
|
{Name: "isPartOf", Type: "object[]", Optional: true},
|
||||||
|
{Name: "hasPart", Type: "object[]", Optional: true},
|
||||||
|
|
||||||
|
// Technical
|
||||||
|
{Name: "duration", Type: "string", Optional: true},
|
||||||
|
|
||||||
|
// Nostr Event
|
||||||
|
{Name: "eventID", Type: "string"},
|
||||||
|
{Name: "eventKind", Type: "int32"},
|
||||||
|
{Name: "eventPubKey", Type: "string"},
|
||||||
|
{Name: "eventSignature", Type: "string"},
|
||||||
|
{Name: "eventCreatedAt", Type: "int64"},
|
||||||
|
{Name: "eventContent", Type: "string"},
|
||||||
|
{Name: "eventRaw", Type: "string"},
|
||||||
|
},
|
||||||
|
DefaultSortingField: "eventCreatedAt",
|
||||||
|
EnableNestedFields: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/collections", ts.Host)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ts.makehttpRequest(url, http.MethodPost, jsonData)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("failed to create collection, status: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes an http request to typesense
|
||||||
|
func (ts *TSBackend) makehttpRequest(url string, method string, reqBody []byte) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-TYPESENSE-API-KEY", ts.ApiKey)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Count events
|
||||||
|
func CountEvents(filter nostr.Filter) (int64, error) {
|
||||||
|
fmt.Println("filter", filter)
|
||||||
|
// search by author
|
||||||
|
// search by d-tag
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue