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"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
@ -39,6 +41,7 @@ type App struct {
|
||||||
isFatalError *sync.RWSync[bool]
|
isFatalError *sync.RWSync[bool]
|
||||||
backupOptions database.BackupOptions
|
backupOptions database.BackupOptions
|
||||||
cancelLogs database.CancelFunc
|
cancelLogs database.CancelFunc
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
db *database.LocalDBClient
|
db *database.LocalDBClient
|
||||||
twitchManager *client.Manager
|
twitchManager *client.Manager
|
||||||
|
@ -52,6 +55,7 @@ func NewApp(cliParams *cli.Context) *App {
|
||||||
cliParams: cliParams,
|
cliParams: cliParams,
|
||||||
ready: sync.NewRWSync(false),
|
ready: sync.NewRWSync(false),
|
||||||
isFatalError: sync.NewRWSync(false),
|
isFatalError: sync.NewRWSync(false),
|
||||||
|
logger: slog.Default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,19 +149,31 @@ func (a *App) initializeComponents() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Create logger and endpoints
|
// Create logger and endpoints
|
||||||
a.httpServer, err = webserver.NewServer(a.db, 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize http server: %w", err)
|
return fmt.Errorf("could not initialize http server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create twitch client
|
// Create twitch client
|
||||||
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer)
|
a.twitchManager, err = client.NewManager(
|
||||||
|
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "twitch"))),
|
||||||
|
a.db,
|
||||||
|
a.httpServer,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize twitch client: %w", err)
|
return fmt.Errorf("could not initialize twitch client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize loyalty system
|
// Initialize loyalty system
|
||||||
a.loyaltyManager, err = loyalty.NewManager(a.db, 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
return fmt.Errorf("could not initialize loyalty manager: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -177,12 +193,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
|
||||||
level = slog.LevelInfo
|
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() {
|
func (a *App) forwardLogs() {
|
||||||
for entry := range incomingLogs {
|
for entry := range log.IncomingLogs {
|
||||||
runtime.EventsEmit(a.ctx, "log-event", entry)
|
runtime.EventsEmit(a.ctx, "log-event", entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,8 +249,8 @@ func (a *App) GetTwitchLoggedUser() (helix.User, error) {
|
||||||
return a.twitchManager.Client().GetLoggedUser()
|
return a.twitchManager.Client().GetLoggedUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetLastLogs() []LogEntry {
|
func (a *App) GetLastLogs() []log.Entry {
|
||||||
return lastLogs.Get()
|
return log.LastLogs.Get()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetDocumentation() map[string]docs.KeyObject {
|
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
|
// Add text fields
|
||||||
if err := w.WriteField("error", errorData); err != nil {
|
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 len(info) > 0 {
|
||||||
if err := w.WriteField("info", info); err != nil {
|
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
|
// Add log files
|
||||||
addFile(w, "log", logFilename)
|
addFile(w, "log", log.Filename)
|
||||||
addFile(w, "paniclog", panicFilename)
|
addFile(w, "paniclog", log.PanicFilename)
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not send crash report", "error", err)
|
slog.Error("Could not send crash report", log.Error(err))
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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) {
|
func (a *App) showFatalError(err error, text string, fields ...any) {
|
||||||
if err != nil {
|
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...)
|
slog.Error(text, fields...)
|
||||||
runtime.EventsEmit(a.ctx, "fatalError")
|
runtime.EventsEmit(a.ctx, "fatalError")
|
||||||
a.isFatalError.Set(true)
|
a.isFatalError.Set(true)
|
||||||
|
@ -343,17 +359,17 @@ func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
|
||||||
func addFile(m *multipart.Writer, field string, filename string) {
|
func addFile(m *multipart.Writer, field string, filename string) {
|
||||||
logfile, err := m.CreateFormFile(field, filename)
|
logfile, err := m.CreateFormFile(field, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.Copy(logfile, file); err != nil {
|
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"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
)
|
)
|
||||||
|
@ -20,7 +22,7 @@ func BackupTask(driver database.Driver, options database.BackupOptions) {
|
||||||
|
|
||||||
err := os.MkdirAll(options.BackupDir, 0o755)
|
err := os.MkdirAll(options.BackupDir, 0o755)
|
||||||
if err != nil {
|
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()
|
options.BackupDir = os.TempDir()
|
||||||
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
|
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
|
||||||
return
|
return
|
||||||
|
@ -37,13 +39,13 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
||||||
// Run backup procedure
|
// Run backup procedure
|
||||||
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
|
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not create backup file", "error", err)
|
slog.Error("Could not create backup file", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.Backup(file)
|
err = driver.Backup(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not backup database", "error", err)
|
slog.Error("Could not backup database", log.Error(err))
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
slog.Info("Database backup created", slog.String("backup-file", file.Name()))
|
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
|
// Remove old backups
|
||||||
files, err := os.ReadDir(options.BackupDir)
|
files, err := os.ReadDir(options.BackupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not read backup directory", "error", err)
|
slog.Error("Could not read backup directory", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +66,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
|
||||||
for _, file := range toRemove {
|
for _, file := range toRemove {
|
||||||
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
|
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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) {
|
func (a *App) GetBackups() (list []BackupInfo) {
|
||||||
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
files, err := os.ReadDir(a.backupOptions.BackupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not read backup directory", "error", err)
|
slog.Error("Could not read backup directory", log.Error(err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
|
||||||
|
|
||||||
info, err := file.Info()
|
info, err := file.Info()
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -249,7 +249,7 @@ const LogDetails = styled('div', {
|
||||||
gridColumn: '2/4',
|
gridColumn: '2/4',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: '1rem',
|
gap: '0.5rem 1rem',
|
||||||
fontSize: '0.8em',
|
fontSize: '0.8em',
|
||||||
color: '$gray11',
|
color: '$gray11',
|
||||||
backgroundColor: '$gray3',
|
backgroundColor: '$gray3',
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {main} from '../models';
|
import {main} from '../models';
|
||||||
import {docs} from '../models';
|
import {docs} from '../models';
|
||||||
|
import {log} from '../models';
|
||||||
import {helix} from '../models';
|
import {helix} from '../models';
|
||||||
|
|
||||||
export function AuthenticateKVClient(arg1:string):Promise<void>;
|
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 GetKilovoltBind():Promise<string>;
|
||||||
|
|
||||||
export function GetLastLogs():Promise<Array<main.LogEntry>>;
|
export function GetLastLogs():Promise<Array<log.Entry>>;
|
||||||
|
|
||||||
export function GetProblems():Promise<Array<main.Problem>>;
|
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 namespace main {
|
||||||
|
|
||||||
export class BackupInfo {
|
export class BackupInfo {
|
||||||
|
@ -72,26 +97,6 @@ export namespace main {
|
||||||
this.size = source["size"];
|
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 {
|
export class Problem {
|
||||||
id: string;
|
id: string;
|
||||||
details: any;
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
|
@ -27,6 +29,7 @@ type twitchIntegration struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
manager *Manager
|
manager *Manager
|
||||||
module *chat.Module
|
module *chat.Module
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
activeUsers *sync.Map[string, bool]
|
activeUsers *sync.Map[string, bool]
|
||||||
}
|
}
|
||||||
|
@ -36,6 +39,7 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
manager: m,
|
manager: m,
|
||||||
module: mod,
|
module: mod,
|
||||||
|
logger: log.GetLogger(ctx),
|
||||||
|
|
||||||
activeUsers: sync.NewMap[string, bool](),
|
activeUsers: sync.NewMap[string, bool](),
|
||||||
}
|
}
|
||||||
|
@ -89,7 +93,7 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
||||||
var streamInfos []helix.Stream
|
var streamInfos []helix.Stream
|
||||||
err := m.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
err := m.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error retrieving stream info", "error", err)
|
li.logger.Error("Error retrieving stream info", log.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(streamInfos) < 1 {
|
if len(streamInfos) < 1 {
|
||||||
|
@ -123,13 +127,13 @@ func setupTwitchIntegration(ctx context.Context, m *Manager, mod *chat.Module) *
|
||||||
if len(users) > 0 {
|
if len(users) > 0 {
|
||||||
err := li.manager.GivePoints(pointsToGive)
|
err := li.manager.GivePoints(pointsToGive)
|
||||||
if err != nil {
|
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
|
return li
|
||||||
}
|
}
|
||||||
|
@ -217,7 +221,7 @@ func (li *twitchIntegration) cmdRedeemReward(message helix.EventSubChannelChatMe
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Error("Error while performing redeem", "error", err)
|
li.logger.Error("Error while performing redeem", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +343,7 @@ func (li *twitchIntegration) cmdContributeGoal(message helix.EventSubChannelChat
|
||||||
// Add points to goal
|
// Add points to goal
|
||||||
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error while contributing to goal", "error", err)
|
li.logger.Error("Error while contributing to goal", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if points == 0 {
|
if points == 0 {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
twitchclient "git.sr.ht/~ashkeel/strimertul/twitch/client"
|
twitchclient "git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||||
|
@ -29,6 +31,7 @@ type Manager struct {
|
||||||
Goals *sync.Slice[Goal]
|
Goals *sync.Slice[Goal]
|
||||||
Queue *sync.Slice[Redeem]
|
Queue *sync.Slice[Redeem]
|
||||||
db database.Database
|
db database.Database
|
||||||
|
logger *slog.Logger
|
||||||
cooldowns map[string]time.Time
|
cooldowns map[string]time.Time
|
||||||
banlist map[string]bool
|
banlist map[string]bool
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -39,19 +42,19 @@ type Manager struct {
|
||||||
twitchIntegration *twitchIntegration
|
twitchIntegration *twitchIntegration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(db database.Database, twitchManager *twitchclient.Manager) (*Manager, error) {
|
func NewManager(ctx context.Context, db database.Database, twitchManager *twitchclient.Manager) (*Manager, error) {
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
loyaltyContext, cancelFn := context.WithCancel(ctx)
|
||||||
loyalty := &Manager{
|
loyalty := &Manager{
|
||||||
Config: sync.NewRWSync(Config{Enabled: false}),
|
Config: sync.NewRWSync(Config{Enabled: false}),
|
||||||
Rewards: sync.NewSlice[Reward](),
|
Rewards: sync.NewSlice[Reward](),
|
||||||
Goals: sync.NewSlice[Goal](),
|
Goals: sync.NewSlice[Goal](),
|
||||||
Queue: sync.NewSlice[Redeem](),
|
Queue: sync.NewSlice[Redeem](),
|
||||||
|
logger: log.GetLogger(ctx),
|
||||||
db: db,
|
db: db,
|
||||||
points: sync.NewMap[string, PointsEntry](),
|
points: sync.NewMap[string, PointsEntry](),
|
||||||
cooldowns: make(map[string]time.Time),
|
cooldowns: make(map[string]time.Time),
|
||||||
banlist: make(map[string]bool),
|
banlist: make(map[string]bool),
|
||||||
ctx: ctx,
|
ctx: loyaltyContext,
|
||||||
cancelFn: cancelFn,
|
cancelFn: cancelFn,
|
||||||
restartTwitchHandler: make(chan struct{}),
|
restartTwitchHandler: make(chan struct{}),
|
||||||
twitchManager: twitchManager,
|
twitchManager: twitchManager,
|
||||||
|
@ -116,14 +119,14 @@ func NewManager(db database.Database, twitchManager *twitchclient.Manager) (*Man
|
||||||
// SubscribePrefix for changes
|
// SubscribePrefix for changes
|
||||||
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
loyalty.SetBanList(config.BanList)
|
||||||
|
|
||||||
// Start twitch handler
|
// Start twitch handler
|
||||||
if twitchManager.Client() != nil {
|
if twitchManager.Client() != nil {
|
||||||
loyalty.twitchIntegration = setupTwitchIntegration(ctx, loyalty, twitchManager.Client().Chat)
|
loyalty.twitchIntegration = setupTwitchIntegration(loyaltyContext, loyalty, twitchManager.Client().Chat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return loyalty, nil
|
return loyalty, nil
|
||||||
|
@ -186,7 +189,7 @@ func (m *Manager) update(key, value string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
slog.Debug("Updated key", slog.String("key", key))
|
slog.Debug("Updated key", slog.String("key", key))
|
||||||
}
|
}
|
||||||
|
|
18
main.go
18
main.go
|
@ -4,12 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
corelog "log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
"github.com/apenwarr/fixconsole"
|
"github.com/apenwarr/fixconsole"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -25,8 +27,6 @@ var appVersion = devVersionMarker
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crashReportURL = "https://crash.strimertul.stream/upload"
|
crashReportURL = "https://crash.strimertul.stream/upload"
|
||||||
logFilename = "strimertul.log"
|
|
||||||
panicFilename = "strimertul-panic.log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend/dist
|
//go:embed frontend/dist
|
||||||
|
@ -34,7 +34,7 @@ var frontend embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
|
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
|
||||||
log.Fatal(err)
|
corelog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var panicLog *os.File
|
var panicLog *os.File
|
||||||
|
@ -88,13 +88,13 @@ func main() {
|
||||||
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
|
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
|
||||||
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
|
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
|
||||||
}
|
}
|
||||||
initLogger(level)
|
log.Init(level)
|
||||||
|
|
||||||
// Create file for panics
|
// Create file for panics
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
slog.Warn("Could not create panic log", "error", err)
|
slog.Warn("Could not create panic log", log.Error(err))
|
||||||
} else {
|
} else {
|
||||||
utils.RedirectStderr(panicLog)
|
utils.RedirectStderr(panicLog)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
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) {
|
func warnOnError(err error, text string, fields ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fields = append(fields, "error", err)
|
fields = append(fields, log.Error(err))
|
||||||
slog.Warn(text, fields...)
|
slog.Warn(text, fields...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"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
|
// Check if the app needs to be authorized again
|
||||||
scopesMatch, err := twitch.CheckScopes(client.DB)
|
scopesMatch, err := twitch.CheckScopes(client.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Could not check scopes for problems", "error", err)
|
slog.Warn("Could not check scopes for problems", log.Error(err))
|
||||||
} else {
|
} else {
|
||||||
if !scopesMatch {
|
if !scopesMatch {
|
||||||
problems = append(problems, Problem{
|
problems = append(problems, Problem{
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
)
|
)
|
||||||
|
@ -12,7 +14,7 @@ import (
|
||||||
func (m *Module) onEventSubEvent(_ string, value string) {
|
func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var ev eventSubNotification
|
var ev eventSubNotification
|
||||||
if err := json.Unmarshal([]byte(value), &ev); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
switch ev.Subscription.Type {
|
switch ev.Subscription.Type {
|
||||||
|
@ -24,7 +26,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as a follow event
|
// Parse as a follow event
|
||||||
var followEv helix.EventSubChannelFollowEvent
|
var followEv helix.EventSubChannelFollowEvent
|
||||||
if err := json.Unmarshal(ev.Event, &followEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &followEv); err != nil {
|
||||||
m.logger.Warn("Error parsing follow event", "error", err)
|
m.logger.Warn("Error parsing follow event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message
|
// Pick a random message
|
||||||
|
@ -49,7 +51,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var raidEv helix.EventSubChannelRaidEvent
|
var raidEv helix.EventSubChannelRaidEvent
|
||||||
|
|
||||||
if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
|
||||||
m.logger.Warn("Error parsing raid event", "error", err)
|
m.logger.Warn("Error parsing raid event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
@ -83,7 +85,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as cheer event
|
// Parse as cheer event
|
||||||
var cheerEv helix.EventSubChannelCheerEvent
|
var cheerEv helix.EventSubChannelCheerEvent
|
||||||
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
|
||||||
m.logger.Warn("Error parsing cheer event", "error", err)
|
m.logger.Warn("Error parsing cheer event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
@ -117,7 +119,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as subscription event
|
// Parse as subscription event
|
||||||
var subEv helix.EventSubChannelSubscribeEvent
|
var subEv helix.EventSubChannelSubscribeEvent
|
||||||
if err := json.Unmarshal(ev.Event, &subEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &subEv); err != nil {
|
||||||
m.logger.Warn("Error parsing new subscription event", "error", err)
|
m.logger.Warn("Error parsing new subscription event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.addMixedEvent(subEv)
|
m.addMixedEvent(subEv)
|
||||||
|
@ -130,7 +132,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
var subEv helix.EventSubChannelSubscriptionMessageEvent
|
var subEv helix.EventSubChannelSubscriptionMessageEvent
|
||||||
err := json.Unmarshal(ev.Event, &subEv)
|
err := json.Unmarshal(ev.Event, &subEv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Error parsing returning subscription event", "error", err)
|
m.logger.Warn("Error parsing returning subscription event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.addMixedEvent(subEv)
|
m.addMixedEvent(subEv)
|
||||||
|
@ -142,7 +144,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
|
||||||
// Parse as gift event
|
// Parse as gift event
|
||||||
var giftEv helix.EventSubChannelSubscriptionGiftEvent
|
var giftEv helix.EventSubChannelSubscriptionGiftEvent
|
||||||
if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
|
if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
|
||||||
m.logger.Warn("Error parsing subscription gifted event", "error", err)
|
m.logger.Warn("Error parsing subscription gifted event", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Pick a random message from base set
|
// Pick a random message from base set
|
||||||
|
|
|
@ -8,8 +8,9 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
templater "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -33,14 +34,14 @@ type Module struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
db database.Database
|
db database.Database
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
templater template2.Engine
|
templater templater.Engine
|
||||||
templates templateCacheMap
|
templates templateCacheMap
|
||||||
|
|
||||||
pendingMux sync.Mutex
|
pendingMux sync.Mutex
|
||||||
pendingSubs map[string]subMixedEvent
|
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{
|
mod := &Module{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -52,12 +53,12 @@ func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templ
|
||||||
|
|
||||||
// Load config from database
|
// Load config from database
|
||||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||||
logger.Debug("Config load error", "error", err)
|
logger.Debug("Config load error", log.Error(err))
|
||||||
mod.Config = Config{}
|
mod.Config = Config{}
|
||||||
// Save empty config
|
// Save empty config
|
||||||
err = db.PutJSON(ConfigKey, mod.Config)
|
err = db.PutJSON(ConfigKey, mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not save default config for bot alerts", "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) {
|
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||||
err := json.Unmarshal([]byte(value), &mod.Config)
|
err := json.Unmarshal([]byte(value), &mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Error loading alert config", "error", err)
|
logger.Warn("Error loading alert config", log.Error(err))
|
||||||
} else {
|
} else {
|
||||||
logger.Info("Reloaded alert config")
|
logger.Info("Reloaded alert config")
|
||||||
}
|
}
|
||||||
mod.compileTemplates()
|
mod.compileTemplates()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error("Could not set-up bot alert reload subscription", "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 {
|
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")
|
logger.Debug("Loaded bot alerts")
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,7 +44,7 @@ func (m *Module) compileTemplates() {
|
||||||
func (m *Module) addTemplate(templateList templateCache, message string) {
|
func (m *Module) addTemplate(templateList templateCache, message string) {
|
||||||
tpl, err := m.templater.MakeTemplate(message)
|
tpl, err := m.templater.MakeTemplate(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Error("Error compiling alert template", "error", err)
|
m.logger.Error("Error compiling alert template", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
templateList[message] = tpl
|
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) {
|
func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tpl.Execute(&buf, data); err != nil {
|
if err := tpl.Execute(&buf, data); err != nil {
|
||||||
m.logger.Error("Error executing template for bot alert", "error", err)
|
m.logger.Error("Error executing template for bot alert", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var accessLevels = map[AccessLevelType]int{
|
var accessLevels = map[AccessLevelType]int{
|
||||||
|
@ -70,7 +72,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := tpl.Execute(&buf, message); err != nil {
|
if err := tpl.Execute(&buf, message); err != nil {
|
||||||
mod.logger.Error("Failed to execute custom command template", "error", err)
|
mod.logger.Error("Failed to execute custom command template", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
textTemplate "text/template"
|
textTemplate "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"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,
|
CommandCooldown: 2,
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
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
|
// 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) {
|
if errors.Is(err, database.ErrEmptyKey) {
|
||||||
customCommands = make(map[string]CustomCommand)
|
customCommands = make(map[string]CustomCommand)
|
||||||
} else {
|
} else {
|
||||||
logger.Error("Failed to load custom commands", "error", err)
|
logger.Error("Failed to load custom commands", log.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod.customCommands.Set(customCommands)
|
mod.customCommands.Set(customCommands)
|
||||||
|
|
||||||
if err := mod.updateTemplates(); err != nil {
|
if err := mod.updateTemplates(); err != nil {
|
||||||
logger.Error("Failed to parse custom commands", "error", err)
|
logger.Error("Failed to parse custom commands", log.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
||||||
logger.Error("Could not set-up chat command reload subscription", "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 {
|
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
|
return mod
|
||||||
|
@ -98,7 +100,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
||||||
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(newValue), &chatMessage); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
||||||
func (mod *Module) handleWriteMessageRPC(value string) {
|
func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
var request WriteMessageRequest
|
var request WriteMessageRequest
|
||||||
if err := json.Unmarshal([]byte(value), &request); err != nil {
|
if err := json.Unmarshal([]byte(value), &request); err != nil {
|
||||||
mod.logger.Warn("Failed to decode write message request", "error", err)
|
mod.logger.Warn("Failed to decode write message request", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +159,7 @@ func (mod *Module) handleWriteMessageRPC(value string) {
|
||||||
Message: request.Message,
|
Message: request.Message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to send announcement", "error", err)
|
mod.logger.Error("Failed to send announcement", log.Error(err))
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
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,
|
Message: request.Message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
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,
|
ReplyParentMessageID: request.ReplyTo,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 != "" {
|
if resp.Error != "" {
|
||||||
mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
|
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) {
|
func (mod *Module) updateCommands(value string) {
|
||||||
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mod.logger.Error("Failed to decode new custom commands", "error", err)
|
mod.logger.Error("Failed to decode new custom commands", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Recreate templates
|
// Recreate templates
|
||||||
if err := mod.updateTemplates(); err != nil {
|
if err := mod.updateTemplates(); err != nil {
|
||||||
mod.logger.Error("Failed to update custom commands templates", "error", err)
|
mod.logger.Error("Failed to update custom commands templates", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +238,7 @@ func (mod *Module) GetChatters() (users []string) {
|
||||||
for {
|
for {
|
||||||
userClient, err := twitch.GetUserClient(mod.db, false)
|
userClient, err := twitch.GetUserClient(mod.db, false)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
|
||||||
|
@ -246,7 +248,7 @@ func (mod *Module) GetChatters() (users []string) {
|
||||||
After: cursor,
|
After: cursor,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
for _, user := range res.Data.Chatters {
|
for _, user := range res.Data.Chatters {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteMessageRequest is an RPC to send a chat message with extra options
|
// 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) {
|
func WriteMessage(db database.Database, logger *slog.Logger, m WriteMessageRequest) {
|
||||||
err := db.PutJSON(WriteMessageRPC, m)
|
err := db.PutJSON(WriteMessageRPC, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to write chat message", "error", err)
|
logger.Error("Failed to write chat message", log.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"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) {
|
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer) (*Manager, error) {
|
||||||
|
logger := log.GetLogger(ctx)
|
||||||
|
|
||||||
// Get Twitch config
|
// Get Twitch config
|
||||||
var config twitch.Config
|
var config twitch.Config
|
||||||
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
||||||
|
@ -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) {
|
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
||||||
var newConfig twitch.Config
|
var newConfig twitch.Config
|
||||||
if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +64,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
clientContext, cancel = context.WithCancel(ctx)
|
clientContext, cancel = context.WithCancel(ctx)
|
||||||
updatedClient, err = newClient(clientContext, newConfig, db, server)
|
updatedClient, err = newClient(clientContext, newConfig, db, server)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +72,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
updatedClient.Merge(manager.client)
|
updatedClient.Merge(manager.client)
|
||||||
manager.client = updatedClient
|
manager.client = updatedClient
|
||||||
|
|
||||||
slog.Info("Reloaded/updated Twitch integration")
|
logger.Info("Reloaded/updated Twitch integration")
|
||||||
}); err != nil {
|
}); 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
|
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 {
|
if userClient, err := twitch.GetUserClient(db, true); err == nil {
|
||||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
users, err := userClient.GetUsers(&helix.UsersParams{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Logger.Error("Failed looking up user", "error", err)
|
client.Logger.Error("Failed looking up user", log.Error(err))
|
||||||
} else if len(users.Data.Users) < 1 {
|
} else if len(users.Data.Users) < 1 {
|
||||||
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,7 +152,7 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
|
||||||
client.User = users.Data.Users[0]
|
client.User = users.Data.Users[0]
|
||||||
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
|
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Logger.Error("Failed to setup EventSub", "error", err)
|
client.Logger.Error("Failed to setup EventSub", log.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl := client.GetTemplateEngine()
|
tpl := client.GetTemplateEngine()
|
||||||
|
@ -184,14 +188,14 @@ func (c *Client) runStatusPoll() {
|
||||||
UserIDs: []string{c.User.ID},
|
UserIDs: []string{c.User.ID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Error("Error checking stream status", "error", err)
|
c.Logger.Error("Error checking stream status", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
||||||
|
|
||||||
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Warn("Error saving stream info", "error", err)
|
c.Logger.Warn("Error saving stream info", log.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
textTemplate "text/template"
|
textTemplate "text/template"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"github.com/Masterminds/sprig/v3"
|
"github.com/Masterminds/sprig/v3"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||||
|
@ -57,7 +59,7 @@ func (c *Client) GetTemplateEngine() template.Engine {
|
||||||
counter++
|
counter++
|
||||||
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
|
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Error("Error saving key", "error", err, slog.String("key", counterKey))
|
c.Logger.Error("Error saving key", slog.String("key", counterKey), log.Error(err))
|
||||||
}
|
}
|
||||||
return counter
|
return counter
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/utils"
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
@ -55,7 +57,7 @@ func (c *Client) eventSubLoop() {
|
||||||
for endpoint != "" {
|
for endpoint != "" {
|
||||||
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("EventSub websocket read error", "error", err)
|
c.logger.Error("EventSub websocket read error", log.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if connection != nil {
|
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) {
|
func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) {
|
||||||
connection, _, err := websocket.DefaultDialer.Dial(url, nil)
|
connection, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Could not establish a connection to the EventSub websocket", "error", err)
|
c.logger.Error("Could not establish a connection to the EventSub websocket", log.Error(err))
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +108,7 @@ func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (st
|
||||||
var wsMessage WebsocketMessage
|
var wsMessage WebsocketMessage
|
||||||
err = json.Unmarshal(messageData, &wsMessage)
|
err = json.Unmarshal(messageData, &wsMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub message", "error", err)
|
c.logger.Error("Error decoding EventSub message", log.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +127,7 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
|
||||||
var welcomeData WelcomeMessagePayload
|
var welcomeData WelcomeMessagePayload
|
||||||
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
|
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub welcome message", 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
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("Connection to EventSub websocket established", slog.String("session-id", welcomeData.Session.ID))
|
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
|
// Add subscription to websocket session
|
||||||
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Could not add subscriptions", "error", err)
|
c.logger.Error("Could not add subscriptions", log.Error(err))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session_reconnect":
|
case "session_reconnect":
|
||||||
var reconnectData WelcomeMessagePayload
|
var reconnectData WelcomeMessagePayload
|
||||||
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
|
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub session reconnect parameters", 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
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("EventSub websocket requested a reconnection", slog.String("session-id", reconnectData.Session.ID), slog.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))
|
||||||
|
@ -173,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
var notificationData NotificationMessagePayload
|
var notificationData NotificationMessagePayload
|
||||||
err := json.Unmarshal(message.Payload, ¬ificationData)
|
err := json.Unmarshal(message.Payload, ¬ificationData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error decoding EventSub notification payload", 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()
|
notificationData.Date = time.Now()
|
||||||
|
|
||||||
|
@ -181,7 +183,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
||||||
err = c.db.PutJSON(eventKey, notificationData)
|
err = c.db.PutJSON(eventKey, notificationData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error storing event to database", slog.String("key", eventKey), "error", err)
|
c.logger.Error("Error storing event to database", slog.String("key", eventKey), log.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var archive []NotificationMessagePayload
|
var archive []NotificationMessagePayload
|
||||||
|
@ -195,7 +197,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
||||||
}
|
}
|
||||||
err = c.db.PutJSON(historyKey, archive)
|
err = c.db.PutJSON(historyKey, archive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("Error storing event to database", 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"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||||
|
@ -42,25 +44,25 @@ func Setup(ctx context.Context, db database.Database, logger *slog.Logger) *Modu
|
||||||
|
|
||||||
// Load config from database
|
// Load config from database
|
||||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||||
logger.Debug("Config load error", "error", err)
|
logger.Debug("Config load error", log.Error(err))
|
||||||
mod.Config = Config{
|
mod.Config = Config{
|
||||||
Timers: make(map[string]ChatTimer),
|
Timers: make(map[string]ChatTimer),
|
||||||
}
|
}
|
||||||
// Save empty config
|
// Save empty config
|
||||||
err = db.PutJSON(ConfigKey, mod.Config)
|
err = db.PutJSON(ConfigKey, mod.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Could not save default config for bot timers", "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 := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||||
if err := json.Unmarshal([]byte(value), &mod.Config); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
logger.Info("Reloaded timer config")
|
logger.Info("Reloaded timer config")
|
||||||
}); err != nil {
|
}); 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)))
|
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())
|
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Error saving chat activity", "error", err)
|
m.logger.Warn("Error saving chat activity", log.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate activity
|
// Calculate activity
|
||||||
|
|
|
@ -5,11 +5,13 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Close(res io.Closer) {
|
func Close(res io.Closer) {
|
||||||
err := res.Close()
|
err := res.Close()
|
||||||
if err != nil {
|
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"
|
"net/http/pprof"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~ashkeel/strimertul/log"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/containers/sync"
|
"git.sr.ht/~ashkeel/containers/sync"
|
||||||
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
||||||
"git.sr.ht/~ashkeel/strimertul/database"
|
"git.sr.ht/~ashkeel/strimertul/database"
|
||||||
|
@ -21,7 +23,9 @@ import (
|
||||||
|
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
Config *sync.RWSync[ServerConfig]
|
Config *sync.RWSync[ServerConfig]
|
||||||
|
ctx context.Context
|
||||||
db database.Database
|
db database.Database
|
||||||
|
logger *slog.Logger
|
||||||
server Server
|
server Server
|
||||||
frontend fs.FS
|
frontend fs.FS
|
||||||
hub *kv.Hub
|
hub *kv.Hub
|
||||||
|
@ -32,9 +36,11 @@ type WebServer struct {
|
||||||
factory ServerFactory
|
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{
|
server := &WebServer{
|
||||||
db: db,
|
db: db,
|
||||||
|
ctx: ctx,
|
||||||
|
logger: log.GetLogger(ctx),
|
||||||
server: nil,
|
server: nil,
|
||||||
requestedRoutes: sync.NewMap[string, http.Handler](),
|
requestedRoutes: sync.NewMap[string, http.Handler](),
|
||||||
restart: sync.NewRWSync(false),
|
restart: sync.NewRWSync(false),
|
||||||
|
@ -46,7 +52,7 @@ func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, e
|
||||||
err := db.GetJSON(ServerConfigKey, &config)
|
err := db.GetJSON(ServerConfigKey, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, database.ErrEmptyKey) {
|
if !errors.Is(err, database.ErrEmptyKey) {
|
||||||
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
|
// Initialize with default config
|
||||||
server.Config.Set(ServerConfig{
|
server.Config.Set(ServerConfig{
|
||||||
|
@ -155,7 +161,7 @@ func (s *WebServer) Listen() error {
|
||||||
for {
|
for {
|
||||||
// Read config and make http request mux
|
// Read config and make http request mux
|
||||||
config := s.Config.Get()
|
config := s.Config.Get()
|
||||||
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()
|
s.mux = s.makeMux()
|
||||||
|
|
||||||
// Make HTTP server instance
|
// Make HTTP server instance
|
||||||
|
@ -167,25 +173,25 @@ func (s *WebServer) Listen() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start HTTP server
|
// 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()
|
err = s.server.Start()
|
||||||
|
|
||||||
// If the server died, we need to see what to do
|
// If the server died, we need to see what to do
|
||||||
slog.Debug("HTTP server died", "error", err)
|
s.logger.Debug("HTTP server died", log.Error(err))
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
exit <- err
|
exit <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we trying to close or restart?
|
// Are we trying to close or restart?
|
||||||
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() {
|
if s.restart.Get() {
|
||||||
s.restart.Set(false)
|
s.restart.Set(false)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
slog.Debug("HTTP server stalled")
|
s.logger.Debug("HTTP server stalled")
|
||||||
exit <- nil
|
exit <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -198,7 +204,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
||||||
var config ServerConfig
|
var config ServerConfig
|
||||||
err := json.Unmarshal([]byte(value), &config)
|
err := json.Unmarshal([]byte(value), &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to unmarshal config", "error", err)
|
s.logger.Error("Failed to unmarshal config", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +221,7 @@ func (s *WebServer) onConfigUpdate(value string) {
|
||||||
s.restart.Set(true)
|
s.restart.Set(true)
|
||||||
err = s.server.Shutdown(context.Background())
|
err = s.server.Shutdown(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to shutdown server", "error", err)
|
s.logger.Error("Failed to shutdown server", log.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue