mirror of https://git.sr.ht/~ashkeel/strimertul
feat: update kilovolt, replace zap with slog
This commit is contained in:
parent
a06b9457ea
commit
f4930d7758
64
app.go
64
app.go
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -13,14 +14,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
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"
|
||||
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/docs"
|
||||
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||||
|
@ -28,6 +22,10 @@ import (
|
|||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||
"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
|
||||
|
@ -60,7 +58,6 @@ func (a *App) startup(ctx context.Context) {
|
|||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
a.stop(ctx)
|
||||
_ = logger.Sync()
|
||||
switch v := r.(type) {
|
||||
case 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
|
||||
|
||||
|
@ -87,7 +84,7 @@ func (a *App) startup(ctx context.Context) {
|
|||
}
|
||||
|
||||
// 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")
|
||||
return
|
||||
}
|
||||
|
@ -103,7 +100,7 @@ func (a *App) startup(ctx context.Context) {
|
|||
|
||||
a.ready.Set(true)
|
||||
runtime.EventsEmit(ctx, "ready", true)
|
||||
logger.Info("Strimertul is ready")
|
||||
slog.Info("Strimertul is ready")
|
||||
|
||||
// Add logs I/O to UI
|
||||
a.cancelLogs, _ = a.listenForLogs()
|
||||
|
@ -134,7 +131,7 @@ func (a *App) initializeDatabase() error {
|
|||
go hub.Run()
|
||||
hub.UseInteractiveAuth(a.interactiveAuth)
|
||||
|
||||
a.db, err = database.NewLocalClient(hub, logger)
|
||||
a.db, err = database.NewLocalClient(hub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize database client: %w", err)
|
||||
}
|
||||
|
@ -146,19 +143,19 @@ func (a *App) initializeComponents() error {
|
|||
var err error
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("could not initialize http server: %w", err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("could not initialize twitch client: %w", err)
|
||||
}
|
||||
|
||||
// Initialize loyalty system
|
||||
a.loyaltyManager, err = loyalty.NewManager(a.db, logger)
|
||||
a.loyaltyManager, err = loyalty.NewManager(a.db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
||||
}
|
||||
|
@ -173,13 +170,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
|
|||
return
|
||||
}
|
||||
|
||||
level, err := zapcore.ParseLevel(string(entry.Level))
|
||||
if err != nil {
|
||||
level = zapcore.InfoLevel
|
||||
var level slog.Level
|
||||
if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
|
||||
fields := parseAsFields(entry.Data)
|
||||
logger.Log(level, entry.Message, fields...)
|
||||
slog.Log(a.ctx, level, entry.Message, parseLogFields(entry.Data)...)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -209,7 +205,7 @@ func (a *App) AuthenticateKVClient(id string) {
|
|||
if err != nil {
|
||||
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 {
|
||||
|
@ -249,27 +245,26 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
|
|||
|
||||
// Add text fields
|
||||
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 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
|
||||
_ = logger.Sync()
|
||||
addFile(w, "log", logFilename)
|
||||
addFile(w, "paniclog", panicFilename)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
||||
if err != nil {
|
||||
logger.Error("Could not send crash report", zap.Error(err))
|
||||
slog.Error("Could not send crash report", "error", err)
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
@ -277,7 +272,7 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
|
|||
// Check the response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -326,11 +321,10 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
|||
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 {
|
||||
fields = append(fields, zap.Error(err))
|
||||
fields = append(fields, zap.String("Z", string(debug.Stack())))
|
||||
logger.Error(text, fields...)
|
||||
fields = append(fields, "error", err, slog.String("Z", string(debug.Stack())))
|
||||
slog.Error(text, fields...)
|
||||
runtime.EventsEmit(a.ctx, "fatalError")
|
||||
a.isFatalError.Set(true)
|
||||
}
|
||||
|
@ -347,17 +341,17 @@ func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
|
|||
func addFile(m *multipart.Writer, field string, filename string) {
|
||||
logfile, err := m.CreateFormFile(field, filename)
|
||||
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
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
27
backup.go
27
backup.go
|
@ -2,28 +2,27 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
)
|
||||
|
||||
func BackupTask(driver database.Driver, options database.BackupOptions) {
|
||||
if options.BackupDir == "" {
|
||||
logger.Warn("Backup directory not set, database backups are disabled")
|
||||
slog.Warn("Backup directory not set, database backups are disabled")
|
||||
return
|
||||
}
|
||||
|
||||
err := os.MkdirAll(options.BackupDir, 0o755)
|
||||
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()
|
||||
logger.Info("Using temporary directory", zap.String("backup-dir", options.BackupDir))
|
||||
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -38,21 +37,21 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
|||
// Run backup procedure
|
||||
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
|
||||
if err != nil {
|
||||
logger.Error("Could not create backup file", zap.Error(err))
|
||||
slog.Error("Could not create backup file", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = driver.Backup(file)
|
||||
if err != nil {
|
||||
logger.Error("Could not backup database", zap.Error(err))
|
||||
slog.Error("Could not backup database", "error", err)
|
||||
}
|
||||
_ = 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
|
||||
files, err := os.ReadDir(options.BackupDir)
|
||||
if err != nil {
|
||||
logger.Error("Could not read backup directory", zap.Error(err))
|
||||
slog.Error("Could not read backup directory", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -65,7 +64,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
|||
for _, file := range toRemove {
|
||||
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
|
||||
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) {
|
||||
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
||||
if err != nil {
|
||||
logger.Error("Could not read backup directory", zap.Error(err))
|
||||
slog.Error("Could not read backup directory", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -91,7 +90,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
|
|||
|
||||
info, err := file.Info()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -111,7 +110,7 @@ func (a *App) RestoreBackup(backupName string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not open import file for reading: %w", err)
|
||||
}
|
||||
defer utils.Close(file, logger)
|
||||
defer utils.Close(file)
|
||||
inStream := file
|
||||
|
||||
if a.driver == nil {
|
||||
|
@ -126,6 +125,6 @@ func (a *App) RestoreBackup(backupName string) error {
|
|||
return fmt.Errorf("could not restore database: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Restored database from backup")
|
||||
slog.Info("Restored database from backup")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
)
|
||||
|
||||
func cliImport(ctx *cli.Context) error {
|
||||
|
@ -18,7 +18,7 @@ func cliImport(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return fatalError(err, "could not open import file for reading")
|
||||
}
|
||||
defer utils.Close(file, logger)
|
||||
defer utils.Close(file)
|
||||
inStream = file
|
||||
}
|
||||
var entries map[string]string
|
||||
|
@ -37,7 +37,7 @@ func cliImport(ctx *cli.Context) error {
|
|||
return fatalError(err, "import failed")
|
||||
}
|
||||
|
||||
logger.Info("Imported database from file")
|
||||
slog.Info("Imported database from file")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ func cliRestore(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return fatalError(err, "could not open import file for reading")
|
||||
}
|
||||
defer utils.Close(file, logger)
|
||||
defer utils.Close(file)
|
||||
inStream = file
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ func cliRestore(ctx *cli.Context) error {
|
|||
return fatalError(err, "restore failed")
|
||||
}
|
||||
|
||||
logger.Info("Restored database from backup")
|
||||
slog.Info("Restored database from backup")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ func cliExport(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return fatalError(err, "could not open output file for writing")
|
||||
}
|
||||
defer utils.Close(file, logger)
|
||||
defer utils.Close(file)
|
||||
outStream = file
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,6 @@ func cliExport(ctx *cli.Context) error {
|
|||
return fatalError(err, "export failed")
|
||||
}
|
||||
|
||||
logger.Info("Exported database")
|
||||
slog.Info("Exported database")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,18 +2,15 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
"go.uber.org/zap"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
)
|
||||
|
||||
type CancelFunc func()
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
||||
var (
|
||||
// ErrUnknown is returned when a response is received that doesn't match any expected outcome.
|
||||
ErrUnknown = errors.New("unknown error")
|
||||
|
@ -52,9 +49,9 @@ type KvPair struct {
|
|||
Data string
|
||||
}
|
||||
|
||||
func NewLocalClient(hub *kv.Hub, logger *zap.Logger) (*LocalDBClient, error) {
|
||||
func NewLocalClient(hub *kv.Hub) (*LocalDBClient, error) {
|
||||
// Create local client
|
||||
localClient := kv.NewLocalClient(kv.ClientOptions{}, logger)
|
||||
localClient := kv.NewLocalClient(kv.ClientOptions{})
|
||||
|
||||
// Run client and add it to the hub
|
||||
go localClient.Run()
|
||||
|
|
|
@ -5,8 +5,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
)
|
||||
|
||||
func TestLocalDBClientPutKey(t *testing.T) {
|
||||
|
|
|
@ -6,11 +6,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
name := getDatabaseDriverName(ctx)
|
||||
dbDirectory := ctx.String("database-dir")
|
||||
logger := ctx.Context.Value(utils.ContextLogger).(*zap.Logger)
|
||||
|
||||
switch name {
|
||||
case "badger":
|
||||
return nil, cli.Exit("Badger is not supported anymore as a database driver", 64)
|
||||
case "pebble":
|
||||
db, err := NewPebble(dbDirectory, logger)
|
||||
db, err := NewPebble(dbDirectory)
|
||||
if err != nil {
|
||||
return nil, cli.Exit(err.Error(), 64)
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
pebbledriver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
|
||||
pebble_driver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
|
||||
"github.com/cockroachdb/pebble"
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PebbleDatabase struct {
|
||||
db *pebble.DB
|
||||
hub *kv.Hub
|
||||
logger *zap.Logger
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// 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{})
|
||||
if err != nil {
|
||||
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{
|
||||
db: db,
|
||||
hub: nil,
|
||||
logger: logger,
|
||||
db: db,
|
||||
hub: nil,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
|
@ -44,7 +43,9 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
|
|||
|
||||
func (p *PebbleDatabase) Hub() *kv.Hub {
|
||||
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
|
||||
}
|
||||
|
@ -95,13 +96,13 @@ func (p *PebbleDatabase) Restore(file io.Reader) error {
|
|||
|
||||
func (p *PebbleDatabase) Backup(file io.Writer) error {
|
||||
snapshot := p.db.NewSnapshot()
|
||||
defer utils.Close(snapshot, p.logger)
|
||||
defer utils.Close(snapshot)
|
||||
|
||||
iter, err := snapshot.NewIter(&pebble.IterOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utils.Close(iter, p.logger)
|
||||
defer utils.Close(iter)
|
||||
|
||||
out := make(map[string]string)
|
||||
for iter.First(); iter.Valid(); iter.Next() {
|
||||
|
|
|
@ -3,23 +3,20 @@ package database
|
|||
import (
|
||||
"testing"
|
||||
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
"go.uber.org/zap/zaptest"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
)
|
||||
|
||||
func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
// Create in-memory store and hub
|
||||
inMemoryStore := kv.MakeBackend()
|
||||
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{}, logger)
|
||||
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go hub.Run()
|
||||
|
||||
// Create local client
|
||||
client, err := NewLocalClient(hub, logger)
|
||||
client, err := NewLocalClient(hub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -453,9 +453,9 @@
|
|||
"dialog-title": "Application logs",
|
||||
"levelFilter": "Filter per log severity",
|
||||
"level": {
|
||||
"info": "Info",
|
||||
"warn": "Warning",
|
||||
"error": "Error"
|
||||
"INFO": "Info",
|
||||
"WARN": "Warning",
|
||||
"ERROR": "Error"
|
||||
},
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"copied": "Copied!",
|
||||
|
|
|
@ -36,9 +36,9 @@
|
|||
"copy-to-clipboard": "Copia negli appunti",
|
||||
"levelFilter": "Filtra per livello",
|
||||
"level": {
|
||||
"error": "Errore",
|
||||
"info": "Info",
|
||||
"warn": "Avvertimento"
|
||||
"ERROR": "Errore",
|
||||
"INFO": "Info",
|
||||
"WARN": "Avvertimento"
|
||||
},
|
||||
"toggle-details": "Mostra dettagli"
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@ import { main } from '@wailsapp/go/models';
|
|||
|
||||
export interface ProcessedLogEntry {
|
||||
time: Date;
|
||||
caller: string;
|
||||
level: string;
|
||||
message: string;
|
||||
data: object;
|
||||
|
@ -12,14 +11,12 @@ export interface ProcessedLogEntry {
|
|||
|
||||
export function processEntry({
|
||||
time,
|
||||
caller,
|
||||
level,
|
||||
message,
|
||||
data,
|
||||
}: main.LogEntry): ProcessedLogEntry {
|
||||
return {
|
||||
time: new Date(time),
|
||||
caller,
|
||||
level,
|
||||
message,
|
||||
data: JSON.parse(data) as object,
|
||||
|
@ -34,12 +31,21 @@ const initialState: LoggingState = {
|
|||
messages: [],
|
||||
};
|
||||
|
||||
const keyfn = (ev: main.LogEntry) => ev.id;
|
||||
|
||||
const loggingReducer = createSlice({
|
||||
name: 'logging',
|
||||
initialState,
|
||||
reducers: {
|
||||
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)
|
||||
.sort((a, b) => a.time.getTime() - b.time.getTime());
|
||||
},
|
||||
|
|
|
@ -46,12 +46,12 @@ const LogBubble = styled('div', {
|
|||
},
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
backgroundColor: '$yellow6',
|
||||
color: '$yellow11',
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
backgroundColor: '$red6',
|
||||
color: '$red11',
|
||||
},
|
||||
|
@ -60,12 +60,12 @@ const LogBubble = styled('div', {
|
|||
});
|
||||
|
||||
const emptyFilter = {
|
||||
info: false,
|
||||
warn: false,
|
||||
error: false,
|
||||
INFO: false,
|
||||
WARN: false,
|
||||
ERROR: false,
|
||||
};
|
||||
type LogLevel = keyof typeof emptyFilter;
|
||||
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
||||
const levels: LogLevel[] = ['INFO', 'WARN', 'ERROR'];
|
||||
|
||||
function isSupportedLevel(level: string): level is LogLevel {
|
||||
return (levels as string[]).includes(level);
|
||||
|
@ -89,7 +89,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
|||
},
|
||||
variants: {
|
||||
level: {
|
||||
info: {
|
||||
INFO: {
|
||||
backgroundColor: '$gray4',
|
||||
[`.${lightMode} &`]: {
|
||||
backgroundColor: '$gray2',
|
||||
|
@ -112,7 +112,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
|||
},
|
||||
},
|
||||
},
|
||||
warn: {
|
||||
WARN: {
|
||||
backgroundColor: '$yellow4',
|
||||
[`.${lightMode} &`]: {
|
||||
backgroundColor: '$yellow2',
|
||||
|
@ -135,7 +135,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
|||
},
|
||||
},
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
backgroundColor: '$red4',
|
||||
[`.${lightMode} &`]: {
|
||||
backgroundColor: '$red2',
|
||||
|
@ -175,11 +175,11 @@ const LogEntryContainer = styled('div', {
|
|||
fontSize: '0.9em',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
backgroundColor: '$yellow4',
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
backgroundColor: '$red6',
|
||||
},
|
||||
},
|
||||
|
@ -199,12 +199,12 @@ const LogTime = styled('div', {
|
|||
borderBottomLeftRadius: theme.borderRadius.form,
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
color: '$yellow11',
|
||||
backgroundColor: '$yellow6',
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
color: '$red11',
|
||||
backgroundColor: '$red7',
|
||||
},
|
||||
|
@ -230,13 +230,13 @@ const LogActions = styled('div', {
|
|||
},
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
'& a:hover': {
|
||||
color: '$yellow11',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
'& a:hover': {
|
||||
color: '$red11',
|
||||
},
|
||||
|
@ -258,11 +258,11 @@ const LogDetails = styled('div', {
|
|||
borderBottomLeftRadius: theme.borderRadius.form,
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
backgroundColor: '$yellow3',
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
backgroundColor: '$red4',
|
||||
},
|
||||
},
|
||||
|
@ -276,11 +276,11 @@ const LogDetailKey = styled('div', {
|
|||
color: '$teal10',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
INFO: {},
|
||||
WARN: {
|
||||
color: '$yellow11',
|
||||
},
|
||||
error: {
|
||||
ERROR: {
|
||||
color: '$red11',
|
||||
},
|
||||
},
|
||||
|
@ -430,10 +430,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
|||
>
|
||||
<LogEntriesContainer>
|
||||
{filtered.reverse().map((entry) => (
|
||||
<LogItem
|
||||
key={entry.caller + entry.time.getTime().toString()}
|
||||
data={entry}
|
||||
/>
|
||||
<LogItem key={entry.time.getTime().toString()} data={entry} />
|
||||
))}
|
||||
</LogEntriesContainer>
|
||||
</Scrollbar>
|
||||
|
|
|
@ -469,7 +469,7 @@ function TwitchSection() {
|
|||
|
||||
const onKeyChange = (value: string) => {
|
||||
const event = JSON.parse(value) as EventSubNotification;
|
||||
void setCleanTwitchEvents((prev) => [event, ...prev]);
|
||||
void setCleanTwitchEvents([event, ...twitchEvents]);
|
||||
};
|
||||
|
||||
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
||||
|
|
|
@ -73,7 +73,7 @@ export namespace main {
|
|||
}
|
||||
}
|
||||
export class LogEntry {
|
||||
caller: string;
|
||||
id: string;
|
||||
time: string;
|
||||
level: string;
|
||||
message: string;
|
||||
|
@ -85,7 +85,7 @@ export namespace main {
|
|||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.caller = source["caller"];
|
||||
this.id = source["id"];
|
||||
this.time = source["time"];
|
||||
this.level = source["level"];
|
||||
this.message = source["message"];
|
||||
|
|
7
go.mod
7
go.mod
|
@ -6,7 +6,8 @@ toolchain go1.22.0
|
|||
|
||||
require (
|
||||
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/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/cockroachdb/pebble v1.1.0
|
||||
|
@ -14,10 +15,9 @@ require (
|
|||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/json-iterator/go v1.1.12
|
||||
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/wailsapp/wails/v2 v2.8.0
|
||||
go.uber.org/zap v1.27.0
|
||||
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/mimetype v1.4.1 // 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/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
|
|
16
go.sum
16
go.sum
|
@ -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=
|
||||
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/kilovolt-driver-pebble v1.2.5 h1:btFQjGLisyJwwLgzQ80l39kGlKyCHkVTnbGMnlRwxIk=
|
||||
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 h1:AQORTHRA6Ce1TW24FgHN1K5hWT8+/5nC7sJ5L2W+tRM=
|
||||
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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
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/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
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/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
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.3/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
|
129
logging.go
129
logging.go
|
@ -1,53 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
slogmulti "github.com/samber/slog-multi"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const LogHistory = 50
|
||||
|
||||
var (
|
||||
logger *zap.Logger
|
||||
lastLogs *sync.Slice[LogEntry]
|
||||
incomingLogs chan LogEntry
|
||||
)
|
||||
|
||||
func initLogger(level zapcore.Level) {
|
||||
func initLogger(level slog.Level) {
|
||||
lastLogs = sync.NewSlice[LogEntry]()
|
||||
incomingLogs = make(chan LogEntry, 100)
|
||||
logStorage := NewLogStorage(level)
|
||||
consoleLogger := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
|
||||
zapcore.Lock(os.Stderr),
|
||||
level,
|
||||
)
|
||||
fileLogger := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
|
||||
zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: logFilename,
|
||||
MaxSize: 20,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
}),
|
||||
level,
|
||||
)
|
||||
core := zapcore.NewTee(
|
||||
consoleLogger,
|
||||
fileLogger,
|
||||
logStorage,
|
||||
)
|
||||
logger = zap.New(core, zap.AddCaller())
|
||||
fileLogger := &lumberjack.Logger{
|
||||
Filename: logFilename,
|
||||
MaxSize: 20,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
}
|
||||
consoleHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
fileHandler := slog.NewJSONHandler(fileLogger, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: level,
|
||||
})
|
||||
logger := slog.New(slogmulti.Fanout(consoleHandler, fileHandler, logStorage))
|
||||
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Caller string `json:"caller"`
|
||||
ID string `json:"id"`
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
|
@ -55,42 +51,32 @@ type LogEntry struct {
|
|||
}
|
||||
|
||||
type LogStorage struct {
|
||||
zapcore.LevelEnabler
|
||||
fields []zapcore.Field
|
||||
encoder zapcore.Encoder
|
||||
minLevel slog.Level
|
||||
attrs []slog.Attr
|
||||
group string
|
||||
}
|
||||
|
||||
func NewLogStorage(enabler zapcore.LevelEnabler) *LogStorage {
|
||||
return &LogStorage{
|
||||
LevelEnabler: enabler,
|
||||
encoder: zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
|
||||
}
|
||||
func (core *LogStorage) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= core.minLevel
|
||||
}
|
||||
|
||||
func (core *LogStorage) With(fields []zapcore.Field) zapcore.Core {
|
||||
clone := *core
|
||||
clone.fields = append(clone.fields, fields...)
|
||||
return &clone
|
||||
}
|
||||
func (core *LogStorage) Handle(_ context.Context, record slog.Record) error {
|
||||
attributes := map[string]any{}
|
||||
record.Attrs(func(attrs slog.Attr) bool {
|
||||
attributes[attrs.Key] = attrs.Value.Any()
|
||||
return true
|
||||
})
|
||||
attrJSON, _ := json.MarshalToString(attributes)
|
||||
|
||||
func (core *LogStorage) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if core.Enabled(entry.Level) {
|
||||
return checked.AddCore(entry, core)
|
||||
}
|
||||
return checked
|
||||
}
|
||||
// Generate unique log ID
|
||||
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
|
||||
|
||||
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{
|
||||
Caller: entry.Caller.String(),
|
||||
Time: entry.Time.Format(time.RFC3339),
|
||||
Level: entry.Level.String(),
|
||||
Message: entry.Message,
|
||||
Data: buf.String(),
|
||||
ID: id,
|
||||
Time: record.Time.Format(time.RFC3339),
|
||||
Level: record.Level.String(),
|
||||
Message: record.Message,
|
||||
Data: attrJSON,
|
||||
}
|
||||
lastLogs.Push(logEntry)
|
||||
if lastLogs.Size() > LogHistory {
|
||||
|
@ -100,13 +86,32 @@ func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (core *LogStorage) Sync() error {
|
||||
return nil
|
||||
func (core *LogStorage) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &LogStorage{
|
||||
minLevel: core.minLevel,
|
||||
attrs: append(core.attrs, attrs...),
|
||||
group: core.group,
|
||||
}
|
||||
}
|
||||
|
||||
func parseAsFields(fields map[string]any) (result []zapcore.Field) {
|
||||
for k, v := range fields {
|
||||
result = append(result, zap.Any(k, v))
|
||||
func (core *LogStorage) WithGroup(name string) slog.Handler {
|
||||
return &LogStorage{
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,15 +4,14 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
@ -31,7 +30,6 @@ type Manager struct {
|
|||
Goals *sync.Slice[Goal]
|
||||
Queue *sync.Slice[Redeem]
|
||||
db database.Database
|
||||
logger *zap.Logger
|
||||
cooldowns map[string]time.Time
|
||||
banlist map[string]bool
|
||||
ctx context.Context
|
||||
|
@ -40,7 +38,7 @@ type Manager 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())
|
||||
loyalty := &Manager{
|
||||
Config: sync.NewRWSync(Config{Enabled: false}),
|
||||
|
@ -48,7 +46,6 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
|
|||
Goals: sync.NewSlice[Goal](),
|
||||
Queue: sync.NewSlice[Redeem](),
|
||||
|
||||
logger: logger,
|
||||
db: db,
|
||||
points: sync.NewMap[string, PointsEntry](),
|
||||
cooldowns: make(map[string]time.Time),
|
||||
|
@ -117,7 +114,7 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
|
|||
// SubscribePrefix for changes
|
||||
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
||||
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)
|
||||
|
@ -177,9 +174,9 @@ func (m *Manager) update(key, value string) {
|
|||
}
|
||||
}
|
||||
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 {
|
||||
m.logger.Debug("Updated key", zap.String("key", key))
|
||||
slog.Debug("Updated key", slog.String("key", key))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
45
main.go
45
main.go
|
@ -5,9 +5,12 @@ import (
|
|||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
"github.com/apenwarr/fixconsole"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
@ -15,15 +18,13 @@ import (
|
|||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"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 appVersion = "v0.0.0-UNKNOWN"
|
||||
const devVersionMarker = "v0.0.0-UNKNOWN"
|
||||
|
||||
var appVersion = devVersionMarker
|
||||
|
||||
const (
|
||||
crashReportURL = "https://crash.strimertul.stream/upload"
|
||||
|
@ -35,8 +36,7 @@ const (
|
|||
var frontend embed.FS
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -86,32 +86,34 @@ func main() {
|
|||
},
|
||||
Before: func(ctx *cli.Context) error {
|
||||
// Initialize logger with global flags
|
||||
level, err := zapcore.ParseLevel(ctx.String("log-level"))
|
||||
if err != nil {
|
||||
level = zapcore.InfoLevel
|
||||
level := slog.LevelInfo
|
||||
|
||||
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
|
||||
}
|
||||
initLogger(level)
|
||||
|
||||
// Create file for panics
|
||||
var err error
|
||||
panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
logger.Warn("Could not create panic log", zap.Error(err))
|
||||
slog.Warn("Could not create panic log", "error", err)
|
||||
} else {
|
||||
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
|
||||
},
|
||||
After: func(_ *cli.Context) error {
|
||||
if panicLog != nil {
|
||||
utils.Close(panicLog, logger)
|
||||
utils.Close(panicLog)
|
||||
}
|
||||
|
||||
_ = logger.Sync()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -164,13 +166,18 @@ func cliMain(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func warnOnError(err error, text string, fields ...zap.Field) {
|
||||
func warnOnError(err error, text string, fields ...any) {
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
logger.Warn(text, fields...)
|
||||
fields = append(fields, "error", err)
|
||||
slog.Warn(text, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func fatalError(err error, text string) error {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,10 +4,9 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
)
|
||||
|
||||
|
@ -33,18 +32,18 @@ func GetCurrentSchemaVersion(db database.Database) (int, error) {
|
|||
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
|
||||
var buffer bytes.Buffer
|
||||
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
|
||||
defer func() {
|
||||
if err != nil {
|
||||
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
|
||||
if restoreErr != nil {
|
||||
logger.Error("Failed to restore database from backup", zap.Error(restoreErr))
|
||||
logger.Error("Failed to restore database from backup", "error", restoreErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -2,6 +2,7 @@ package migrations
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"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/timers"
|
||||
"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")
|
||||
|
||||
// Rename keys that have no schema changes
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ProblemID string
|
||||
|
@ -26,7 +27,7 @@ func (a *App) GetProblems() (problems []Problem) {
|
|||
// Check if the app needs to be authorized again
|
||||
scopesMatch, err := twitch.CheckScopes(client.DB)
|
||||
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 {
|
||||
if !scopesMatch {
|
||||
problems = append(problems, Problem{
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
package alerts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"text/template"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
)
|
||||
|
||||
func (m *Module) onEventSubEvent(_ string, value string) {
|
||||
var ev eventSubNotification
|
||||
if err := json.UnmarshalFromString(value, &ev); err != nil {
|
||||
m.logger.Warn("Error parsing webhook payload", zap.Error(err))
|
||||
if err := json.Unmarshal([]byte(value), &ev); err != nil {
|
||||
m.logger.Warn("Error parsing webhook payload", "error", err)
|
||||
return
|
||||
}
|
||||
switch ev.Subscription.Type {
|
||||
|
@ -25,7 +24,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
// Parse as a follow event
|
||||
var followEv helix.EventSubChannelFollowEvent
|
||||
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
|
||||
}
|
||||
// Pick a random message
|
||||
|
@ -50,7 +49,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
var raidEv helix.EventSubChannelRaidEvent
|
||||
|
||||
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
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
@ -84,7 +83,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
// Parse as cheer event
|
||||
var cheerEv helix.EventSubChannelCheerEvent
|
||||
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
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
@ -118,7 +117,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
// Parse as subscription event
|
||||
var subEv helix.EventSubChannelSubscribeEvent
|
||||
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
|
||||
}
|
||||
m.addMixedEvent(subEv)
|
||||
|
@ -131,7 +130,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
var subEv helix.EventSubChannelSubscriptionMessageEvent
|
||||
err := json.Unmarshal(ev.Event, &subEv)
|
||||
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
|
||||
}
|
||||
m.addMixedEvent(subEv)
|
||||
|
@ -143,7 +142,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
|||
// Parse as gift event
|
||||
var giftEv helix.EventSubChannelSubscriptionGiftEvent
|
||||
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
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
|
|
@ -2,19 +2,16 @@ package alerts
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
||||
type (
|
||||
templateCache map[string]*template.Template
|
||||
templateCacheMap map[templateType]templateCache
|
||||
|
@ -35,7 +32,7 @@ type Module struct {
|
|||
|
||||
ctx context.Context
|
||||
db database.Database
|
||||
logger *zap.Logger
|
||||
logger *slog.Logger
|
||||
templater template2.Engine
|
||||
templates templateCacheMap
|
||||
|
||||
|
@ -43,7 +40,7 @@ type Module struct {
|
|||
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{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
|
@ -55,31 +52,31 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templa
|
|||
|
||||
// Load config from database
|
||||
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{}
|
||||
// Save empty config
|
||||
err = db.PutJSON(ConfigKey, mod.Config)
|
||||
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()
|
||||
|
||||
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 {
|
||||
logger.Warn("Error loading alert config", zap.Error(err))
|
||||
logger.Warn("Error loading alert config", "error", err)
|
||||
} else {
|
||||
logger.Info("Reloaded alert config")
|
||||
}
|
||||
mod.compileTemplates()
|
||||
}); 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 {
|
||||
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")
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
)
|
||||
|
||||
|
@ -44,7 +42,7 @@ func (m *Module) compileTemplates() {
|
|||
func (m *Module) addTemplate(templateList templateCache, message string) {
|
||||
tpl, err := m.templater.MakeTemplate(message)
|
||||
if err != nil {
|
||||
m.logger.Error("Error compiling alert template", zap.Error(err))
|
||||
m.logger.Error("Error compiling alert template", "error", err)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
var buf bytes.Buffer
|
||||
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
|
||||
}
|
||||
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
||||
|
|
|
@ -2,9 +2,9 @@ package chat
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var accessLevels = map[AccessLevelType]int{
|
||||
|
@ -70,7 +70,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
|
|||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
|
|||
Announce: true,
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -12,10 +12,8 @@ import (
|
|||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -91,7 +89,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
|||
var streamInfos []helix.Stream
|
||||
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
||||
if err != nil {
|
||||
mod.logger.Error("Error retrieving stream info", zap.Error(err))
|
||||
mod.logger.Error("Error retrieving stream info", "error", err)
|
||||
continue
|
||||
}
|
||||
if len(streamInfos) < 1 {
|
||||
|
@ -104,7 +102,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
|||
for {
|
||||
userClient, err := twitch.GetUserClient(mod.db, false)
|
||||
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
|
||||
}
|
||||
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
||||
|
@ -114,7 +112,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
|||
After: cursor,
|
||||
})
|
||||
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
|
||||
}
|
||||
for _, user := range res.Data.Chatters {
|
||||
|
@ -150,7 +148,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
|||
if len(users) > 0 {
|
||||
err := li.manager.GivePoints(pointsToGive)
|
||||
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
|
||||
}
|
||||
li.module.logger.Error("Error while performing redeem", zap.Error(err))
|
||||
li.module.logger.Error("Error while performing redeem", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -258,7 +256,7 @@ func (li *loyaltyIntegration) cmdGoalList(message helix.EventSubChannelChatMessa
|
|||
goals := li.manager.Goals.Get()
|
||||
if len(goals) < 1 {
|
||||
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,
|
||||
})
|
||||
return
|
||||
|
@ -300,12 +298,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
|||
if goalIndex < 0 {
|
||||
if hasGoals {
|
||||
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,
|
||||
})
|
||||
} else {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
@ -319,7 +317,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
|||
if err == nil {
|
||||
if newPoints <= 0 {
|
||||
li.module.WriteMessage(WriteMessageRequest{
|
||||
Message: fmt.Sprintf("Nice try SoBayed"),
|
||||
Message: "Nice try SoBayed",
|
||||
ReplyTo: message.MessageID,
|
||||
})
|
||||
return
|
||||
|
@ -343,7 +341,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
|||
// Invalid goal ID provided
|
||||
if !found {
|
||||
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,
|
||||
})
|
||||
return
|
||||
|
@ -357,7 +355,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
|||
// Check if goal was reached already
|
||||
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
|
||||
li.module.WriteMessage(WriteMessageRequest{
|
||||
Message: fmt.Sprintf("This goal was already reached! ヾ(•ω•`)o"),
|
||||
Message: "This goal was already reached! ヾ(•ω•`)o",
|
||||
ReplyTo: message.MessageID,
|
||||
})
|
||||
return
|
||||
|
@ -366,12 +364,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
|||
// Add points to goal
|
||||
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
||||
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
|
||||
}
|
||||
if points == 0 {
|
||||
li.module.WriteMessage(WriteMessageRequest{
|
||||
Message: fmt.Sprintf("Sorry but you're broke"),
|
||||
Message: "Sorry but you're broke",
|
||||
ReplyTo: message.MessageID,
|
||||
})
|
||||
return
|
||||
|
|
|
@ -3,19 +3,18 @@ package chat
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
textTemplate "text/template"
|
||||
"time"
|
||||
|
||||
"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/twitch/eventsub"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
@ -27,7 +26,7 @@ type Module struct {
|
|||
db database.Database
|
||||
api *helix.Client
|
||||
user helix.User
|
||||
logger *zap.Logger
|
||||
logger *slog.Logger
|
||||
templater template.Engine
|
||||
lastMessage *sync.RWSync[time.Time]
|
||||
|
||||
|
@ -35,11 +34,9 @@ type Module struct {
|
|||
customCommands *sync.Map[string, CustomCommand]
|
||||
customTemplates *sync.Map[string, *textTemplate.Template]
|
||||
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{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
|
@ -62,12 +59,12 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
|
|||
CommandCooldown: 2,
|
||||
}
|
||||
} 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 {
|
||||
logger.Error("Could not subscribe to chat messages", zap.Error(err))
|
||||
logger.Error("Could not subscribe to chat messages", "error", err)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
customCommands = make(map[string]CustomCommand)
|
||||
} else {
|
||||
logger.Error("Failed to load custom commands", zap.Error(err))
|
||||
logger.Error("Failed to load custom commands", "error", err)
|
||||
}
|
||||
}
|
||||
mod.customCommands.Set(customCommands)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
|
@ -101,7 +98,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
|||
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -149,7 +146,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
|||
func (mod *Module) handleWriteMessageRPC(value string) {
|
||||
var request WriteMessageRequest
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -160,10 +157,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
Message: request.Message,
|
||||
})
|
||||
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 != "" {
|
||||
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
|
||||
}
|
||||
|
@ -175,10 +172,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
Message: request.Message,
|
||||
})
|
||||
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 != "" {
|
||||
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
|
||||
}
|
||||
|
@ -190,22 +187,22 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
ReplyParentMessageID: request.ReplyTo,
|
||||
})
|
||||
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 != "" {
|
||||
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) {
|
||||
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
||||
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
|
||||
}
|
||||
// Recreate templates
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write chat message", zap.Error(err))
|
||||
logger.Error("Failed to write chat message", "error", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"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/twitch"
|
||||
|
@ -20,13 +20,11 @@ import (
|
|||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
||||
type Manager struct {
|
||||
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
|
||||
var config twitch.Config
|
||||
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
|
||||
clientContext, cancel := context.WithCancel(ctx)
|
||||
client, err := newClient(clientContext, config, db, server, logger)
|
||||
client, err := newClient(clientContext, config, db, server)
|
||||
if err != nil {
|
||||
cancel()
|
||||
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
|
||||
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
||||
var newConfig twitch.Config
|
||||
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
|
||||
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
|
||||
if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
|
||||
slog.Error("Failed to decode Twitch integration config", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -60,9 +58,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
|||
|
||||
var updatedClient *Client
|
||||
clientContext, cancel = context.WithCancel(ctx)
|
||||
updatedClient, err = newClient(clientContext, newConfig, db, server, logger)
|
||||
updatedClient, err = newClient(clientContext, newConfig, db, server)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -70,9 +68,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
|||
updatedClient.Merge(manager.client)
|
||||
manager.client = updatedClient
|
||||
|
||||
logger.Info("Reloaded/updated Twitch integration")
|
||||
slog.Info("Reloaded/updated Twitch integration")
|
||||
}); 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
|
||||
|
@ -87,7 +85,7 @@ type Client struct {
|
|||
DB database.Database
|
||||
API *helix.Client
|
||||
User helix.User
|
||||
Logger *zap.Logger
|
||||
Logger *slog.Logger
|
||||
|
||||
Chat *chat.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
|
||||
client := &Client{
|
||||
Config: sync.NewRWSync(config),
|
||||
DB: db,
|
||||
Logger: logger.With(zap.String("service", "twitch")),
|
||||
Logger: slog.With(slog.String("service", "twitch")),
|
||||
restart: make(chan bool, 128),
|
||||
streamOnline: sync.NewRWSync(false),
|
||||
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 {
|
||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
||||
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 {
|
||||
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
||||
} 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.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 {
|
||||
client.Logger.Error("Failed to setup EventSub", zap.Error(err))
|
||||
client.Logger.Error("Failed to setup EventSub", "error", err)
|
||||
}
|
||||
|
||||
tpl := client.GetTemplateEngine()
|
||||
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl)
|
||||
client.Alerts = alerts.Setup(ctx, db, logger, tpl)
|
||||
client.Timers = timers.Setup(ctx, db, logger)
|
||||
client.Chat = chat.Setup(ctx, db, userClient, client.User, client.Logger, tpl)
|
||||
client.Alerts = alerts.Setup(ctx, db, client.Logger, tpl)
|
||||
client.Timers = timers.Setup(ctx, db, client.Logger)
|
||||
}
|
||||
} else {
|
||||
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},
|
||||
})
|
||||
if err != nil {
|
||||
c.Logger.Error("Error checking stream status", zap.Error(err))
|
||||
c.Logger.Error("Error checking stream status", "error", err)
|
||||
return
|
||||
}
|
||||
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
||||
|
||||
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
||||
if err != nil {
|
||||
c.Logger.Warn("Error saving stream info", zap.Error(err))
|
||||
c.Logger.Warn("Error saving stream info", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
client, _ := database.CreateInMemoryLocalClient(t)
|
||||
defer database.CleanupLocalClient(client)
|
||||
|
||||
server, err := webserver.NewServer(client, logger, webserver.DefaultServerFactory)
|
||||
server, err := webserver.NewServer(client, webserver.DefaultServerFactory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := twitch.Config{}
|
||||
_, err = newClient(config, client, server, logger)
|
||||
_, err = newClient(context.Background(), config, client, server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -11,7 +12,6 @@ import (
|
|||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const ChatCounterPrefix = "twitch/chat/counters/"
|
||||
|
@ -57,7 +57,7 @@ func (c *Client) GetTemplateEngine() template.Engine {
|
|||
counter++
|
||||
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
|
||||
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
|
||||
},
|
||||
|
|
|
@ -3,16 +3,15 @@ package eventsub
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
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/utils"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
@ -23,14 +22,14 @@ type Client struct {
|
|||
ctx context.Context
|
||||
twitchAPI *helix.Client
|
||||
db database.Database
|
||||
logger *zap.Logger
|
||||
logger *slog.Logger
|
||||
user helix.User
|
||||
|
||||
eventCache *lru.Cache[string, time.Time]
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
|
||||
|
@ -58,11 +57,11 @@ func (c *Client) eventSubLoop() {
|
|||
for endpoint != "" {
|
||||
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
||||
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 {
|
||||
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) {
|
||||
connection, _, err := websocket.DefaultDialer.Dial(url, 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
|
||||
}
|
||||
|
||||
|
@ -109,7 +108,7 @@ func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (st
|
|||
var wsMessage WebsocketMessage
|
||||
err = json.Unmarshal(messageData, &wsMessage)
|
||||
if err != nil {
|
||||
c.logger.Error("Error decoding EventSub message", zap.Error(err))
|
||||
c.logger.Error("Error decoding EventSub message", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -128,30 +127,30 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
|
|||
var welcomeData WelcomeMessagePayload
|
||||
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
|
||||
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
|
||||
}
|
||||
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
|
||||
if oldConnection != nil {
|
||||
utils.Close(oldConnection, c.logger)
|
||||
utils.Close(oldConnection)
|
||||
}
|
||||
|
||||
// Add subscription to websocket session
|
||||
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
||||
if err != nil {
|
||||
c.logger.Error("Could not add subscriptions", zap.Error(err))
|
||||
c.logger.Error("Could not add subscriptions", "error", err)
|
||||
break
|
||||
}
|
||||
case "session_reconnect":
|
||||
var reconnectData WelcomeMessagePayload
|
||||
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
|
||||
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
|
||||
}
|
||||
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
|
||||
case "notification":
|
||||
|
@ -166,7 +165,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
// Check if we processed this already
|
||||
if 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
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
var notificationData NotificationMessagePayload
|
||||
err := json.Unmarshal(message.Payload, ¬ificationData)
|
||||
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()
|
||||
|
||||
|
@ -184,7 +183,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
||||
err = c.db.PutJSON(eventKey, notificationData)
|
||||
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
|
||||
|
@ -198,7 +197,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
}
|
||||
err = c.db.PutJSON(historyKey, archive)
|
||||
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),
|
||||
})
|
||||
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)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -2,15 +2,14 @@ package timers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"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/twitch/chat"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
@ -23,12 +22,12 @@ type Module struct {
|
|||
lastTrigger *sync.Map[string, time.Time]
|
||||
messages *sync.Slice[int]
|
||||
|
||||
logger *zap.Logger
|
||||
logger *slog.Logger
|
||||
db database.Database
|
||||
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{
|
||||
lastTrigger: sync.NewMap[string, time.Time](),
|
||||
messages: sync.NewSlice[int](),
|
||||
|
@ -45,28 +44,28 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Modul
|
|||
|
||||
// Load config from database
|
||||
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{
|
||||
Timers: make(map[string]ChatTimer),
|
||||
}
|
||||
// Save empty config
|
||||
err = db.PutJSON(ConfigKey, mod.Config)
|
||||
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 := 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
|
||||
}
|
||||
logger.Info("Reloaded timer config")
|
||||
}); 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
|
||||
go mod.runTimers()
|
||||
|
@ -84,7 +83,7 @@ func (m *Module) runTimers() {
|
|||
|
||||
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
|
||||
if err != nil {
|
||||
m.logger.Warn("Error saving chat activity", zap.Error(err))
|
||||
m.logger.Warn("Error saving chat activity", "error", err)
|
||||
}
|
||||
|
||||
// Calculate activity
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package utils
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextLogger ContextKey = "logger"
|
||||
)
|
|
@ -2,15 +2,14 @@ package utils
|
|||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Close(res io.Closer, logger *zap.Logger) {
|
||||
func Close(res io.Closer) {
|
||||
err := res.Close()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
"encoding/json"
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
)
|
||||
|
||||
func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error {
|
||||
var result T
|
||||
err := json.UnmarshalFromString(data, &result)
|
||||
err := json.Unmarshal([]byte(data), &result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,28 +4,24 @@ import (
|
|||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
mrand "math/rand"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
"go.uber.org/zap"
|
||||
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
||||
type WebServer struct {
|
||||
Config *sync.RWSync[ServerConfig]
|
||||
db database.Database
|
||||
logger *zap.Logger
|
||||
server Server
|
||||
frontend fs.FS
|
||||
hub *kv.Hub
|
||||
|
@ -36,9 +32,8 @@ type WebServer struct {
|
|||
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{
|
||||
logger: logger,
|
||||
db: db,
|
||||
server: nil,
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
server.Config.Set(ServerConfig{
|
||||
|
@ -124,7 +119,7 @@ func (s *WebServer) makeMux() *http.ServeMux {
|
|||
}
|
||||
if s.hub != nil {
|
||||
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()
|
||||
|
@ -160,7 +155,7 @@ func (s *WebServer) Listen() error {
|
|||
for {
|
||||
// Read config and make http request mux
|
||||
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()
|
||||
|
||||
// Make HTTP server instance
|
||||
|
@ -172,25 +167,25 @@ func (s *WebServer) Listen() error {
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// 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) {
|
||||
exit <- err
|
||||
return
|
||||
}
|
||||
|
||||
// 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() {
|
||||
s.restart.Set(false)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
s.logger.Debug("HTTP server stalled")
|
||||
slog.Debug("HTTP server stalled")
|
||||
exit <- nil
|
||||
}()
|
||||
|
||||
|
@ -203,7 +198,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
|||
var config ServerConfig
|
||||
err := json.Unmarshal([]byte(value), &config)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to unmarshal config", zap.Error(err))
|
||||
slog.Error("Failed to unmarshal config", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -220,7 +215,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
|||
s.restart.Set(true)
|
||||
err = s.server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to shutdown server", zap.Error(err))
|
||||
slog.Error("Failed to shutdown server", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,42 +8,37 @@ import (
|
|||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
)
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
client, _ := database.CreateInMemoryLocalClient(t)
|
||||
defer database.CleanupLocalClient(client)
|
||||
|
||||
_, err := NewServer(client, logger, DefaultServerFactory)
|
||||
_, err := NewServer(client, DefaultServerFactory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerWithTestFactory(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
client, _ := database.CreateInMemoryLocalClient(t)
|
||||
defer database.CleanupLocalClient(client)
|
||||
|
||||
testServer := NewTestServer()
|
||||
_, err := NewServer(client, logger, testServer.Factory())
|
||||
_, err := NewServer(client, testServer.Factory())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListen(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
client, _ := database.CreateInMemoryLocalClient(t)
|
||||
defer database.CleanupLocalClient(client)
|
||||
|
||||
// Create a test server
|
||||
testServer := NewTestServer()
|
||||
server, err := NewServer(client, logger, testServer.Factory())
|
||||
server, err := NewServer(client, testServer.Factory())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -96,12 +91,11 @@ func (t *testCustomHandler) ServeHTTP(http.ResponseWriter, *http.Request) {
|
|||
}
|
||||
|
||||
func TestCustomRoute(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
client, _ := database.CreateInMemoryLocalClient(t)
|
||||
defer database.CleanupLocalClient(client)
|
||||
|
||||
// Create test server
|
||||
server, err := NewServer(client, logger, nil)
|
||||
server, err := NewServer(client, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue