1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-30 02:40:33 +00:00

Compare commits

...

3 commits

Author SHA1 Message Date
Ash Keel
edcc4fb7f9
fix: use log ID 2024-03-14 13:48:15 +01:00
Ash Keel
97a81373ab
docs: update changelog 2024-03-14 13:48:05 +01:00
Ash Keel
f4930d7758
feat: update kilovolt, replace zap with slog 2024-03-14 13:33:52 +01:00
40 changed files with 396 additions and 427 deletions

View file

@ -9,14 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- The windows can now shrink no more than 480x300 but the UI will now better adapt to small window sizes - The UI sidebar has been modified to better adapt to small window sizes
- A new part of the dashboard will now inform the user if any configuration problems have been detected. - A new part of the dashboard will now inform the user if any configuration problems have been detected.
### Changed ### Changed
- The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül. - The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül.
- The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same. - The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same.
- The `twitch/bot/@send-message` key has been renamed to `twitch/chat/@send-message`. The data structure is the same.
- A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys. - A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys.
- The log format has changed significantly as the internal logging library has been replaced.
- The Twitch chat integration has been rewritten from the ground up to not use an IRC bot and rely on EventSub. This means that you will need to reconfigure your twitch account, especially if you used a different account as the "bot" account. Because of this rewrite, the terminology around chat functionalities have been renamed from "Bot" to "Chat" (e.g. "Bot commands" are now "Chat commands").
- The (i) icon next to "Recent events" in the dashboard now uses a custom tooltip that shows up more consistently.
- The "strimertul is already running" message now pops up from the currently running instance.
### Removed
- `twitch/@send-chat-message` has been removed. Use `twitch/chat/@send-message` instead.
- The `twitch/ev/chat-message` and `twitch/chat-history` keys have been removed. Use the EventSub keys `twitch/ev/eventsub-event/channel.chat.message` and `twitch/eventsub-history/channel.chat.message` instead. The data structure will be different!
## 3.3.1 - 2023-11-12 ## 3.3.1 - 2023-11-12

64
app.go
View file

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
@ -13,14 +14,7 @@ import (
"strconv" "strconv"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
"github.com/nicklaw5/helix/v2" kv "git.sr.ht/~ashkeel/kilovolt/v12"
kv "github.com/strimertul/kilovolt/v11"
"github.com/urfave/cli/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/docs" "git.sr.ht/~ashkeel/strimertul/docs"
"git.sr.ht/~ashkeel/strimertul/loyalty" "git.sr.ht/~ashkeel/strimertul/loyalty"
@ -28,6 +22,10 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch" "git.sr.ht/~ashkeel/strimertul/twitch"
"git.sr.ht/~ashkeel/strimertul/twitch/client" "git.sr.ht/~ashkeel/strimertul/twitch/client"
"git.sr.ht/~ashkeel/strimertul/webserver" "git.sr.ht/~ashkeel/strimertul/webserver"
"github.com/nicklaw5/helix/v2"
"github.com/urfave/cli/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
// App struct // App struct
@ -60,7 +58,6 @@ func (a *App) startup(ctx context.Context) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
a.stop(ctx) a.stop(ctx)
_ = logger.Sync()
switch v := r.(type) { switch v := r.(type) {
case error: case error:
a.showFatalError(v, v.Error()) a.showFatalError(v, v.Error())
@ -70,7 +67,7 @@ func (a *App) startup(ctx context.Context) {
} }
}() }()
logger.Info("Started", zap.String("version", appVersion)) slog.Info("Started", slog.String("version", appVersion))
a.ctx = ctx a.ctx = ctx
@ -87,7 +84,7 @@ func (a *App) startup(ctx context.Context) {
} }
// Check for migrations // Check for migrations
if err := migrations.Run(a.driver, a.db, logger.With(zap.String("operation", "migration"))); err != nil { if err := migrations.Run(a.driver, a.db, slog.With(slog.String("operation", "migration"))); err != nil {
a.showFatalError(err, "Failed to migrate database to latest version") a.showFatalError(err, "Failed to migrate database to latest version")
return return
} }
@ -103,7 +100,7 @@ func (a *App) startup(ctx context.Context) {
a.ready.Set(true) a.ready.Set(true)
runtime.EventsEmit(ctx, "ready", true) runtime.EventsEmit(ctx, "ready", true)
logger.Info("Strimertul is ready") slog.Info("Strimertul is ready")
// Add logs I/O to UI // Add logs I/O to UI
a.cancelLogs, _ = a.listenForLogs() a.cancelLogs, _ = a.listenForLogs()
@ -134,7 +131,7 @@ func (a *App) initializeDatabase() error {
go hub.Run() go hub.Run()
hub.UseInteractiveAuth(a.interactiveAuth) hub.UseInteractiveAuth(a.interactiveAuth)
a.db, err = database.NewLocalClient(hub, logger) a.db, err = database.NewLocalClient(hub)
if err != nil { if err != nil {
return fmt.Errorf("could not initialize database client: %w", err) return fmt.Errorf("could not initialize database client: %w", err)
} }
@ -146,19 +143,19 @@ func (a *App) initializeComponents() error {
var err error var err error
// Create logger and endpoints // Create logger and endpoints
a.httpServer, err = webserver.NewServer(a.db, logger, webserver.DefaultServerFactory) a.httpServer, err = webserver.NewServer(a.db, webserver.DefaultServerFactory)
if err != nil { if err != nil {
return fmt.Errorf("could not initialize http server: %w", err) return fmt.Errorf("could not initialize http server: %w", err)
} }
// Create twitch client // Create twitch client
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer, logger) a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer)
if err != nil { if err != nil {
return fmt.Errorf("could not initialize twitch client: %w", err) return fmt.Errorf("could not initialize twitch client: %w", err)
} }
// Initialize loyalty system // Initialize loyalty system
a.loyaltyManager, err = loyalty.NewManager(a.db, logger) a.loyaltyManager, err = loyalty.NewManager(a.db)
if err != nil { if err != nil {
return fmt.Errorf("could not initialize loyalty manager: %w", err) return fmt.Errorf("could not initialize loyalty manager: %w", err)
} }
@ -173,13 +170,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
return return
} }
level, err := zapcore.ParseLevel(string(entry.Level)) var level slog.Level
if err != nil { if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
level = zapcore.InfoLevel level = slog.LevelInfo
} }
fields := parseAsFields(entry.Data) slog.Log(a.ctx, level, entry.Message, parseLogFields(entry.Data)...)
logger.Log(level, entry.Message, fields...)
}) })
} }
@ -209,7 +205,7 @@ func (a *App) AuthenticateKVClient(id string) {
if err != nil { if err != nil {
return return
} }
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", zap.String("session-id", id)) warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", slog.String("session-id", id))
} }
func (a *App) IsServerReady() bool { func (a *App) IsServerReady() bool {
@ -249,27 +245,26 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
// Add text fields // Add text fields
if err := w.WriteField("error", errorData); err != nil { if err := w.WriteField("error", errorData); err != nil {
logger.Error("Could not encode field error for crash report", zap.Error(err)) slog.Error("Could not encode field error for crash report", "error", err)
} }
if len(info) > 0 { if len(info) > 0 {
if err := w.WriteField("info", info); err != nil { if err := w.WriteField("info", info); err != nil {
logger.Error("Could not encode field info for crash report", zap.Error(err)) slog.Error("Could not encode field info for crash report", "error", err)
} }
} }
// Add log files // Add log files
_ = logger.Sync()
addFile(w, "log", logFilename) addFile(w, "log", logFilename)
addFile(w, "paniclog", panicFilename) addFile(w, "paniclog", panicFilename)
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
logger.Error("Could not prepare request for crash report", zap.Error(err)) slog.Error("Could not prepare request for crash report", "error", err)
return "", err return "", err
} }
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b) resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
if err != nil { if err != nil {
logger.Error("Could not send crash report", zap.Error(err)) slog.Error("Could not send crash report", "error", err)
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -277,7 +272,7 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
// Check the response // Check the response
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
byt, _ := io.ReadAll(resp.Body) byt, _ := io.ReadAll(resp.Body)
logger.Error("Crash report server returned error", zap.String("status", resp.Status), zap.String("response", string(byt))) slog.Error("Crash report server returned error", slog.String("status", resp.Status), slog.String("response", string(byt)))
return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt)) return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
} }
@ -326,11 +321,10 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
return <-authResult return <-authResult
} }
func (a *App) showFatalError(err error, text string, fields ...zap.Field) { func (a *App) showFatalError(err error, text string, fields ...any) {
if err != nil { if err != nil {
fields = append(fields, zap.Error(err)) fields = append(fields, "error", err, slog.String("Z", string(debug.Stack())))
fields = append(fields, zap.String("Z", string(debug.Stack()))) slog.Error(text, fields...)
logger.Error(text, fields...)
runtime.EventsEmit(a.ctx, "fatalError") runtime.EventsEmit(a.ctx, "fatalError")
a.isFatalError.Set(true) a.isFatalError.Set(true)
} }
@ -347,17 +341,17 @@ func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
func addFile(m *multipart.Writer, field string, filename string) { func addFile(m *multipart.Writer, field string, filename string) {
logfile, err := m.CreateFormFile(field, filename) logfile, err := m.CreateFormFile(field, filename)
if err != nil { if err != nil {
logger.Error("Could not encode field log for crash report", zap.Error(err)) slog.Error("Could not encode field log for crash report", "error", err)
return return
} }
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
logger.Error("Could not open file for including in crash report", zap.Error(err), zap.String("file", filename)) slog.Error("Could not open file for including in crash report", slog.String("file", filename), "error", err)
return return
} }
if _, err = io.Copy(logfile, file); err != nil { if _, err = io.Copy(logfile, file); err != nil {
logger.Error("Could not read from file for including in crash report", zap.Error(err), zap.String("file", filename)) slog.Error("Could not read from file for including in crash report", slog.String("file", filename), "error", err)
} }
} }

View file

