mirror of https://git.sr.ht/~ashkeel/strimertul
fix: sane errors
This commit is contained in:
parent
ce2ce81768
commit
bc83a743f3
50
app.go
50
app.go
|
@ -14,6 +14,8 @@ import (
|
|||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
|
@ -39,6 +41,7 @@ type App struct {
|
|||
isFatalError *sync.RWSync[bool]
|
||||
backupOptions database.BackupOptions
|
||||
cancelLogs database.CancelFunc
|
||||
logger *slog.Logger
|
||||
|
||||
db *database.LocalDBClient
|
||||
twitchManager *client.Manager
|
||||
|
@ -52,6 +55,7 @@ func NewApp(cliParams *cli.Context) *App {
|
|||
cliParams: cliParams,
|
||||
ready: sync.NewRWSync(false),
|
||||
isFatalError: sync.NewRWSync(false),
|
||||
logger: slog.Default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,19 +149,31 @@ func (a *App) initializeComponents() error {
|
|||
var err error
|
||||
|
||||
// Create logger and endpoints
|
||||
a.httpServer, err = webserver.NewServer(a.db, webserver.DefaultServerFactory)
|
||||
a.httpServer, err = webserver.NewServer(
|
||||
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "webserver"))),
|
||||
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)
|
||||
a.twitchManager, err = client.NewManager(
|
||||
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "twitch"))),
|
||||
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, a.twitchManager)
|
||||
a.loyaltyManager, err = loyalty.NewManager(
|
||||
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "logger"))),
|
||||
a.db,
|
||||
a.twitchManager,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
||||
}
|
||||
|
@ -177,12 +193,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
|
|||
level = slog.LevelInfo
|
||||
}
|
||||
|
||||
slog.Log(a.ctx, level, entry.Message, parseLogFields(entry.Data)...)
|
||||
slog.Log(a.ctx, level, entry.Message, log.ParseLogFields(entry.Data)...)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) forwardLogs() {
|
||||
for entry := range incomingLogs {
|
||||
for entry := range log.IncomingLogs {
|
||||
runtime.EventsEmit(a.ctx, "log-event", entry)
|
||||
}
|
||||
}
|
||||
|
@ -233,8 +249,8 @@ func (a *App) GetTwitchLoggedUser() (helix.User, error) {
|
|||
return a.twitchManager.Client().GetLoggedUser()
|
||||
}
|
||||
|
||||
func (a *App) GetLastLogs() []LogEntry {
|
||||
return lastLogs.Get()
|
||||
func (a *App) GetLastLogs() []log.Entry {
|
||||
return log.LastLogs.Get()
|
||||
}
|
||||
|
||||
func (a *App) GetDocumentation() map[string]docs.KeyObject {
|
||||
|
@ -247,26 +263,26 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
|
|||
|
||||
// Add text fields
|
||||
if err := w.WriteField("error", errorData); err != nil {
|
||||
slog.Error("Could not encode field error for crash report", "error", err)
|
||||
slog.Error("Could not encode field error for crash report", log.Error(err))
|
||||
}
|
||||
if len(info) > 0 {
|
||||
if err := w.WriteField("info", info); err != nil {
|
||||
slog.Error("Could not encode field info for crash report", "error", err)
|
||||
slog.Error("Could not encode field info for crash report", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Add log files
|
||||
addFile(w, "log", logFilename)
|
||||
addFile(w, "paniclog", panicFilename)
|
||||
addFile(w, "log", log.Filename)
|
||||
addFile(w, "paniclog", log.PanicFilename)
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
slog.Error("Could not prepare request for crash report", "error", err)
|
||||
slog.Error("Could not prepare request for crash report", log.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
||||
if err != nil {
|
||||
slog.Error("Could not send crash report", "error", err)
|
||||
slog.Error("Could not send crash report", log.Error(err))
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
@ -325,7 +341,7 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
|||
|
||||
func (a *App) showFatalError(err error, text string, fields ...any) {
|
||||
if err != nil {
|
||||
fields = append(fields, "error", err, slog.String("Z", string(debug.Stack())))
|
||||
fields = append(fields, log.Error(err), slog.String("Z", string(debug.Stack())))
|
||||
slog.Error(text, fields...)
|
||||
runtime.EventsEmit(a.ctx, "fatalError")
|
||||
a.isFatalError.Set(true)
|
||||
|
@ -343,17 +359,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 {
|
||||
slog.Error("Could not encode field log for crash report", "error", err)
|
||||
slog.Error("Could not encode field log for crash report", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
slog.Error("Could not open file for including in crash report", slog.String("file", filename), "error", err)
|
||||
slog.Error("Could not open file for including in crash report", slog.String("file", filename), log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(logfile, file); err != nil {
|
||||
slog.Error("Could not read from file for including in crash report", slog.String("file", filename), "error", err)
|
||||
slog.Error("Could not read from file for including in crash report", slog.String("file", filename), log.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
16
backup.go
16
backup.go
|
@ -8,6 +8,8 @@ import (
|
|||
"sort"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
)
|
||||
|
@ -20,7 +22,7 @@ func BackupTask(driver database.Driver, options database.BackupOptions) {
|
|||
|
||||
err := os.MkdirAll(options.BackupDir, 0o755)
|
||||
if err != nil {
|
||||
slog.Error("Could not create backup directory, moving to a temporary folder", "error", err)
|
||||
slog.Error("Could not create backup directory, moving to a temporary folder", log.Error(err))
|
||||
options.BackupDir = os.TempDir()
|
||||
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
|
||||
return
|
||||
|
@ -37,13 +39,13 @@ 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 {
|
||||
slog.Error("Could not create backup file", "error", err)
|
||||
slog.Error("Could not create backup file", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = driver.Backup(file)
|
||||
if err != nil {
|
||||
slog.Error("Could not backup database", "error", err)
|
||||
slog.Error("Could not backup database", log.Error(err))
|
||||
}
|
||||
_ = file.Close()
|
||||
slog.Info("Database backup created", slog.String("backup-file", file.Name()))
|
||||
|
@ -51,7 +53,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
|||
// Remove old backups
|
||||
files, err := os.ReadDir(options.BackupDir)
|
||||
if err != nil {
|
||||
slog.Error("Could not read backup directory", "error", err)
|
||||
slog.Error("Could not read backup directory", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -64,7 +66,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 {
|
||||
slog.Error("Could not remove backup file", "error", err)
|
||||
slog.Error("Could not remove backup file", log.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +81,7 @@ type BackupInfo struct {
|
|||
func (a *App) GetBackups() (list []BackupInfo) {
|
||||
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
||||
if err != nil {
|
||||
slog.Error("Could not read backup directory", "error", err)
|
||||
slog.Error("Could not read backup directory", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -90,7 +92,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
|
|||
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
slog.Error("Could not get info for backup file", "error", err)
|
||||
slog.Error("Could not get info for backup file", log.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -249,7 +249,7 @@ const LogDetails = styled('div', {
|
|||
gridColumn: '2/4',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1rem',
|
||||
gap: '0.5rem 1rem',
|
||||
fontSize: '0.8em',
|
||||
color: '$gray11',
|
||||
backgroundColor: '$gray3',
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
import {docs} from '../models';
|
||||
import {log} from '../models';
|
||||
import {helix} from '../models';
|
||||
|
||||
export function AuthenticateKVClient(arg1:string):Promise<void>;
|
||||
|
@ -14,7 +15,7 @@ export function GetDocumentation():Promise<{[key: string]: docs.KeyObject}>;
|
|||
|
||||
export function GetKilovoltBind():Promise<string>;
|
||||
|
||||
export function GetLastLogs():Promise<Array<main.LogEntry>>;
|
||||
export function GetLastLogs():Promise<Array<log.Entry>>;
|
||||
|
||||
export function GetProblems():Promise<Array<main.Problem>>;
|
||||
|
||||
|
|
|
@ -54,6 +54,31 @@ export namespace helix {
|
|||
|
||||
}
|
||||
|
||||
export namespace log {
|
||||
|
||||
export class Entry {
|
||||
id: string;
|
||||
time: string;
|
||||
level: string;
|
||||
message: string;
|
||||
data: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Entry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.time = source["time"];
|
||||
this.level = source["level"];
|
||||
this.message = source["message"];
|
||||
this.data = source["data"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace main {
|
||||
|
||||
export class BackupInfo {
|
||||
|
@ -72,26 +97,6 @@ export namespace main {
|
|||
this.size = source["size"];
|
||||
}
|
||||
}
|
||||
export class LogEntry {
|
||||
id: string;
|
||||
time: string;
|
||||
level: string;
|
||||
message: string;
|
||||
data: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new LogEntry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.time = source["time"];
|
||||
this.level = source["level"];
|
||||
this.message = source["message"];
|
||||
this.data = source["data"];
|
||||
}
|
||||
}
|
||||
export class Problem {
|
||||
id: string;
|
||||
details: any;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Error(err error) slog.Attr {
|
||||
pc, filename, line, _ := runtime.Caller(1)
|
||||
|
||||
return slog.Group("error",
|
||||
slog.String("message", err.Error()),
|
||||
slog.String("file", fmt.Sprintf("%s@%d", filename, line)),
|
||||
slog.String("func", runtime.FuncForPC(pc).Name()),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextLogger ContextKey = "logger"
|
||||
)
|
||||
|
||||
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
|
||||
return context.WithValue(ctx, ContextLogger, logger)
|
||||
}
|
||||
|
||||
func GetLogger(ctx context.Context) *slog.Logger {
|
||||
logger, ok := ctx.Value(ContextLogger).(*slog.Logger)
|
||||
if !ok {
|
||||
return slog.Default()
|
||||
}
|
||||
return logger
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
slogmulti "github.com/samber/slog-multi"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
History = 50
|
||||
Filename = "strimertul.log"
|
||||
PanicFilename = "strimertul-panic.log"
|
||||
)
|
||||
|
||||
var (
|
||||
LastLogs = sync.NewSlice[Entry]()
|
||||
IncomingLogs = make(chan Entry, 100)
|
||||
)
|
||||
|
||||
func Init(level slog.Level) {
|
||||
logStorage := NewLogStorage(level)
|
||||
fileLogger := &lumberjack.Logger{
|
||||
Filename: Filename,
|
||||
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 Entry struct {
|
||||
ID string `json:"id"`
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
minLevel slog.Level
|
||||
attrs []slog.Attr
|
||||
group string
|
||||
}
|
||||
|
||||
func (core *Storage) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= core.minLevel
|
||||
}
|
||||
|
||||
func (core *Storage) Handle(_ context.Context, record slog.Record) error {
|
||||
attributes := flatAttributeMap(record)
|
||||
attrJSON, _ := json.Marshal(attributes)
|
||||
|
||||
// Generate unique log ID
|
||||
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
|
||||
|
||||
logEntry := Entry{
|
||||
ID: id,
|
||||
Time: record.Time.Format(time.RFC3339),
|
||||
Level: record.Level.String(),
|
||||
Message: record.Message,
|
||||
Data: string(attrJSON),
|
||||
}
|
||||
LastLogs.Push(logEntry)
|
||||
if LastLogs.Size() > History {
|
||||
LastLogs.Splice(0, 1)
|
||||
}
|
||||
IncomingLogs <- logEntry
|
||||
return nil
|
||||
}
|
||||
|
||||
func flatAttributeMap(record slog.Record) map[string]any {
|
||||
attributes := map[string]any{}
|
||||
flatAttributes := func(attr slog.Attr) bool {
|
||||
type attrToParse struct {
|
||||
Prefix string
|
||||
Attr slog.Attr
|
||||
}
|
||||
remaining := []attrToParse{{"", attr}}
|
||||
for len(remaining) > 0 {
|
||||
var current attrToParse
|
||||
current, remaining = remaining[0], remaining[1:]
|
||||
|
||||
switch current.Attr.Value.Kind() {
|
||||
case slog.KindGroup:
|
||||
for _, subAttr := range current.Attr.Value.Group() {
|
||||
remaining = append(remaining, attrToParse{
|
||||
Prefix: current.Attr.Key + ".",
|
||||
Attr: subAttr,
|
||||
})
|
||||
}
|
||||
default:
|
||||
attributes[current.Prefix+current.Attr.Key] = current.Attr.Value.Any()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
record.Attrs(flatAttributes)
|
||||
return attributes
|
||||
}
|
||||
|
||||
func (core *Storage) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &Storage{
|
||||
minLevel: core.minLevel,
|
||||
attrs: append(core.attrs, attrs...),
|
||||
group: core.group,
|
||||
}
|
||||
}
|
||||
|
||||
func (core *Storage) WithGroup(name string) slog.Handler {
|
||||
return &Storage{
|
||||
minLevel: core.minLevel,
|
||||
attrs: core.attrs,
|
||||
group: name,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogStorage(level slog.Level) *Storage {
|
||||
return &Storage{
|
||||
minLevel: level,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseLogFields(data map[string]any) []any {
|
||||
fields := []any{}
|
||||
for k, v := range data {
|
||||
fields = append(fields, k, v)
|
||||
}
|
||||
return fields
|
||||
}
|
118
logging.go
118
logging.go
|
@ -1,118 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
slogmulti "github.com/samber/slog-multi"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const LogHistory = 50
|
||||
|
||||
var (
|
||||
lastLogs *sync.Slice[LogEntry]
|
||||
incomingLogs chan LogEntry
|
||||
)
|
||||
|
||||
func initLogger(level slog.Level) {
|
||||
lastLogs = sync.NewSlice[LogEntry]()
|
||||
incomingLogs = make(chan LogEntry, 100)
|
||||
logStorage := NewLogStorage(level)
|
||||
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 {
|
||||
ID string `json:"id"`
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type LogStorage struct {
|
||||
minLevel slog.Level
|
||||
attrs []slog.Attr
|
||||
group string
|
||||
}
|
||||
|
||||
func (core *LogStorage) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= core.minLevel
|
||||
}
|
||||
|
||||
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.Marshal(attributes)
|
||||
|
||||
// Generate unique log ID
|
||||
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
|
||||
|
||||
logEntry := LogEntry{
|
||||
ID: id,
|
||||
Time: record.Time.Format(time.RFC3339),
|
||||
Level: record.Level.String(),
|
||||
Message: record.Message,
|
||||
Data: string(attrJSON),
|
||||
}
|
||||
lastLogs.Push(logEntry)
|
||||
if lastLogs.Size() > LogHistory {
|
||||
lastLogs.Splice(0, 1)
|
||||
}
|
||||
incomingLogs <- logEntry
|
||||
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 (core *LogStorage) WithGroup(name string) slog.Handler {
|
||||
return &LogStorage{
|
||||
minLevel: core.minLevel,
|
||||
attrs: core.attrs,
|
||||
group: name,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
|
@ -27,6 +29,7 @@ type twitchIntegration struct {
|
|||
ctx context.Context
|
||||
manager *Manager
|
||||
module *chat.Module
|
||||
logger *slog.Logger
|
||||
|
||||
activeUsers *sync.Map[string, bool]
|
||||
}
|
||||
|
@ -36,6 +39,7 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
|||
ctx: ctx,
|
||||
manager: m,
|
||||
module: mod,
|
||||
logger: log.GetLogger(ctx),
|
||||
|
||||
activeUsers: sync.NewMap[string, bool](),
|
||||
}
|
||||
|
@ -89,7 +93,7 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
|||
var streamInfos []helix.Stream
|
||||
err := m.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
||||
if err != nil {
|
||||
slog.Error("Error retrieving stream info", "error", err)
|
||||
li.logger.Error("Error retrieving stream info", log.Error(err))
|
||||
continue
|
||||
}
|
||||
if len(streamInfos) < 1 {
|
||||
|
@ -123,13 +127,13 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
|||
if len(users) > 0 {
|
||||
err := li.manager.GivePoints(pointsToGive)
|
||||
if err != nil {
|
||||
slog.Error("Error awarding loyalty points to user", "error", err)
|
||||
li.logger.Error("Error awarding loyalty points to user", log.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
slog.Info("Loyalty system integration with Twitch is ready")
|
||||
li.logger.Info("Loyalty system integration with Twitch is ready")
|
||||
|
||||
return li
|
||||
}
|
||||
|
@ -217,7 +221,7 @@ func (li *twitchIntegration) cmdRedeemReward(message helix.EventSubChannelChatMe
|
|||
})
|
||||
return
|
||||
}
|
||||
slog.Error("Error while performing redeem", "error", err)
|
||||
li.logger.Error("Error while performing redeem", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -339,7 +343,7 @@ func (li *twitchIntegration) cmdContributeGoal(message helix.EventSubChannelChat
|
|||
// Add points to goal
|
||||
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
||||
if err != nil {
|
||||
slog.Error("Error while contributing to goal", "error", err)
|
||||
li.logger.Error("Error while contributing to goal", log.Error(err))
|
||||
return
|
||||
}
|
||||
if points == 0 {
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
twitchclient "git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||
|
@ -29,6 +31,7 @@ type Manager struct {
|
|||
Goals *sync.Slice[Goal]
|
||||
Queue *sync.Slice[Redeem]
|
||||
db database.Database
|
||||
logger *slog.Logger
|
||||
cooldowns map[string]time.Time
|
||||
banlist map[string]bool
|
||||
ctx context.Context
|
||||
|
@ -39,19 +42,19 @@ type Manager struct {
|
|||
twitchIntegration *twitchIntegration
|
||||
}
|
||||
|
||||
func NewManager(db database.Database, twitchManager *twitchclient.Manager) (*Manager, error) {
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
func NewManager(ctx context.Context, db database.Database, twitchManager *twitchclient.Manager) (*Manager, error) {
|
||||
loyaltyContext, cancelFn := context.WithCancel(ctx)
|
||||
loyalty := &Manager{
|
||||
Config: sync.NewRWSync(Config{Enabled: false}),
|
||||
Rewards: sync.NewSlice[Reward](),
|
||||
Goals: sync.NewSlice[Goal](),
|
||||
Queue: sync.NewSlice[Redeem](),
|
||||
|
||||
Config: sync.NewRWSync(Config{Enabled: false}),
|
||||
Rewards: sync.NewSlice[Reward](),
|
||||
Goals: sync.NewSlice[Goal](),
|
||||
Queue: sync.NewSlice[Redeem](),
|
||||
logger: log.GetLogger(ctx),
|
||||
db: db,
|
||||
points: sync.NewMap[string, PointsEntry](),
|
||||
cooldowns: make(map[string]time.Time),
|
||||
banlist: make(map[string]bool),
|
||||
ctx: ctx,
|
||||
ctx: loyaltyContext,
|
||||
cancelFn: cancelFn,
|
||||
restartTwitchHandler: make(chan struct{}),
|
||||
twitchManager: twitchManager,
|
||||
|
@ -116,14 +119,14 @@ func NewManager(db database.Database, twitchManager *twitchclient.Manager) (*Man
|
|||
// SubscribePrefix for changes
|
||||
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
||||
if err != nil {
|
||||
slog.Error("Could not setup loyalty reload subscription", "error", err)
|
||||
loyalty.logger.Error("Could not setup loyalty reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
loyalty.SetBanList(config.BanList)
|
||||
|
||||
// Start twitch handler
|
||||
if twitchManager.Client() != nil {
|
||||
loyalty.twitchIntegration = setupTwitchIntegration(ctx, loyalty, twitchManager.Client().Chat)
|
||||
loyalty.twitchIntegration = setupTwitchIntegration(loyaltyContext, loyalty, twitchManager.Client().Chat)
|
||||
}
|
||||
|
||||
return loyalty, nil
|
||||
|
@ -186,7 +189,7 @@ func (m *Manager) update(key, value string) {
|
|||
}
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("Subscribe error: invalid JSON received on key", slog.String("key", key), "error", err)
|
||||
slog.Error("Subscribe error: invalid JSON received on key", slog.String("key", key), log.Error(err))
|
||||
} else {
|
||||
slog.Debug("Updated key", slog.String("key", key))
|
||||
}
|
||||
|
|
18
main.go
18
main.go
|
@ -4,12 +4,14 @@ import (
|
|||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
corelog "log"
|
||||
"log/slog"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
@ -25,8 +27,6 @@ var appVersion = devVersionMarker
|
|||
|
||||
const (
|
||||
crashReportURL = "https://crash.strimertul.stream/upload"
|
||||
logFilename = "strimertul.log"
|
||||
panicFilename = "strimertul-panic.log"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
|
@ -34,7 +34,7 @@ var frontend embed.FS
|
|||
|
||||
func main() {
|
||||
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
|
||||
log.Fatal(err)
|
||||
corelog.Fatal(err)
|
||||
}
|
||||
|
||||
var panicLog *os.File
|
||||
|
@ -88,13 +88,13 @@ func main() {
|
|||
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
|
||||
}
|
||||
initLogger(level)
|
||||
log.Init(level)
|
||||
|
||||
// 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(log.PanicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
slog.Warn("Could not create panic log", "error", err)
|
||||
slog.Warn("Could not create panic log", log.Error(err))
|
||||
} else {
|
||||
utils.RedirectStderr(panicLog)
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ func main() {
|
|||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
corelog.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,7 +165,7 @@ func cliMain(ctx *cli.Context) error {
|
|||
|
||||
func warnOnError(err error, text string, fields ...any) {
|
||||
if err != nil {
|
||||
fields = append(fields, "error", err)
|
||||
fields = append(fields, log.Error(err))
|
||||
slog.Warn(text, fields...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
)
|
||||
|
||||
|
@ -27,7 +28,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 {
|
||||
slog.Warn("Could not check scopes for problems", "error", err)
|
||||
slog.Warn("Could not check scopes for problems", log.Error(err))
|
||||
} else {
|
||||
if !scopesMatch {
|
||||
problems = append(problems, Problem{
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"math/rand"
|
||||
"text/template"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
)
|
||||
|
@ -12,7 +14,7 @@ import (
|
|||
func (m *Module) onEventSubEvent(_ string, value string) {
|
||||
var ev eventSubNotification
|
||||
if err := json.Unmarshal([]byte(value), &ev); err != nil {
|
||||
m.logger.Warn("Error parsing webhook payload", "error", err)
|
||||
m.logger.Warn("Error parsing webhook payload", log.Error(err))
|
||||
return
|
||||
}
|
||||
switch ev.Subscription.Type {
|
||||
|
@ -24,7 +26,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", "error", err)
|
||||
m.logger.Warn("Error parsing follow event", log.Error(err))
|
||||
return
|
||||
}
|
||||
// Pick a random message
|
||||
|
@ -49,7 +51,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", "error", err)
|
||||
m.logger.Warn("Error parsing raid event", log.Error(err))
|
||||
return
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
@ -83,7 +85,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", "error", err)
|
||||
m.logger.Warn("Error parsing cheer event", log.Error(err))
|
||||
return
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
@ -117,7 +119,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", "error", err)
|
||||
m.logger.Warn("Error parsing new subscription event", log.Error(err))
|
||||
return
|
||||
}
|
||||
m.addMixedEvent(subEv)
|
||||
|
@ -130,7 +132,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", "error", err)
|
||||
m.logger.Warn("Error parsing returning subscription event", log.Error(err))
|
||||
return
|
||||
}
|
||||
m.addMixedEvent(subEv)
|
||||
|
@ -142,7 +144,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", "error", err)
|
||||
m.logger.Warn("Error parsing subscription gifted event", log.Error(err))
|
||||
return
|
||||
}
|
||||
// Pick a random message from base set
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"text/template"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
templater "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -33,14 +34,14 @@ type Module struct {
|
|||
ctx context.Context
|
||||
db database.Database
|
||||
logger *slog.Logger
|
||||
templater template2.Engine
|
||||
templater templater.Engine
|
||||
templates templateCacheMap
|
||||
|
||||
pendingMux sync.Mutex
|
||||
pendingSubs map[string]subMixedEvent
|
||||
}
|
||||
|
||||
func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templater template2.Engine) *Module {
|
||||
func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templater templater.Engine) *Module {
|
||||
mod := &Module{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
|
@ -52,12 +53,12 @@ func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templ
|
|||
|
||||
// Load config from database
|
||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||
logger.Debug("Config load error", "error", err)
|
||||
logger.Debug("Config load error", log.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", "error", err)
|
||||
logger.Warn("Could not save default config for bot alerts", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,17 +67,17 @@ func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templ
|
|||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||
err := json.Unmarshal([]byte(value), &mod.Config)
|
||||
if err != nil {
|
||||
logger.Warn("Error loading alert config", "error", err)
|
||||
logger.Warn("Error loading alert config", log.Error(err))
|
||||
} else {
|
||||
logger.Info("Reloaded alert config")
|
||||
}
|
||||
mod.compileTemplates()
|
||||
}); err != nil {
|
||||
logger.Error("Could not set-up bot alert reload subscription", "error", err)
|
||||
logger.Error("Could not set-up bot alert reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
|
||||
logger.Error("Could not setup twitch alert subscription", "error", err)
|
||||
logger.Error("Could not setup twitch alert subscription", log.Error(err))
|
||||
}
|
||||
|
||||
logger.Debug("Loaded bot alerts")
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
)
|
||||
|
||||
|
@ -42,7 +44,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", "error", err)
|
||||
m.logger.Error("Error compiling alert template", log.Error(err))
|
||||
return
|
||||
}
|
||||
templateList[message] = tpl
|
||||
|
@ -58,7 +60,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", "error", err)
|
||||
m.logger.Error("Error executing template for bot alert", log.Error(err))
|
||||
return
|
||||
}
|
||||
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
)
|
||||
|
||||
var accessLevels = map[AccessLevelType]int{
|
||||
|
@ -70,7 +72,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", "error", err)
|
||||
mod.logger.Error("Failed to execute custom command template", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
textTemplate "text/template"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
|
@ -59,12 +61,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", "error", err)
|
||||
logger.Error("Failed to load chat module config", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
|
||||
logger.Error("Could not subscribe to chat messages", "error", err)
|
||||
logger.Error("Could not subscribe to chat messages", log.Error(err))
|
||||
}
|
||||
|
||||
// Load custom commands
|
||||
|
@ -73,21 +75,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", "error", err)
|
||||
logger.Error("Failed to load custom commands", log.Error(err))
|
||||
}
|
||||
}
|
||||
mod.customCommands.Set(customCommands)
|
||||
|
||||
if err := mod.updateTemplates(); err != nil {
|
||||
logger.Error("Failed to parse custom commands", "error", err)
|
||||
logger.Error("Failed to parse custom commands", log.Error(err))
|
||||
}
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
||||
logger.Error("Could not set-up chat command reload subscription", "error", err)
|
||||
logger.Error("Could not set-up chat command reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
|
||||
logger.Error("Could not set-up chat command reload subscription", "error", err)
|
||||
logger.Error("Could not set-up chat command reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
return mod
|
||||
|
@ -98,7 +100,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
|||
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(newValue), &chatMessage); err != nil {
|
||||
mod.logger.Error("Failed to decode incoming chat message", slog.String("error", err.Error()))
|
||||
mod.logger.Error("Failed to decode incoming chat message", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -146,7 +148,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", "error", err)
|
||||
mod.logger.Warn("Failed to decode write message request", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -157,7 +159,7 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
Message: request.Message,
|
||||
})
|
||||
if err != nil {
|
||||
mod.logger.Error("Failed to send announcement", "error", err)
|
||||
mod.logger.Error("Failed to send announcement", log.Error(err))
|
||||
}
|
||||
if resp.Error != "" {
|
||||
mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||
|
@ -172,7 +174,7 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
Message: request.Message,
|
||||
})
|
||||
if err != nil {
|
||||
mod.logger.Error("Failed to send whisper", "error", err)
|
||||
mod.logger.Error("Failed to send whisper", log.Error(err))
|
||||
}
|
||||
if resp.Error != "" {
|
||||
mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||
|
@ -187,7 +189,7 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
ReplyParentMessageID: request.ReplyTo,
|
||||
})
|
||||
if err != nil {
|
||||
mod.logger.Error("Failed to send chat message", "error", err)
|
||||
mod.logger.Error("Failed to send chat message", log.Error(err))
|
||||
}
|
||||
if resp.Error != "" {
|
||||
mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
||||
|
@ -197,12 +199,12 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
|||
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", "error", err)
|
||||
mod.logger.Error("Failed to decode new custom commands", log.Error(err))
|
||||
return
|
||||
}
|
||||
// Recreate templates
|
||||
if err := mod.updateTemplates(); err != nil {
|
||||
mod.logger.Error("Failed to update custom commands templates", "error", err)
|
||||
mod.logger.Error("Failed to update custom commands templates", log.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -236,7 +238,7 @@ func (mod *Module) GetChatters() (users []string) {
|
|||
for {
|
||||
userClient, err := twitch.GetUserClient(mod.db, false)
|
||||
if err != nil {
|
||||
slog.Error("Could not get user api client for list of chatters", "error", err)
|
||||
slog.Error("Could not get user api client for list of chatters", log.Error(err))
|
||||
return
|
||||
}
|
||||
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
||||
|
@ -246,7 +248,7 @@ func (mod *Module) GetChatters() (users []string) {
|
|||
After: cursor,
|
||||
})
|
||||
if err != nil {
|
||||
mod.logger.Error("Could not retrieve list of chatters", "error", err)
|
||||
mod.logger.Error("Could not retrieve list of chatters", log.Error(err))
|
||||
return
|
||||
}
|
||||
for _, user := range res.Data.Chatters {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
)
|
||||
|
||||
// WriteMessageRequest is an RPC to send a chat message with extra options
|
||||
|
@ -17,6 +18,6 @@ type WriteMessageRequest struct {
|
|||
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", "error", err)
|
||||
logger.Error("Failed to write chat message", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
|
||||
|
@ -25,6 +27,8 @@ type Manager struct {
|
|||
}
|
||||
|
||||
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer) (*Manager, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
// Get Twitch config
|
||||
var config twitch.Config
|
||||
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
||||
|
@ -50,7 +54,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
|||
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
||||
var newConfig twitch.Config
|
||||
if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
|
||||
slog.Error("Failed to decode Twitch integration config", "error", err)
|
||||
logger.Error("Failed to decode Twitch integration config", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -60,7 +64,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
|||
clientContext, cancel = context.WithCancel(ctx)
|
||||
updatedClient, err = newClient(clientContext, newConfig, db, server)
|
||||
if err != nil {
|
||||
slog.Error("Could not create twitch client with new config, keeping old", "error", err)
|
||||
logger.Error("Could not create twitch client with new config, keeping old", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -68,9 +72,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
|||
updatedClient.Merge(manager.client)
|
||||
manager.client = updatedClient
|
||||
|
||||
slog.Info("Reloaded/updated Twitch integration")
|
||||
logger.Info("Reloaded/updated Twitch integration")
|
||||
}); err != nil {
|
||||
slog.Error("Could not setup twitch config reload subscription", "error", err)
|
||||
logger.Error("Could not setup twitch config reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
|
@ -140,7 +144,7 @@ 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", "error", err)
|
||||
client.Logger.Error("Failed looking up user", log.Error(err))
|
||||
} else if len(users.Data.Users) < 1 {
|
||||
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
||||
} else {
|
||||
|
@ -148,7 +152,7 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
|
|||
client.User = users.Data.Users[0]
|
||||
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
|
||||
if err != nil {
|
||||
client.Logger.Error("Failed to setup EventSub", "error", err)
|
||||
client.Logger.Error("Failed to setup EventSub", log.Error(err))
|
||||
}
|
||||
|
||||
tpl := client.GetTemplateEngine()
|
||||
|
@ -184,14 +188,14 @@ func (c *Client) runStatusPoll() {
|
|||
UserIDs: []string{c.User.ID},
|
||||
})
|
||||
if err != nil {
|
||||
c.Logger.Error("Error checking stream status", "error", err)
|
||||
c.Logger.Error("Error checking stream status", log.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", "error", err)
|
||||
c.Logger.Warn("Error saving stream info", log.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"strings"
|
||||
textTemplate "text/template"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
|
@ -57,7 +59,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", "error", err, slog.String("key", counterKey))
|
||||
c.Logger.Error("Error saving key", slog.String("key", counterKey), log.Error(err))
|
||||
}
|
||||
return counter
|
||||
},
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
|
@ -55,7 +57,7 @@ func (c *Client) eventSubLoop() {
|
|||
for endpoint != "" {
|
||||
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
||||
if err != nil {
|
||||
c.logger.Error("EventSub websocket read error", "error", err)
|
||||
c.logger.Error("EventSub websocket read error", log.Error(err))
|
||||
}
|
||||
}
|
||||
if connection != nil {
|
||||
|
@ -83,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", "error", err)
|
||||
c.logger.Error("Could not establish a connection to the EventSub websocket", log.Error(err))
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
|
@ -106,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", "error", err)
|
||||
c.logger.Error("Error decoding EventSub message", log.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -125,7 +127,7 @@ 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", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
|
||||
c.logger.Error("Error decoding EventSub welcome message", slog.String("message-type", wsMessage.Metadata.MessageType), log.Error(err))
|
||||
break
|
||||
}
|
||||
c.logger.Info("Connection to EventSub websocket established", slog.String("session-id", welcomeData.Session.ID))
|
||||
|
@ -138,14 +140,14 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
|
|||
// Add subscription to websocket session
|
||||
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
||||
if err != nil {
|
||||
c.logger.Error("Could not add subscriptions", "error", err)
|
||||
c.logger.Error("Could not add subscriptions", log.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", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
|
||||
c.logger.Error("Error decoding EventSub session reconnect parameters", slog.String("message-type", wsMessage.Metadata.MessageType), log.Error(err))
|
||||
break
|
||||
}
|
||||
c.logger.Info("EventSub websocket requested a reconnection", slog.String("session-id", reconnectData.Session.ID), slog.String("reconnect-url", reconnectData.Session.ReconnectURL))
|
||||
|
@ -173,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
var notificationData NotificationMessagePayload
|
||||
err := json.Unmarshal(message.Payload, ¬ificationData)
|
||||
if err != nil {
|
||||
c.logger.Error("Error decoding EventSub notification payload", slog.String("message-type", message.Metadata.MessageType), "error", err)
|
||||
c.logger.Error("Error decoding EventSub notification payload", slog.String("message-type", message.Metadata.MessageType), log.Error(err))
|
||||
}
|
||||
notificationData.Date = time.Now()
|
||||
|
||||
|
@ -181,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", slog.String("key", eventKey), "error", err)
|
||||
c.logger.Error("Error storing event to database", slog.String("key", eventKey), log.Error(err))
|
||||
}
|
||||
|
||||
var archive []NotificationMessagePayload
|
||||
|
@ -195,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", slog.String("key", historyKey), "error", err)
|
||||
c.logger.Error("Error storing event to database", slog.String("key", historyKey), log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"math/rand"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
|
@ -42,25 +44,25 @@ func Setup(ctx context.Context, db database.Database, logger *slog.Logger) *Modu
|
|||
|
||||
// Load config from database
|
||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||
logger.Debug("Config load error", "error", err)
|
||||
logger.Debug("Config load error", log.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", "error", err)
|
||||
logger.Warn("Could not save default config for bot timers", log.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||
if err := json.Unmarshal([]byte(value), &mod.Config); err != nil {
|
||||
logger.Warn("Error reloading timer config", "error", err)
|
||||
logger.Warn("Error reloading timer config", log.Error(err))
|
||||
return
|
||||
}
|
||||
logger.Info("Reloaded timer config")
|
||||
}); err != nil {
|
||||
logger.Error("Could not set-up timer reload subscription", "error", err)
|
||||
logger.Error("Could not set-up timer reload subscription", log.Error(err))
|
||||
}
|
||||
|
||||
logger.Debug("Loaded timers", slog.Int("timers", len(mod.Config.Timers)))
|
||||
|
@ -81,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", "error", err)
|
||||
m.logger.Warn("Error saving chat activity", log.Error(err))
|
||||
}
|
||||
|
||||
// Calculate activity
|
||||
|
|
|
@ -5,11 +5,13 @@ import (
|
|||
"log/slog"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
)
|
||||
|
||||
func Close(res io.Closer) {
|
||||
err := res.Close()
|
||||
if err != nil {
|
||||
slog.Error("Could not close resource", slog.String("name", reflect.TypeOf(res).String()), slog.String("stack", string(debug.Stack())), "error", err)
|
||||
slog.Error("Could not close resource", slog.String("name", reflect.TypeOf(res).String()), slog.String("stack", string(debug.Stack())), log.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"net/http/pprof"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/log"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
|
@ -21,7 +23,9 @@ import (
|
|||
|
||||
type WebServer struct {
|
||||
Config *sync.RWSync[ServerConfig]
|
||||
ctx context.Context
|
||||
db database.Database
|
||||
logger *slog.Logger
|
||||
server Server
|
||||
frontend fs.FS
|
||||
hub *kv.Hub
|
||||
|
@ -32,9 +36,11 @@ type WebServer struct {
|
|||
factory ServerFactory
|
||||
}
|
||||
|
||||
func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, error) {
|
||||
func NewServer(ctx context.Context, db database.Database, serverFactory ServerFactory) (*WebServer, error) {
|
||||
server := &WebServer{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
logger: log.GetLogger(ctx),
|
||||
server: nil,
|
||||
requestedRoutes: sync.NewMap[string, http.Handler](),
|
||||
restart: sync.NewRWSync(false),
|
||||
|
@ -46,7 +52,7 @@ func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, e
|
|||
err := db.GetJSON(ServerConfigKey, &config)
|
||||
if err != nil {
|
||||
if !errors.Is(err, database.ErrEmptyKey) {
|
||||
slog.Warn("HTTP config is corrupted or could not be read", "error", err)
|
||||
server.logger.Warn("HTTP config is corrupted or could not be read", log.Error(err))
|
||||
}
|
||||
// Initialize with default config
|
||||
server.Config.Set(ServerConfig{
|
||||
|
@ -155,7 +161,7 @@ func (s *WebServer) Listen() error {
|
|||
for {
|
||||
// Read config and make http request mux
|
||||
config := s.Config.Get()
|
||||
slog.Info("Starting HTTP server", slog.String("bind", config.Bind))
|
||||
s.logger.Info("Starting HTTP server", slog.String("bind", config.Bind))
|
||||
s.mux = s.makeMux()
|
||||
|
||||
// Make HTTP server instance
|
||||
|
@ -167,25 +173,25 @@ func (s *WebServer) Listen() error {
|
|||
}
|
||||
|
||||
// Start HTTP server
|
||||
slog.Info("HTTP server started", slog.String("bind", config.Bind))
|
||||
s.logger.Info("HTTP server started", slog.String("bind", config.Bind))
|
||||
err = s.server.Start()
|
||||
|
||||
// If the server died, we need to see what to do
|
||||
slog.Debug("HTTP server died", "error", err)
|
||||
s.logger.Debug("HTTP server died", log.Error(err))
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
exit <- err
|
||||
return
|
||||
}
|
||||
|
||||
// Are we trying to close or restart?
|
||||
slog.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
|
||||
s.logger.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
|
||||
if s.restart.Get() {
|
||||
s.restart.Set(false)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
slog.Debug("HTTP server stalled")
|
||||
s.logger.Debug("HTTP server stalled")
|
||||
exit <- nil
|
||||
}()
|
||||
|
||||
|
@ -198,7 +204,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
|||
var config ServerConfig
|
||||
err := json.Unmarshal([]byte(value), &config)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal config", "error", err)
|
||||
s.logger.Error("Failed to unmarshal config", log.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -215,7 +221,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
|||
s.restart.Set(true)
|
||||
err = s.server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Failed to shutdown server", "error", err)
|
||||
s.logger.Error("Failed to shutdown server", log.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue