strimertul/app.go

390 lines
9.8 KiB
Go
Raw Permalink Normal View History

2022-11-16 11:23:54 +00:00
package main
import (
2023-04-14 18:04:21 +00:00
"bytes"
2022-11-16 11:23:54 +00:00
"context"
2024-03-15 22:48:34 +00:00
"encoding/json"
2023-04-14 18:04:21 +00:00
"errors"
"fmt"
"io"
"log/slog"
2023-04-14 18:04:21 +00:00
"mime/multipart"
"net/http"
2023-04-14 18:04:21 +00:00
"os"
"runtime/debug"
2022-11-18 19:28:13 +00:00
"strconv"
2024-03-16 00:20:15 +00:00
"git.sr.ht/~ashkeel/strimertul/log"
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/containers/sync"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
2024-03-15 22:48:34 +00:00
"github.com/nicklaw5/helix/v2"
"github.com/urfave/cli/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/docs"
"git.sr.ht/~ashkeel/strimertul/loyalty"
2024-03-12 23:50:59 +00:00
"git.sr.ht/~ashkeel/strimertul/migrations"
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/strimertul/twitch"
2024-03-12 23:50:59 +00:00
"git.sr.ht/~ashkeel/strimertul/twitch/client"
2023-11-10 20:36:15 +00:00
"git.sr.ht/~ashkeel/strimertul/webserver"
2022-11-16 11:23:54 +00:00
)
// App struct
type App struct {
2023-04-14 18:04:21 +00:00
ctx context.Context
cliParams *cli.Context
2024-02-25 13:58:35 +00:00
driver database.Driver
2023-04-14 18:04:21 +00:00
ready *sync.RWSync[bool]
isFatalError *sync.RWSync[bool]
backupOptions database.BackupOptions
cancelLogs database.CancelFunc
2024-03-16 00:20:15 +00:00
logger *slog.Logger
2022-11-30 18:15:47 +00:00
db *database.LocalDBClient
2024-03-10 16:38:18 +00:00
twitchManager *client.Manager
httpServer *webserver.WebServer
2022-11-30 18:15:47 +00:00
loyaltyManager *loyalty.Manager
2022-11-16 11:23:54 +00:00
}
// NewApp creates a new App application struct
func NewApp(cliParams *cli.Context) *App {
return &App{
2023-04-14 18:04:21 +00:00
cliParams: cliParams,
ready: sync.NewRWSync(false),
isFatalError: sync.NewRWSync(false),
2024-03-16 00:20:15 +00:00
logger: slog.Default(),
}
2022-11-16 11:23:54 +00:00
}
// startup is called when the app starts
2022-11-16 11:23:54 +00:00
func (a *App) startup(ctx context.Context) {
2023-04-14 18:04:21 +00:00
defer func() {
if r := recover(); r != nil {
a.stop(ctx)
switch v := r.(type) {
case error:
a.showFatalError(v, v.Error())
default:
a.showFatalError(errors.New(fmt.Sprint(v)), "Runtime error encountered")
}
}
}()
slog.Info("Started", slog.String("version", appVersion))
2023-04-19 16:06:41 +00:00
2022-11-16 11:23:54 +00:00
a.ctx = ctx
2023-05-03 12:37:28 +00:00
2023-04-14 18:04:21 +00:00
a.backupOptions = database.BackupOptions{
BackupDir: a.cliParams.String("backup-dir"),
BackupInterval: a.cliParams.Int("backup-interval"),
MaxBackups: a.cliParams.Int("max-backups"),
}
2023-05-03 12:37:28 +00:00
// Initialize database
if err := a.initializeDatabase(); err != nil {
2023-05-03 12:37:28 +00:00
a.showFatalError(err, "Failed to initialize database")
return
}
2024-03-12 22:39:18 +00:00
// Check for migrations
if err := migrations.Run(a.driver, a.db, slog.With(slog.String("operation", "migration"))); err != nil {
2024-03-12 22:39:18 +00:00
a.showFatalError(err, "Failed to migrate database to latest version")
return
}
2023-05-03 12:37:28 +00:00
// Initialize components
if err := a.initializeComponents(); err != nil {
2023-05-03 12:37:28 +00:00
a.showFatalError(err, "Failed to initialize required component")
return
}
// Set meta keys
2023-11-05 12:34:33 +00:00
_ = a.db.PutKey(docs.VersionKey, appVersion)
2023-05-03 12:37:28 +00:00
a.ready.Set(true)
runtime.EventsEmit(ctx, "ready", true)
slog.Info("Strimertul is ready")
2023-05-03 12:37:28 +00:00
// Add logs I/O to UI
2024-02-25 13:46:59 +00:00
a.cancelLogs, _ = a.listenForLogs()
2023-05-03 12:37:28 +00:00
go a.forwardLogs()
// Run HTTP server
if err := a.httpServer.Listen(); err != nil {
2023-05-03 12:37:28 +00:00
a.showFatalError(err, "HTTP server stopped")
return
}
}
func (a *App) initializeDatabase() error {
var err error
// Make KV hub
2022-11-30 18:15:47 +00:00
a.driver, err = database.GetDatabaseDriver(a.cliParams)
2023-04-14 18:04:21 +00:00
if err != nil {
2023-05-03 12:37:28 +00:00
return fmt.Errorf("could not get database driver: %w", err)
2023-04-14 18:04:21 +00:00
}
// Start database backup task
2023-04-14 18:04:21 +00:00
if a.backupOptions.BackupInterval > 0 {
go BackupTask(a.driver, a.backupOptions)
}
hub := a.driver.Hub()
go hub.Run()
2023-04-19 16:06:41 +00:00
hub.UseInteractiveAuth(a.interactiveAuth)
a.db, err = database.NewLocalClient(hub)
2023-04-14 18:04:21 +00:00
if err != nil {
2023-05-03 12:37:28 +00:00
return fmt.Errorf("could not initialize database client: %w", err)
2023-04-14 18:04:21 +00:00
}
2023-05-03 12:37:28 +00:00
return nil
}
func (a *App) initializeComponents() error {
var err error
// Create logger and endpoints
2024-03-16 00:20:15 +00:00
a.httpServer, err = webserver.NewServer(
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "webserver"))),
a.db,
webserver.DefaultServerFactory,
)
2023-04-14 18:04:21 +00:00
if err != nil {
2023-05-03 12:37:28 +00:00
return fmt.Errorf("could not initialize http server: %w", err)
2023-04-14 18:04:21 +00:00
}
2022-11-30 18:15:47 +00:00
// Create twitch client
2024-03-16 00:20:15 +00:00
a.twitchManager, err = client.NewManager(
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "twitch"))),
a.db,
a.httpServer,
)
2023-04-14 18:04:21 +00:00
if err != nil {
2023-05-03 12:37:28 +00:00
return fmt.Errorf("could not initialize twitch client: %w", err)
2023-04-14 18:04:21 +00:00
}
2022-11-30 18:15:47 +00:00
// Initialize loyalty system
2024-03-16 00:20:15 +00:00
a.loyaltyManager, err = loyalty.NewManager(
log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "logger"))),
a.db,
a.twitchManager,
)
2023-04-14 18:04:21 +00:00
if err != nil {
2023-05-03 12:37:28 +00:00
return fmt.Errorf("could not initialize loyalty manager: %w", err)
2023-04-14 18:04:21 +00:00
}
2023-05-03 12:37:28 +00:00
return nil
}
2024-02-25 13:46:59 +00:00
func (a *App) listenForLogs() (database.CancelFunc, error) {
2023-11-05 12:34:33 +00:00
return a.db.SubscribeKey(docs.LogRPCKey, func(newValue string) {
var entry docs.ExternalLog
if err := json.Unmarshal([]byte(newValue), &entry); err != nil {
return
}
var level slog.Level
if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
level = slog.LevelInfo
}
2024-03-16 00:20:15 +00:00
slog.Log(a.ctx, level, entry.Message, log.ParseLogFields(entry.Data)...)
})
}
2023-05-03 12:37:28 +00:00
func (a *App) forwardLogs() {
2024-03-16 00:20:15 +00:00
for entry := range log.IncomingLogs {
2023-05-03 12:37:28 +00:00
runtime.EventsEmit(a.ctx, "log-event", entry)
2023-04-14 18:04:21 +00:00
}
}
func (a *App) stop(_ context.Context) {
if a.cancelLogs != nil {
a.cancelLogs()
}
2022-11-30 18:15:47 +00:00
if a.loyaltyManager != nil {
2023-04-23 00:26:46 +00:00
warnOnError(a.loyaltyManager.Close(), "Could not cleanly close loyalty manager")
2022-11-30 18:15:47 +00:00
}
if a.httpServer != nil {
2023-04-23 00:26:46 +00:00
warnOnError(a.httpServer.Close(), "Could not cleanly close HTTP server")
2022-11-30 18:15:47 +00:00
}
2023-04-23 00:26:46 +00:00
warnOnError(a.db.Close(), "Could not cleanly close database")
2023-04-23 00:26:46 +00:00
warnOnError(a.driver.Close(), "Could not close driver")
}
2022-11-18 19:28:13 +00:00
func (a *App) AuthenticateKVClient(id string) {
idInt, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return
}
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", slog.String("session-id", id))
2022-11-18 19:28:13 +00:00
}
func (a *App) IsServerReady() bool {
2022-11-30 18:15:47 +00:00
return a.ready.Get()
2022-11-18 19:28:13 +00:00
}
2023-04-14 18:04:21 +00:00
func (a *App) IsFatalError() bool {
return a.isFatalError.Get()
}
2022-11-18 19:28:13 +00:00
func (a *App) GetKilovoltBind() string {
2022-11-30 18:15:47 +00:00
if a.httpServer == nil {
return ""
}
return a.httpServer.Config.Get().Bind
}
func (a *App) GetTwitchAuthURL(state string) string {
return twitch.GetAuthorizationURL(a.twitchManager.Client().API, state)
}
func (a *App) GetTwitchLoggedUser(key string) (helix.User, error) {
userClient, err := twitch.GetUserClient(a.db, key, false)
if err != nil {
return helix.User{}, err
}
users, err := userClient.GetUsers(&helix.UsersParams{})
if err != nil {
return helix.User{}, err
}
if len(users.Data.Users) < 1 {
return helix.User{}, errors.New("no users found")
}
return users.Data.Users[0], nil
2022-11-16 11:23:54 +00:00
}
2024-03-16 00:20:15 +00:00
func (a *App) GetLastLogs() []log.Entry {
return log.LastLogs.Get()
}
func (a *App) GetDocumentation() map[string]docs.KeyObject {
return docs.Keys
}
2023-04-14 18:04:21 +00:00
func (a *App) SendCrashReport(errorData string, info string) (string, error) {
var b bytes.Buffer
w := multipart.NewWriter(&b)
// Add text fields
if err := w.WriteField("error", errorData); err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not encode field error for crash report", log.Error(err))
2023-04-14 18:04:21 +00:00
}
if len(info) > 0 {
if err := w.WriteField("info", info); err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not encode field info for crash report", log.Error(err))
2023-04-14 18:04:21 +00:00
}
}
// Add log files
2024-03-16 00:20:15 +00:00
addFile(w, "log", log.Filename)
addFile(w, "paniclog", log.PanicFilename)
2023-04-14 18:04:21 +00:00
if err := w.Close(); err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not prepare request for crash report", log.Error(err))
2023-04-14 18:04:21 +00:00
return "", err
}
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
2023-04-14 18:04:21 +00:00
if err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not send crash report", log.Error(err))
2023-04-14 18:04:21 +00:00
return "", err
}
2024-02-25 13:58:35 +00:00
defer resp.Body.Close()
2023-04-14 18:04:21 +00:00
// Check the response
if resp.StatusCode != http.StatusOK {
2023-04-14 18:04:21 +00:00
byt, _ := io.ReadAll(resp.Body)
slog.Error("Crash report server returned error", slog.String("status", resp.Status), slog.String("response", string(byt)))
2023-04-14 18:04:21 +00:00
return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
}
byt, err := io.ReadAll(resp.Body)
return string(byt), err
}
type VersionInfo struct {
Release string `json:"release"`
BuildInfo *debug.BuildInfo `json:"build"`
}
func (a *App) GetAppVersion() VersionInfo {
info, _ := debug.ReadBuildInfo()
return VersionInfo{
Release: appVersion,
BuildInfo: info,
}
}
2023-04-14 18:04:21 +00:00
func (a *App) TestTemplate(message string, data any) error {
2024-03-10 16:38:18 +00:00
tpl, err := a.twitchManager.Client().GetTemplateEngine().MakeTemplate(message)
if err != nil {
return err
}
return tpl.Execute(io.Discard, data)
}
func (a *App) TestCommandTemplate(message string) error {
return a.TestTemplate(message, twitch.TestMessageData)
}
func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
2023-04-19 16:06:41 +00:00
callbackID := fmt.Sprintf("auth-callback-%d", client.UID())
authResult := make(chan bool)
runtime.EventsOnce(a.ctx, callbackID, func(optional ...any) {
if len(optional) == 0 {
authResult <- false
return
}
val, _ := optional[0].(bool)
authResult <- val
})
runtime.EventsEmit(a.ctx, "interactiveAuth", client.UID(), message, callbackID)
return <-authResult
}
func (a *App) showFatalError(err error, text string, fields ...any) {
2023-04-14 18:04:21 +00:00
if err != nil {
fields = append(fields, log.ErrorSkip(err, 2), slog.String("Z", string(debug.Stack())))
slog.Error(text, fields...)
2023-04-14 18:04:21 +00:00
runtime.EventsEmit(a.ctx, "fatalError")
a.isFatalError.Set(true)
}
}
func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.ErrorDialog,
Title: "strimertul is already running",
Message: "Only one copy of strimertul can run at the same time, make sure to close other instances first",
})
}
2023-04-14 18:04:21 +00:00
func addFile(m *multipart.Writer, field string, filename string) {
logfile, err := m.CreateFormFile(field, filename)
if err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not encode field log for crash report", log.Error(err))
2023-04-14 18:04:21 +00:00
return
}
file, err := os.Open(filename)
if err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not open file for including in crash report", slog.String("file", filename), log.Error(err))
2023-04-14 18:04:21 +00:00
return
}
if _, err = io.Copy(logfile, file); err != nil {
2024-03-16 00:20:15 +00:00
slog.Error("Could not read from file for including in crash report", slog.String("file", filename), log.Error(err))
2023-04-14 18:04:21 +00:00
}
}