@ -2,28 +2,27 @@ package main
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"time" "time"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils" "git.sr.ht/~ashkeel/strimertul/utils"
) )
func BackupTask(driver database.Driver, options database.BackupOptions) { func BackupTask(driver database.Driver, options database.BackupOptions) {
if options.BackupDir == "" { if options.BackupDir == "" {
logger.Warn("Backup directory not set, database backups are disabled") slog.Warn("Backup directory not set, database backups are disabled")
return return
} }
err := os.MkdirAll(options.BackupDir, 0o755) err := os.MkdirAll(options.BackupDir, 0o755)
if err != nil { if err != nil {
logger.Error("Could not create backup directory, moving to a temporary folder", zap.Error(err)) slog.Error("Could not create backup directory, moving to a temporary folder", "error", err)
options.BackupDir = os.TempDir() options.BackupDir = os.TempDir()
logger.Info("Using temporary directory", zap.String("backup-dir", options.BackupDir)) slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
return return
} }
@ -38,21 +37,21 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
// Run backup procedure // Run backup procedure
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405"))) file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
if err != nil { if err != nil {
logger.Error("Could not create backup file", zap.Error(err)) slog.Error("Could not create backup file", "error", err)
return return
} }
err = driver.Backup(file) err = driver.Backup(file)
if err != nil { if err != nil {
logger.Error("Could not backup database", zap.Error(err)) slog.Error("Could not backup database", "error", err)
} }
_ = file.Close() _ = file.Close()
logger.Info("Database backup created", zap.String("backup-file", file.Name())) slog.Info("Database backup created", slog.String("backup-file", file.Name()))
// Remove old backups // Remove old backups
files, err := os.ReadDir(options.BackupDir) files, err := os.ReadDir(options.BackupDir)
if err != nil { if err != nil {
logger.Error("Could not read backup directory", zap.Error(err)) slog.Error("Could not read backup directory", "error", err)
return return
} }
@ -65,7 +64,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
for _, file := range toRemove { for _, file := range toRemove {
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name())) err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
if err != nil { if err != nil {
logger.Error("Could not remove backup file", zap.Error(err)) slog.Error("Could not remove backup file", "error", err)
} }
} }
} }
@ -80,7 +79,7 @@ type BackupInfo struct {
func (a *App) GetBackups() (list []BackupInfo) { func (a *App) GetBackups() (list []BackupInfo) {
files, err := os.ReadDir(a.backupOptions.BackupDir) files, err := os.ReadDir(a.backupOptions.BackupDir)
if err != nil { if err != nil {
logger.Error("Could not read backup directory", zap.Error(err)) slog.Error("Could not read backup directory", "error", err)
return nil return nil
} }
@ -91,7 +90,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
info, err := file.Info() info, err := file.Info()
if err != nil { if err != nil {
logger.Error("Could not get info for backup file", zap.Error(err)) slog.Error("Could not get info for backup file", "error", err)
continue continue
} }
@ -111,7 +110,7 @@ func (a *App) RestoreBackup(backupName string) error {
if err != nil { if err != nil {
return fmt.Errorf("could not open import file for reading: %w", err) return fmt.Errorf("could not open import file for reading: %w", err)
} }
defer utils.Close(file, logger) defer utils.Close(file)
inStream := file inStream := file
if a.driver == nil { if a.driver == nil {
@ -126,6 +125,6 @@ func (a *App) RestoreBackup(backupName string) error {
return fmt.Errorf("could not restore database: %w", err) return fmt.Errorf("could not restore database: %w", err)
} }
logger.Info("Restored database from backup") slog.Info("Restored database from backup")
return nil return nil
} }

View file

@ -1,13 +1,13 @@
package main package main
import ( import (
"log/slog"
"os" "os"
"git.sr.ht/~ashkeel/strimertul/utils" "github.com/urfave/cli/v2"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/urfave/cli/v2"
) )
func cliImport(ctx *cli.Context) error { func cliImport(ctx *cli.Context) error {
@ -18,7 +18,7 @@ func cliImport(ctx *cli.Context) error {
if err != nil { if err != nil {
return fatalError(err, "could not open import file for reading") return fatalError(err, "could not open import file for reading")
} }
defer utils.Close(file, logger) defer utils.Close(file)
inStream = file inStream = file
} }
var entries map[string]string var entries map[string]string
@ -37,7 +37,7 @@ func cliImport(ctx *cli.Context) error {
return fatalError(err, "import failed") return fatalError(err, "import failed")
} }
logger.Info("Imported database from file") slog.Info("Imported database from file")
return nil return nil
} }
@ -49,7 +49,7 @@ func cliRestore(ctx *cli.Context) error {
if err != nil { if err != nil {
return fatalError(err, "could not open import file for reading") return fatalError(err, "could not open import file for reading")
} }
defer utils.Close(file, logger) defer utils.Close(file)
inStream = file inStream = file
} }
@ -63,7 +63,7 @@ func cliRestore(ctx *cli.Context) error {
return fatalError(err, "restore failed") return fatalError(err, "restore failed")
} }
logger.Info("Restored database from backup") slog.Info("Restored database from backup")
return nil return nil
} }
@ -75,7 +75,7 @@ func cliExport(ctx *cli.Context) error {
if err != nil { if err != nil {
return fatalError(err, "could not open output file for writing") return fatalError(err, "could not open output file for writing")
} }
defer utils.Close(file, logger) defer utils.Close(file)
outStream = file outStream = file
} }
@ -89,6 +89,6 @@ func cliExport(ctx *cli.Context) error {
return fatalError(err, "export failed") return fatalError(err, "export failed")
} }
logger.Info("Exported database") slog.Info("Exported database")
return nil return nil
} }

View file

@ -2,18 +2,15 @@ package database
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
jsoniter "github.com/json-iterator/go" kv "git.sr.ht/~ashkeel/kilovolt/v12"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
) )
type CancelFunc func() type CancelFunc func()
var json = jsoniter.ConfigFastest
var ( var (
// ErrUnknown is returned when a response is received that doesn't match any expected outcome. // ErrUnknown is returned when a response is received that doesn't match any expected outcome.
ErrUnknown = errors.New("unknown error") ErrUnknown = errors.New("unknown error")
@ -52,9 +49,9 @@ type KvPair struct {
Data string Data string
} }
func NewLocalClient(hub *kv.Hub, logger *zap.Logger) (*LocalDBClient, error) { func NewLocalClient(hub *kv.Hub) (*LocalDBClient, error) {
// Create local client // Create local client
localClient := kv.NewLocalClient(kv.ClientOptions{}, logger) localClient := kv.NewLocalClient(kv.ClientOptions{})
// Run client and add it to the hub // Run client and add it to the hub
go localClient.Run() go localClient.Run()

View file

@ -5,8 +5,9 @@ import (
"testing" "testing"
"time" "time"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
kv "github.com/strimertul/kilovolt/v11"
) )
func TestLocalDBClientPutKey(t *testing.T) { func TestLocalDBClientPutKey(t *testing.T) {

View file

@ -6,11 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
kv "github.com/strimertul/kilovolt/v11" kv "git.sr.ht/~ashkeel/kilovolt/v12"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/utils"
) )
// Driver is a driver wrapping a supported database // Driver is a driver wrapping a supported database
@ -49,13 +46,12 @@ func getDatabaseDriverName(ctx *cli.Context) string {
func GetDatabaseDriver(ctx *cli.Context) (Driver, error) { func GetDatabaseDriver(ctx *cli.Context) (Driver, error) {
name := getDatabaseDriverName(ctx) name := getDatabaseDriverName(ctx)
dbDirectory := ctx.String("database-dir") dbDirectory := ctx.String("database-dir")
logger := ctx.Context.Value(utils.ContextLogger).(*zap.Logger)
switch name { switch name {
case "badger": case "badger":
return nil, cli.Exit("Badger is not supported anymore as a database driver", 64) return nil, cli.Exit("Badger is not supported anymore as a database driver", 64)
case "pebble": case "pebble":
db, err := NewPebble(dbDirectory, logger) db, err := NewPebble(dbDirectory)
if err != nil { if err != nil {
return nil, cli.Exit(err.Error(), 64) return nil, cli.Exit(err.Error(), 64)
} }

View file

@ -1,27 +1,27 @@
package database package database
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
pebbledriver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
"git.sr.ht/~ashkeel/strimertul/utils" "git.sr.ht/~ashkeel/strimertul/utils"
pebble_driver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
"github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
) )
type PebbleDatabase struct { type PebbleDatabase struct {
db *pebble.DB db *pebble.DB
hub *kv.Hub hub *kv.Hub
logger *zap.Logger logger *slog.Logger
} }
// NewPebble creates a new database driver instance with an underlying Pebble database // NewPebble creates a new database driver instance with an underlying Pebble database
func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) { func NewPebble(directory string) (*PebbleDatabase, error) {
db, err := pebble.Open(directory, &pebble.Options{}) db, err := pebble.Open(directory, &pebble.Options{})
if err != nil { if err != nil {
return nil, fmt.Errorf("could not open DB: %w", err) return nil, fmt.Errorf("could not open DB: %w", err)
@ -34,9 +34,8 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
} }
p := &PebbleDatabase{ p := &PebbleDatabase{
db: db, db: db,
hub: nil, hub: nil,
logger: logger,
} }
return p, nil return p, nil
@ -44,7 +43,9 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
func (p *PebbleDatabase) Hub() *kv.Hub { func (p *PebbleDatabase) Hub() *kv.Hub {
if p.hub == nil { if p.hub == nil {
p.hub, _ = kv.NewHub(pebble_driver.NewPebbleBackend(p.db, true), kv.HubOptions{}, p.logger) p.hub, _ = kv.NewHub(pebbledriver.NewPebbleBackend(p.db, true), kv.HubOptions{
Logger: p.logger,
})
} }
return p.hub return p.hub
} }
@ -95,13 +96,13 @@ func (p *PebbleDatabase) Restore(file io.Reader) error {
func (p *PebbleDatabase) Backup(file io.Writer) error { func (p *PebbleDatabase) Backup(file io.Writer) error {
snapshot := p.db.NewSnapshot() snapshot := p.db.NewSnapshot()
defer utils.Close(snapshot, p.logger) defer utils.Close(snapshot)
iter, err := snapshot.NewIter(&pebble.IterOptions{}) iter, err := snapshot.NewIter(&pebble.IterOptions{})
if err != nil { if err != nil {
return err return err
} }
defer utils.Close(iter, p.logger) defer utils.Close(iter)
out := make(map[string]string) out := make(map[string]string)
for iter.First(); iter.Valid(); iter.Next() { for iter.First(); iter.Valid(); iter.Next() {

View file

@ -3,23 +3,20 @@ package database
import ( import (
"testing" "testing"
kv "github.com/strimertul/kilovolt/v11" kv "git.sr.ht/~ashkeel/kilovolt/v12"
"go.uber.org/zap/zaptest"
) )
func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) { func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) {
logger := zaptest.NewLogger(t)
// Create in-memory store and hub // Create in-memory store and hub
inMemoryStore := kv.MakeBackend() inMemoryStore := kv.MakeBackend()
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{}, logger) hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
go hub.Run() go hub.Run()
// Create local client // Create local client
client, err := NewLocalClient(hub, logger) client, err := NewLocalClient(hub)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -453,9 +453,9 @@
"dialog-title": "Application logs", "dialog-title": "Application logs",
"levelFilter": "Filter per log severity", "levelFilter": "Filter per log severity",
"level": { "level": {
"info": "Info", "INFO": "Info",
"warn": "Warning", "WARN": "Warning",
"error": "Error" "ERROR": "Error"
}, },
"copy-to-clipboard": "Copy to clipboard", "copy-to-clipboard": "Copy to clipboard",
"copied": "Copied!", "copied": "Copied!",

View file

@ -36,9 +36,9 @@
"copy-to-clipboard": "Copia negli appunti", "copy-to-clipboard": "Copia negli appunti",
"levelFilter": "Filtra per livello", "levelFilter": "Filtra per livello",
"level": { "level": {
"error": "Errore", "ERROR": "Errore",
"info": "Info", "INFO": "Info",
"warn": "Avvertimento" "WARN": "Avvertimento"
}, },
"toggle-details": "Mostra dettagli" "toggle-details": "Mostra dettagli"
}, },

View file

@ -3,23 +3,23 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { main } from '@wailsapp/go/models'; import { main } from '@wailsapp/go/models';
export interface ProcessedLogEntry { export interface ProcessedLogEntry {
id: string;
time: Date; time: Date;
caller: string;
level: string; level: string;
message: string; message: string;
data: object; data: object;
} }
export function processEntry({ export function processEntry({
id,
time, time,
caller,
level, level,
message, message,
data, data,
}: main.LogEntry): ProcessedLogEntry { }: main.LogEntry): ProcessedLogEntry {
return { return {
id,
time: new Date(time), time: new Date(time),
caller,
level, level,
message, message,
data: JSON.parse(data) as object, data: JSON.parse(data) as object,
@ -34,12 +34,21 @@ const initialState: LoggingState = {
messages: [], messages: [],
}; };
const keyfn = (ev: main.LogEntry) => ev.id;
const loggingReducer = createSlice({ const loggingReducer = createSlice({
name: 'logging', name: 'logging',
initialState, initialState,
reducers: { reducers: {
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) { loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
state.messages = payload const logKeys = payload.map(keyfn);
// Clean up duplicates before setting to state
const uniqueLogs = payload.filter(
(ev, pos) => logKeys.indexOf(keyfn(ev)) === pos,
);
state.messages = uniqueLogs
.map(processEntry) .map(processEntry)
.sort((a, b) => a.time.getTime() - b.time.getTime()); .sort((a, b) => a.time.getTime() - b.time.getTime());
}, },

View file

@ -46,12 +46,12 @@ const LogBubble = styled('div', {
}, },
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
backgroundColor: '$yellow6', backgroundColor: '$yellow6',
color: '$yellow11', color: '$yellow11',
}, },
error: { ERROR: {
backgroundColor: '$red6', backgroundColor: '$red6',
color: '$red11', color: '$red11',
}, },
@ -60,12 +60,12 @@ const LogBubble = styled('div', {
}); });
const emptyFilter = { const emptyFilter = {
info: false, INFO: false,
warn: false, WARN: false,
error: false, ERROR: false,
}; };
type LogLevel = keyof typeof emptyFilter; type LogLevel = keyof typeof emptyFilter;
const levels: LogLevel[] = ['info', 'warn', 'error']; const levels: LogLevel[] = ['INFO', 'WARN', 'ERROR'];
function isSupportedLevel(level: string): level is LogLevel { function isSupportedLevel(level: string): level is LogLevel {
return (levels as string[]).includes(level); return (levels as string[]).includes(level);
@ -89,7 +89,7 @@ const LevelToggle = styled(MultiToggleItem, {
}, },
variants: { variants: {
level: { level: {
info: { INFO: {
backgroundColor: '$gray4', backgroundColor: '$gray4',
[`.${lightMode} &`]: { [`.${lightMode} &`]: {
backgroundColor: '$gray2', backgroundColor: '$gray2',
@ -112,7 +112,7 @@ const LevelToggle = styled(MultiToggleItem, {
}, },
}, },
}, },
warn: { WARN: {
backgroundColor: '$yellow4', backgroundColor: '$yellow4',
[`.${lightMode} &`]: { [`.${lightMode} &`]: {
backgroundColor: '$yellow2', backgroundColor: '$yellow2',
@ -135,7 +135,7 @@ const LevelToggle = styled(MultiToggleItem, {
}, },
}, },
}, },
error: { ERROR: {
backgroundColor: '$red4', backgroundColor: '$red4',
[`.${lightMode} &`]: { [`.${lightMode} &`]: {
backgroundColor: '$red2', backgroundColor: '$red2',
@ -175,11 +175,11 @@ const LogEntryContainer = styled('div', {
fontSize: '0.9em', fontSize: '0.9em',
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
backgroundColor: '$yellow4', backgroundColor: '$yellow4',
}, },
error: { ERROR: {
backgroundColor: '$red6', backgroundColor: '$red6',
}, },
}, },
@ -199,12 +199,12 @@ const LogTime = styled('div', {
borderBottomLeftRadius: theme.borderRadius.form, borderBottomLeftRadius: theme.borderRadius.form,
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
color: '$yellow11', color: '$yellow11',
backgroundColor: '$yellow6', backgroundColor: '$yellow6',
}, },
error: { ERROR: {
color: '$red11', color: '$red11',
backgroundColor: '$red7', backgroundColor: '$red7',
}, },
@ -230,13 +230,13 @@ const LogActions = styled('div', {
}, },
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
'& a:hover': { '& a:hover': {
color: '$yellow11', color: '$yellow11',
}, },
}, },
error: { ERROR: {
'& a:hover': { '& a:hover': {
color: '$red11', color: '$red11',
}, },
@ -258,11 +258,11 @@ const LogDetails = styled('div', {
borderBottomLeftRadius: theme.borderRadius.form, borderBottomLeftRadius: theme.borderRadius.form,
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
backgroundColor: '$yellow3', backgroundColor: '$yellow3',
}, },
error: { ERROR: {
backgroundColor: '$red4', backgroundColor: '$red4',
}, },
}, },
@ -276,11 +276,11 @@ const LogDetailKey = styled('div', {
color: '$teal10', color: '$teal10',
variants: { variants: {
level: { level: {
info: {}, INFO: {},
warn: { WARN: {
color: '$yellow11', color: '$yellow11',
}, },
error: { ERROR: {
color: '$red11', color: '$red11',
}, },
}, },
@ -430,10 +430,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
> >
<LogEntriesContainer> <LogEntriesContainer>
{filtered.reverse().map((entry) => ( {filtered.reverse().map((entry) => (
<LogItem <LogItem key={entry.id} data={entry} />
key={entry.caller + entry.time.getTime().toString()}
data={entry}
/>
))} ))}
</LogEntriesContainer> </LogEntriesContainer>
</Scrollbar> </Scrollbar>

View file

@ -469,7 +469,7 @@ function TwitchSection() {
const onKeyChange = (value: string) => { const onKeyChange = (value: string) => {
const event = JSON.parse(value) as EventSubNotification; const event = JSON.parse(value) as EventSubNotification;
void setCleanTwitchEvents((prev) => [event, ...prev]); void setCleanTwitchEvents([event, ...twitchEvents]);
}; };
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange); void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);

View file

@ -73,7 +73,7 @@ export namespace main {
} }
} }
export class LogEntry { export class LogEntry {
caller: string; id: string;
time: string; time: string;
level: string; level: string;
message: string; message: string;
@ -85,7 +85,7 @@ export namespace main {
constructor(source: any = {}) { constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.caller = source["caller"]; this.id = source["id"];
this.time = source["time"]; this.time = source["time"];
this.level = source["level"]; this.level = source["level"];
this.message = source["message"]; this.message = source["message"];

7
go.mod
View file

@ -6,7 +6,8 @@ toolchain go1.22.0
require ( require (
git.sr.ht/~ashkeel/containers v0.3.6 git.sr.ht/~ashkeel/containers v0.3.6
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5 git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
github.com/cockroachdb/pebble v1.1.0 github.com/cockroachdb/pebble v1.1.0
@ -14,10 +15,9 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/nicklaw5/helix/v2 v2.28.1 github.com/nicklaw5/helix/v2 v2.28.1
github.com/strimertul/kilovolt/v11 v11.0.1 github.com/samber/slog-multi v1.0.2
github.com/urfave/cli/v2 v2.27.1 github.com/urfave/cli/v2 v2.27.1
github.com/wailsapp/wails/v2 v2.8.0 github.com/wailsapp/wails/v2 v2.8.0
go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
@ -79,7 +79,6 @@ require (
github.com/wailsapp/go-webview2 v1.0.10 // indirect github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.20.0 // indirect

16
go.sum
View file

@ -33,8 +33,10 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.sr.ht/~ashkeel/containers v0.3.6 h1:+umWlQGKhLxGQlaEUt/F6rBZGpeBd1T01fM3wro+qTY= git.sr.ht/~ashkeel/containers v0.3.6 h1:+umWlQGKhLxGQlaEUt/F6rBZGpeBd1T01fM3wro+qTY=
git.sr.ht/~ashkeel/containers v0.3.6/go.mod h1:i2KocnJfRH0FwfgPi4nw7/ehYLEoLlP3iwdDoBeVdME= git.sr.ht/~ashkeel/containers v0.3.6/go.mod h1:i2KocnJfRH0FwfgPi4nw7/ehYLEoLlP3iwdDoBeVdME=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5 h1:btFQjGLisyJwwLgzQ80l39kGlKyCHkVTnbGMnlRwxIk= git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1 h1:AQORTHRA6Ce1TW24FgHN1K5hWT8+/5nC7sJ5L2W+tRM=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5/go.mod h1:MPfBoPjCurdVQzwvSg49VDkeS/B+Ja96JnZ5cykksL8= git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1/go.mod h1:lPYwJ9bumCDzeklkhXh0OCasiB7omzW+ykaLUZ30waE=
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0 h1:whWstBXDkj3WEEDpQu3+zTWYZNlJtmlDcUkAnkleY4g=
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0/go.mod h1:FGg4hwyY/e0G/mpLgUA/nYVIpHUTc4uFIwesM/MJVtA=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
@ -297,6 +299,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=
github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@ -313,8 +317,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/strimertul/kilovolt/v11 v11.0.1 h1:fV33Z3b168LeSROzzV0dZcI2Jptg3TvF2FbEGxfEVGI=
github.com/strimertul/kilovolt/v11 v11.0.1/go.mod h1:PjhGVWb74lB8dXSGWA7GmVSbZAoGV/WGGmjS2Zz/UBg=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
@ -342,12 +344,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View file

@ -1,53 +1,49 @@
package main package main
import ( import (
"context"
"fmt"
"log/slog"
"math/rand"
"os" "os"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
slogmulti "github.com/samber/slog-multi"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
const LogHistory = 50 const LogHistory = 50
var ( var (
logger *zap.Logger
lastLogs *sync.Slice[LogEntry] lastLogs *sync.Slice[LogEntry]
incomingLogs chan LogEntry incomingLogs chan LogEntry
) )
func initLogger(level zapcore.Level) { func initLogger(level slog.Level) {
lastLogs = sync.NewSlice[LogEntry]() lastLogs = sync.NewSlice[LogEntry]()
incomingLogs = make(chan LogEntry, 100) incomingLogs = make(chan LogEntry, 100)
logStorage := NewLogStorage(level) logStorage := NewLogStorage(level)
consoleLogger := zapcore.NewCore( fileLogger := &lumberjack.Logger{
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), Filename: logFilename,
zapcore.Lock(os.Stderr), MaxSize: 20,
level, MaxBackups: 3,
) MaxAge: 28,
fileLogger := zapcore.NewCore( }
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), consoleHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
zapcore.AddSync(&lumberjack.Logger{ Level: level,
Filename: logFilename, })
MaxSize: 20, fileHandler := slog.NewJSONHandler(fileLogger, &slog.HandlerOptions{
MaxBackups: 3, AddSource: true,
MaxAge: 28, Level: level,
}), })
level, logger := slog.New(slogmulti.Fanout(consoleHandler, fileHandler, logStorage))
)
core := zapcore.NewTee( slog.SetDefault(logger)
consoleLogger,
fileLogger,
logStorage,
)
logger = zap.New(core, zap.AddCaller())
} }
type LogEntry struct { type LogEntry struct {
Caller string `json:"caller"` ID string `json:"id"`
Time string `json:"time"` Time string `json:"time"`
Level string `json:"level"` Level string `json:"level"`
Message string `json:"message"` Message string `json:"message"`
@ -55,42 +51,32 @@ type LogEntry struct {
} }
type LogStorage struct { type LogStorage struct {
zapcore.LevelEnabler minLevel slog.Level
fields []zapcore.Field attrs []slog.Attr
encoder zapcore.Encoder group string
} }
func NewLogStorage(enabler zapcore.LevelEnabler) *LogStorage { func (core *LogStorage) Enabled(_ context.Context, level slog.Level) bool {
return &LogStorage{ return level >= core.minLevel
LevelEnabler: enabler,
encoder: zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
}
} }
func (core *LogStorage) With(fields []zapcore.Field) zapcore.Core { func (core *LogStorage) Handle(_ context.Context, record slog.Record) error {
clone := *core attributes := map[string]any{}
clone.fields = append(clone.fields, fields...) record.Attrs(func(attrs slog.Attr) bool {
return &clone attributes[attrs.Key] = attrs.Value.Any()
} return true
})
attrJSON, _ := json.MarshalToString(attributes)
func (core *LogStorage) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { // Generate unique log ID
if core.Enabled(entry.Level) { id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
return checked.AddCore(entry, core)
}
return checked
}
func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error {
buf, err := core.encoder.EncodeEntry(entry, append(core.fields, fields...))
if err != nil {
return err
}
logEntry := LogEntry{ logEntry := LogEntry{
Caller: entry.Caller.String(), ID: id,
Time: entry.Time.Format(time.RFC3339), Time: record.Time.Format(time.RFC3339),
Level: entry.Level.String(), Level: record.Level.String(),
Message: entry.Message, Message: record.Message,
Data: buf.String(), Data: attrJSON,
} }
lastLogs.Push(logEntry) lastLogs.Push(logEntry)
if lastLogs.Size() > LogHistory { if lastLogs.Size() > LogHistory {
@ -100,13 +86,32 @@ func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error
return nil return nil
} }
func (core *LogStorage) Sync() error { func (core *LogStorage) WithAttrs(attrs []slog.Attr) slog.Handler {
return nil return &LogStorage{
minLevel: core.minLevel,
attrs: append(core.attrs, attrs...),
group: core.group,
}
} }
func parseAsFields(fields map[string]any) (result []zapcore.Field) { func (core *LogStorage) WithGroup(name string) slog.Handler {
for k, v := range fields { return &LogStorage{
result = append(result, zap.Any(k, v)) minLevel: core.minLevel,
attrs: core.attrs,
group: name,
} }
return }
func NewLogStorage(level slog.Level) *LogStorage {
return &LogStorage{
minLevel: level,
}
}
func parseLogFields(data map[string]any) []any {
fields := []any{}
for k, v := range data {
fields = append(fields, k, v)
}
return fields
} }

View file

@ -4,15 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"strings" "strings"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils" "git.sr.ht/~ashkeel/strimertul/utils"
jsoniter "github.com/json-iterator/go"
) )
var json = jsoniter.ConfigFastest var json = jsoniter.ConfigFastest
@ -31,7 +30,6 @@ type Manager struct {
Goals *sync.Slice[Goal] Goals *sync.Slice[Goal]
Queue *sync.Slice[Redeem] Queue *sync.Slice[Redeem]
db database.Database db database.Database
logger *zap.Logger
cooldowns map[string]time.Time cooldowns map[string]time.Time
banlist map[string]bool banlist map[string]bool
ctx context.Context ctx context.Context
@ -40,7 +38,7 @@ type Manager struct {
restartTwitchHandler chan struct{} restartTwitchHandler chan struct{}
} }
func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) { func NewManager(db database.Database) (*Manager, error) {
ctx, cancelFn := context.WithCancel(context.Background()) ctx, cancelFn := context.WithCancel(context.Background())
loyalty := &Manager{ loyalty := &Manager{
Config: sync.NewRWSync(Config{Enabled: false}), Config: sync.NewRWSync(Config{Enabled: false}),
@ -48,7 +46,6 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
Goals: sync.NewSlice[Goal](), Goals: sync.NewSlice[Goal](),
Queue: sync.NewSlice[Redeem](), Queue: sync.NewSlice[Redeem](),
logger: logger,
db: db, db: db,
points: sync.NewMap[string, PointsEntry](), points: sync.NewMap[string, PointsEntry](),
cooldowns: make(map[string]time.Time), cooldowns: make(map[string]time.Time),
@ -117,7 +114,7 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
// SubscribePrefix for changes // SubscribePrefix for changes
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/") loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
if err != nil { if err != nil {
logger.Error("Could not setup loyalty reload subscription", zap.Error(err)) slog.Error("Could not setup loyalty reload subscription", "error", err)
} }
loyalty.SetBanList(config.BanList) loyalty.SetBanList(config.BanList)
@ -177,9 +174,9 @@ func (m *Manager) update(key, value string) {
} }
} }
if err != nil { if err != nil {
m.logger.Error("Subscribe error: invalid JSON received on key", zap.Error(err), zap.String("key", key)) slog.Error("Subscribe error: invalid JSON received on key", slog.String("key", key), "error", err)
} else { } else {
m.logger.Debug("Updated key", zap.String("key", key)) slog.Debug("Updated key", slog.String("key", key))
} }
} }

45
main.go
View file

@ -5,9 +5,12 @@ import (
"embed" "embed"
"fmt" "fmt"
"log" "log"
"log/slog"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"runtime/debug"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/apenwarr/fixconsole" "github.com/apenwarr/fixconsole"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -15,15 +18,13 @@ import (
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"git.sr.ht/~ashkeel/strimertul/utils"
) )
var json = jsoniter.ConfigFastest var json = jsoniter.ConfigFastest
var appVersion = "v0.0.0-UNKNOWN" const devVersionMarker = "v0.0.0-UNKNOWN"
var appVersion = devVersionMarker
const ( const (
crashReportURL = "https://crash.strimertul.stream/upload" crashReportURL = "https://crash.strimertul.stream/upload"
@ -35,8 +36,7 @@ const (
var frontend embed.FS var frontend embed.FS
func main() { func main() {
err := fixconsole.FixConsoleIfNeeded() if err := fixconsole.FixConsoleIfNeeded(); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -86,32 +86,34 @@ func main() {
}, },
Before: func(ctx *cli.Context) error { Before: func(ctx *cli.Context) error {
// Initialize logger with global flags // Initialize logger with global flags
level, err := zapcore.ParseLevel(ctx.String("log-level")) level := slog.LevelInfo
if err != nil {
level = zapcore.InfoLevel if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
} }
initLogger(level) initLogger(level)
// Create file for panics // Create file for panics
var err error
panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666) panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil { if err != nil {
logger.Warn("Could not create panic log", zap.Error(err)) slog.Warn("Could not create panic log", "error", err)
} else { } else {
utils.RedirectStderr(panicLog) utils.RedirectStderr(panicLog)
} }
zap.RedirectStdLog(logger)() // For development builds, force crash dumps
if isDev() {
debug.SetTraceback("crash")
}
ctx.Context = context.WithValue(ctx.Context, utils.ContextLogger, logger)
return nil return nil
}, },
After: func(_ *cli.Context) error { After: func(_ *cli.Context) error {
if panicLog != nil { if panicLog != nil {
utils.Close(panicLog, logger) utils.Close(panicLog)
} }
_ = logger.Sync()
return nil return nil
}, },
} }
@ -164,13 +166,18 @@ func cliMain(ctx *cli.Context) error {
return nil return nil
} }
func warnOnError(err error, text string, fields ...zap.Field) { func warnOnError(err error, text string, fields ...any) {
if err != nil { if err != nil {
fields = append(fields, zap.Error(err)) fields = append(fields, "error", err)
logger.Warn(text, fields...) slog.Warn(text, fields...)
} }
} }
func fatalError(err error, text string) error { func fatalError(err error, text string) error {
return cli.Exit(fmt.Errorf("%s: %w", text, err), 1) return cli.Exit(fmt.Errorf("%s: %w", text, err), 1)
} }
// isDev checks if the running code is a development version
func isDev() bool {
return appVersion == devVersionMarker
}

View file

@ -4,10 +4,9 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"strconv" "strconv"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
) )
@ -33,18 +32,18 @@ func GetCurrentSchemaVersion(db database.Database) (int, error) {
return strconv.Atoi(versionStr) return strconv.Atoi(versionStr)
} }
func Run(driver database.Driver, db database.Database, logger *zap.Logger) (err error) { func Run(driver database.Driver, db database.Database, logger *slog.Logger) (err error) {
// Make a backup of the database // Make a backup of the database
var buffer bytes.Buffer var buffer bytes.Buffer
if err = driver.Backup(&buffer); err != nil { if err = driver.Backup(&buffer); err != nil {
return fmt.Errorf("failed to backup database: %s", err) return fmt.Errorf("failed to backup database: %w", err)
} }
// Restore the backup if an error occurs // Restore the backup if an error occurs
defer func() { defer func() {
if err != nil { if err != nil {
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes())) restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
if restoreErr != nil { if restoreErr != nil {
logger.Error("Failed to restore database from backup", zap.Error(restoreErr)) logger.Error("Failed to restore database from backup", "error", restoreErr)
} }
} }
}() }()

View file

@ -2,6 +2,7 @@ package migrations
import ( import (
"encoding/json" "encoding/json"
"log/slog"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub" "git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
@ -10,10 +11,9 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch/chat" "git.sr.ht/~ashkeel/strimertul/twitch/chat"
"git.sr.ht/~ashkeel/strimertul/twitch/timers" "git.sr.ht/~ashkeel/strimertul/twitch/timers"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
) )
func migrateToV4(db database.Database, logger *zap.Logger) error { func migrateToV4(db database.Database, logger *slog.Logger) error {
logger.Info("Migrating database from v3 to v4") logger.Info("Migrating database from v3 to v4")
// Rename keys that have no schema changes // Rename keys that have no schema changes

View file

@ -1,8 +1,9 @@
package main package main
import ( import (
"log/slog"
"git.sr.ht/~ashkeel/strimertul/twitch" "git.sr.ht/~ashkeel/strimertul/twitch"
"go.uber.org/zap"
) )
type ProblemID string type ProblemID string
@ -26,7 +27,7 @@ func (a *App) GetProblems() (problems []Problem) {
// Check if the app needs to be authorized again // Check if the app needs to be authorized again
scopesMatch, err := twitch.CheckScopes(client.DB) scopesMatch, err := twitch.CheckScopes(client.DB)
if err != nil { if err != nil {
logger.Warn("Could not check scopes for problems", zap.Error(err)) slog.Warn("Could not check scopes for problems", "error", err)
} else { } else {
if !scopesMatch { if !scopesMatch {
problems = append(problems, Problem{ problems = append(problems, Problem{

View file

@ -1,19 +1,18 @@
package alerts package alerts
import ( import (
"encoding/json"
"math/rand" "math/rand"
"text/template" "text/template"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/twitch/chat" "git.sr.ht/~ashkeel/strimertul/twitch/chat"
"github.com/nicklaw5/helix/v2"
) )
func (m *Module) onEventSubEvent(_ string, value string) { func (m *Module) onEventSubEvent(_ string, value string) {
var ev eventSubNotification var ev eventSubNotification
if err := json.UnmarshalFromString(value, &ev); err != nil { if err := json.Unmarshal([]byte(value), &ev); err != nil {
m.logger.Warn("Error parsing webhook payload", zap.Error(err)) m.logger.Warn("Error parsing webhook payload", "error", err)
return return
} }
switch ev.Subscription.Type { switch ev.Subscription.Type {
@ -25,7 +24,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as a follow event // Parse as a follow event
var followEv helix.EventSubChannelFollowEvent var followEv helix.EventSubChannelFollowEvent
if err := json.Unmarshal(ev.Event, &followEv); err != nil { if err := json.Unmarshal(ev.Event, &followEv); err != nil {
m.logger.Warn("Error parsing follow event", zap.Error(err)) m.logger.Warn("Error parsing follow event", "error", err)
return return
} }
// Pick a random message // Pick a random message
@ -50,7 +49,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
var raidEv helix.EventSubChannelRaidEvent var raidEv helix.EventSubChannelRaidEvent
if err := json.Unmarshal(ev.Event, &raidEv); err != nil { if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
m.logger.Warn("Error parsing raid event", zap.Error(err)) m.logger.Warn("Error parsing raid event", "error", err)
return return
} }
// Pick a random message from base set // Pick a random message from base set
@ -84,7 +83,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as cheer event // Parse as cheer event
var cheerEv helix.EventSubChannelCheerEvent var cheerEv helix.EventSubChannelCheerEvent
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil { if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
m.logger.Warn("Error parsing cheer event", zap.Error(err)) m.logger.Warn("Error parsing cheer event", "error", err)
return return
} }
// Pick a random message from base set // Pick a random message from base set
@ -118,7 +117,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as subscription event // Parse as subscription event
var subEv helix.EventSubChannelSubscribeEvent var subEv helix.EventSubChannelSubscribeEvent
if err := json.Unmarshal(ev.Event, &subEv); err != nil { if err := json.Unmarshal(ev.Event, &subEv); err != nil {
m.logger.Warn("Error parsing new subscription event", zap.Error(err)) m.logger.Warn("Error parsing new subscription event", "error", err)
return return
} }
m.addMixedEvent(subEv) m.addMixedEvent(subEv)
@ -131,7 +130,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
var subEv helix.EventSubChannelSubscriptionMessageEvent var subEv helix.EventSubChannelSubscriptionMessageEvent
err := json.Unmarshal(ev.Event, &subEv) err := json.Unmarshal(ev.Event, &subEv)
if err != nil { if err != nil {
m.logger.Warn("Error parsing returning subscription event", zap.Error(err)) m.logger.Warn("Error parsing returning subscription event", "error", err)
return return
} }
m.addMixedEvent(subEv) m.addMixedEvent(subEv)
@ -143,7 +142,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as gift event // Parse as gift event
var giftEv helix.EventSubChannelSubscriptionGiftEvent var giftEv helix.EventSubChannelSubscriptionGiftEvent
if err := json.Unmarshal(ev.Event, &giftEv); err != nil { if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
m.logger.Warn("Error parsing subscription gifted event", zap.Error(err)) m.logger.Warn("Error parsing subscription gifted event", "error", err)
return return
} }
// Pick a random message from base set // Pick a random message from base set

View file

@ -2,19 +2,16 @@ package alerts
import ( import (
"context" "context"
"encoding/json"
"log/slog"
"sync" "sync"
"text/template" "text/template"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub" "git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template" template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
) )
var json = jsoniter.ConfigFastest
type ( type (
templateCache map[string]*template.Template templateCache map[string]*template.Template
templateCacheMap map[templateType]templateCache templateCacheMap map[templateType]templateCache
@ -35,7 +32,7 @@ type Module struct {
ctx context.Context ctx context.Context
db database.Database db database.Database
logger *zap.Logger logger *slog.Logger
templater template2.Engine templater template2.Engine
templates templateCacheMap templates templateCacheMap
@ -43,7 +40,7 @@ type Module struct {
pendingSubs map[string]subMixedEvent pendingSubs map[string]subMixedEvent
} }
func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templater template2.Engine) *Module { func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templater template2.Engine) *Module {
mod := &Module{ mod := &Module{
ctx: ctx, ctx: ctx,
db: db, db: db,
@ -55,31 +52,31 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templa
// Load config from database // Load config from database
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil { if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
logger.Debug("Config load error", zap.Error(err)) logger.Debug("Config load error", "error", err)
mod.Config = Config{} mod.Config = Config{}
// Save empty config // Save empty config
err = db.PutJSON(ConfigKey, mod.Config) err = db.PutJSON(ConfigKey, mod.Config)
if err != nil { if err != nil {
logger.Warn("Could not save default config for bot alerts", zap.Error(err)) logger.Warn("Could not save default config for bot alerts", "error", err)
} }
} }
mod.compileTemplates() mod.compileTemplates()
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) { if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
err := json.UnmarshalFromString(value, &mod.Config) err := json.Unmarshal([]byte(value), &mod.Config)
if err != nil { if err != nil {
logger.Warn("Error loading alert config", zap.Error(err)) logger.Warn("Error loading alert config", "error", err)
} else { } else {
logger.Info("Reloaded alert config") logger.Info("Reloaded alert config")
} }
mod.compileTemplates() mod.compileTemplates()
}); err != nil { }); err != nil {
logger.Error("Could not set-up bot alert reload subscription", zap.Error(err)) logger.Error("Could not set-up bot alert reload subscription", "error", err)
} }
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil { if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
logger.Error("Could not setup twitch alert subscription", zap.Error(err)) logger.Error("Could not setup twitch alert subscription", "error", err)
} }
logger.Debug("Loaded bot alerts") logger.Debug("Loaded bot alerts")

View file

@ -4,8 +4,6 @@ import (
"bytes" "bytes"
"text/template" "text/template"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/twitch/chat" "git.sr.ht/~ashkeel/strimertul/twitch/chat"
) )
@ -44,7 +42,7 @@ func (m *Module) compileTemplates() {
func (m *Module) addTemplate(templateList templateCache, message string) { func (m *Module) addTemplate(templateList templateCache, message string) {
tpl, err := m.templater.MakeTemplate(message) tpl, err := m.templater.MakeTemplate(message)
if err != nil { if err != nil {
m.logger.Error("Error compiling alert template", zap.Error(err)) m.logger.Error("Error compiling alert template", "error", err)
return return
} }
templateList[message] = tpl templateList[message] = tpl
@ -60,7 +58,7 @@ func (m *Module) addTemplatesForType(templateList templateType, messages []strin
func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) { func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) {
var buf bytes.Buffer var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil { if err := tpl.Execute(&buf, data); err != nil {
m.logger.Error("Error executing template for bot alert", zap.Error(err)) m.logger.Error("Error executing template for bot alert", "error", err)
return return
} }
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{

View file

@ -2,9 +2,9 @@ package chat
import ( import (
"bytes" "bytes"
"log/slog"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
) )
var accessLevels = map[AccessLevelType]int{ var accessLevels = map[AccessLevelType]int{
@ -70,7 +70,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
return return
} }
if err := tpl.Execute(&buf, message); err != nil { if err := tpl.Execute(&buf, message); err != nil {
mod.logger.Error("Failed to execute custom command template", zap.Error(err)) mod.logger.Error("Failed to execute custom command template", "error", err)
return return
} }
@ -97,7 +97,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
Announce: true, Announce: true,
} }
default: default:
mod.logger.Error("Unknown response type", zap.String("type", string(data.ResponseType))) mod.logger.Error("Unknown response type", slog.String("type", string(data.ResponseType)))
} }
WriteMessage(mod.db, mod.logger, request) WriteMessage(mod.db, mod.logger, request)

View file

@ -12,10 +12,8 @@ import (
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/loyalty" "git.sr.ht/~ashkeel/strimertul/loyalty"
"github.com/nicklaw5/helix/v2"
) )
const ( const (
@ -91,7 +89,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
var streamInfos []helix.Stream var streamInfos []helix.Stream
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos) err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
if err != nil { if err != nil {
mod.logger.Error("Error retrieving stream info", zap.Error(err)) mod.logger.Error("Error retrieving stream info", "error", err)
continue continue
} }
if len(streamInfos) < 1 { if len(streamInfos) < 1 {
@ -104,7 +102,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
for { for {
userClient, err := twitch.GetUserClient(mod.db, false) userClient, err := twitch.GetUserClient(mod.db, false)
if err != nil { if err != nil {
mod.logger.Error("Could not get user api client for list of chatters", zap.Error(err)) mod.logger.Error("Could not get user api client for list of chatters", "error", err)
return return
} }
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{ res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
@ -114,7 +112,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
After: cursor, After: cursor,
}) })
if err != nil { if err != nil {
mod.logger.Error("Could not retrieve list of chatters", zap.Error(err)) mod.logger.Error("Could not retrieve list of chatters", "error", err)
return return
} }
for _, user := range res.Data.Chatters { for _, user := range res.Data.Chatters {
@ -150,7 +148,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
if len(users) > 0 { if len(users) > 0 {
err := li.manager.GivePoints(pointsToGive) err := li.manager.GivePoints(pointsToGive)
if err != nil { if err != nil {
mod.logger.Error("Error awarding loyalty points to user", zap.Error(err)) mod.logger.Error("Error awarding loyalty points to user", "error", err)
} }
} }
} }
@ -244,7 +242,7 @@ func (li *loyaltyIntegration) cmdRedeemReward(message helix.EventSubChannelChatM
}) })
return return
} }
li.module.logger.Error("Error while performing redeem", zap.Error(err)) li.module.logger.Error("Error while performing redeem", "error", err)
return return
} }
@ -258,7 +256,7 @@ func (li *loyaltyIntegration) cmdGoalList(message helix.EventSubChannelChatMessa
goals := li.manager.Goals.Get() goals := li.manager.Goals.Get()
if len(goals) < 1 { if len(goals) < 1 {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("There are no active community goals right now :(!"), Message: "There are no active community goals right now :(!",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
return return
@ -300,12 +298,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
if goalIndex < 0 { if goalIndex < 0 {
if hasGoals { if hasGoals {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("All active community goals have been reached already! NewRecord"), Message: "All active community goals have been reached already! NewRecord",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
} else { } else {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("There are no active community goals right now :(!"), Message: "There are no active community goals right now :(!",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
} }
@ -319,7 +317,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
if err == nil { if err == nil {
if newPoints <= 0 { if newPoints <= 0 {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("Nice try SoBayed"), Message: "Nice try SoBayed",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
return return
@ -343,7 +341,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Invalid goal ID provided // Invalid goal ID provided
if !found { if !found {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("I couldn't find that goal ID :("), Message: "I couldn't find that goal ID :(",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
return return
@ -357,7 +355,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Check if goal was reached already // Check if goal was reached already
if selectedGoal.Contributed >= selectedGoal.TotalGoal { if selectedGoal.Contributed >= selectedGoal.TotalGoal {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("This goal was already reached! ヾ(•ω•`)o"), Message: "This goal was already reached! ヾ(•ω•`)o",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
return return
@ -366,12 +364,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Add points to goal // Add points to goal
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points) points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
if err != nil { if err != nil {
li.module.logger.Error("Error while contributing to goal", zap.Error(err)) li.module.logger.Error("Error while contributing to goal", "error", err)
return return
} }
if points == 0 { if points == 0 {
li.module.WriteMessage(WriteMessageRequest{ li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("Sorry but you're broke"), Message: "Sorry but you're broke",
ReplyTo: message.MessageID, ReplyTo: message.MessageID,
}) })
return return

View file

@ -3,19 +3,18 @@ package chat
import ( import (
"context" "context"
"errors" "errors"
"log/slog"
"strings" "strings"
textTemplate "text/template" textTemplate "text/template"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub" "git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
"git.sr.ht/~ashkeel/strimertul/twitch/template" "git.sr.ht/~ashkeel/strimertul/twitch/template"
"git.sr.ht/~ashkeel/strimertul/utils" "git.sr.ht/~ashkeel/strimertul/utils"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
) )
var json = jsoniter.ConfigFastest var json = jsoniter.ConfigFastest
@ -27,7 +26,7 @@ type Module struct {
db database.Database db database.Database
api *helix.Client api *helix.Client
user helix.User user helix.User
logger *zap.Logger logger *slog.Logger
templater template.Engine templater template.Engine
lastMessage *sync.RWSync[time.Time] lastMessage *sync.RWSync[time.Time]
@ -35,11 +34,9 @@ type Module struct {
customCommands *sync.Map[string, CustomCommand] customCommands *sync.Map[string, CustomCommand]
customTemplates *sync.Map[string, *textTemplate.Template] customTemplates *sync.Map[string, *textTemplate.Template]
customFunctions textTemplate.FuncMap customFunctions textTemplate.FuncMap
cancelContext context.CancelFunc
} }
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *zap.Logger, templater template.Engine) *Module { func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
mod := &Module{ mod := &Module{
ctx: ctx, ctx: ctx,
db: db, db: db,
@ -62,12 +59,12 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
CommandCooldown: 2, CommandCooldown: 2,
} }
} else { } else {
logger.Error("Failed to load chat module config", zap.Error(err)) logger.Error("Failed to load chat module config", "error", err)
} }
} }
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil { if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
logger.Error("Could not subscribe to chat messages", zap.Error(err)) logger.Error("Could not subscribe to chat messages", "error", err)
} }
// Load custom commands // Load custom commands
@ -76,21 +73,21 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
if errors.Is(err, database.ErrEmptyKey) { if errors.Is(err, database.ErrEmptyKey) {
customCommands = make(map[string]CustomCommand) customCommands = make(map[string]CustomCommand)
} else { } else {
logger.Error("Failed to load custom commands", zap.Error(err)) logger.Error("Failed to load custom commands", "error", err)
} }
} }
mod.customCommands.Set(customCommands) mod.customCommands.Set(customCommands)
if err := mod.updateTemplates(); err != nil { if err := mod.updateTemplates(); err != nil {
logger.Error("Failed to parse custom commands", zap.Error(err)) logger.Error("Failed to parse custom commands", "error", err)
} }
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil { if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
logger.Error("Could not set-up chat command reload subscription", zap.Error(err)) logger.Error("Could not set-up chat command reload subscription", "error", err)
} }
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil { if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
logger.Error("Could not set-up chat command reload subscription", zap.Error(err)) logger.Error("Could not set-up chat command reload subscription", "error", err)
} }
return mod return mod
@ -101,7 +98,7 @@ func (mod *Module) onChatMessage(newValue string) {
Event helix.EventSubChannelChatMessageEvent `json:"event"` Event helix.EventSubChannelChatMessageEvent `json:"event"`
} }
if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil { if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil {
mod.logger.Error("Failed to decode incoming chat message", zap.Error(err)) mod.logger.Error("Failed to decode incoming chat message", "error", err)
return return
} }
@ -149,7 +146,7 @@ func (mod *Module) onChatMessage(newValue string) {
func (mod *Module) handleWriteMessageRPC(value string) { func (mod *Module) handleWriteMessageRPC(value string) {
var request WriteMessageRequest var request WriteMessageRequest
if err := json.Unmarshal([]byte(value), &request); err != nil { if err := json.Unmarshal([]byte(value), &request); err != nil {
mod.logger.Warn("Failed to decode write message request", zap.Error(err)) mod.logger.Warn("Failed to decode write message request", "error", err)
return return
} }
@ -160,10 +157,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
Message: request.Message, Message: request.Message,
}) })
if err != nil { if err != nil {
mod.logger.Error("Failed to send announcement", zap.Error(err)) mod.logger.Error("Failed to send announcement", "error", err)
} }
if resp.Error != "" { if resp.Error != "" {
mod.logger.Error("Failed to send announcement", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage)) mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
} }
return return
} }
@ -175,10 +172,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
Message: request.Message, Message: request.Message,
}) })
if err != nil { if err != nil {
mod.logger.Error("Failed to send whisper", zap.Error(err)) mod.logger.Error("Failed to send whisper", "error", err)
} }
if resp.Error != "" { if resp.Error != "" {
mod.logger.Error("Failed to send whisper", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage)) mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
} }
return return
} }
@ -190,22 +187,22 @@ func (mod *Module) handleWriteMessageRPC(value string) {
ReplyParentMessageID: request.ReplyTo, ReplyParentMessageID: request.ReplyTo,
}) })
if err != nil { if err != nil {
mod.logger.Error("Failed to send chat message", zap.Error(err)) mod.logger.Error("Failed to send chat message", "error", err)
} }
if resp.Error != "" { if resp.Error != "" {
mod.logger.Error("Failed to send chat message", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage)) mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
} }
} }
func (mod *Module) updateCommands(value string) { func (mod *Module) updateCommands(value string) {
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands) err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
if err != nil { if err != nil {
mod.logger.Error("Failed to decode new custom commands", zap.Error(err)) mod.logger.Error("Failed to decode new custom commands", "error", err)
return return
} }
// Recreate templates // Recreate templates
if err := mod.updateTemplates(); err != nil { if err := mod.updateTemplates(); err != nil {
mod.logger.Error("Failed to update custom commands templates", zap.Error(err)) mod.logger.Error("Failed to update custom commands templates", "error", err)
return return
} }
} }

View file

@ -1,8 +1,9 @@
package chat package chat
import ( import (
"log/slog"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"go.uber.org/zap"
) )
// WriteMessageRequest is an RPC to send a chat message with extra options // WriteMessageRequest is an RPC to send a chat message with extra options
@ -13,9 +14,9 @@ type WriteMessageRequest struct {
Announce bool `json:"announce" desc:"If true, send as announcement"` Announce bool `json:"announce" desc:"If true, send as announcement"`
} }
func WriteMessage(db database.Database, logger *zap.Logger, m WriteMessageRequest) { func WriteMessage(db database.Database, logger *slog.Logger, m WriteMessageRequest) {
err := db.PutJSON(WriteMessageRPC, m) err := db.PutJSON(WriteMessageRPC, m)
if err != nil { if err != nil {
logger.Error("Failed to write chat message", zap.Error(err)) logger.Error("Failed to write chat message", "error", err)
} }
} }

View file

@ -2,14 +2,14 @@ package client
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch" "git.sr.ht/~ashkeel/strimertul/twitch"
@ -20,13 +20,11 @@ import (
"git.sr.ht/~ashkeel/strimertul/webserver" "git.sr.ht/~ashkeel/strimertul/webserver"
) )
var json = jsoniter.ConfigFastest
type Manager struct { type Manager struct {
client *Client client *Client
} }
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) { func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer) (*Manager, error) {
// Get Twitch config // Get Twitch config
var config twitch.Config var config twitch.Config
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil { if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
@ -38,7 +36,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
// Create new client // Create new client
clientContext, cancel := context.WithCancel(ctx) clientContext, cancel := context.WithCancel(ctx)
client, err := newClient(clientContext, config, db, server, logger) client, err := newClient(clientContext, config, db, server)
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("failed to create twitch client: %w", err) return nil, fmt.Errorf("failed to create twitch client: %w", err)
@ -51,8 +49,8 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
// Listen for client config changes // Listen for client config changes
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) { if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
var newConfig twitch.Config var newConfig twitch.Config
if err := json.UnmarshalFromString(value, &newConfig); err != nil { if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
logger.Error("Failed to decode Twitch integration config", zap.Error(err)) slog.Error("Failed to decode Twitch integration config", "error", err)
return return
} }
@ -60,9 +58,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
var updatedClient *Client var updatedClient *Client
clientContext, cancel = context.WithCancel(ctx) clientContext, cancel = context.WithCancel(ctx)
updatedClient, err = newClient(clientContext, newConfig, db, server, logger) updatedClient, err = newClient(clientContext, newConfig, db, server)
if err != nil { if err != nil {
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err)) slog.Error("Could not create twitch client with new config, keeping old", "error", err)
return return
} }
@ -70,9 +68,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
updatedClient.Merge(manager.client) updatedClient.Merge(manager.client)
manager.client = updatedClient manager.client = updatedClient
logger.Info("Reloaded/updated Twitch integration") slog.Info("Reloaded/updated Twitch integration")
}); err != nil { }); err != nil {
logger.Error("Could not setup twitch config reload subscription", zap.Error(err)) slog.Error("Could not setup twitch config reload subscription", "error", err)
} }
return manager, nil return manager, nil
@ -87,7 +85,7 @@ type Client struct {
DB database.Database DB database.Database
API *helix.Client API *helix.Client
User helix.User User helix.User
Logger *zap.Logger Logger *slog.Logger
Chat *chat.Module Chat *chat.Module
Alerts *alerts.Module Alerts *alerts.Module
@ -115,12 +113,12 @@ func (c *Client) ensureRoute() {
} }
} }
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) { func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer) (*Client, error) {
// Create Twitch client // Create Twitch client
client := &Client{ client := &Client{
Config: sync.NewRWSync(config), Config: sync.NewRWSync(config),
DB: db, DB: db,
Logger: logger.With(zap.String("service", "twitch")), Logger: slog.With(slog.String("service", "twitch")),
restart: make(chan bool, 128), restart: make(chan bool, 128),
streamOnline: sync.NewRWSync(false), streamOnline: sync.NewRWSync(false),
ctx: ctx, ctx: ctx,
@ -142,21 +140,21 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
if userClient, err := twitch.GetUserClient(db, true); err == nil { if userClient, err := twitch.GetUserClient(db, true); err == nil {
users, err := userClient.GetUsers(&helix.UsersParams{}) users, err := userClient.GetUsers(&helix.UsersParams{})
if err != nil { if err != nil {
client.Logger.Error("Failed looking up user", zap.Error(err)) client.Logger.Error("Failed looking up user", "error", err)
} else if len(users.Data.Users) < 1 { } else if len(users.Data.Users) < 1 {
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events") client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
} else { } else {
client.Logger.Info("Twitch user identified", zap.String("user", users.Data.Users[0].ID)) client.Logger.Info("Twitch user identified", slog.String("user", users.Data.Users[0].ID))
client.User = users.Data.Users[0] client.User = users.Data.Users[0]
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, logger) client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
if err != nil { if err != nil {
client.Logger.Error("Failed to setup EventSub", zap.Error(err)) client.Logger.Error("Failed to setup EventSub", "error", err)
} }
tpl := client.GetTemplateEngine() tpl := client.GetTemplateEngine()
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl) client.Chat = chat.Setup(ctx, db, userClient, client.User, client.Logger, tpl)
client.Alerts = alerts.Setup(ctx, db, logger, tpl) client.Alerts = alerts.Setup(ctx, db, client.Logger, tpl)
client.Timers = timers.Setup(ctx, db, logger) client.Timers = timers.Setup(ctx, db, client.Logger)
} }
} else { } else {
client.Logger.Warn("Twitch user not identified, this will break most features") client.Logger.Warn("Twitch user not identified, this will break most features")
@ -186,14 +184,14 @@ func (c *Client) runStatusPoll() {
UserIDs: []string{c.User.ID}, UserIDs: []string{c.User.ID},
}) })
if err != nil { if err != nil {
c.Logger.Error("Error checking stream status", zap.Error(err)) c.Logger.Error("Error checking stream status", "error", err)
return return
} }
c.streamOnline.Set(len(status.Data.Streams) > 0) c.streamOnline.Set(len(status.Data.Streams) > 0)
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams) err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
if err != nil { if err != nil {
c.Logger.Warn("Error saving stream info", zap.Error(err)) c.Logger.Warn("Error saving stream info", "error", err)
} }
}() }()

