Live video on the AT Protocol

model: add sqlite database and /notification endpoint

See merge request aquareum-tv/aquareum!18

+181 -36
+8
go.mod
··· 6 6 github.com/adrg/xdg v0.4.0 7 7 github.com/fatih/color v1.17.0 8 8 github.com/golang/glog v1.2.0 9 + github.com/lmittmann/tint v1.0.4 10 + github.com/orandin/slog-gorm v1.3.2 9 11 github.com/peterbourgon/ff/v3 v3.3.1 10 12 golang.org/x/sync v0.6.0 13 + gorm.io/driver/sqlite v1.5.5 11 14 ) 12 15 13 16 require ( 17 + github.com/jinzhu/inflection v1.0.0 // indirect 18 + github.com/jinzhu/now v1.1.5 // indirect 14 19 github.com/mattn/go-colorable v0.1.13 // indirect 15 20 github.com/mattn/go-isatty v0.0.20 // indirect 16 21 golang.org/x/sys v0.18.0 // indirect 22 + gorm.io/gorm v1.25.9 17 23 ) 24 + 25 + require github.com/mattn/go-sqlite3 v1.14.22 // indirect
+20 -3
go.sum
··· 1 1 github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= 2 2 github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= 3 - github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 6 github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 6 7 github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 7 8 github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= 8 9 github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 9 10 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 11 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 13 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 14 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 15 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 16 + github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= 17 + github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 11 18 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 19 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 20 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 21 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 22 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 + github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 24 + github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 25 + github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g= 26 + github.com/orandin/slog-gorm v1.3.2/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 16 27 github.com/peterbourgon/ff/v3 v3.3.1 h1:XSWvXxeNdgeppLNGGJEAOiXRdX2YMF/LuZfdnqQ1SNc= 17 28 github.com/peterbourgon/ff/v3 v3.3.1/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 18 29 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 30 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 31 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 - github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 22 32 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 + github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 34 + github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 35 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 24 36 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 25 37 golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 28 40 golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 29 41 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 42 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 32 43 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 + gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= 47 + gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= 48 + gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= 49 + gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+40 -7
pkg/api/api.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "fmt" 7 + "io" 6 8 "net/http" 7 9 "time" 8 10 9 11 "aquareum.tv/aquareum/js/app" 10 12 "aquareum.tv/aquareum/pkg/config" 11 13 "aquareum.tv/aquareum/pkg/log" 14 + "aquareum.tv/aquareum/pkg/model" 12 15 ) 13 16 14 - func Handler() (http.Handler, error) { 17 + func Handler(ctx context.Context, mod model.Model) (http.Handler, error) { 15 18 mux := http.NewServeMux() 16 19 files, err := app.Files() 17 20 if err != nil { 18 21 return nil, err 19 22 } 23 + mux.Handle("/notification", HandleNotificationCreate(ctx, mod)) 20 24 mux.Handle("/", http.FileServer(http.FS(files))) 21 25 return mux, nil 22 26 } 23 27 24 - func ServeHTTP(ctx context.Context, cli config.CLI) error { 25 - return ServerWithShutdown(ctx, cli, func(s *http.Server) error { 28 + type NotificationPayload struct { 29 + Token string `json:"token"` 30 + } 31 + 32 + func HandleNotificationCreate(ctx context.Context, mod model.Model) http.HandlerFunc { 33 + return func(w http.ResponseWriter, req *http.Request) { 34 + payload, err := io.ReadAll(req.Body) 35 + if err != nil { 36 + log.Log(ctx, "error reading notification create", "error", err) 37 + w.WriteHeader(400) 38 + return 39 + } 40 + n := NotificationPayload{} 41 + err = json.Unmarshal(payload, &n) 42 + if err != nil { 43 + log.Log(ctx, "error unmarshalling notification create", "error", err) 44 + w.WriteHeader(400) 45 + return 46 + } 47 + err = mod.CreateNotification(n.Token) 48 + if err != nil { 49 + log.Log(ctx, "error creating notification", "error", err) 50 + w.WriteHeader(400) 51 + return 52 + } 53 + w.WriteHeader(200) 54 + } 55 + } 56 + 57 + func ServeHTTP(ctx context.Context, cli config.CLI, mod model.Model) error { 58 + return ServerWithShutdown(ctx, cli, mod, func(s *http.Server) error { 26 59 s.Addr = cli.HttpAddr 27 60 log.Log(ctx, "http server starting", "addr", s.Addr) 28 61 return s.ListenAndServe() 29 62 }) 30 63 } 31 64 32 - func ServeHTTPS(ctx context.Context, cli config.CLI) error { 33 - return ServerWithShutdown(ctx, cli, func(s *http.Server) error { 65 + func ServeHTTPS(ctx context.Context, cli config.CLI, mod model.Model) error { 66 + return ServerWithShutdown(ctx, cli, mod, func(s *http.Server) error { 34 67 s.Addr = cli.HttpsAddr 35 68 log.Log(ctx, "https server starting", 36 69 "addr", s.Addr, ··· 41 74 }) 42 75 } 43 76 44 - func ServerWithShutdown(ctx context.Context, cli config.CLI, serve func(*http.Server) error) error { 45 - handler, err := Handler() 77 + func ServerWithShutdown(ctx context.Context, cli config.CLI, mod model.Model, serve func(*http.Server) error) error { 78 + handler, err := Handler(ctx, mod) 46 79 if err != nil { 47 80 return err 48 81 }
+17 -6
pkg/cmd/aquareum.go
··· 8 8 "os/signal" 9 9 "syscall" 10 10 11 - "log" 11 + "aquareum.tv/aquareum/pkg/log" 12 12 13 13 "aquareum.tv/aquareum/pkg/api" 14 14 "aquareum.tv/aquareum/pkg/config" 15 + "aquareum.tv/aquareum/pkg/model" 15 16 "github.com/adrg/xdg" 16 17 "github.com/peterbourgon/ff/v3" 17 18 "golang.org/x/sync/errgroup" 18 19 ) 19 - 20 - var Version = "unknown" 21 20 22 21 type BuildFlags struct { 23 22 Version string ··· 38 37 if err != nil { 39 38 return err 40 39 } 40 + dbFile, err := xdg.DataFile("aquareum/db.sqlite") 41 + if err != nil { 42 + return err 43 + } 44 + dbFile = fmt.Sprintf("sqlite://%s", dbFile) 41 45 42 46 fs := flag.NewFlagSet("aquareum", flag.ExitOnError) 43 47 cli := config.CLI{} ··· 46 50 fs.BoolVar(&cli.Insecure, "insecure", false, "Run without HTTPS. not recomended, as WebRTC support requires HTTPS") 47 51 fs.StringVar(&cli.TLSCertPath, "tls-cert", tlsCertFile, "Path to TLS certificate") 48 52 fs.StringVar(&cli.TLSKeyPath, "tls-key", tlsKeyFile, "Path to TLS key") 53 + fs.StringVar(&cli.DBPath, "db-path", dbFile, "path to sqlite database file") 49 54 50 55 ff.Parse( 51 56 fs, os.Args[1:], ··· 53 58 ff.WithEnvVarSplit(","), 54 59 ) 55 60 61 + mod, err := model.MakeDB(cli.DBPath) 62 + if err != nil { 63 + return err 64 + } 65 + 56 66 group, ctx := errgroup.WithContext(context.Background()) 67 + ctx = log.WithLogValues(ctx, "version", build.Version) 57 68 58 69 group.Go(func() error { 59 70 return handleSignals(ctx) 60 71 }) 61 72 62 73 group.Go(func() error { 63 - return api.ServeHTTP(ctx, cli) 74 + return api.ServeHTTP(ctx, cli, mod) 64 75 }) 65 76 66 77 if !cli.Insecure { 67 78 group.Go(func() error { 68 - return api.ServeHTTPS(ctx, cli) 79 + return api.ServeHTTPS(ctx, cli, mod) 69 80 }) 70 81 } 71 82 ··· 94 105 for { 95 106 select { 96 107 case s := <-c: 97 - log.Printf("caught signal=%v, attempting clean shutdown", s) 108 + log.Log(ctx, "caught signal, attempting clean shutdown", "signal", s) 98 109 return fmt.Errorf("caught signal=%v", s) 99 110 case <-ctx.Done(): 100 111 return nil
+1
pkg/config/config.go
··· 3 3 type CLI struct { 4 4 TLSCertPath string 5 5 TLSKeyPath string 6 + DBPath string 6 7 Insecure bool 7 8 HttpAddr string 8 9 HttpsAddr string
+21 -20
pkg/log/log.go
··· 7 7 "context" 8 8 "flag" 9 9 "fmt" 10 + "log/slog" 11 + "os" 10 12 "path/filepath" 11 13 "runtime" 12 14 "strconv" 13 - "strings" 15 + "time" 14 16 15 - "github.com/fatih/color" 16 17 "github.com/golang/glog" 18 + "github.com/lmittmann/tint" 19 + "github.com/mattn/go-isatty" 17 20 ) 21 + 22 + func init() { 23 + w := os.Stderr 24 + 25 + // set global logger with custom options 26 + slog.SetDefault(slog.New( 27 + tint.NewHandler(w, &tint.Options{ 28 + Level: slog.LevelDebug, 29 + TimeFormat: time.RFC3339, 30 + NoColor: !isatty.IsTerminal(w.Fd()), 31 + }), 32 + )) 33 + } 18 34 19 35 // unique type to prevent assignment. 20 36 type clogContextKeyType struct{} ··· 73 89 return context.WithValue(ctx, clogContextKey, newMetadata) 74 90 } 75 91 76 - var badGlyphs = " \n" 77 - 78 92 // Actual log handler; the others have wrappers to properly handle stack depth 79 93 func (v *VerboseLogger) log(ctx context.Context, message string, args ...any) { 80 94 if !glog.V(v.level) { 81 95 return 82 96 } 83 - keyCol := color.New(color.FgMagenta).SprintFunc() 84 - valCol := color.New(color.FgGreen).SprintFunc() 85 - messageCol := color.New(color.FgCyan).SprintFunc() 86 97 meta, _ := ctx.Value(clogContextKey).(metadata) 87 - allArgs := append([]any{}, meta.Flat()...) 98 + allArgs := []any{} 88 99 allArgs = append(allArgs, args...) 100 + allArgs = append(allArgs, meta.Flat()...) 89 101 allArgs = append(allArgs, "caller", caller(3)) 90 - str := messageCol(message) 91 - for i := range allArgs { 92 - if i%2 == 0 { 93 - continue 94 - } 95 - safeVal := fmt.Sprintf("%s", allArgs[i]) 96 - if strings.ContainsAny(safeVal, badGlyphs) { 97 - safeVal = fmt.Sprintf("%q", allArgs[i]) 98 - } 99 - str = fmt.Sprintf("%s %s=%s", str, keyCol(allArgs[i-1]), valCol(safeVal)) 100 - } 101 - fmt.Println(str) 102 + slog.Info(message, allArgs...) 102 103 } 103 104 104 105 func (v *VerboseLogger) Log(ctx context.Context, message string, args ...any) {
+73
pkg/model/model.go
··· 1 + package model 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "strings" 9 + "time" 10 + 11 + "aquareum.tv/aquareum/pkg/log" 12 + "github.com/lmittmann/tint" 13 + slogGorm "github.com/orandin/slog-gorm" 14 + "gorm.io/driver/sqlite" 15 + "gorm.io/gorm" 16 + ) 17 + 18 + type DBModel struct { 19 + DB *gorm.DB 20 + } 21 + 22 + type Model interface { 23 + CreateNotification(token string) error 24 + } 25 + 26 + type Notification struct { 27 + Token string `gorm:"primarykey"` 28 + CreatedAt time.Time 29 + UpdatedAt time.Time 30 + DeletedAt gorm.DeletedAt `gorm:"index"` 31 + } 32 + 33 + func MakeDB(dbURL string) (Model, error) { 34 + log.Log(context.Background(), "starting database", "dbURL", dbURL) 35 + if !strings.HasPrefix(dbURL, "sqlite://") { 36 + return nil, fmt.Errorf("only sqlite:// urls currently supported, got %s", dbURL) 37 + } 38 + sqliteSuffix := dbURL[len("sqlite://"):] 39 + // if this isn't ":memory:", ensure that directory exists (eg, if db 40 + // file is being initialized) 41 + if !strings.Contains(sqliteSuffix, ":?") { 42 + os.MkdirAll(filepath.Dir(sqliteSuffix), os.ModePerm) 43 + } 44 + dial := sqlite.Open(sqliteSuffix) 45 + 46 + gormLogger := slogGorm.New(slogGorm.WithHandler(tint.NewHandler(os.Stderr, &tint.Options{ 47 + TimeFormat: time.RFC3339, 48 + }))) 49 + 50 + db, err := gorm.Open(dial, &gorm.Config{ 51 + SkipDefaultTransaction: true, 52 + TranslateError: true, 53 + Logger: gormLogger, 54 + }) 55 + if err != nil { 56 + return nil, fmt.Errorf("error starting database: %w", err) 57 + } 58 + err = db.AutoMigrate(Notification{}) 59 + if err != nil { 60 + return nil, err 61 + } 62 + return &DBModel{DB: db}, nil 63 + } 64 + 65 + func (m *DBModel) CreateNotification(token string) error { 66 + err := m.DB.Model(Notification{}).Create(&Notification{ 67 + Token: token, 68 + }).Error 69 + if err != nil { 70 + return err 71 + } 72 + return nil 73 + }
+1
pkg/model/model_test.go
··· 1 + package model