mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-30 02:40:33 +00:00
Compare commits
3 commits
a06b9457ea
...
edcc4fb7f9
Author | SHA1 | Date | |
---|---|---|---|
|
edcc4fb7f9 | ||
|
97a81373ab | ||
|
f4930d7758 |
40 changed files with 396 additions and 427 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -9,14 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- The windows can now shrink no more than 480x300 but the UI will now better adapt to small window sizes
|
||||
- The UI sidebar has been modified to better adapt to small window sizes
|
||||
- A new part of the dashboard will now inform the user if any configuration problems have been detected.
|
||||
|
||||
### Changed
|
||||
|
||||
- The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül.
|
||||
- The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same.
|
||||
- The `twitch/bot/@send-message` key has been renamed to `twitch/chat/@send-message`. The data structure is the same.
|
||||
- A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys.
|
||||
- The log format has changed significantly as the internal logging library has been replaced.
|
||||
- The Twitch chat integration has been rewritten from the ground up to not use an IRC bot and rely on EventSub. This means that you will need to reconfigure your twitch account, especially if you used a different account as the "bot" account. Because of this rewrite, the terminology around chat functionalities have been renamed from "Bot" to "Chat" (e.g. "Bot commands" are now "Chat commands").
|
||||
- The (i) icon next to "Recent events" in the dashboard now uses a custom tooltip that shows up more consistently.
|
||||
- The "strimertul is already running" message now pops up from the currently running instance.
|
||||
|
||||
### Removed
|
||||
|
||||
- `twitch/@send-chat-message` has been removed. Use `twitch/chat/@send-message` instead.
|
||||
- The `twitch/ev/chat-message` and `twitch/chat-history` keys have been removed. Use the EventSub keys `twitch/ev/eventsub-event/channel.chat.message` and `twitch/eventsub-history/channel.chat.message` instead. The data structure will be different!
|
||||
|
||||
## 3.3.1 - 2023-11-12
|
||||
|
||||
|
|
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"
|
||||
},
|
||||
|
|
|
@ -3,23 +3,23 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||
import { main } from '@wailsapp/go/models';
|
||||
|
||||
export interface ProcessedLogEntry {
|
||||
id: string;
|
||||
time: Date;
|
||||
caller: string;
|
||||
level: string;
|
||||
message: string;
|
||||
data: object;
|
||||
}
|
||||
|
||||
export function processEntry({
|
||||
id,
|
||||
time,
|
||||
caller,
|
||||
level,
|
||||
message,
|
||||
data,
|
||||
}: main.LogEntry): ProcessedLogEntry {
|
||||
return {
|
||||
id,
|
||||
time: new Date(time),
|
||||
caller,
|
||||
level,
|
||||
message,
|
||||
data: JSON.parse(data) as object,
|
||||
|
@ -34,12 +34,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.id} 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 a new issue