View file

@ -1,28 +1,26 @@
package client package client
import ( import (
"context"
"testing" "testing"
"git.sr.ht/~ashkeel/strimertul/twitch" "git.sr.ht/~ashkeel/strimertul/twitch"
"go.uber.org/zap/zaptest"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/webserver" "git.sr.ht/~ashkeel/strimertul/webserver"
) )
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t) client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client) defer database.CleanupLocalClient(client)
server, err := webserver.NewServer(client, logger, webserver.DefaultServerFactory) server, err := webserver.NewServer(client, webserver.DefaultServerFactory)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
config := twitch.Config{} config := twitch.Config{}
_, err = newClient(config, client, server, logger) _, err = newClient(context.Background(), config, client, server)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -1,6 +1,7 @@
package client package client
import ( import (
"log/slog"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
@ -11,7 +12,6 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch/template" "git.sr.ht/~ashkeel/strimertul/twitch/template"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
) )
const ChatCounterPrefix = "twitch/chat/counters/" const ChatCounterPrefix = "twitch/chat/counters/"
@ -57,7 +57,7 @@ func (c *Client) GetTemplateEngine() template.Engine {
counter++ counter++
err := c.DB.PutKey(counterKey, strconv.Itoa(counter)) err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
if err != nil { if err != nil {
c.Logger.Error("Error saving key", zap.Error(err), zap.String("key", counterKey)) c.Logger.Error("Error saving key", "error", err, slog.String("key", counterKey))
} }
return counter return counter
}, },

View file

@ -3,16 +3,15 @@ package eventsub
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"time" "time"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
lru "github.com/hashicorp/golang-lru/v2" lru "github.com/hashicorp/golang-lru/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
) )
var json = jsoniter.ConfigFastest var json = jsoniter.ConfigFastest
@ -23,14 +22,14 @@ type Client struct {
ctx context.Context ctx context.Context
twitchAPI *helix.Client twitchAPI *helix.Client
db database.Database db database.Database
logger *zap.Logger logger *slog.Logger
user helix.User user helix.User
eventCache *lru.Cache[string, time.Time] eventCache *lru.Cache[string, time.Time]
savedSubscriptions map[string]bool savedSubscriptions map[string]bool
} }
func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *zap.Logger) (*Client, error) { func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *slog.Logger) (*Client, error) {
eventCache, err := lru.New[string, time.Time](128) eventCache, err := lru.New[string, time.Time](128)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create LRU cache for events: %w", err) return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
@ -58,11 +57,11 @@ func (c *Client) eventSubLoop() {
for endpoint != "" { for endpoint != "" {
endpoint, connection, err = c.connectWebsocket(endpoint, connection) endpoint, connection, err = c.connectWebsocket(endpoint, connection)
if err != nil { if err != nil {
c.logger.Error("EventSub websocket read error", zap.Error(err)) c.logger.Error("EventSub websocket read error", "error", err)
} }
} }
if connection != nil { if connection != nil {
utils.Close(connection, c.logger) utils.Close(connection)
} }
} }
@ -86,7 +85,7 @@ func readLoop(connection *websocket.Conn, recv chan<- []byte, wsErr chan<- error
func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) { func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) {
connection, _, err := websocket.DefaultDialer.Dial(url, nil) connection, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil { if err != nil {
c.logger.Error("Could not establish a connection to the EventSub websocket", zap.Error(err)) c.logger.Error("Could not establish a connection to the EventSub websocket", "error", err)
return "", nil, err return "", nil, err
} }
@ -109,7 +108,7 @@ func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (st
var wsMessage WebsocketMessage var wsMessage WebsocketMessage
err = json.Unmarshal(messageData, &wsMessage) err = json.Unmarshal(messageData, &wsMessage)
if err != nil { if err != nil {
c.logger.Error("Error decoding EventSub message", zap.Error(err)) c.logger.Error("Error decoding EventSub message", "error", err)
continue continue
} }
@ -128,30 +127,30 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
var welcomeData WelcomeMessagePayload var welcomeData WelcomeMessagePayload
err := json.Unmarshal(wsMessage.Payload, &welcomeData) err := json.Unmarshal(wsMessage.Payload, &welcomeData)
if err != nil { if err != nil {
c.logger.Error("Error decoding EventSub welcome message", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err)) c.logger.Error("Error decoding EventSub welcome message", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
break break
} }
c.logger.Info("Connection to EventSub websocket established", zap.String("session-id", welcomeData.Session.ID)) c.logger.Info("Connection to EventSub websocket established", slog.String("session-id", welcomeData.Session.ID))
// We can only close the old connection once the new one has been established // We can only close the old connection once the new one has been established
if oldConnection != nil { if oldConnection != nil {
utils.Close(oldConnection, c.logger) utils.Close(oldConnection)
} }
// Add subscription to websocket session // Add subscription to websocket session
err = c.addSubscriptionsForSession(welcomeData.Session.ID) err = c.addSubscriptionsForSession(welcomeData.Session.ID)
if err != nil { if err != nil {
c.logger.Error("Could not add subscriptions", zap.Error(err)) c.logger.Error("Could not add subscriptions", "error", err)
break break
} }
case "session_reconnect": case "session_reconnect":
var reconnectData WelcomeMessagePayload var reconnectData WelcomeMessagePayload
err := json.Unmarshal(wsMessage.Payload, &reconnectData) err := json.Unmarshal(wsMessage.Payload, &reconnectData)
if err != nil { if err != nil {
c.logger.Error("Error decoding EventSub session reconnect parameters", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err)) c.logger.Error("Error decoding EventSub session reconnect parameters", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
break break
} }
c.logger.Info("EventSub websocket requested a reconnection", zap.String("session-id", reconnectData.Session.ID), zap.String("reconnect-url", reconnectData.Session.ReconnectURL)) c.logger.Info("EventSub websocket requested a reconnection", slog.String("session-id", reconnectData.Session.ID), slog.String("reconnect-url", reconnectData.Session.ReconnectURL))
return reconnectData.Session.ReconnectURL, true, nil return reconnectData.Session.ReconnectURL, true, nil
case "notification": case "notification":
@ -166,7 +165,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
// Check if we processed this already // Check if we processed this already
if message.Metadata.MessageID != "" { if message.Metadata.MessageID != "" {
if c.eventCache.Contains(message.Metadata.MessageID) { if c.eventCache.Contains(message.Metadata.MessageID) {
c.logger.Debug("Received duplicate event, ignoring", zap.String("message-id", message.Metadata.MessageID)) c.logger.Debug("Received duplicate event, ignoring", slog.String("message-id", message.Metadata.MessageID))
return return
} }
} }
@ -176,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
var notificationData NotificationMessagePayload var notificationData NotificationMessagePayload
err := json.Unmarshal(message.Payload, &notificationData) err := json.Unmarshal(message.Payload, &notificationData)
if err != nil { if err != nil {
c.logger.Error("Error decoding EventSub notification payload", zap.String("message-type", message.Metadata.MessageType), zap.Error(err)) c.logger.Error("Error decoding EventSub notification payload", slog.String("message-type", message.Metadata.MessageType), "error", err)
} }
notificationData.Date = time.Now() notificationData.Date = time.Now()
@ -184,7 +183,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type) historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
err = c.db.PutJSON(eventKey, notificationData) err = c.db.PutJSON(eventKey, notificationData)
if err != nil { if err != nil {
c.logger.Error("Error storing event to database", zap.String("key", eventKey), zap.Error(err)) c.logger.Error("Error storing event to database", slog.String("key", eventKey), "error", err)
} }
var archive []NotificationMessagePayload var archive []NotificationMessagePayload
@ -198,7 +197,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
} }
err = c.db.PutJSON(historyKey, archive) err = c.db.PutJSON(historyKey, archive)
if err != nil { if err != nil {
c.logger.Error("Error storing event to database", zap.String("key", historyKey), zap.Error(err)) c.logger.Error("Error storing event to database", slog.String("key", historyKey), "error", err)
} }
} }
@ -221,7 +220,7 @@ func (c *Client) addSubscriptionsForSession(session string) error {
Condition: topicCondition(topic, c.user.ID), Condition: topicCondition(topic, c.user.ID),
}) })
if sub.Error != "" || sub.ErrorMessage != "" { if sub.Error != "" || sub.ErrorMessage != "" {
c.logger.Error("EventSub Subscription error", zap.String("topic", topic), zap.String("topic-version", version), zap.String("err", sub.Error), zap.String("message", sub.ErrorMessage)) c.logger.Error("EventSub Subscription error", slog.String("topic", topic), slog.String("topic-version", version), slog.String("err", sub.Error), slog.String("message", sub.ErrorMessage))
return fmt.Errorf("%s: %s", sub.Error, sub.ErrorMessage) return fmt.Errorf("%s: %s", sub.Error, sub.ErrorMessage)
} }
if err != nil { if err != nil {

View file

@ -2,15 +2,14 @@ package timers
import ( import (
"context" "context"
"log/slog"
"math/rand" "math/rand"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/chat" "git.sr.ht/~ashkeel/strimertul/twitch/chat"
jsoniter "github.com/json-iterator/go"
) )
var json = jsoniter.ConfigFastest var json = jsoniter.ConfigFastest
@ -23,12 +22,12 @@ type Module struct {
lastTrigger *sync.Map[string, time.Time] lastTrigger *sync.Map[string, time.Time]
messages *sync.Slice[int] messages *sync.Slice[int]
logger *zap.Logger logger *slog.Logger
db database.Database db database.Database
ctx context.Context ctx context.Context
} }
func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Module { func Setup(ctx context.Context, db database.Database, logger *slog.Logger) *Module {
mod := &Module{ mod := &Module{
lastTrigger: sync.NewMap[string, time.Time](), lastTrigger: sync.NewMap[string, time.Time](),
messages: sync.NewSlice[int](), messages: sync.NewSlice[int](),
@ -45,28 +44,28 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Modul
// Load config from database // Load config from database
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil { if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
logger.Debug("Config load error", zap.Error(err)) logger.Debug("Config load error", "error", err)
mod.Config = Config{ mod.Config = Config{
Timers: make(map[string]ChatTimer), Timers: make(map[string]ChatTimer),
} }
// Save empty config // Save empty config
err = db.PutJSON(ConfigKey, mod.Config) err = db.PutJSON(ConfigKey, mod.Config)
if err != nil { if err != nil {
logger.Warn("Could not save default config for bot timers", zap.Error(err)) logger.Warn("Could not save default config for bot timers", "error", err)
} }
} }
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) { if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
if err := json.UnmarshalFromString(value, &mod.Config); err != nil { if err := json.UnmarshalFromString(value, &mod.Config); err != nil {
logger.Warn("Error reloading timer config", zap.Error(err)) logger.Warn("Error reloading timer config", "error", err)
return return
} }
logger.Info("Reloaded timer config") logger.Info("Reloaded timer config")
}); err != nil { }); err != nil {
logger.Error("Could not set-up timer reload subscription", zap.Error(err)) logger.Error("Could not set-up timer reload subscription", "error", err)
} }
logger.Debug("Loaded timers", zap.Int("timers", len(mod.Config.Timers))) logger.Debug("Loaded timers", slog.Int("timers", len(mod.Config.Timers)))
// Start goroutine for clearing message counters and running timers // Start goroutine for clearing message counters and running timers
go mod.runTimers() go mod.runTimers()
@ -84,7 +83,7 @@ func (m *Module) runTimers() {
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get()) err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
if err != nil { if err != nil {
m.logger.Warn("Error saving chat activity", zap.Error(err)) m.logger.Warn("Error saving chat activity", "error", err)
} }
// Calculate activity // Calculate activity

