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

Compare commits

..

3 commits

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

View file

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

64
app.go
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

7
go.mod
View file

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

16
go.sum
View file

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

View file

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

View file

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

45
main.go
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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