diff --git a/.gitignore b/.gitignore index 2eea525..cb7b2f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +*.db \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index e053ca4..197f2fa 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,62 +1,58 @@ package main import ( + "context" "fmt" + "net/http" + "time" "http-nostr/internal/nostr" - "time" - "github.com/joho/godotenv" - "github.com/kelseyhightower/envconfig" + echologrus "github.com/davrux/echo-logrus/v4" "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" ddEcho "gopkg.in/DataDog/dd-trace-go.v1/contrib/labstack/echo.v4" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" - - "github.com/getsentry/sentry-go" ) + func main() { - logrus.SetFormatter(&logrus.JSONFormatter{}) - // Load env file as env variables - err := godotenv.Load(".env") + ctx := context.Background() + svc, err := nostr.NewService(ctx) if err != nil { - logrus.Errorf("Error loading environment variables: %v", err) - } - //load global config - type config struct { - SentryDSN string `envconfig:"SENTRY_DSN"` - DatadogAgentUrl string `envconfig:"DATADOG_AGENT_URL"` - Port int `default:"8080"` - } - globalConf := &config{} - err = envconfig.Process("", globalConf) - if err != nil { - logrus.Fatal(err) - } - - // Setup exception tracking with Sentry if configured - if globalConf.SentryDSN != "" { - if err = sentry.Init(sentry.ClientOptions{ - Dsn: globalConf.SentryDSN, - IgnoreErrors: []string{"401"}, - }); err != nil { - logrus.Errorf("sentry init error: %v", err) - } - defer sentry.Flush(2 * time.Second) + logrus.Fatalf("Failed to initialize service: %v", err) } + echologrus.Logger = svc.Logger e := echo.New() - if globalConf.DatadogAgentUrl != "" { - tracer.Start(tracer.WithAgentAddr(globalConf.DatadogAgentUrl)) + if svc.Cfg.DatadogAgentUrl != "" { + tracer.Start(tracer.WithAgentAddr(svc.Cfg.DatadogAgentUrl)) defer tracer.Stop() e.Use(ddEcho.Middleware(ddEcho.WithServiceName("http-nostr"))) } - e.GET("/info", nostr.InfoHandler) - e.POST("/nip47", nostr.NIP47Handler) - // r.Use(loggingMiddleware) + e.GET("/info", svc.InfoHandler) + e.POST("/nip47", svc.NIP47Handler) + e.POST("/subscribe", svc.SubscriptionHandler) + e.DELETE("/subscribe/:id", svc.StopSubscriptionHandler) + e.Use(echologrus.Middleware()) - logrus.Infof("Server starting on port %d", globalConf.Port) - logrus.Fatal(e.Start(fmt.Sprintf(":%d", globalConf.Port))) + //start Echo server + go func() { + if err := e.Start(fmt.Sprintf(":%v", svc.Cfg.Port)); err != nil && err != http.ErrServerClosed { + svc.Logger.Fatalf("Shutting down the server: %v", err) + } + }() + //handle graceful shutdown + <-svc.Ctx.Done() + svc.Logger.Infof("Shutting down echo server...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + e.Shutdown(ctx) + svc.Logger.Info("Echo server exited") + svc.Relay.Close() + svc.Logger.Info("Relay connection closed") + svc.Logger.Info("Waiting for service to exit...") + svc.Wg.Wait() + svc.Logger.Info("Service exited") } diff --git a/go.mod b/go.mod index e13c20f..574afce 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,21 @@ module http-nostr go 1.21.3 require ( - github.com/getsentry/sentry-go v0.25.0 + github.com/jackc/pgx/v5 v5.3.1 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo/v4 v4.11.1 github.com/nbd-wtf/go-nostr v0.25.7 github.com/sirupsen/logrus v1.9.3 gopkg.in/DataDog/dd-trace-go.v1 v1.58.0 + gorm.io/driver/postgres v1.4.6 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect ) require ( @@ -24,11 +32,13 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davrux/echo-logrus/v4 v4.0.3 github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.5.0 // indirect + github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.0 // indirect @@ -64,5 +74,6 @@ require ( golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/protobuf v1.30.0 // indirect + gorm.io/gorm v1.25.4 inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect ) diff --git a/go.sum b/go.sum index a7f2f49..81c4006 100644 --- a/go.sum +++ b/go.sum @@ -24,16 +24,22 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davrux/echo-logrus/v4 v4.0.3 h1:V5bM43A+3PNdpiGC2TS8HKAeaUWQph/j8utG7/mwQ5w= +github.com/davrux/echo-logrus/v4 v4.0.3/go.mod h1:+1y03d0joOKfwnPN4GSFhh/ViG3newZtYZfAPB6yf+g= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -42,16 +48,20 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= -github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gormigrate/gormigrate/v2 v2.1.1 h1:eGS0WTFRV30r103lU8JNXY27KbviRnqqIDobW3EV3iY= +github.com/go-gormigrate/gormigrate/v2 v2.1.1/go.mod h1:L7nJ620PFDKei9QOhJzqA8kRCk+E3UbV2f5gv+1ndLc= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -69,29 +79,64 @@ github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBB github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/echo/v4 v4.1.13/go.mod h1:3WZNypykZ3tnqpF2Qb4fPg27XDunFqgP3HGDmCMgv7U= github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v0.21.0 h1:p2rpHIL7TlSv1QrbXJUAcbyRKnIT0C9rRkH2E4OjLn8= +github.com/microsoft/go-mssqldb v0.21.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM= github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -100,8 +145,6 @@ github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOv github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -111,14 +154,18 @@ github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9p github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= @@ -126,6 +173,7 @@ github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdr github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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= @@ -144,6 +192,8 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -151,6 +201,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= @@ -161,8 +212,12 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= @@ -175,9 +230,12 @@ golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= @@ -186,17 +244,25 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -216,8 +282,11 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -247,13 +316,24 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/DataDog/dd-trace-go.v1 v1.58.0 h1:ixIUarsu0RrOt7xfdrE5YSFvjgaWsP3cC3G342jTIuw= gopkg.in/DataDog/dd-trace-go.v1 v1.58.0/go.mod h1:SmnEjjV9ZQr4MWRSUYEpoPyNtmtRK5J6UuJdAma+Yxw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= +gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= +gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc= +gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= +gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk= +gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY= +gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ= honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc= inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM= diff --git a/internal/nostr/models.go b/internal/nostr/models.go new file mode 100644 index 0000000..c5fd1cc --- /dev/null +++ b/internal/nostr/models.go @@ -0,0 +1,165 @@ +package nostr + +import ( + "encoding/json" + "time" + + "github.com/nbd-wtf/go-nostr" + "gorm.io/gorm" +) + +const ( + NIP_47_INFO_EVENT_KIND = 13194 + NIP_47_REQUEST_KIND = 23194 + NIP_47_RESPONSE_KIND = 23195 + + // state of request event + REQUEST_EVENT_PUBLISH_CONFIRMED = "confirmed" + REQUEST_EVENT_PUBLISH_FAILED = "failed" + REQUEST_EVENT_PUBLISH_UNCONFIRMED = "unconfirmed" +) + +type Subscription struct { + ID uint + RelayUrl string `validate:"required"` + WebhookUrl string + Open bool + Ids *[]string `gorm:"-"` + Kinds *[]int `gorm:"-"` + Authors *[]string `gorm:"-"` // WalletPubkey is included in this + Tags *nostr.TagMap `gorm:"-"` // RequestEvent ID goes in the "e" tag + Since time.Time + Until time.Time + Limit int + Search string + CreatedAt time.Time + UpdatedAt time.Time + + // TODO: fix an elegant solution to store datatypes + IdsString string + KindsString string + AuthorsString string + TagsString string +} + +func (s *Subscription) BeforeSave(tx *gorm.DB) error { + var err error + if s.Ids != nil { + var idsJson []byte + idsJson, err = json.Marshal(s.Ids) + if err != nil { + return err + } + s.IdsString = string(idsJson) + } + + if s.Kinds != nil { + var kindsJson []byte + kindsJson, err = json.Marshal(s.Kinds) + if err != nil { + return err + } + s.KindsString = string(kindsJson) + } + + if s.Authors != nil { + var authorsJson []byte + authorsJson, err = json.Marshal(s.Authors) + if err != nil { + return err + } + s.AuthorsString = string(authorsJson) + } + + if s.Tags != nil { + var tagsJson []byte + tagsJson, err = json.Marshal(s.Tags) + if err != nil { + return err + } + s.TagsString = string(tagsJson) + } + + return nil +} + +func (s *Subscription) AfterFind(tx *gorm.DB) error { + var err error + if s.IdsString != "" { + err = json.Unmarshal([]byte(s.IdsString), &s.Ids) + if err != nil { + return err + } + } + + if s.KindsString != "" { + err = json.Unmarshal([]byte(s.KindsString), &s.Kinds) + if err != nil { + return err + } + } + + if s.AuthorsString != "" { + err = json.Unmarshal([]byte(s.AuthorsString), &s.Authors) + if err != nil { + return err + } + } + + if s.TagsString != "" { + err = json.Unmarshal([]byte(s.TagsString), &s.Tags) + if err != nil { + return err + } + } + + return nil +} + +type RequestEvent struct { + ID uint + SubscriptionId uint `validate:"required"` + NostrId string `validate:"required"` + Content string + State string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ResponseEvent struct { + ID uint + RequestId *uint + SubscriptionId uint `validate:"required"` + NostrId string `validate:"required"` + Content string + RepliedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type ErrorResponse struct { + Message string `json:"message"` +} + +type InfoRequest struct { + RelayURL string `json:"relayUrl"` + WalletPubkey string `json:"walletPubkey"` +} + +type NIP47Request struct { + RelayUrl string `json:"relayUrl"` + WalletPubkey string `json:"walletPubkey"` + WebhookUrl string `json:"webhookUrl"` + SignedEvent *nostr.Event `json:"event"` +} + +type SubscriptionRequest struct { + RelayUrl string `json:"relayUrl"` + WebhookUrl string `json:"webhookUrl"` + Filter *nostr.Filter `json:"filter"` +} + +type SubscriptionResponse struct { + SubscriptionId uint `json:"subscription_id"` + WebhookUrl string `json:"webhookUrl"` +} \ No newline at end of file diff --git a/internal/nostr/nostr.go b/internal/nostr/nostr.go index e95b92a..c0f35b5 100644 --- a/internal/nostr/nostr.go +++ b/internal/nostr/nostr.go @@ -1,49 +1,160 @@ package nostr import ( + "bytes" "context" + "database/sql" + "encoding/json" "fmt" + "http-nostr/migrations" "net/http" + "os" + "os/signal" + "strconv" + "sync" "time" + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" "github.com/labstack/echo/v4" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" -) + "gorm.io/driver/postgres" + "gorm.io/gorm" -const ( - NIP_47_INFO_EVENT_KIND = 13194 - NIP_47_REQUEST_KIND = 23194 - NIP_47_RESPONSE_KIND = 23195 + "github.com/jackc/pgx/v5/stdlib" + sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" + gormtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1" ) -type WalletConnectInfo struct { - RelayURL string - WalletPubkey string - Secret string +type Config struct { + SentryDSN string `envconfig:"SENTRY_DSN"` + DatadogAgentUrl string `envconfig:"DATADOG_AGENT_URL"` + DefaultRelayURL string `envconfig:"DEFAULT_RELAY_URL"` + DatabaseUri string `envconfig:"DATABASE_URI" default:"http-nostr.db"` + DatabaseMaxConns int `envconfig:"DATABASE_MAX_CONNS" default:"10"` + DatabaseMaxIdleConns int `envconfig:"DATABASE_MAX_IDLE_CONNS" default:"5"` + DatabaseConnMaxLifetime int `envconfig:"DATABASE_CONN_MAX_LIFETIME" default:"1800"` // 30 minutes + Port int `default:"8080"` } -type ErrorResponse struct { - Message string `json:"message"` +type Service struct { + db *gorm.DB + Ctx context.Context + Wg *sync.WaitGroup + Relay *nostr.Relay + Cfg *Config + Logger *logrus.Logger + subscriptions map[uint]context.CancelFunc + mu sync.Mutex } -type InfoRequest struct { - RelayURL string `json:"relayUrl"` - WalletPubkey string `json:"walletPubkey"` -} +func NewService(ctx context.Context) (*Service, error) { + // Load env file as env variables + godotenv.Load(".env") -type NIP47Request struct { - RelayURL string `json:"relayUrl"` - WalletPubkey string `json:"walletPubkey"` - SignedEvent nostr.Event `json:"event"` -} + cfg := &Config{} + err := envconfig.Process("", cfg) + if err != nil { + return nil, err + } + + logger := logrus.New() + logger.SetFormatter(&logrus.JSONFormatter{}) + logger.SetOutput(os.Stdout) + logger.SetLevel(logrus.InfoLevel) + + var db *gorm.DB + var sqlDb *sql.DB + + if os.Getenv("DATADOG_AGENT_URL") != "" { + sqltrace.Register("pgx", &stdlib.Driver{}, sqltrace.WithServiceName("nostr-wallet-connect")) + sqlDb, err = sqltrace.Open("pgx", cfg.DatabaseUri) + if err != nil { + logger.Fatalf("Failed to open DB %v", err) + return nil, err + } + db, err = gormtrace.Open(postgres.New(postgres.Config{Conn: sqlDb}), &gorm.Config{}) + if err != nil { + logger.Fatalf("Failed to open DB %v", err) + return nil, err + } + } else { + db, err = gorm.Open(postgres.Open(cfg.DatabaseUri), &gorm.Config{}) + if err != nil { + logger.Fatalf("Failed to open DB %v", err) + return nil, err + } + sqlDb, err = db.DB() + if err != nil { + logger.Fatalf("Failed to set DB config: %v", err) + return nil, err + } + } + + sqlDb.SetMaxOpenConns(cfg.DatabaseMaxConns) + sqlDb.SetMaxIdleConns(cfg.DatabaseMaxIdleConns) + sqlDb.SetConnMaxLifetime(time.Duration(cfg.DatabaseConnMaxLifetime) * time.Second) + + err = migrations.Migrate(db) + if err != nil { + logger.Fatalf("Failed to migrate: %v", err) + return nil, err + } + logger.Info("Any pending migrations ran successfully") + + ctx, _ = signal.NotifyContext(ctx, os.Interrupt) + + logger.Info("Connecting to the relay...") + relay, err := nostr.RelayConnect(ctx, cfg.DefaultRelayURL) + if err != nil { + logger.Fatalf("Failed to connect to default relay: %v", err) + return nil, err + } + + subscriptions := make(map[uint]context.CancelFunc) + + var wg sync.WaitGroup + svc := &Service{ + Cfg: cfg, + db: db, + Ctx: ctx, + Wg: &wg, + Logger: logger, + subscriptions: subscriptions, + Relay: relay, + } + + logger.Info("starting all open subscriptions...") -func handleError(w http.ResponseWriter, err error, message string, httpStatusCode int) { - logrus.WithError(err).Error(message) - http.Error(w, message, httpStatusCode) + var openSubscriptions []Subscription + if err := svc.db.Where("open = ?", true).Find(&openSubscriptions).Error; err != nil { + logger.Errorf("Failed to query open subscriptions: %v", err) + return nil, err + } + + for _, sub := range openSubscriptions { + go func(sub Subscription) { + ctx, cancel := context.WithCancel(svc.Ctx) + svc.mu.Lock() + svc.subscriptions[sub.ID] = cancel + svc.mu.Unlock() + errorChan := make(chan error) + go svc.handleSubscription(ctx, &sub, errorChan) + + err := <-errorChan + if err != nil { + svc.stopSubscription(&sub) + svc.Logger.Errorf("error opening subscription %d: %v", sub.ID, err) + } + svc.Logger.Infof("opened subscription %d", sub.ID) + }(sub) + } + + return svc, nil } -func InfoHandler(c echo.Context) error { +func (svc *Service) InfoHandler(c echo.Context) error { var requestData InfoRequest if err := c.Bind(&requestData); err != nil { return c.JSON(http.StatusBadRequest, ErrorResponse{ @@ -51,15 +162,18 @@ func InfoHandler(c echo.Context) error { }) } - logrus.Info("Connecting to the relay...") - relay, err := nostr.RelayConnect(c.Request().Context(), requestData.RelayURL) + relay, isCustomRelay, err := svc.getRelayConnection(c.Request().Context(), requestData.RelayURL) if err != nil { - return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: fmt.Sprintf("error connecting to relay: %s", err.Error()), - }) + if isCustomRelay { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error connecting to relay: %s", err)) + } + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error connecting to default relay: %s", err)) + } + if isCustomRelay { + defer relay.Close() } - logrus.Info("Subscribing to info event...") + svc.Logger.Info("subscribing to info event...") filter := nostr.Filter{ Authors: []string{requestData.WalletPubkey}, Kinds: []int{NIP_47_INFO_EVENT_KIND}, @@ -76,7 +190,7 @@ func InfoHandler(c echo.Context) error { select { case <-ctx.Done(): - logrus.Info("Exiting subscription.") + svc.Logger.Info("exiting subscription.") return c.JSON(http.StatusRequestTimeout, ErrorResponse{ Message: "request canceled or timed out", }) @@ -85,77 +199,371 @@ func InfoHandler(c echo.Context) error { } } -func NIP47Handler(c echo.Context) error { +func (svc *Service) NIP47Handler(c echo.Context) error { var requestData NIP47Request if err := c.Bind(&requestData); err != nil { return c.JSON(http.StatusBadRequest, ErrorResponse{ Message: fmt.Sprintf("error decoding nip47 request: %s", err.Error()), }) } - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + + if (requestData.WalletPubkey == "") { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "wallet pubkey is empty", + }) + } + + if (requestData.SignedEvent == nil) { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "signed event is empty", + }) + } + + subscription := Subscription{} + requestEvent := RequestEvent{} + findRequestResult := svc.db.Where("nostr_id = ?", requestData.SignedEvent.ID).Find(&requestEvent) + if findRequestResult.RowsAffected != 0 { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "request event is already processed", + }) + } + + subscription = Subscription{ + RelayUrl: requestData.RelayUrl, + WebhookUrl: requestData.WebhookUrl, + Open: true, + Authors: &[]string{requestData.WalletPubkey}, + Kinds: &[]int{NIP_47_RESPONSE_KIND}, + Tags: &nostr.TagMap{"e": []string{requestData.SignedEvent.ID}}, + Since: time.Now(), + Limit: 1, + } + svc.db.Create(&subscription) + + requestEvent = RequestEvent{ + NostrId: requestData.SignedEvent.ID, + Content: requestData.SignedEvent.Content, + SubscriptionId: subscription.ID, + } + svc.db.Create(&requestEvent) + + if subscription.WebhookUrl != "" { + go func() { + ctx, cancel := context.WithTimeout(svc.Ctx, 120*time.Second) + defer cancel() + event, _, err := svc.processRequest(ctx, &subscription, &requestEvent, &requestData) + if err != nil { + svc.Logger.WithError(err).Error("failed to process request for webhook") + // what to pass to the webhook? + return + } + svc.postEventToWebhook(event, requestData.WebhookUrl) + }() + return c.JSON(http.StatusOK, "webhook received") + } + + ctx, cancel := context.WithTimeout(svc.Ctx, 120*time.Second) defer cancel() + event, code, err := svc.processRequest(ctx, &subscription, &requestEvent, &requestData) + if err != nil { + return c.JSON(code, ErrorResponse{ + Message: err.Error(), + }) + } + return c.JSON(http.StatusOK, event) +} + +func (svc *Service) SubscriptionHandler(c echo.Context) error { + var requestData SubscriptionRequest + // send in a pubkey and authenticate by signing + if err := c.Bind(&requestData); err != nil { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: fmt.Sprintf("error decoding subscription request: %s", err.Error()), + }) + } + + if (requestData.Filter == nil) { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "filters are empty", + }) + } + + if (requestData.WebhookUrl == "") { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "webhook url is empty", + }) + } + + subscription := Subscription{ + RelayUrl: requestData.RelayUrl, + WebhookUrl: requestData.WebhookUrl, + Open: true, + Ids: &requestData.Filter.IDs, + Authors: &requestData.Filter.Authors, + Kinds: &requestData.Filter.Kinds, + Tags: &requestData.Filter.Tags, + Limit: requestData.Filter.Limit, + Search: requestData.Filter.Search, + } + if requestData.Filter.Since != nil { + subscription.Since = requestData.Filter.Since.Time() + } + if requestData.Filter.Until != nil { + subscription.Until = requestData.Filter.Until.Time() + } + svc.db.Create(&subscription) + + errorChan := make(chan error, 1) + ctx, cancel := context.WithCancel(svc.Ctx) + svc.mu.Lock() + svc.subscriptions[subscription.ID] = cancel + svc.mu.Unlock() + go svc.handleSubscription(ctx, &subscription, errorChan) - relay, err := nostr.RelayConnect(ctx, requestData.RelayURL) + err := <-errorChan if err != nil { + svc.stopSubscription(&subscription) return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: fmt.Sprintf("error connecting to relay: %s", err.Error()), + Message: fmt.Sprintf("error setting up subscription %s",err.Error()), }) } - // Start subscribing to the event for response - logrus.WithFields(logrus.Fields{"e": requestData.SignedEvent.ID, "author": requestData.WalletPubkey}).Info("Subscribing to events for response...") + return c.JSON(http.StatusOK, SubscriptionResponse{ + SubscriptionId: subscription.ID, + WebhookUrl: requestData.WebhookUrl, + }) +} + +func (svc *Service) handleSubscription(ctx context.Context, subscription *Subscription, errorChan chan error) { + relay, isCustomRelay, err := svc.getRelayConnection(ctx, subscription.RelayUrl) + if err != nil { + errorChan <- err + return + } + if isCustomRelay { + defer relay.Close() + } + filter := nostr.Filter{ - Authors: []string{requestData.WalletPubkey}, - Kinds: []int{NIP_47_RESPONSE_KIND}, - Tags: nostr.TagMap{"e": []string{requestData.SignedEvent.ID}}, + Limit: subscription.Limit, + Search: subscription.Search, + } + if subscription.Ids != nil { + filter.IDs = *subscription.Ids + } + if subscription.Kinds != nil { + filter.Kinds = *subscription.Kinds + } + if subscription.Authors != nil { + filter.Authors = *subscription.Authors } + if subscription.Tags != nil { + filter.Tags = *subscription.Tags + } + if !subscription.Since.IsZero() { + since := nostr.Timestamp(subscription.Since.Unix()) + filter.Since = &since + } + if !subscription.Until.IsZero() { + until := nostr.Timestamp(subscription.Until.Unix()) + filter.Until = &until + } + sub, err := relay.Subscribe(ctx, []nostr.Filter{filter}) if err != nil { - return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: fmt.Sprintf("error subscribing to relay: %s", err.Error()), - }) + errorChan <- err + return } + errorChan <- nil + go func(){ + for event := range sub.Events { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Infof("received event on subscription %d", subscription.ID) + responseEvent := ResponseEvent{ + SubscriptionId: subscription.ID, + NostrId: event.ID, + Content: event.Content, + RepliedAt: event.CreatedAt.Time(), + } + svc.db.Save(&responseEvent) + svc.postEventToWebhook(event, subscription.WebhookUrl) + } + }() + svc.Logger.Infof("subscription %d started", subscription.ID) + <-ctx.Done() + svc.Logger.Infof("subscription %d closed", subscription.ID) + // delete svix app +} + +func (svc *Service) StopSubscriptionHandler(c echo.Context) error { + id := c.Param("id") + uint64Id, _ := strconv.ParseUint(id, 10, 64) + subId := uint(uint64Id) - // Publish the request event - logrus.Info("Publishing request event...") - status, err := relay.Publish(ctx, requestData.SignedEvent) + subscription := Subscription{} + if err := svc.db.First(&subscription, subId).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: "subscription does not exist", + }) + } else { + return c.JSON(http.StatusInternalServerError, ErrorResponse{ + Message: fmt.Sprintf("error occurred while fetching user: %s", err.Error()), + }) + } + } + + err := svc.stopSubscription(&subscription) if err != nil { - return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: fmt.Sprintf("error publishing request event: %s", err.Error()), + return c.JSON(http.StatusNotFound, ErrorResponse{ + Message: err.Error(), }) } + return c.JSON(http.StatusOK, fmt.Sprintf("subscription %d stopped", subId)) +} + +func (svc *Service) stopSubscription(sub *Subscription) error { + svc.mu.Lock() + cancel, exists := svc.subscriptions[sub.ID] + if exists { + cancel() + delete(svc.subscriptions, sub.ID) + sub.Open = false + svc.db.Save(sub) + } + svc.mu.Unlock() + if (!exists) { + return fmt.Errorf("cancel function of subscription doesn't exist") + } + return nil +} + +func (svc *Service) getRelayConnection(ctx context.Context, customRelayURL string) (*nostr.Relay, bool, error) { + if customRelayURL != "" && customRelayURL != svc.Cfg.DefaultRelayURL { + svc.Logger.WithFields(logrus.Fields{ + "customRelayURL": customRelayURL, + }).Infof("connecting to custom relay") + relay, err := nostr.RelayConnect(ctx, customRelayURL) + return relay, true, err // true means custom and the relay should be closed + } + // check if the default relay is active + if svc.Relay.IsConnected() { + return svc.Relay, false, nil + } else { + svc.Logger.Info("lost connection to default relay, reconnecting...") + relay, err := nostr.RelayConnect(svc.Ctx, svc.Cfg.DefaultRelayURL) + return relay, false, err + } +} + +func (svc *Service) processRequest(ctx context.Context, subscription *Subscription, requestEvent *RequestEvent, requestData *NIP47Request) (*nostr.Event, int, error) { + publishState := REQUEST_EVENT_PUBLISH_FAILED + defer func() { + subscription.Open = false + requestEvent.State = publishState + svc.db.Save(subscription) + svc.db.Save(requestEvent) + }() + relay, isCustomRelay, err := svc.getRelayConnection(ctx, subscription.RelayUrl) + if err != nil { + return &nostr.Event{}, http.StatusBadRequest, fmt.Errorf("error connecting to relay: %w", err) + } + if isCustomRelay { + defer relay.Close() + } + + svc.Logger.WithFields(logrus.Fields{ + "e": requestEvent.ID, + "authors": subscription.Authors, + }).Info("subscribing to events for response...") + + since := nostr.Timestamp(subscription.Since.Unix()) + filter := nostr.Filter{ + Kinds: *subscription.Kinds, + Authors: *subscription.Authors, + Tags: *subscription.Tags, + Since: &since, + Limit: subscription.Limit, + Search: subscription.Search, + } + + if subscription.Ids != nil { + filter.IDs = *subscription.Ids + } + if !subscription.Until.IsZero() { + until := nostr.Timestamp(subscription.Until.Unix()) + filter.Until = &until + } + + sub, err := relay.Subscribe(ctx, []nostr.Filter{filter}) + if err != nil { + return &nostr.Event{}, http.StatusBadRequest, fmt.Errorf("error subscribing to relay: %w", err) + } + + status, err := relay.Publish(ctx, *requestData.SignedEvent) + if err != nil { + return &nostr.Event{}, http.StatusBadRequest, fmt.Errorf("error publishing request event: %w", err) + } + if status == nostr.PublishStatusSucceeded { - logrus.WithFields(logrus.Fields{ + svc.Logger.WithFields(logrus.Fields{ "status": status, - "eventId": requestData.SignedEvent.ID, - }).Info("Published request") + "eventId": requestEvent.ID, + }).Info("published request") + publishState = REQUEST_EVENT_PUBLISH_CONFIRMED } else if status == nostr.PublishStatusFailed { - logrus.WithFields(logrus.Fields{ + svc.Logger.WithFields(logrus.Fields{ "status": status, - "eventId": requestData.SignedEvent.ID, - }).Info("Failed to publish request") - return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: fmt.Sprintf("error publishing request event: %s", err.Error()), - }) + "eventId": requestEvent.ID, + }).Info("failed to publish request") + return &nostr.Event{}, http.StatusBadRequest, fmt.Errorf("error publishing request event: %s", err.Error()) } else { - logrus.WithFields(logrus.Fields{ + svc.Logger.WithFields(logrus.Fields{ "status": status, - "eventId": requestData.SignedEvent.ID, - }).Info("Request sent but no response from relay (timeout)") + "eventId": requestEvent.ID, + }).Info("request sent but no response from relay (timeout)") + // If we can somehow handle this case, then publishState can be removed + publishState = REQUEST_EVENT_PUBLISH_UNCONFIRMED } select { case <-ctx.Done(): - logrus.Info("Exiting subscription.") - return c.JSON(http.StatusRequestTimeout, ErrorResponse{ - Message: "request canceled or timed out", - }) + return &nostr.Event{}, http.StatusRequestTimeout, fmt.Errorf("request canceled or timed out") case event := <-sub.Events: - // TODO: Store the req.SignedEvent.IDs which didn't get - // a response in a DB and use a global subscription to - // respond to them - logrus.Infof("Successfully received event: %s", event.ID) - return c.JSON(http.StatusOK, event) + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Infof("successfully received event") + responseEvent := ResponseEvent{ + SubscriptionId: subscription.ID, + RequestId: &requestEvent.ID, + NostrId: event.ID, + Content: event.Content, + RepliedAt: event.CreatedAt.Time(), + } + svc.db.Save(&responseEvent) + return event, http.StatusOK, nil } } + +func (svc *Service) postEventToWebhook(event *nostr.Event, webhookURL string) { + eventData, err := json.Marshal(event) + if err != nil { + svc.Logger.WithError(err).Error("failed to marshal event for webhook") + return + } + + // TODO: add svix functionality + _, err = http.Post(webhookURL, "application/json", bytes.NewBuffer(eventData)) + if err != nil { + svc.Logger.WithError(err).Error("failed to post event to webhook") + } + + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + }).Infof("successfully posted event to webhook") +} diff --git a/internal/nostr/nostr_test.go b/internal/nostr/nostr_test.go index 74f96b8..3ab940c 100644 --- a/internal/nostr/nostr_test.go +++ b/internal/nostr/nostr_test.go @@ -10,13 +10,13 @@ import ( // unused func eventGenerator() (*nostr.Event) { - pubkey := "0b48335c805607f26a88150135dedc5b6d998f404aa12a9f906da3d8c65ec7f9" - secret := "b1166fd992669c4ffaa16224b1f94dd2597f9cf5cbcbacabcdebaf3339f2b535" + pubkey := "xxxx" + secret := "xxxx" var params map[string]interface{} jsonStr := `{ "method": "pay_invoice", "params": { - "invoice": "lnbc120n1pj6e529pp5r427asvnqetgdju8f9utd5wxq4xj34tpqzwvf53vu0c6rv7cl3sqdp8v3352nnpg42yxnnpwvcny5jyw3gkvmtkd4u9vjqcqzzsxqyz5vqsp5ylmdsdrh23pj65frym4330uj5a7p7qztxxnsvn6qyleqkakgcuqs9qyyssq09t3q4xsr0q76e4mhz9fv3fv9w4pkh3dplyr5fspa72mg8a0jnj374wlsmyswqd0j653tz33mtafggravwx8rykcpkchhvgftpde3qsqe6l0mp" + "invoice": "lnbcxxx" } }` decoder := json.NewDecoder(strings.NewReader(jsonStr)) diff --git a/migrations/202402161653_initial_migration.go b/migrations/202402161653_initial_migration.go new file mode 100644 index 0000000..d98a239 --- /dev/null +++ b/migrations/202402161653_initial_migration.go @@ -0,0 +1,42 @@ +package migrations + +import ( + _ "embed" + "log" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +//go:embed initial_migration_postgres.sql +var initialMigrationPostgres string +//go:embed initial_migration_sqlite.sql +var initialMigrationSqlite string + +var initialMigrations = map[string]string { + "postgres": initialMigrationPostgres, + "sqlite": initialMigrationSqlite, +} + +// Initial migration +var _202402161653_initial_migration = &gormigrate.Migration { + ID: "202402161653_initial_migration", + Migrate: func(tx *gorm.DB) error { + // only execute migration if subscriptions table doesn't exist + err := tx.Exec("SELECT * FROM subscriptions").Error; + if err != nil { + // find which initial migration should be executed + initialMigration := initialMigrations[tx.Dialector.Name()] + if initialMigration == "" { + log.Fatalf("unsupported database type: %s", tx.Dialector.Name()) + } + + return tx.Exec(initialMigration).Error + } + + return nil + }, + Rollback: func(tx *gorm.DB) error { + return nil; + }, +} diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..1f7f95a --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,28 @@ +# Creating a new migration + +1. Create a new file based on the current date and time (see existing migration format) +2. Copy the following code and update MY_ID_HERE and MY_COMMENT_HERE and DO_SOMETHING_HERE +3. Add the ID to the list of migrations in migrate.go +4. If possible, add a rollback function. + +*For Postgres/Sqlite specific migrations, see the [initial migration](202402161653.go)* + +```go +package migrations + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// MY_COMMENT_HERE +var _MY_ID_HERE = &gormigrate.Migration { + ID: "MY_ID_HERE", + Migrate: func(tx *gorm.DB) error { + return DO_SOMETHING_HERE.Error; + }, + Rollback: func(tx *gorm.DB) error { + return nil; + }, +} +``` diff --git a/migrations/initial_migration_postgres.sql b/migrations/initial_migration_postgres.sql new file mode 100644 index 0000000..84fd1f1 --- /dev/null +++ b/migrations/initial_migration_postgres.sql @@ -0,0 +1,93 @@ +CREATE TABLE subscriptions ( + id bigint NOT NULL, + relay_url text, + webhook_url text, + ids_string text, + kinds_string text, + authors_string text, + tags_string text, + since timestamp with time zone, + until timestamp with time zone, + "limit" integer, + search text, + "open" boolean, + created_at timestamp with time zone, + updated_at timestamp with time zone +); + +-- Create sequence for subscriptions +CREATE SEQUENCE subscriptions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE subscriptions_id_seq OWNED BY subscriptions.id; + +ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass); + +ALTER TABLE ONLY subscriptions + ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id); + +-- Create request_events table +CREATE TABLE request_events ( + id bigint NOT NULL, + subscription_id bigint, + nostr_id text UNIQUE, + content text, + "state" text, + created_at timestamp with time zone, + updated_at timestamp with time zone +); + +-- Create sequence for request_events +CREATE SEQUENCE request_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE request_events_id_seq OWNED BY request_events.id; + +ALTER TABLE ONLY request_events ALTER COLUMN id SET DEFAULT nextval('request_events_id_seq'::regclass); + +ALTER TABLE ONLY request_events + ADD CONSTRAINT request_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY request_events + ADD CONSTRAINT fk_request_events_subscription FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE; + +-- Create response_events table +CREATE TABLE response_events ( + id bigint NOT NULL, + subscription_id bigint, + request_id bigint NULL, + nostr_id text, + content text, + replied_at timestamp with time zone, + created_at timestamp with time zone, + updated_at timestamp with time zone +); + +-- Create sequence for response_events +CREATE SEQUENCE response_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE response_events_id_seq OWNED BY response_events.id; + +ALTER TABLE ONLY response_events ALTER COLUMN id SET DEFAULT nextval('response_events_id_seq'::regclass); + +ALTER TABLE ONLY response_events + ADD CONSTRAINT response_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY response_events + ADD CONSTRAINT fk_response_events_subscription FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE; + +ALTER TABLE ONLY response_events + ADD CONSTRAINT fk_response_events_request_event FOREIGN KEY (request_id) REFERENCES request_events(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/migrations/initial_migration_sqlite.sql b/migrations/initial_migration_sqlite.sql new file mode 100644 index 0000000..2170ea7 --- /dev/null +++ b/migrations/initial_migration_sqlite.sql @@ -0,0 +1,3 @@ +CREATE TABLE "subscriptions" (`id` integer,`relay_url` text,`webhook_url` text,`ids_string` text,`kinds_string` text,`authors_string` text,`tags_string` text,`since` datetime,`until` datetime,`limit` integer,`search` text,`open` boolean,`created_at` datetime,`updated_at` datetime,PRIMARY KEY (`id`)); +CREATE TABLE "request_events" (`id` integer,`subscription_id` integer,`nostr_id` text UNIQUE,`content` text,`state` text,`created_at` datetime,`updated_at` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_request_events_subscription` FOREIGN KEY (`subscription_id`) REFERENCES `subscriptions`(`id`) ON DELETE CASCADE); +CREATE TABLE "response_events" (`id` integer,`subscription_id` integer,`request_id` integer null,`nostr_id` text,`content` text,`replied_at` datetime,`created_at` datetime,`updated_at` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_response_events_subscription` FOREIGN KEY (`subscription_id`) REFERENCES `subscriptions`(`id`) ON DELETE CASCADE,CONSTRAINT `fk_response_events_request_event` FOREIGN KEY (`request_id`) REFERENCES `request_events`(`id`) ON DELETE CASCADE); diff --git a/migrations/migrate.go b/migrations/migrate.go new file mode 100644 index 0000000..f8e2331 --- /dev/null +++ b/migrations/migrate.go @@ -0,0 +1,15 @@ +package migrations + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +func Migrate(db *gorm.DB) error { + + m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{ + _202402161653_initial_migration, + }) + + return m.Migrate() +} \ No newline at end of file