View file

@ -1,7 +0,0 @@
package utils
type ContextKey string
const (
ContextLogger ContextKey = "logger"
)

View file

@ -2,15 +2,14 @@ package utils
import ( import (
"io" "io"
"log/slog"
"reflect" "reflect"
"runtime/debug" "runtime/debug"
"go.uber.org/zap"
) )
func Close(res io.Closer, logger *zap.Logger) { func Close(res io.Closer) {
err := res.Close() err := res.Close()
if err != nil { if err != nil {
logger.Error("Could not close resource", zap.String("name", reflect.TypeOf(res).String()), zap.Error(err), zap.String("stack", string(debug.Stack()))) slog.Error("Could not close resource", slog.String("name", reflect.TypeOf(res).String()), slog.String("stack", string(debug.Stack())), "error", err)
} }
} }

View file

@ -1,15 +1,14 @@
package utils package utils
import ( import (
"git.sr.ht/~ashkeel/containers/sync" "encoding/json"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigFastest "git.sr.ht/~ashkeel/containers/sync"
)
func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error { func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error {
var result T var result T
err := json.UnmarshalFromString(data, &result) err := json.Unmarshal([]byte(data), &result)
if err != nil { if err != nil {
return err return err
} }

View file

@ -4,28 +4,24 @@ import (
"context" "context"
crand "crypto/rand" crand "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"log/slog"
mrand "math/rand" mrand "math/rand"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go" kv "git.sr.ht/~ashkeel/kilovolt/v12"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
) )
var json = jsoniter.ConfigFastest
type WebServer struct { type WebServer struct {
Config *sync.RWSync[ServerConfig] Config *sync.RWSync[ServerConfig]
db database.Database db database.Database
logger *zap.Logger
server Server server Server
frontend fs.FS frontend fs.FS
hub *kv.Hub hub *kv.Hub
@ -36,9 +32,8 @@ type WebServer struct {
factory ServerFactory factory ServerFactory
} }
func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFactory) (*WebServer, error) { func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, error) {
server := &WebServer{ server := &WebServer{
logger: logger,
db: db, db: db,
server: nil, server: nil,
requestedRoutes: sync.NewMap[string, http.Handler](), requestedRoutes: sync.NewMap[string, http.Handler](),
@ -51,7 +46,7 @@ func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFac
err := db.GetJSON(ServerConfigKey, &config) err := db.GetJSON(ServerConfigKey, &config)
if err != nil { if err != nil {
if !errors.Is(err, database.ErrEmptyKey) { if !errors.Is(err, database.ErrEmptyKey) {
logger.Warn("HTTP config is corrupted or could not be read", zap.Error(err)) slog.Warn("HTTP config is corrupted or could not be read", "error", err)
} }
// Initialize with default config // Initialize with default config
server.Config.Set(ServerConfig{ server.Config.Set(ServerConfig{
@ -124,7 +119,7 @@ func (s *WebServer) makeMux() *http.ServeMux {
} }
if s.hub != nil { if s.hub != nil {
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
kv.ServeWs(s.hub, w, r) s.hub.CreateWebsocketClient(w, r, kv.ClientOptions{})
}) })
} }
config := s.Config.Get() config := s.Config.Get()
@ -160,7 +155,7 @@ func (s *WebServer) Listen() error {
for { for {
// Read config and make http request mux // Read config and make http request mux
config := s.Config.Get() config := s.Config.Get()
s.logger.Info("Starting HTTP server", zap.String("bind", config.Bind)) slog.Info("Starting HTTP server", slog.String("bind", config.Bind))
s.mux = s.makeMux() s.mux = s.makeMux()
// Make HTTP server instance // Make HTTP server instance
@ -172,25 +167,25 @@ func (s *WebServer) Listen() error {
} }
// Start HTTP server // Start HTTP server
s.logger.Info("HTTP server started", zap.String("bind", config.Bind)) slog.Info("HTTP server started", slog.String("bind", config.Bind))
err = s.server.Start() err = s.server.Start()
// If the server died, we need to see what to do // If the server died, we need to see what to do
s.logger.Debug("HTTP server died", zap.Error(err)) slog.Debug("HTTP server died", "error", err)
if err != nil && !errors.Is(err, http.ErrServerClosed) { if err != nil && !errors.Is(err, http.ErrServerClosed) {
exit <- err exit <- err
return return
} }
// Are we trying to close or restart? // Are we trying to close or restart?
s.logger.Debug("HTTP server stopped", zap.Bool("restart", s.restart.Get())) slog.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
if s.restart.Get() { if s.restart.Get() {
s.restart.Set(false) s.restart.Set(false)
continue continue
} }
break break
} }
s.logger.Debug("HTTP server stalled") slog.Debug("HTTP server stalled")
exit <- nil exit <- nil
}() }()
@ -203,7 +198,7 @@ func (s *WebServer) onConfigUpdate(value string) {
var config ServerConfig var config ServerConfig
err := json.Unmarshal([]byte(value), &config) err := json.Unmarshal([]byte(value), &config)
if err != nil { if err != nil {
s.logger.Error("Failed to unmarshal config", zap.Error(err)) slog.Error("Failed to unmarshal config", "error", err)
return return
} }
@ -220,7 +215,7 @@ func (s *WebServer) onConfigUpdate(value string) {
s.restart.Set(true) s.restart.Set(true)
err = s.server.Shutdown(context.Background()) err = s.server.Shutdown(context.Background())
if err != nil { if err != nil {
s.logger.Error("Failed to shutdown server", zap.Error(err)) slog.Error("Failed to shutdown server", "error", err)
return return
} }
} }

View file

@ -8,42 +8,37 @@ import (
"time" "time"
"git.sr.ht/~ashkeel/containers/sync" "git.sr.ht/~ashkeel/containers/sync"
"go.uber.org/zap/zaptest"
"git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/database"
) )
func TestNewServer(t *testing.T) { func TestNewServer(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t) client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client) defer database.CleanupLocalClient(client)
_, err := NewServer(client, logger, DefaultServerFactory) _, err := NewServer(client, DefaultServerFactory)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestNewServerWithTestFactory(t *testing.T) { func TestNewServerWithTestFactory(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t) client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client) defer database.CleanupLocalClient(client)
testServer := NewTestServer() testServer := NewTestServer()
_, err := NewServer(client, logger, testServer.Factory()) _, err := NewServer(client, testServer.Factory())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestListen(t *testing.T) { func TestListen(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t) client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client) defer database.CleanupLocalClient(client)
// Create a test server // Create a test server
testServer := NewTestServer() testServer := NewTestServer()
server, err := NewServer(client, logger, testServer.Factory()) server, err := NewServer(client, testServer.Factory())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -96,12 +91,11 @@ func (t *testCustomHandler) ServeHTTP(http.ResponseWriter, *http.Request) {
} }
func TestCustomRoute(t *testing.T) { func TestCustomRoute(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t) client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client) defer database.CleanupLocalClient(client)
// Create test server // Create test server
server, err := NewServer(client, logger, nil) server, err := NewServer(client, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }