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
|
### Added
|
||||||
|
|
||||||
- The windows can now shrink no more than 480x300 but the UI will now better adapt to small window sizes
|
- The UI sidebar has been modified to better adapt to small window sizes
|
||||||
- A new part of the dashboard will now inform the user if any configuration problems have been detected.
|
- A new part of the dashboard will now inform the user if any configuration problems have been detected.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül.
|
- The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül.
|
||||||
- The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same.
|
- The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same.
|
||||||
|
- The `twitch/bot/@send-message` key has been renamed to `twitch/chat/@send-message`. The data structure is the same.
|
||||||
- A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys.
|
- A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys.
|
||||||
|
- The log format has changed significantly as the internal logging library has been replaced.
|
||||||
|
- The Twitch chat integration has been rewritten from the ground up to not use an IRC bot and rely on EventSub. This means that you will need to reconfigure your twitch account, especially if you used a different account as the "bot" account. Because of this rewrite, the terminology around chat functionalities have been renamed from "Bot" to "Chat" (e.g. "Bot commands" are now "Chat commands").
|
||||||
|
- The (i) icon next to "Recent events" in the dashboard now uses a custom tooltip that shows up more consistently.
|
||||||
|
- The "strimertul is already running" message now pops up from the currently running instance.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `twitch/@send-chat-message` has been removed. Use `twitch/chat/@send-message` instead.
|
||||||
|
- The `twitch/ev/chat-message` and `twitch/chat-history` keys have been removed. Use the EventSub keys `twitch/ev/eventsub-event/channel.chat.message` and `twitch/eventsub-history/channel.chat.message` instead. The data structure will be different!
|
||||||
|
|
||||||
## 3.3.1 - 2023-11-12
|
## 3.3.1 - 2023-11-12
|
||||||
|
|
||||||
|
|
64
app.go
64
app.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -13,14 +14,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
"github.com/nicklaw5/helix/v2"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/docs"
|
"git.sr.ht/~ashkeel/strimertul/docs"
|
||||||
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||||||
|
@ -28,6 +22,10 @@ import (
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/client"
|
"git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App struct
|
// App struct
|
||||||
|
@ -60,7 +58,6 @@ func (a *App) startup(ctx context.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
a.stop(ctx)
|
a.stop(ctx)
|
||||||
_ = logger.Sync()
|
|
||||||
switch v := r.(type) {
|
switch v := r.(type) {
|
||||||
case error:
|
case error:
|
||||||
a.showFatalError(v, v.Error())
|
a.showFatalError(v, v.Error())
|
||||||
|
@ -70,7 +67,7 @@ func (a *App) startup(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logger.Info("Started", zap.String("version", appVersion))
|
slog.Info("Started", slog.String("version", appVersion))
|
||||||
|
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
|
||||||
|
@ -87,7 +84,7 @@ func (a *App) startup(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for migrations
|
// Check for migrations
|
||||||
if err := migrations.Run(a.driver, a.db, logger.With(zap.String("operation", "migration"))); err != nil {
|
if err := migrations.Run(a.driver, a.db, slog.With(slog.String("operation", "migration"))); err != nil {
|
||||||
a.showFatalError(err, "Failed to migrate database to latest version")
|
a.showFatalError(err, "Failed to migrate database to latest version")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -103,7 +100,7 @@ func (a *App) startup(ctx context.Context) {
|
||||||
|
|
||||||
a.ready.Set(true)
|
a.ready.Set(true)
|
||||||
runtime.EventsEmit(ctx, "ready", true)
|
runtime.EventsEmit(ctx, "ready", true)
|
||||||
logger.Info("Strimertul is ready")
|
slog.Info("Strimertul is ready")
|
||||||
|
|
||||||
// Add logs I/O to UI
|
// Add logs I/O to UI
|
||||||
a.cancelLogs, _ = a.listenForLogs()
|
a.cancelLogs, _ = a.listenForLogs()
|
||||||
|
@ -134,7 +131,7 @@ func (a *App) initializeDatabase() error {
|
||||||
go hub.Run()
|
go hub.Run()
|
||||||
hub.UseInteractiveAuth(a.interactiveAuth)
|
hub.UseInteractiveAuth(a.interactiveAuth)
|
||||||
|
|
||||||
a.db, err = database.NewLocalClient(hub, logger)
|
a.db, err = database.NewLocalClient(hub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize database client: %w", err)
|
return fmt.Errorf("could not initialize database client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -146,19 +143,19 @@ func (a *App) initializeComponents() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Create logger and endpoints
|
// Create logger and endpoints
|
||||||
a.httpServer, err = webserver.NewServer(a.db, logger, webserver.DefaultServerFactory)
|
a.httpServer, err = webserver.NewServer(a.db, webserver.DefaultServerFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize http server: %w", err)
|
return fmt.Errorf("could not initialize http server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create twitch client
|
// Create twitch client
|
||||||
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer, logger)
|
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize twitch client: %w", err)
|
return fmt.Errorf("could not initialize twitch client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize loyalty system
|
// Initialize loyalty system
|
||||||
a.loyaltyManager, err = loyalty.NewManager(a.db, logger)
|
a.loyaltyManager, err = loyalty.NewManager(a.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -173,13 +170,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
level, err := zapcore.ParseLevel(string(entry.Level))
|
var level slog.Level
|
||||||
if err != nil {
|
if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
|
||||||
level = zapcore.InfoLevel
|
level = slog.LevelInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := parseAsFields(entry.Data)
|
slog.Log(a.ctx, level, entry.Message, parseLogFields(entry.Data)...)
|
||||||
logger.Log(level, entry.Message, fields...)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +205,7 @@ func (a *App) AuthenticateKVClient(id string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", zap.String("session-id", id))
|
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", slog.String("session-id", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) IsServerReady() bool {
|
func (a *App) IsServerReady() bool {
|
||||||
|
@ -249,27 +245,26 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
|
||||||
|
|
||||||
// Add text fields
|
// Add text fields
|
||||||
if err := w.WriteField("error", errorData); err != nil {
|
if err := w.WriteField("error", errorData); err != nil {
|
||||||
logger.Error("Could not encode field error for crash report", zap.Error(err))
|
slog.Error("Could not encode field error for crash report", "error", err)
|
||||||
}
|
}
|
||||||
if len(info) > 0 {
|
if len(info) > 0 {
|
||||||
if err := w.WriteField("info", info); err != nil {
|
if err := w.WriteField("info", info); err != nil {
|
||||||
logger.Error("Could not encode field info for crash report", zap.Error(err))
|
slog.Error("Could not encode field info for crash report", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add log files
|
// Add log files
|
||||||
_ = logger.Sync()
|
|
||||||
addFile(w, "log", logFilename)
|
addFile(w, "log", logFilename)
|
||||||
addFile(w, "paniclog", panicFilename)
|
addFile(w, "paniclog", panicFilename)
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
logger.Error("Could not prepare request for crash report", zap.Error(err))
|
slog.Error("Could not prepare request for crash report", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not send crash report", zap.Error(err))
|
slog.Error("Could not send crash report", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
@ -277,7 +272,7 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
|
||||||
// Check the response
|
// Check the response
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
byt, _ := io.ReadAll(resp.Body)
|
byt, _ := io.ReadAll(resp.Body)
|
||||||
logger.Error("Crash report server returned error", zap.String("status", resp.Status), zap.String("response", string(byt)))
|
slog.Error("Crash report server returned error", slog.String("status", resp.Status), slog.String("response", string(byt)))
|
||||||
return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
|
return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,11 +321,10 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
||||||
return <-authResult
|
return <-authResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) showFatalError(err error, text string, fields ...zap.Field) {
|
func (a *App) showFatalError(err error, text string, fields ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fields = append(fields, zap.Error(err))
|
fields = append(fields, "error", err, slog.String("Z", string(debug.Stack())))
|
||||||
fields = append(fields, zap.String("Z", string(debug.Stack())))
|
slog.Error(text, fields...)
|
||||||
logger.Error(text, fields...)
|
|
||||||
runtime.EventsEmit(a.ctx, "fatalError")
|
runtime.EventsEmit(a.ctx, "fatalError")
|
||||||
a.isFatalError.Set(true)
|
a.isFatalError.Set(true)
|
||||||
}
|
}
|
||||||
|
@ -347,17 +341,17 @@ func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
|
||||||
func addFile(m *multipart.Writer, field string, filename string) {
|
func addFile(m *multipart.Writer, field string, filename string) {
|
||||||
logfile, err := m.CreateFormFile(field, filename)
|
logfile, err := m.CreateFormFile(field, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not encode field log for crash report", zap.Error(err))
|
slog.Error("Could not encode field log for crash report", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not open file for including in crash report", zap.Error(err), zap.String("file", filename))
|
slog.Error("Could not open file for including in crash report", slog.String("file", filename), "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.Copy(logfile, file); err != nil {
|
if _, err = io.Copy(logfile, file); err != nil {
|
||||||
logger.Error("Could not read from file for including in crash report", zap.Error(err), zap.String("file", filename))
|
slog.Error("Could not read from file for including in crash report", slog.String("file", filename), "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
backup.go
27
backup.go
|
@ -2,28 +2,27 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BackupTask(driver database.Driver, options database.BackupOptions) {
|
func BackupTask(driver database.Driver, options database.BackupOptions) {
|
||||||
if options.BackupDir == "" {
|
if options.BackupDir == "" {
|
||||||
logger.Warn("Backup directory not set, database backups are disabled")
|
slog.Warn("Backup directory not set, database backups are disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := os.MkdirAll(options.BackupDir, 0o755)
|
err := os.MkdirAll(options.BackupDir, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not create backup directory, moving to a temporary folder", zap.Error(err))
|
slog.Error("Could not create backup directory, moving to a temporary folder", "error", err)
|
||||||
options.BackupDir = os.TempDir()
|
options.BackupDir = os.TempDir()
|
||||||
logger.Info("Using temporary directory", zap.String("backup-dir", options.BackupDir))
|
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,21 +37,21 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
||||||
// Run backup procedure
|
// Run backup procedure
|
||||||
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
|
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not create backup file", zap.Error(err))
|
slog.Error("Could not create backup file", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.Backup(file)
|
err = driver.Backup(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not backup database", zap.Error(err))
|
slog.Error("Could not backup database", "error", err)
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
logger.Info("Database backup created", zap.String("backup-file", file.Name()))
|
slog.Info("Database backup created", slog.String("backup-file", file.Name()))
|
||||||
|
|
||||||
// Remove old backups
|
// Remove old backups
|
||||||
files, err := os.ReadDir(options.BackupDir)
|
files, err := os.ReadDir(options.BackupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not read backup directory", zap.Error(err))
|
slog.Error("Could not read backup directory", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +64,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
||||||
for _, file := range toRemove {
|
for _, file := range toRemove {
|
||||||
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
|
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not remove backup file", zap.Error(err))
|
slog.Error("Could not remove backup file", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +79,7 @@ type BackupInfo struct {
|
||||||
func (a *App) GetBackups() (list []BackupInfo) {
|
func (a *App) GetBackups() (list []BackupInfo) {
|
||||||
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not read backup directory", zap.Error(err))
|
slog.Error("Could not read backup directory", "error", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +90,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
|
||||||
|
|
||||||
info, err := file.Info()
|
info, err := file.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not get info for backup file", zap.Error(err))
|
slog.Error("Could not get info for backup file", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +110,7 @@ func (a *App) RestoreBackup(backupName string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not open import file for reading: %w", err)
|
return fmt.Errorf("could not open import file for reading: %w", err)
|
||||||
}
|
}
|
||||||
defer utils.Close(file, logger)
|
defer utils.Close(file)
|
||||||
inStream := file
|
inStream := file
|
||||||
|
|
||||||
if a.driver == nil {
|
if a.driver == nil {
|
||||||
|
@ -126,6 +125,6 @@ func (a *App) RestoreBackup(backupName string) error {
|
||||||
return fmt.Errorf("could not restore database: %w", err)
|
return fmt.Errorf("could not restore database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Restored database from backup")
|
slog.Info("Restored database from backup")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func cliImport(ctx *cli.Context) error {
|
func cliImport(ctx *cli.Context) error {
|
||||||
|
@ -18,7 +18,7 @@ func cliImport(ctx *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatalError(err, "could not open import file for reading")
|
return fatalError(err, "could not open import file for reading")
|
||||||
}
|
}
|
||||||
defer utils.Close(file, logger)
|
defer utils.Close(file)
|
||||||
inStream = file
|
inStream = file
|
||||||
}
|
}
|
||||||
var entries map[string]string
|
var entries map[string]string
|
||||||
|
@ -37,7 +37,7 @@ func cliImport(ctx *cli.Context) error {
|
||||||
return fatalError(err, "import failed")
|
return fatalError(err, "import failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Imported database from file")
|
slog.Info("Imported database from file")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ func cliRestore(ctx *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatalError(err, "could not open import file for reading")
|
return fatalError(err, "could not open import file for reading")
|
||||||
}
|
}
|
||||||
defer utils.Close(file, logger)
|
defer utils.Close(file)
|
||||||
inStream = file
|
inStream = file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ func cliRestore(ctx *cli.Context) error {
|
||||||
return fatalError(err, "restore failed")
|
return fatalError(err, "restore failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Restored database from backup")
|
slog.Info("Restored database from backup")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ func cliExport(ctx *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fatalError(err, "could not open output file for writing")
|
return fatalError(err, "could not open output file for writing")
|
||||||
}
|
}
|
||||||
defer utils.Close(file, logger)
|
defer utils.Close(file)
|
||||||
outStream = file
|
outStream = file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +89,6 @@ func cliExport(ctx *cli.Context) error {
|
||||||
return fatalError(err, "export failed")
|
return fatalError(err, "export failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Exported database")
|
slog.Info("Exported database")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,15 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CancelFunc func()
|
type CancelFunc func()
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrUnknown is returned when a response is received that doesn't match any expected outcome.
|
// ErrUnknown is returned when a response is received that doesn't match any expected outcome.
|
||||||
ErrUnknown = errors.New("unknown error")
|
ErrUnknown = errors.New("unknown error")
|
||||||
|
@ -52,9 +49,9 @@ type KvPair struct {
|
||||||
Data string
|
Data string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalClient(hub *kv.Hub, logger *zap.Logger) (*LocalDBClient, error) {
|
func NewLocalClient(hub *kv.Hub) (*LocalDBClient, error) {
|
||||||
// Create local client
|
// Create local client
|
||||||
localClient := kv.NewLocalClient(kv.ClientOptions{}, logger)
|
localClient := kv.NewLocalClient(kv.ClientOptions{})
|
||||||
|
|
||||||
// Run client and add it to the hub
|
// Run client and add it to the hub
|
||||||
go localClient.Run()
|
go localClient.Run()
|
||||||
|
|
|
@ -5,8 +5,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalDBClientPutKey(t *testing.T) {
|
func TestLocalDBClientPutKey(t *testing.T) {
|
||||||
|
|
|
@ -6,11 +6,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Driver is a driver wrapping a supported database
|
// Driver is a driver wrapping a supported database
|
||||||
|
@ -49,13 +46,12 @@ func getDatabaseDriverName(ctx *cli.Context) string {
|
||||||
func GetDatabaseDriver(ctx *cli.Context) (Driver, error) {
|
func GetDatabaseDriver(ctx *cli.Context) (Driver, error) {
|
||||||
name := getDatabaseDriverName(ctx)
|
name := getDatabaseDriverName(ctx)
|
||||||
dbDirectory := ctx.String("database-dir")
|
dbDirectory := ctx.String("database-dir")
|
||||||
logger := ctx.Context.Value(utils.ContextLogger).(*zap.Logger)
|
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
case "badger":
|
case "badger":
|
||||||
return nil, cli.Exit("Badger is not supported anymore as a database driver", 64)
|
return nil, cli.Exit("Badger is not supported anymore as a database driver", 64)
|
||||||
case "pebble":
|
case "pebble":
|
||||||
db, err := NewPebble(dbDirectory, logger)
|
db, err := NewPebble(dbDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cli.Exit(err.Error(), 64)
|
return nil, cli.Exit(err.Error(), 64)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
pebbledriver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
|
||||||
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
|
|
||||||
pebble_driver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
|
|
||||||
"github.com/cockroachdb/pebble"
|
"github.com/cockroachdb/pebble"
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PebbleDatabase struct {
|
type PebbleDatabase struct {
|
||||||
db *pebble.DB
|
db *pebble.DB
|
||||||
hub *kv.Hub
|
hub *kv.Hub
|
||||||
logger *zap.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPebble creates a new database driver instance with an underlying Pebble database
|
// NewPebble creates a new database driver instance with an underlying Pebble database
|
||||||
func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
|
func NewPebble(directory string) (*PebbleDatabase, error) {
|
||||||
db, err := pebble.Open(directory, &pebble.Options{})
|
db, err := pebble.Open(directory, &pebble.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not open DB: %w", err)
|
return nil, fmt.Errorf("could not open DB: %w", err)
|
||||||
|
@ -34,9 +34,8 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &PebbleDatabase{
|
p := &PebbleDatabase{
|
||||||
db: db,
|
db: db,
|
||||||
hub: nil,
|
hub: nil,
|
||||||
logger: logger,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
|
@ -44,7 +43,9 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
|
||||||
|
|
||||||
func (p *PebbleDatabase) Hub() *kv.Hub {
|
func (p *PebbleDatabase) Hub() *kv.Hub {
|
||||||
if p.hub == nil {
|
if p.hub == nil {
|
||||||
p.hub, _ = kv.NewHub(pebble_driver.NewPebbleBackend(p.db, true), kv.HubOptions{}, p.logger)
|
p.hub, _ = kv.NewHub(pebbledriver.NewPebbleBackend(p.db, true), kv.HubOptions{
|
||||||
|
Logger: p.logger,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return p.hub
|
return p.hub
|
||||||
}
|
}
|
||||||
|
@ -95,13 +96,13 @@ func (p *PebbleDatabase) Restore(file io.Reader) error {
|
||||||
|
|
||||||
func (p *PebbleDatabase) Backup(file io.Writer) error {
|
func (p *PebbleDatabase) Backup(file io.Writer) error {
|
||||||
snapshot := p.db.NewSnapshot()
|
snapshot := p.db.NewSnapshot()
|
||||||
defer utils.Close(snapshot, p.logger)
|
defer utils.Close(snapshot)
|
||||||
|
|
||||||
iter, err := snapshot.NewIter(&pebble.IterOptions{})
|
iter, err := snapshot.NewIter(&pebble.IterOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer utils.Close(iter, p.logger)
|
defer utils.Close(iter)
|
||||||
|
|
||||||
out := make(map[string]string)
|
out := make(map[string]string)
|
||||||
for iter.First(); iter.Valid(); iter.Next() {
|
for iter.First(); iter.Valid(); iter.Next() {
|
||||||
|
|
|
@ -3,23 +3,20 @@ package database
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) {
|
func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
|
|
||||||
// Create in-memory store and hub
|
// Create in-memory store and hub
|
||||||
inMemoryStore := kv.MakeBackend()
|
inMemoryStore := kv.MakeBackend()
|
||||||
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{}, logger)
|
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
go hub.Run()
|
go hub.Run()
|
||||||
|
|
||||||
// Create local client
|
// Create local client
|
||||||
client, err := NewLocalClient(hub, logger)
|
client, err := NewLocalClient(hub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,9 +453,9 @@
|
||||||
"dialog-title": "Application logs",
|
"dialog-title": "Application logs",
|
||||||
"levelFilter": "Filter per log severity",
|
"levelFilter": "Filter per log severity",
|
||||||
"level": {
|
"level": {
|
||||||
"info": "Info",
|
"INFO": "Info",
|
||||||
"warn": "Warning",
|
"WARN": "Warning",
|
||||||
"error": "Error"
|
"ERROR": "Error"
|
||||||
},
|
},
|
||||||
"copy-to-clipboard": "Copy to clipboard",
|
"copy-to-clipboard": "Copy to clipboard",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
|
|
|
@ -36,9 +36,9 @@
|
||||||
"copy-to-clipboard": "Copia negli appunti",
|
"copy-to-clipboard": "Copia negli appunti",
|
||||||
"levelFilter": "Filtra per livello",
|
"levelFilter": "Filtra per livello",
|
||||||
"level": {
|
"level": {
|
||||||
"error": "Errore",
|
"ERROR": "Errore",
|
||||||
"info": "Info",
|
"INFO": "Info",
|
||||||
"warn": "Avvertimento"
|
"WARN": "Avvertimento"
|
||||||
},
|
},
|
||||||
"toggle-details": "Mostra dettagli"
|
"toggle-details": "Mostra dettagli"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,23 +3,23 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { main } from '@wailsapp/go/models';
|
import { main } from '@wailsapp/go/models';
|
||||||
|
|
||||||
export interface ProcessedLogEntry {
|
export interface ProcessedLogEntry {
|
||||||
|
id: string;
|
||||||
time: Date;
|
time: Date;
|
||||||
caller: string;
|
|
||||||
level: string;
|
level: string;
|
||||||
message: string;
|
message: string;
|
||||||
data: object;
|
data: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processEntry({
|
export function processEntry({
|
||||||
|
id,
|
||||||
time,
|
time,
|
||||||
caller,
|
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
data,
|
data,
|
||||||
}: main.LogEntry): ProcessedLogEntry {
|
}: main.LogEntry): ProcessedLogEntry {
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
time: new Date(time),
|
time: new Date(time),
|
||||||
caller,
|
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
data: JSON.parse(data) as object,
|
data: JSON.parse(data) as object,
|
||||||
|
@ -34,12 +34,21 @@ const initialState: LoggingState = {
|
||||||
messages: [],
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const keyfn = (ev: main.LogEntry) => ev.id;
|
||||||
|
|
||||||
const loggingReducer = createSlice({
|
const loggingReducer = createSlice({
|
||||||
name: 'logging',
|
name: 'logging',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
|
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
|
||||||
state.messages = payload
|
const logKeys = payload.map(keyfn);
|
||||||
|
|
||||||
|
// Clean up duplicates before setting to state
|
||||||
|
const uniqueLogs = payload.filter(
|
||||||
|
(ev, pos) => logKeys.indexOf(keyfn(ev)) === pos,
|
||||||
|
);
|
||||||
|
|
||||||
|
state.messages = uniqueLogs
|
||||||
.map(processEntry)
|
.map(processEntry)
|
||||||
.sort((a, b) => a.time.getTime() - b.time.getTime());
|
.sort((a, b) => a.time.getTime() - b.time.getTime());
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,12 +46,12 @@ const LogBubble = styled('div', {
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
backgroundColor: '$yellow6',
|
backgroundColor: '$yellow6',
|
||||||
color: '$yellow11',
|
color: '$yellow11',
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
backgroundColor: '$red6',
|
backgroundColor: '$red6',
|
||||||
color: '$red11',
|
color: '$red11',
|
||||||
},
|
},
|
||||||
|
@ -60,12 +60,12 @@ const LogBubble = styled('div', {
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyFilter = {
|
const emptyFilter = {
|
||||||
info: false,
|
INFO: false,
|
||||||
warn: false,
|
WARN: false,
|
||||||
error: false,
|
ERROR: false,
|
||||||
};
|
};
|
||||||
type LogLevel = keyof typeof emptyFilter;
|
type LogLevel = keyof typeof emptyFilter;
|
||||||
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
const levels: LogLevel[] = ['INFO', 'WARN', 'ERROR'];
|
||||||
|
|
||||||
function isSupportedLevel(level: string): level is LogLevel {
|
function isSupportedLevel(level: string): level is LogLevel {
|
||||||
return (levels as string[]).includes(level);
|
return (levels as string[]).includes(level);
|
||||||
|
@ -89,7 +89,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {
|
INFO: {
|
||||||
backgroundColor: '$gray4',
|
backgroundColor: '$gray4',
|
||||||
[`.${lightMode} &`]: {
|
[`.${lightMode} &`]: {
|
||||||
backgroundColor: '$gray2',
|
backgroundColor: '$gray2',
|
||||||
|
@ -112,7 +112,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warn: {
|
WARN: {
|
||||||
backgroundColor: '$yellow4',
|
backgroundColor: '$yellow4',
|
||||||
[`.${lightMode} &`]: {
|
[`.${lightMode} &`]: {
|
||||||
backgroundColor: '$yellow2',
|
backgroundColor: '$yellow2',
|
||||||
|
@ -135,7 +135,7 @@ const LevelToggle = styled(MultiToggleItem, {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
backgroundColor: '$red4',
|
backgroundColor: '$red4',
|
||||||
[`.${lightMode} &`]: {
|
[`.${lightMode} &`]: {
|
||||||
backgroundColor: '$red2',
|
backgroundColor: '$red2',
|
||||||
|
@ -175,11 +175,11 @@ const LogEntryContainer = styled('div', {
|
||||||
fontSize: '0.9em',
|
fontSize: '0.9em',
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
backgroundColor: '$yellow4',
|
backgroundColor: '$yellow4',
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
backgroundColor: '$red6',
|
backgroundColor: '$red6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -199,12 +199,12 @@ const LogTime = styled('div', {
|
||||||
borderBottomLeftRadius: theme.borderRadius.form,
|
borderBottomLeftRadius: theme.borderRadius.form,
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
color: '$yellow11',
|
color: '$yellow11',
|
||||||
backgroundColor: '$yellow6',
|
backgroundColor: '$yellow6',
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
color: '$red11',
|
color: '$red11',
|
||||||
backgroundColor: '$red7',
|
backgroundColor: '$red7',
|
||||||
},
|
},
|
||||||
|
@ -230,13 +230,13 @@ const LogActions = styled('div', {
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
'& a:hover': {
|
'& a:hover': {
|
||||||
color: '$yellow11',
|
color: '$yellow11',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
'& a:hover': {
|
'& a:hover': {
|
||||||
color: '$red11',
|
color: '$red11',
|
||||||
},
|
},
|
||||||
|
@ -258,11 +258,11 @@ const LogDetails = styled('div', {
|
||||||
borderBottomLeftRadius: theme.borderRadius.form,
|
borderBottomLeftRadius: theme.borderRadius.form,
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
backgroundColor: '$yellow3',
|
backgroundColor: '$yellow3',
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
backgroundColor: '$red4',
|
backgroundColor: '$red4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -276,11 +276,11 @@ const LogDetailKey = styled('div', {
|
||||||
color: '$teal10',
|
color: '$teal10',
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
INFO: {},
|
||||||
warn: {
|
WARN: {
|
||||||
color: '$yellow11',
|
color: '$yellow11',
|
||||||
},
|
},
|
||||||
error: {
|
ERROR: {
|
||||||
color: '$red11',
|
color: '$red11',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -430,10 +430,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
||||||
>
|
>
|
||||||
<LogEntriesContainer>
|
<LogEntriesContainer>
|
||||||
{filtered.reverse().map((entry) => (
|
{filtered.reverse().map((entry) => (
|
||||||
<LogItem
|
<LogItem key={entry.id} data={entry} />
|
||||||
key={entry.caller + entry.time.getTime().toString()}
|
|
||||||
data={entry}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</LogEntriesContainer>
|
</LogEntriesContainer>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
|
|
|
@ -469,7 +469,7 @@ function TwitchSection() {
|
||||||
|
|
||||||
const onKeyChange = (value: string) => {
|
const onKeyChange = (value: string) => {
|
||||||
const event = JSON.parse(value) as EventSubNotification;
|
const event = JSON.parse(value) as EventSubNotification;
|
||||||
void setCleanTwitchEvents((prev) => [event, ...prev]);
|
void setCleanTwitchEvents([event, ...twitchEvents]);
|
||||||
};
|
};
|
||||||
|
|
||||||
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
||||||
|
|
|
@ -73,7 +73,7 @@ export namespace main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class LogEntry {
|
export class LogEntry {
|
||||||
caller: string;
|
id: string;
|
||||||
time: string;
|
time: string;
|
||||||
level: string;
|
level: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -85,7 +85,7 @@ export namespace main {
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.caller = source["caller"];
|
this.id = source["id"];
|
||||||
this.time = source["time"];
|
this.time = source["time"];
|
||||||
this.level = source["level"];
|
this.level = source["level"];
|
||||||
this.message = source["message"];
|
this.message = source["message"];
|
||||||
|
|
7
go.mod
7
go.mod
|
@ -6,7 +6,8 @@ toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~ashkeel/containers v0.3.6
|
git.sr.ht/~ashkeel/containers v0.3.6
|
||||||
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5
|
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1
|
||||||
|
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||||
github.com/cockroachdb/pebble v1.1.0
|
github.com/cockroachdb/pebble v1.1.0
|
||||||
|
@ -14,10 +15,9 @@ require (
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/nicklaw5/helix/v2 v2.28.1
|
github.com/nicklaw5/helix/v2 v2.28.1
|
||||||
github.com/strimertul/kilovolt/v11 v11.0.1
|
github.com/samber/slog-multi v1.0.2
|
||||||
github.com/urfave/cli/v2 v2.27.1
|
github.com/urfave/cli/v2 v2.27.1
|
||||||
github.com/wailsapp/wails/v2 v2.8.0
|
github.com/wailsapp/wails/v2 v2.8.0
|
||||||
go.uber.org/zap v1.27.0
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +79,6 @@ require (
|
||||||
github.com/wailsapp/go-webview2 v1.0.10 // indirect
|
github.com/wailsapp/go-webview2 v1.0.10 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
|
||||||
golang.org/x/crypto v0.18.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||||
golang.org/x/net v0.20.0 // indirect
|
golang.org/x/net v0.20.0 // indirect
|
||||||
|
|
16
go.sum
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=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
git.sr.ht/~ashkeel/containers v0.3.6 h1:+umWlQGKhLxGQlaEUt/F6rBZGpeBd1T01fM3wro+qTY=
|
git.sr.ht/~ashkeel/containers v0.3.6 h1:+umWlQGKhLxGQlaEUt/F6rBZGpeBd1T01fM3wro+qTY=
|
||||||
git.sr.ht/~ashkeel/containers v0.3.6/go.mod h1:i2KocnJfRH0FwfgPi4nw7/ehYLEoLlP3iwdDoBeVdME=
|
git.sr.ht/~ashkeel/containers v0.3.6/go.mod h1:i2KocnJfRH0FwfgPi4nw7/ehYLEoLlP3iwdDoBeVdME=
|
||||||
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5 h1:btFQjGLisyJwwLgzQ80l39kGlKyCHkVTnbGMnlRwxIk=
|
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1 h1:AQORTHRA6Ce1TW24FgHN1K5hWT8+/5nC7sJ5L2W+tRM=
|
||||||
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5/go.mod h1:MPfBoPjCurdVQzwvSg49VDkeS/B+Ja96JnZ5cykksL8=
|
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1/go.mod h1:lPYwJ9bumCDzeklkhXh0OCasiB7omzW+ykaLUZ30waE=
|
||||||
|
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0 h1:whWstBXDkj3WEEDpQu3+zTWYZNlJtmlDcUkAnkleY4g=
|
||||||
|
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0/go.mod h1:FGg4hwyY/e0G/mpLgUA/nYVIpHUTc4uFIwesM/MJVtA=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||||
|
@ -297,6 +299,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
|
github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=
|
||||||
|
github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
|
||||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
@ -313,8 +317,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/strimertul/kilovolt/v11 v11.0.1 h1:fV33Z3b168LeSROzzV0dZcI2Jptg3TvF2FbEGxfEVGI=
|
|
||||||
github.com/strimertul/kilovolt/v11 v11.0.1/go.mod h1:PjhGVWb74lB8dXSGWA7GmVSbZAoGV/WGGmjS2Zz/UBg=
|
|
||||||
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||||
|
@ -342,12 +344,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|
129
logging.go
129
logging.go
|
@ -1,53 +1,49 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
|
slogmulti "github.com/samber/slog-multi"
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const LogHistory = 50
|
const LogHistory = 50
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *zap.Logger
|
|
||||||
lastLogs *sync.Slice[LogEntry]
|
lastLogs *sync.Slice[LogEntry]
|
||||||
incomingLogs chan LogEntry
|
incomingLogs chan LogEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
func initLogger(level zapcore.Level) {
|
func initLogger(level slog.Level) {
|
||||||
lastLogs = sync.NewSlice[LogEntry]()
|
lastLogs = sync.NewSlice[LogEntry]()
|
||||||
incomingLogs = make(chan LogEntry, 100)
|
incomingLogs = make(chan LogEntry, 100)
|
||||||
logStorage := NewLogStorage(level)
|
logStorage := NewLogStorage(level)
|
||||||
consoleLogger := zapcore.NewCore(
|
fileLogger := &lumberjack.Logger{
|
||||||
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
|
Filename: logFilename,
|
||||||
zapcore.Lock(os.Stderr),
|
MaxSize: 20,
|
||||||
level,
|
MaxBackups: 3,
|
||||||
)
|
MaxAge: 28,
|
||||||
fileLogger := zapcore.NewCore(
|
}
|
||||||
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
|
consoleHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
zapcore.AddSync(&lumberjack.Logger{
|
Level: level,
|
||||||
Filename: logFilename,
|
})
|
||||||
MaxSize: 20,
|
fileHandler := slog.NewJSONHandler(fileLogger, &slog.HandlerOptions{
|
||||||
MaxBackups: 3,
|
AddSource: true,
|
||||||
MaxAge: 28,
|
Level: level,
|
||||||
}),
|
})
|
||||||
level,
|
logger := slog.New(slogmulti.Fanout(consoleHandler, fileHandler, logStorage))
|
||||||
)
|
|
||||||
core := zapcore.NewTee(
|
slog.SetDefault(logger)
|
||||||
consoleLogger,
|
|
||||||
fileLogger,
|
|
||||||
logStorage,
|
|
||||||
)
|
|
||||||
logger = zap.New(core, zap.AddCaller())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
Caller string `json:"caller"`
|
ID string `json:"id"`
|
||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
@ -55,42 +51,32 @@ type LogEntry struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStorage struct {
|
type LogStorage struct {
|
||||||
zapcore.LevelEnabler
|
minLevel slog.Level
|
||||||
fields []zapcore.Field
|
attrs []slog.Attr
|
||||||
encoder zapcore.Encoder
|
group string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogStorage(enabler zapcore.LevelEnabler) *LogStorage {
|
func (core *LogStorage) Enabled(_ context.Context, level slog.Level) bool {
|
||||||
return &LogStorage{
|
return level >= core.minLevel
|
||||||
LevelEnabler: enabler,
|
|
||||||
encoder: zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (core *LogStorage) With(fields []zapcore.Field) zapcore.Core {
|
func (core *LogStorage) Handle(_ context.Context, record slog.Record) error {
|
||||||
clone := *core
|
attributes := map[string]any{}
|
||||||
clone.fields = append(clone.fields, fields...)
|
record.Attrs(func(attrs slog.Attr) bool {
|
||||||
return &clone
|
attributes[attrs.Key] = attrs.Value.Any()
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
attrJSON, _ := json.MarshalToString(attributes)
|
||||||
|
|
||||||
func (core *LogStorage) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
// Generate unique log ID
|
||||||
if core.Enabled(entry.Level) {
|
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
|
||||||
return checked.AddCore(entry, core)
|
|
||||||
}
|
|
||||||
return checked
|
|
||||||
}
|
|
||||||
|
|
||||||
func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
|
||||||
buf, err := core.encoder.EncodeEntry(entry, append(core.fields, fields...))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logEntry := LogEntry{
|
logEntry := LogEntry{
|
||||||
Caller: entry.Caller.String(),
|
ID: id,
|
||||||
Time: entry.Time.Format(time.RFC3339),
|
Time: record.Time.Format(time.RFC3339),
|
||||||
Level: entry.Level.String(),
|
Level: record.Level.String(),
|
||||||
Message: entry.Message,
|
Message: record.Message,
|
||||||
Data: buf.String(),
|
Data: attrJSON,
|
||||||
}
|
}
|
||||||
lastLogs.Push(logEntry)
|
lastLogs.Push(logEntry)
|
||||||
if lastLogs.Size() > LogHistory {
|
if lastLogs.Size() > LogHistory {
|
||||||
|
@ -100,13 +86,32 @@ func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (core *LogStorage) Sync() error {
|
func (core *LogStorage) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
return nil
|
return &LogStorage{
|
||||||
|
minLevel: core.minLevel,
|
||||||
|
attrs: append(core.attrs, attrs...),
|
||||||
|
group: core.group,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAsFields(fields map[string]any) (result []zapcore.Field) {
|
func (core *LogStorage) WithGroup(name string) slog.Handler {
|
||||||
for k, v := range fields {
|
return &LogStorage{
|
||||||
result = append(result, zap.Any(k, v))
|
minLevel: core.minLevel,
|
||||||
|
attrs: core.attrs,
|
||||||
|
group: name,
|
||||||
}
|
}
|
||||||
return
|
}
|
||||||
|
|
||||||
|
func NewLogStorage(level slog.Level) *LogStorage {
|
||||||
|
return &LogStorage{
|
||||||
|
minLevel: level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLogFields(data map[string]any) []any {
|
||||||
|
fields := []any{}
|
||||||
|
for k, v := range data {
|
||||||
|
fields = append(fields, k, v)
|
||||||
|
}
|
||||||
|
return fields
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
@ -31,7 +30,6 @@ type Manager struct {
|
||||||
Goals *sync.Slice[Goal]
|
Goals *sync.Slice[Goal]
|
||||||
Queue *sync.Slice[Redeem]
|
Queue *sync.Slice[Redeem]
|
||||||
db database.Database
|
db database.Database
|
||||||
logger *zap.Logger
|
|
||||||
cooldowns map[string]time.Time
|
cooldowns map[string]time.Time
|
||||||
banlist map[string]bool
|
banlist map[string]bool
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -40,7 +38,7 @@ type Manager struct {
|
||||||
restartTwitchHandler chan struct{}
|
restartTwitchHandler chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
|
func NewManager(db database.Database) (*Manager, error) {
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
loyalty := &Manager{
|
loyalty := &Manager{
|
||||||
Config: sync.NewRWSync(Config{Enabled: false}),
|
Config: sync.NewRWSync(Config{Enabled: false}),
|
||||||
|
@ -48,7 +46,6 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
|
||||||
Goals: sync.NewSlice[Goal](),
|
Goals: sync.NewSlice[Goal](),
|
||||||
Queue: sync.NewSlice[Redeem](),
|
Queue: sync.NewSlice[Redeem](),
|
||||||
|
|
||||||
logger: logger,
|
|
||||||
db: db,
|
db: db,
|
||||||
points: sync.NewMap[string, PointsEntry](),
|
points: sync.NewMap[string, PointsEntry](),
|
||||||
cooldowns: make(map[string]time.Time),
|
cooldowns: make(map[string]time.Time),
|
||||||
|
@ -117,7 +114,7 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
|
||||||
// SubscribePrefix for changes
|
// SubscribePrefix for changes
|
||||||
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not setup loyalty reload subscription", zap.Error(err))
|
slog.Error("Could not setup loyalty reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loyalty.SetBanList(config.BanList)
|
loyalty.SetBanList(config.BanList)
|
||||||
|
@ -177,9 +174,9 @@ func (m *Manager) update(key, value string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Error("Subscribe error: invalid JSON received on key", zap.Error(err), zap.String("key", key))
|
slog.Error("Subscribe error: invalid JSON received on key", slog.String("key", key), "error", err)
|
||||||
} else {
|
} else {
|
||||||
m.logger.Debug("Updated key", zap.String("key", key))
|
slog.Debug("Updated key", slog.String("key", key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
main.go
45
main.go
|
@ -5,9 +5,12 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
"github.com/apenwarr/fixconsole"
|
"github.com/apenwarr/fixconsole"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -15,15 +18,13 @@ import (
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
|
||||||
var appVersion = "v0.0.0-UNKNOWN"
|
const devVersionMarker = "v0.0.0-UNKNOWN"
|
||||||
|
|
||||||
|
var appVersion = devVersionMarker
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crashReportURL = "https://crash.strimertul.stream/upload"
|
crashReportURL = "https://crash.strimertul.stream/upload"
|
||||||
|
@ -35,8 +36,7 @@ const (
|
||||||
var frontend embed.FS
|
var frontend embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := fixconsole.FixConsoleIfNeeded()
|
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,32 +86,34 @@ func main() {
|
||||||
},
|
},
|
||||||
Before: func(ctx *cli.Context) error {
|
Before: func(ctx *cli.Context) error {
|
||||||
// Initialize logger with global flags
|
// Initialize logger with global flags
|
||||||
level, err := zapcore.ParseLevel(ctx.String("log-level"))
|
level := slog.LevelInfo
|
||||||
if err != nil {
|
|
||||||
level = zapcore.InfoLevel
|
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
|
||||||
|
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
|
||||||
}
|
}
|
||||||
initLogger(level)
|
initLogger(level)
|
||||||
|
|
||||||
// Create file for panics
|
// Create file for panics
|
||||||
|
var err error
|
||||||
panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
|
panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not create panic log", zap.Error(err))
|
slog.Warn("Could not create panic log", "error", err)
|
||||||
} else {
|
} else {
|
||||||
utils.RedirectStderr(panicLog)
|
utils.RedirectStderr(panicLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.RedirectStdLog(logger)()
|
// For development builds, force crash dumps
|
||||||
|
if isDev() {
|
||||||
|
debug.SetTraceback("crash")
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Context = context.WithValue(ctx.Context, utils.ContextLogger, logger)
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
After: func(_ *cli.Context) error {
|
After: func(_ *cli.Context) error {
|
||||||
if panicLog != nil {
|
if panicLog != nil {
|
||||||
utils.Close(panicLog, logger)
|
utils.Close(panicLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = logger.Sync()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -164,13 +166,18 @@ func cliMain(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func warnOnError(err error, text string, fields ...zap.Field) {
|
func warnOnError(err error, text string, fields ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fields = append(fields, zap.Error(err))
|
fields = append(fields, "error", err)
|
||||||
logger.Warn(text, fields...)
|
slog.Warn(text, fields...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fatalError(err error, text string) error {
|
func fatalError(err error, text string) error {
|
||||||
return cli.Exit(fmt.Errorf("%s: %w", text, err), 1)
|
return cli.Exit(fmt.Errorf("%s: %w", text, err), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDev checks if the running code is a development version
|
||||||
|
func isDev() bool {
|
||||||
|
return appVersion == devVersionMarker
|
||||||
|
}
|
||||||
|
|
|
@ -4,10 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,18 +32,18 @@ func GetCurrentSchemaVersion(db database.Database) (int, error) {
|
||||||
return strconv.Atoi(versionStr)
|
return strconv.Atoi(versionStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(driver database.Driver, db database.Database, logger *zap.Logger) (err error) {
|
func Run(driver database.Driver, db database.Database, logger *slog.Logger) (err error) {
|
||||||
// Make a backup of the database
|
// Make a backup of the database
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if err = driver.Backup(&buffer); err != nil {
|
if err = driver.Backup(&buffer); err != nil {
|
||||||
return fmt.Errorf("failed to backup database: %s", err)
|
return fmt.Errorf("failed to backup database: %w", err)
|
||||||
}
|
}
|
||||||
// Restore the backup if an error occurs
|
// Restore the backup if an error occurs
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
|
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
|
||||||
if restoreErr != nil {
|
if restoreErr != nil {
|
||||||
logger.Error("Failed to restore database from backup", zap.Error(restoreErr))
|
logger.Error("Failed to restore database from backup", "error", restoreErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -2,6 +2,7 @@ package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||||
|
|
||||||
|
@ -10,10 +11,9 @@ import (
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
|
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrateToV4(db database.Database, logger *zap.Logger) error {
|
func migrateToV4(db database.Database, logger *slog.Logger) error {
|
||||||
logger.Info("Migrating database from v3 to v4")
|
logger.Info("Migrating database from v3 to v4")
|
||||||
|
|
||||||
// Rename keys that have no schema changes
|
// Rename keys that have no schema changes
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProblemID string
|
type ProblemID string
|
||||||
|
@ -26,7 +27,7 @@ func (a *App) GetProblems() (problems []Problem) {
|
||||||
// Check if the app needs to be authorized again
|
// Check if the app needs to be authorized again
|
||||||
scopesMatch, err := twitch.CheckScopes(client.DB)
|
scopesMatch, err := twitch.CheckScopes(client.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not check scopes for problems", zap.Error(err))
|
slog.Warn("Could not check scopes for problems", "error", err)
|
||||||
} else {
|
} else {
|
||||||
if !scopesMatch {
|
if !scopesMatch {
|
||||||
problems = append(problems, Problem{
|
problems = append(problems, Problem{
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
package alerts
|
package alerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) onEventSubEvent(_ string, value string) {
|
func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var ev eventSubNotification
|
var ev eventSubNotification
|
||||||
if err := json.UnmarshalFromString(value, &ev); err != nil {
|
if err := json.Unmarshal([]byte(value), &ev); err != nil {
|
||||||
m.logger.Warn("Error parsing webhook payload", zap.Error(err))
|
m.logger.Warn("Error parsing webhook payload", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch ev.Subscription.Type {
|
switch ev.Subscription.Type {
|
||||||
|
@ -25,7 +24,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as a follow event
|
// Parse as a follow event
|
||||||
var followEv helix.EventSubChannelFollowEvent
|
var followEv helix.EventSubChannelFollowEvent
|
||||||
if err := json.Unmarshal(ev.Event, &followEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &followEv); err != nil {
|
||||||
m.logger.Warn("Error parsing follow event", zap.Error(err))
|
m.logger.Warn("Error parsing follow event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message
|
// Pick a random message
|
||||||
|
@ -50,7 +49,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var raidEv helix.EventSubChannelRaidEvent
|
var raidEv helix.EventSubChannelRaidEvent
|
||||||
|
|
||||||
if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
|
||||||
m.logger.Warn("Error parsing raid event", zap.Error(err))
|
m.logger.Warn("Error parsing raid event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
@ -84,7 +83,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as cheer event
|
// Parse as cheer event
|
||||||
var cheerEv helix.EventSubChannelCheerEvent
|
var cheerEv helix.EventSubChannelCheerEvent
|
||||||
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
|
||||||
m.logger.Warn("Error parsing cheer event", zap.Error(err))
|
m.logger.Warn("Error parsing cheer event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
@ -118,7 +117,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as subscription event
|
// Parse as subscription event
|
||||||
var subEv helix.EventSubChannelSubscribeEvent
|
var subEv helix.EventSubChannelSubscribeEvent
|
||||||
if err := json.Unmarshal(ev.Event, &subEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &subEv); err != nil {
|
||||||
m.logger.Warn("Error parsing new subscription event", zap.Error(err))
|
m.logger.Warn("Error parsing new subscription event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.addMixedEvent(subEv)
|
m.addMixedEvent(subEv)
|
||||||
|
@ -131,7 +130,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var subEv helix.EventSubChannelSubscriptionMessageEvent
|
var subEv helix.EventSubChannelSubscriptionMessageEvent
|
||||||
err := json.Unmarshal(ev.Event, &subEv)
|
err := json.Unmarshal(ev.Event, &subEv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Error parsing returning subscription event", zap.Error(err))
|
m.logger.Warn("Error parsing returning subscription event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.addMixedEvent(subEv)
|
m.addMixedEvent(subEv)
|
||||||
|
@ -143,7 +142,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as gift event
|
// Parse as gift event
|
||||||
var giftEv helix.EventSubChannelSubscriptionGiftEvent
|
var giftEv helix.EventSubChannelSubscriptionGiftEvent
|
||||||
if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
|
||||||
m.logger.Warn("Error parsing subscription gifted event", zap.Error(err))
|
m.logger.Warn("Error parsing subscription gifted event", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
|
|
@ -2,19 +2,16 @@ package alerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
templateCache map[string]*template.Template
|
templateCache map[string]*template.Template
|
||||||
templateCacheMap map[templateType]templateCache
|
templateCacheMap map[templateType]templateCache
|
||||||
|
@ -35,7 +32,7 @@ type Module struct {
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db database.Database
|
db database.Database
|
||||||
logger *zap.Logger
|
logger *slog.Logger
|
||||||
templater template2.Engine
|
templater template2.Engine
|
||||||
templates templateCacheMap
|
templates templateCacheMap
|
||||||
|
|
||||||
|
@ -43,7 +40,7 @@ type Module struct {
|
||||||
pendingSubs map[string]subMixedEvent
|
pendingSubs map[string]subMixedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templater template2.Engine) *Module {
|
func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templater template2.Engine) *Module {
|
||||||
mod := &Module{
|
mod := &Module{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -55,31 +52,31 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templa
|
||||||
|
|
||||||
// Load config from database
|
// Load config from database
|
||||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||||
logger.Debug("Config load error", zap.Error(err))
|
logger.Debug("Config load error", "error", err)
|
||||||
mod.Config = Config{}
|
mod.Config = Config{}
|
||||||
// Save empty config
|
// Save empty config
|
||||||
err = db.PutJSON(ConfigKey, mod.Config)
|
err = db.PutJSON(ConfigKey, mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not save default config for bot alerts", zap.Error(err))
|
logger.Warn("Could not save default config for bot alerts", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.compileTemplates()
|
mod.compileTemplates()
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||||
err := json.UnmarshalFromString(value, &mod.Config)
|
err := json.Unmarshal([]byte(value), &mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Error loading alert config", zap.Error(err))
|
logger.Warn("Error loading alert config", "error", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Info("Reloaded alert config")
|
logger.Info("Reloaded alert config")
|
||||||
}
|
}
|
||||||
mod.compileTemplates()
|
mod.compileTemplates()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error("Could not set-up bot alert reload subscription", zap.Error(err))
|
logger.Error("Could not set-up bot alert reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
|
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
|
||||||
logger.Error("Could not setup twitch alert subscription", zap.Error(err))
|
logger.Error("Could not setup twitch alert subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Loaded bot alerts")
|
logger.Debug("Loaded bot alerts")
|
||||||
|
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ func (m *Module) compileTemplates() {
|
||||||
func (m *Module) addTemplate(templateList templateCache, message string) {
|
func (m *Module) addTemplate(templateList templateCache, message string) {
|
||||||
tpl, err := m.templater.MakeTemplate(message)
|
tpl, err := m.templater.MakeTemplate(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Error("Error compiling alert template", zap.Error(err))
|
m.logger.Error("Error compiling alert template", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
templateList[message] = tpl
|
templateList[message] = tpl
|
||||||
|
@ -60,7 +58,7 @@ func (m *Module) addTemplatesForType(templateList templateType, messages []strin
|
||||||
func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) {
|
func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tpl.Execute(&buf, data); err != nil {
|
if err := tpl.Execute(&buf, data); err != nil {
|
||||||
m.logger.Error("Error executing template for bot alert", zap.Error(err))
|
m.logger.Error("Error executing template for bot alert", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
||||||
|
|
|
@ -2,9 +2,9 @@ package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var accessLevels = map[AccessLevelType]int{
|
var accessLevels = map[AccessLevelType]int{
|
||||||
|
@ -70,7 +70,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := tpl.Execute(&buf, message); err != nil {
|
if err := tpl.Execute(&buf, message); err != nil {
|
||||||
mod.logger.Error("Failed to execute custom command template", zap.Error(err))
|
mod.logger.Error("Failed to execute custom command template", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
|
||||||
Announce: true,
|
Announce: true,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
mod.logger.Error("Unknown response type", zap.String("type", string(data.ResponseType)))
|
mod.logger.Error("Unknown response type", slog.String("type", string(data.ResponseType)))
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteMessage(mod.db, mod.logger, request)
|
WriteMessage(mod.db, mod.logger, request)
|
||||||
|
|
|
@ -12,10 +12,8 @@ import (
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -91,7 +89,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
||||||
var streamInfos []helix.Stream
|
var streamInfos []helix.Stream
|
||||||
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Error retrieving stream info", zap.Error(err))
|
mod.logger.Error("Error retrieving stream info", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(streamInfos) < 1 {
|
if len(streamInfos) < 1 {
|
||||||
|
@ -104,7 +102,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
||||||
for {
|
for {
|
||||||
userClient, err := twitch.GetUserClient(mod.db, false)
|
userClient, err := twitch.GetUserClient(mod.db, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Could not get user api client for list of chatters", zap.Error(err))
|
mod.logger.Error("Could not get user api client for list of chatters", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
||||||
|
@ -114,7 +112,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
||||||
After: cursor,
|
After: cursor,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Could not retrieve list of chatters", zap.Error(err))
|
mod.logger.Error("Could not retrieve list of chatters", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, user := range res.Data.Chatters {
|
for _, user := range res.Data.Chatters {
|
||||||
|
@ -150,7 +148,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
|
||||||
if len(users) > 0 {
|
if len(users) > 0 {
|
||||||
err := li.manager.GivePoints(pointsToGive)
|
err := li.manager.GivePoints(pointsToGive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Error awarding loyalty points to user", zap.Error(err))
|
mod.logger.Error("Error awarding loyalty points to user", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,7 +242,7 @@ func (li *loyaltyIntegration) cmdRedeemReward(message helix.EventSubChannelChatM
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
li.module.logger.Error("Error while performing redeem", zap.Error(err))
|
li.module.logger.Error("Error while performing redeem", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,7 +256,7 @@ func (li *loyaltyIntegration) cmdGoalList(message helix.EventSubChannelChatMessa
|
||||||
goals := li.manager.Goals.Get()
|
goals := li.manager.Goals.Get()
|
||||||
if len(goals) < 1 {
|
if len(goals) < 1 {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("There are no active community goals right now :(!"),
|
Message: "There are no active community goals right now :(!",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -300,12 +298,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
||||||
if goalIndex < 0 {
|
if goalIndex < 0 {
|
||||||
if hasGoals {
|
if hasGoals {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("All active community goals have been reached already! NewRecord"),
|
Message: "All active community goals have been reached already! NewRecord",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("There are no active community goals right now :(!"),
|
Message: "There are no active community goals right now :(!",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -319,7 +317,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if newPoints <= 0 {
|
if newPoints <= 0 {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("Nice try SoBayed"),
|
Message: "Nice try SoBayed",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -343,7 +341,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
||||||
// Invalid goal ID provided
|
// Invalid goal ID provided
|
||||||
if !found {
|
if !found {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("I couldn't find that goal ID :("),
|
Message: "I couldn't find that goal ID :(",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -357,7 +355,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
||||||
// Check if goal was reached already
|
// Check if goal was reached already
|
||||||
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
|
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("This goal was already reached! ヾ(•ω•`)o"),
|
Message: "This goal was already reached! ヾ(•ω•`)o",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -366,12 +364,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
|
||||||
// Add points to goal
|
// Add points to goal
|
||||||
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
li.module.logger.Error("Error while contributing to goal", zap.Error(err))
|
li.module.logger.Error("Error while contributing to goal", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if points == 0 {
|
if points == 0 {
|
||||||
li.module.WriteMessage(WriteMessageRequest{
|
li.module.WriteMessage(WriteMessageRequest{
|
||||||
Message: fmt.Sprintf("Sorry but you're broke"),
|
Message: "Sorry but you're broke",
|
||||||
ReplyTo: message.MessageID,
|
ReplyTo: message.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
|
@ -3,19 +3,18 @@ package chat
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
textTemplate "text/template"
|
textTemplate "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
@ -27,7 +26,7 @@ type Module struct {
|
||||||
db database.Database
|
db database.Database
|
||||||
api *helix.Client
|
api *helix.Client
|
||||||
user helix.User
|
user helix.User
|
||||||
logger *zap.Logger
|
logger *slog.Logger
|
||||||
templater template.Engine
|
templater template.Engine
|
||||||
lastMessage *sync.RWSync[time.Time]
|
lastMessage *sync.RWSync[time.Time]
|
||||||
|
|
||||||
|
@ -35,11 +34,9 @@ type Module struct {
|
||||||
customCommands *sync.Map[string, CustomCommand]
|
customCommands *sync.Map[string, CustomCommand]
|
||||||
customTemplates *sync.Map[string, *textTemplate.Template]
|
customTemplates *sync.Map[string, *textTemplate.Template]
|
||||||
customFunctions textTemplate.FuncMap
|
customFunctions textTemplate.FuncMap
|
||||||
|
|
||||||
cancelContext context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *zap.Logger, templater template.Engine) *Module {
|
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
|
||||||
mod := &Module{
|
mod := &Module{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -62,12 +59,12 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
|
||||||
CommandCooldown: 2,
|
CommandCooldown: 2,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Error("Failed to load chat module config", zap.Error(err))
|
logger.Error("Failed to load chat module config", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
|
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
|
||||||
logger.Error("Could not subscribe to chat messages", zap.Error(err))
|
logger.Error("Could not subscribe to chat messages", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load custom commands
|
// Load custom commands
|
||||||
|
@ -76,21 +73,21 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
|
||||||
if errors.Is(err, database.ErrEmptyKey) {
|
if errors.Is(err, database.ErrEmptyKey) {
|
||||||
customCommands = make(map[string]CustomCommand)
|
customCommands = make(map[string]CustomCommand)
|
||||||
} else {
|
} else {
|
||||||
logger.Error("Failed to load custom commands", zap.Error(err))
|
logger.Error("Failed to load custom commands", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod.customCommands.Set(customCommands)
|
mod.customCommands.Set(customCommands)
|
||||||
|
|
||||||
if err := mod.updateTemplates(); err != nil {
|
if err := mod.updateTemplates(); err != nil {
|
||||||
logger.Error("Failed to parse custom commands", zap.Error(err))
|
logger.Error("Failed to parse custom commands", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
||||||
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
logger.Error("Could not set-up chat command reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
|
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
|
||||||
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
logger.Error("Could not set-up chat command reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod
|
return mod
|
||||||
|
@ -101,7 +98,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
||||||
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
||||||
}
|
}
|
||||||
if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil {
|
if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil {
|
||||||
mod.logger.Error("Failed to decode incoming chat message", zap.Error(err))
|
mod.logger.Error("Failed to decode incoming chat message", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +146,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
||||||
func (mod *Module) handleWriteMessageRPC(value string) {
|
func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
var request WriteMessageRequest
|
var request WriteMessageRequest
|
||||||
if err := json.Unmarshal([]byte(value), &request); err != nil {
|
if err := json.Unmarshal([]byte(value), &request); err != nil {
|
||||||
mod.logger.Warn("Failed to decode write message request", zap.Error(err))
|
mod.logger.Warn("Failed to decode write message request", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,10 +157,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
Message: request.Message,
|
Message: request.Message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to send announcement", zap.Error(err))
|
mod.logger.Error("Failed to send announcement", "error", err)
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send announcement", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -175,10 +172,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
Message: request.Message,
|
Message: request.Message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to send whisper", zap.Error(err))
|
mod.logger.Error("Failed to send whisper", "error", err)
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send whisper", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -190,22 +187,22 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
ReplyParentMessageID: request.ReplyTo,
|
ReplyParentMessageID: request.ReplyTo,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to send chat message", zap.Error(err))
|
mod.logger.Error("Failed to send chat message", "error", err)
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send chat message", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mod *Module) updateCommands(value string) {
|
func (mod *Module) updateCommands(value string) {
|
||||||
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to decode new custom commands", zap.Error(err))
|
mod.logger.Error("Failed to decode new custom commands", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Recreate templates
|
// Recreate templates
|
||||||
if err := mod.updateTemplates(); err != nil {
|
if err := mod.updateTemplates(); err != nil {
|
||||||
mod.logger.Error("Failed to update custom commands templates", zap.Error(err))
|
mod.logger.Error("Failed to update custom commands templates", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteMessageRequest is an RPC to send a chat message with extra options
|
// WriteMessageRequest is an RPC to send a chat message with extra options
|
||||||
|
@ -13,9 +14,9 @@ type WriteMessageRequest struct {
|
||||||
Announce bool `json:"announce" desc:"If true, send as announcement"`
|
Announce bool `json:"announce" desc:"If true, send as announcement"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteMessage(db database.Database, logger *zap.Logger, m WriteMessageRequest) {
|
func WriteMessage(db database.Database, logger *slog.Logger, m WriteMessageRequest) {
|
||||||
err := db.PutJSON(WriteMessageRPC, m)
|
err := db.PutJSON(WriteMessageRPC, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to write chat message", zap.Error(err))
|
logger.Error("Failed to write chat message", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||||
|
@ -20,13 +20,11 @@ import (
|
||||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
client *Client
|
client *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
|
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer) (*Manager, error) {
|
||||||
// Get Twitch config
|
// Get Twitch config
|
||||||
var config twitch.Config
|
var config twitch.Config
|
||||||
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
||||||
|
@ -38,7 +36,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
|
|
||||||
// Create new client
|
// Create new client
|
||||||
clientContext, cancel := context.WithCancel(ctx)
|
clientContext, cancel := context.WithCancel(ctx)
|
||||||
client, err := newClient(clientContext, config, db, server, logger)
|
client, err := newClient(clientContext, config, db, server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
||||||
|
@ -51,8 +49,8 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
// Listen for client config changes
|
// Listen for client config changes
|
||||||
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
||||||
var newConfig twitch.Config
|
var newConfig twitch.Config
|
||||||
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
|
if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
|
||||||
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
|
slog.Error("Failed to decode Twitch integration config", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +58,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
|
|
||||||
var updatedClient *Client
|
var updatedClient *Client
|
||||||
clientContext, cancel = context.WithCancel(ctx)
|
clientContext, cancel = context.WithCancel(ctx)
|
||||||
updatedClient, err = newClient(clientContext, newConfig, db, server, logger)
|
updatedClient, err = newClient(clientContext, newConfig, db, server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err))
|
slog.Error("Could not create twitch client with new config, keeping old", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,9 +68,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
updatedClient.Merge(manager.client)
|
updatedClient.Merge(manager.client)
|
||||||
manager.client = updatedClient
|
manager.client = updatedClient
|
||||||
|
|
||||||
logger.Info("Reloaded/updated Twitch integration")
|
slog.Info("Reloaded/updated Twitch integration")
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error("Could not setup twitch config reload subscription", zap.Error(err))
|
slog.Error("Could not setup twitch config reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager, nil
|
return manager, nil
|
||||||
|
@ -87,7 +85,7 @@ type Client struct {
|
||||||
DB database.Database
|
DB database.Database
|
||||||
API *helix.Client
|
API *helix.Client
|
||||||
User helix.User
|
User helix.User
|
||||||
Logger *zap.Logger
|
Logger *slog.Logger
|
||||||
|
|
||||||
Chat *chat.Module
|
Chat *chat.Module
|
||||||
Alerts *alerts.Module
|
Alerts *alerts.Module
|
||||||
|
@ -115,12 +113,12 @@ func (c *Client) ensureRoute() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
|
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer) (*Client, error) {
|
||||||
// Create Twitch client
|
// Create Twitch client
|
||||||
client := &Client{
|
client := &Client{
|
||||||
Config: sync.NewRWSync(config),
|
Config: sync.NewRWSync(config),
|
||||||
DB: db,
|
DB: db,
|
||||||
Logger: logger.With(zap.String("service", "twitch")),
|
Logger: slog.With(slog.String("service", "twitch")),
|
||||||
restart: make(chan bool, 128),
|
restart: make(chan bool, 128),
|
||||||
streamOnline: sync.NewRWSync(false),
|
streamOnline: sync.NewRWSync(false),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
@ -142,21 +140,21 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
|
||||||
if userClient, err := twitch.GetUserClient(db, true); err == nil {
|
if userClient, err := twitch.GetUserClient(db, true); err == nil {
|
||||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
users, err := userClient.GetUsers(&helix.UsersParams{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Logger.Error("Failed looking up user", zap.Error(err))
|
client.Logger.Error("Failed looking up user", "error", err)
|
||||||
} else if len(users.Data.Users) < 1 {
|
} else if len(users.Data.Users) < 1 {
|
||||||
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
||||||
} else {
|
} else {
|
||||||
client.Logger.Info("Twitch user identified", zap.String("user", users.Data.Users[0].ID))
|
client.Logger.Info("Twitch user identified", slog.String("user", users.Data.Users[0].ID))
|
||||||
client.User = users.Data.Users[0]
|
client.User = users.Data.Users[0]
|
||||||
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, logger)
|
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Logger.Error("Failed to setup EventSub", zap.Error(err))
|
client.Logger.Error("Failed to setup EventSub", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl := client.GetTemplateEngine()
|
tpl := client.GetTemplateEngine()
|
||||||
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl)
|
client.Chat = chat.Setup(ctx, db, userClient, client.User, client.Logger, tpl)
|
||||||
client.Alerts = alerts.Setup(ctx, db, logger, tpl)
|
client.Alerts = alerts.Setup(ctx, db, client.Logger, tpl)
|
||||||
client.Timers = timers.Setup(ctx, db, logger)
|
client.Timers = timers.Setup(ctx, db, client.Logger)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client.Logger.Warn("Twitch user not identified, this will break most features")
|
client.Logger.Warn("Twitch user not identified, this will break most features")
|
||||||
|
@ -186,14 +184,14 @@ func (c *Client) runStatusPoll() {
|
||||||
UserIDs: []string{c.User.ID},
|
UserIDs: []string{c.User.ID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Error("Error checking stream status", zap.Error(err))
|
c.Logger.Error("Error checking stream status", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
||||||
|
|
||||||
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Warn("Error saving stream info", zap.Error(err))
|
c.Logger.Warn("Error saving stream info", "error", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||||
|
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
server, err := webserver.NewServer(client, logger, webserver.DefaultServerFactory)
|
server, err := webserver.NewServer(client, webserver.DefaultServerFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := twitch.Config{}
|
config := twitch.Config{}
|
||||||
_, err = newClient(config, client, server, logger)
|
_, err = newClient(context.Background(), config, client, server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -11,7 +12,6 @@ import (
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ChatCounterPrefix = "twitch/chat/counters/"
|
const ChatCounterPrefix = "twitch/chat/counters/"
|
||||||
|
@ -57,7 +57,7 @@ func (c *Client) GetTemplateEngine() template.Engine {
|
||||||
counter++
|
counter++
|
||||||
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
|
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Error("Error saving key", zap.Error(err), zap.String("key", counterKey))
|
c.Logger.Error("Error saving key", "error", err, slog.String("key", counterKey))
|
||||||
}
|
}
|
||||||
return counter
|
return counter
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,16 +3,15 @@ package eventsub
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
@ -23,14 +22,14 @@ type Client struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
twitchAPI *helix.Client
|
twitchAPI *helix.Client
|
||||||
db database.Database
|
db database.Database
|
||||||
logger *zap.Logger
|
logger *slog.Logger
|
||||||
user helix.User
|
user helix.User
|
||||||
|
|
||||||
eventCache *lru.Cache[string, time.Time]
|
eventCache *lru.Cache[string, time.Time]
|
||||||
savedSubscriptions map[string]bool
|
savedSubscriptions map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *zap.Logger) (*Client, error) {
|
func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *slog.Logger) (*Client, error) {
|
||||||
eventCache, err := lru.New[string, time.Time](128)
|
eventCache, err := lru.New[string, time.Time](128)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
|
return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
|
||||||
|
@ -58,11 +57,11 @@ func (c *Client) eventSubLoop() {
|
||||||
for endpoint != "" {
|
for endpoint != "" {
|
||||||
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("EventSub websocket read error", zap.Error(err))
|
c.logger.Error("EventSub websocket read error", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if connection != nil {
|
if connection != nil {
|
||||||
utils.Close(connection, c.logger)
|
utils.Close(connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ func readLoop(connection *websocket.Conn, recv chan<- []byte, wsErr chan<- error
|
||||||
func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) {
|
func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) {
|
||||||
connection, _, err := websocket.DefaultDialer.Dial(url, nil)
|
connection, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Could not establish a connection to the EventSub websocket", zap.Error(err))
|
c.logger.Error("Could not establish a connection to the EventSub websocket", "error", err)
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (st
|
||||||
var wsMessage WebsocketMessage
|
var wsMessage WebsocketMessage
|
||||||
err = json.Unmarshal(messageData, &wsMessage)
|
err = json.Unmarshal(messageData, &wsMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub message", zap.Error(err))
|
c.logger.Error("Error decoding EventSub message", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,30 +127,30 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
|
||||||
var welcomeData WelcomeMessagePayload
|
var welcomeData WelcomeMessagePayload
|
||||||
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
|
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub welcome message", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
c.logger.Error("Error decoding EventSub welcome message", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("Connection to EventSub websocket established", zap.String("session-id", welcomeData.Session.ID))
|
c.logger.Info("Connection to EventSub websocket established", slog.String("session-id", welcomeData.Session.ID))
|
||||||
|
|
||||||
// We can only close the old connection once the new one has been established
|
// We can only close the old connection once the new one has been established
|
||||||
if oldConnection != nil {
|
if oldConnection != nil {
|
||||||
utils.Close(oldConnection, c.logger)
|
utils.Close(oldConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subscription to websocket session
|
// Add subscription to websocket session
|
||||||
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Could not add subscriptions", zap.Error(err))
|
c.logger.Error("Could not add subscriptions", "error", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session_reconnect":
|
case "session_reconnect":
|
||||||
var reconnectData WelcomeMessagePayload
|
var reconnectData WelcomeMessagePayload
|
||||||
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
|
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub session reconnect parameters", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
c.logger.Error("Error decoding EventSub session reconnect parameters", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("EventSub websocket requested a reconnection", zap.String("session-id", reconnectData.Session.ID), zap.String("reconnect-url", reconnectData.Session.ReconnectURL))
|
c.logger.Info("EventSub websocket requested a reconnection", slog.String("session-id", reconnectData.Session.ID), slog.String("reconnect-url", reconnectData.Session.ReconnectURL))
|
||||||
|
|
||||||
return reconnectData.Session.ReconnectURL, true, nil
|
return reconnectData.Session.ReconnectURL, true, nil
|
||||||
case "notification":
|
case "notification":
|
||||||
|
@ -166,7 +165,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
// Check if we processed this already
|
// Check if we processed this already
|
||||||
if message.Metadata.MessageID != "" {
|
if message.Metadata.MessageID != "" {
|
||||||
if c.eventCache.Contains(message.Metadata.MessageID) {
|
if c.eventCache.Contains(message.Metadata.MessageID) {
|
||||||
c.logger.Debug("Received duplicate event, ignoring", zap.String("message-id", message.Metadata.MessageID))
|
c.logger.Debug("Received duplicate event, ignoring", slog.String("message-id", message.Metadata.MessageID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
var notificationData NotificationMessagePayload
|
var notificationData NotificationMessagePayload
|
||||||
err := json.Unmarshal(message.Payload, ¬ificationData)
|
err := json.Unmarshal(message.Payload, ¬ificationData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub notification payload", zap.String("message-type", message.Metadata.MessageType), zap.Error(err))
|
c.logger.Error("Error decoding EventSub notification payload", slog.String("message-type", message.Metadata.MessageType), "error", err)
|
||||||
}
|
}
|
||||||
notificationData.Date = time.Now()
|
notificationData.Date = time.Now()
|
||||||
|
|
||||||
|
@ -184,7 +183,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
||||||
err = c.db.PutJSON(eventKey, notificationData)
|
err = c.db.PutJSON(eventKey, notificationData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error storing event to database", zap.String("key", eventKey), zap.Error(err))
|
c.logger.Error("Error storing event to database", slog.String("key", eventKey), "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var archive []NotificationMessagePayload
|
var archive []NotificationMessagePayload
|
||||||
|
@ -198,7 +197,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
}
|
}
|
||||||
err = c.db.PutJSON(historyKey, archive)
|
err = c.db.PutJSON(historyKey, archive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error storing event to database", zap.String("key", historyKey), zap.Error(err))
|
c.logger.Error("Error storing event to database", slog.String("key", historyKey), "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +220,7 @@ func (c *Client) addSubscriptionsForSession(session string) error {
|
||||||
Condition: topicCondition(topic, c.user.ID),
|
Condition: topicCondition(topic, c.user.ID),
|
||||||
})
|
})
|
||||||
if sub.Error != "" || sub.ErrorMessage != "" {
|
if sub.Error != "" || sub.ErrorMessage != "" {
|
||||||
c.logger.Error("EventSub Subscription error", zap.String("topic", topic), zap.String("topic-version", version), zap.String("err", sub.Error), zap.String("message", sub.ErrorMessage))
|
c.logger.Error("EventSub Subscription error", slog.String("topic", topic), slog.String("topic-version", version), slog.String("err", sub.Error), slog.String("message", sub.ErrorMessage))
|
||||||
return fmt.Errorf("%s: %s", sub.Error, sub.ErrorMessage)
|
return fmt.Errorf("%s: %s", sub.Error, sub.ErrorMessage)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,15 +2,14 @@ package timers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
@ -23,12 +22,12 @@ type Module struct {
|
||||||
lastTrigger *sync.Map[string, time.Time]
|
lastTrigger *sync.Map[string, time.Time]
|
||||||
messages *sync.Slice[int]
|
messages *sync.Slice[int]
|
||||||
|
|
||||||
logger *zap.Logger
|
logger *slog.Logger
|
||||||
db database.Database
|
db database.Database
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Module {
|
func Setup(ctx context.Context, db database.Database, logger *slog.Logger) *Module {
|
||||||
mod := &Module{
|
mod := &Module{
|
||||||
lastTrigger: sync.NewMap[string, time.Time](),
|
lastTrigger: sync.NewMap[string, time.Time](),
|
||||||
messages: sync.NewSlice[int](),
|
messages: sync.NewSlice[int](),
|
||||||
|
@ -45,28 +44,28 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Modul
|
||||||
|
|
||||||
// Load config from database
|
// Load config from database
|
||||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||||
logger.Debug("Config load error", zap.Error(err))
|
logger.Debug("Config load error", "error", err)
|
||||||
mod.Config = Config{
|
mod.Config = Config{
|
||||||
Timers: make(map[string]ChatTimer),
|
Timers: make(map[string]ChatTimer),
|
||||||
}
|
}
|
||||||
// Save empty config
|
// Save empty config
|
||||||
err = db.PutJSON(ConfigKey, mod.Config)
|
err = db.PutJSON(ConfigKey, mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not save default config for bot timers", zap.Error(err))
|
logger.Warn("Could not save default config for bot timers", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||||
if err := json.UnmarshalFromString(value, &mod.Config); err != nil {
|
if err := json.UnmarshalFromString(value, &mod.Config); err != nil {
|
||||||
logger.Warn("Error reloading timer config", zap.Error(err))
|
logger.Warn("Error reloading timer config", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Info("Reloaded timer config")
|
logger.Info("Reloaded timer config")
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error("Could not set-up timer reload subscription", zap.Error(err))
|
logger.Error("Could not set-up timer reload subscription", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Loaded timers", zap.Int("timers", len(mod.Config.Timers)))
|
logger.Debug("Loaded timers", slog.Int("timers", len(mod.Config.Timers)))
|
||||||
|
|
||||||
// Start goroutine for clearing message counters and running timers
|
// Start goroutine for clearing message counters and running timers
|
||||||
go mod.runTimers()
|
go mod.runTimers()
|
||||||
|
@ -84,7 +83,7 @@ func (m *Module) runTimers() {
|
||||||
|
|
||||||
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
|
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Error saving chat activity", zap.Error(err))
|
m.logger.Warn("Error saving chat activity", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate activity
|
// Calculate activity
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
type ContextKey string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContextLogger ContextKey = "logger"
|
|
||||||
)
|
|
|
@ -2,15 +2,14 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Close(res io.Closer, logger *zap.Logger) {
|
func Close(res io.Closer) {
|
||||||
err := res.Close()
|
err := res.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not close resource", zap.String("name", reflect.TypeOf(res).String()), zap.Error(err), zap.String("stack", string(debug.Stack())))
|
slog.Error("Could not close resource", slog.String("name", reflect.TypeOf(res).String()), slog.String("stack", string(debug.Stack())), "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"encoding/json"
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
|
)
|
||||||
|
|
||||||
func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error {
|
func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error {
|
||||||
var result T
|
var result T
|
||||||
err := json.UnmarshalFromString(data, &result)
|
err := json.Unmarshal([]byte(data), &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,24 @@ import (
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
mrand "math/rand"
|
mrand "math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
jsoniter "github.com/json-iterator/go"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
kv "github.com/strimertul/kilovolt/v11"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
|
||||||
|
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
Config *sync.RWSync[ServerConfig]
|
Config *sync.RWSync[ServerConfig]
|
||||||
db database.Database
|
db database.Database
|
||||||
logger *zap.Logger
|
|
||||||
server Server
|
server Server
|
||||||
frontend fs.FS
|
frontend fs.FS
|
||||||
hub *kv.Hub
|
hub *kv.Hub
|
||||||
|
@ -36,9 +32,8 @@ type WebServer struct {
|
||||||
factory ServerFactory
|
factory ServerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFactory) (*WebServer, error) {
|
func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, error) {
|
||||||
server := &WebServer{
|
server := &WebServer{
|
||||||
logger: logger,
|
|
||||||
db: db,
|
db: db,
|
||||||
server: nil,
|
server: nil,
|
||||||
requestedRoutes: sync.NewMap[string, http.Handler](),
|
requestedRoutes: sync.NewMap[string, http.Handler](),
|
||||||
|
@ -51,7 +46,7 @@ func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFac
|
||||||
err := db.GetJSON(ServerConfigKey, &config)
|
err := db.GetJSON(ServerConfigKey, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, database.ErrEmptyKey) {
|
if !errors.Is(err, database.ErrEmptyKey) {
|
||||||
logger.Warn("HTTP config is corrupted or could not be read", zap.Error(err))
|
slog.Warn("HTTP config is corrupted or could not be read", "error", err)
|
||||||
}
|
}
|
||||||
// Initialize with default config
|
// Initialize with default config
|
||||||
server.Config.Set(ServerConfig{
|
server.Config.Set(ServerConfig{
|
||||||
|
@ -124,7 +119,7 @@ func (s *WebServer) makeMux() *http.ServeMux {
|
||||||
}
|
}
|
||||||
if s.hub != nil {
|
if s.hub != nil {
|
||||||
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
kv.ServeWs(s.hub, w, r)
|
s.hub.CreateWebsocketClient(w, r, kv.ClientOptions{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
config := s.Config.Get()
|
config := s.Config.Get()
|
||||||
|
@ -160,7 +155,7 @@ func (s *WebServer) Listen() error {
|
||||||
for {
|
for {
|
||||||
// Read config and make http request mux
|
// Read config and make http request mux
|
||||||
config := s.Config.Get()
|
config := s.Config.Get()
|
||||||
s.logger.Info("Starting HTTP server", zap.String("bind", config.Bind))
|
slog.Info("Starting HTTP server", slog.String("bind", config.Bind))
|
||||||
s.mux = s.makeMux()
|
s.mux = s.makeMux()
|
||||||
|
|
||||||
// Make HTTP server instance
|
// Make HTTP server instance
|
||||||
|
@ -172,25 +167,25 @@ func (s *WebServer) Listen() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
s.logger.Info("HTTP server started", zap.String("bind", config.Bind))
|
slog.Info("HTTP server started", slog.String("bind", config.Bind))
|
||||||
err = s.server.Start()
|
err = s.server.Start()
|
||||||
|
|
||||||
// If the server died, we need to see what to do
|
// If the server died, we need to see what to do
|
||||||
s.logger.Debug("HTTP server died", zap.Error(err))
|
slog.Debug("HTTP server died", "error", err)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
exit <- err
|
exit <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we trying to close or restart?
|
// Are we trying to close or restart?
|
||||||
s.logger.Debug("HTTP server stopped", zap.Bool("restart", s.restart.Get()))
|
slog.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
|
||||||
if s.restart.Get() {
|
if s.restart.Get() {
|
||||||
s.restart.Set(false)
|
s.restart.Set(false)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
s.logger.Debug("HTTP server stalled")
|
slog.Debug("HTTP server stalled")
|
||||||
exit <- nil
|
exit <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -203,7 +198,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
||||||
var config ServerConfig
|
var config ServerConfig
|
||||||
err := json.Unmarshal([]byte(value), &config)
|
err := json.Unmarshal([]byte(value), &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to unmarshal config", zap.Error(err))
|
slog.Error("Failed to unmarshal config", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +215,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
||||||
s.restart.Set(true)
|
s.restart.Set(true)
|
||||||
err = s.server.Shutdown(context.Background())
|
err = s.server.Shutdown(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to shutdown server", zap.Error(err))
|
slog.Error("Failed to shutdown server", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,42 +8,37 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewServer(t *testing.T) {
|
func TestNewServer(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
_, err := NewServer(client, logger, DefaultServerFactory)
|
_, err := NewServer(client, DefaultServerFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewServerWithTestFactory(t *testing.T) {
|
func TestNewServerWithTestFactory(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
testServer := NewTestServer()
|
testServer := NewTestServer()
|
||||||
_, err := NewServer(client, logger, testServer.Factory())
|
_, err := NewServer(client, testServer.Factory())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListen(t *testing.T) {
|
func TestListen(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
// Create a test server
|
// Create a test server
|
||||||
testServer := NewTestServer()
|
testServer := NewTestServer()
|
||||||
server, err := NewServer(client, logger, testServer.Factory())
|
server, err := NewServer(client, testServer.Factory())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -96,12 +91,11 @@ func (t *testCustomHandler) ServeHTTP(http.ResponseWriter, *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomRoute(t *testing.T) {
|
func TestCustomRoute(t *testing.T) {
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
// Create test server
|
// Create test server
|
||||||
server, err := NewServer(client, logger, nil)
|
server, err := NewServer(client, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue