diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..ad439e1 --- /dev/null +++ b/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 0 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a2cb61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db.sqlite +obs-replay-server \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6deb9a6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go" + } + ] +} \ No newline at end of file diff --git a/db/query.sql b/db/query.sql new file mode 100644 index 0000000..8ba36d9 --- /dev/null +++ b/db/query.sql @@ -0,0 +1,11 @@ +-- name: UserByName :one +SELECT * FROM users WHERE name = ? LIMIT 1; + +-- name: ReplaysForUser :many +SELECT * FROM replays JOIN users ON replays.owner = users.id WHERE users.name = ?; + +-- name: CreateUser :one +INSERT INTO users (name) VALUES (?) RETURNING *; + +-- name: AddReplay :one +INSERT INTO replays (file_path, owner) VALUES (?, ?) RETURNING *; \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..be454ba --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name text NOT NULL, + CONSTRAINT username_unique UNIQUE (name) +); + +CREATE TABLE replays ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path text NOT NULL, + owner INTEGER NOT NULL, + FOREIGN KEY (owner) REFERENCES users (id) +); diff --git a/go.mod b/go.mod index de20cb2..1aba16d 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,19 @@ go 1.20 require ( github.com/andreykaipov/goobs v0.12.0 - github.com/labstack/echo v3.3.10+incompatible + github.com/charmbracelet/log v0.2.2 github.com/labstack/echo/v4 v4.10.2 ) +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + golang.org/x/time v0.3.0 // indirect +) + require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect - github.com/charmbracelet/log v0.2.2 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect @@ -21,6 +25,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.17 github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect diff --git a/go.sum b/go.sum index 0d7e4ac..5d0ec28 100644 --- a/go.sum +++ b/go.sum @@ -13,12 +13,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= -github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= @@ -30,13 +30,13 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= @@ -50,7 +50,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -64,12 +64,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/handlers.go b/handlers/handlers.go index efa82a3..2ecd63c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -1,17 +1,63 @@ package handlers import ( + "database/sql" "html/template" + "net/http" - "github.com/charmbracelet/log" + "github.com/radiden/obs-replay-server/services" "github.com/labstack/echo/v4" ) +var sv *services.Services + +func InitHandlers(svs *services.Services) { + sv = svs +} + func IndexHandler(c echo.Context) error { t, err := template.ParseFiles("views/base.html", "views/public/index.html") if err != nil { - log.Error("failed to parse template", "err", err) + return err + } + + return t.Execute(c.Response().Writer, nil) +} + +func AuthHandler(c echo.Context) error { + userName := c.FormValue("username") + c.SetCookie(&http.Cookie{ + Name: "sdvxreplay_session", + Value: userName, + }) + + _, err := sv.DB.Queries.UserByName(c.Request().Context(), userName) + if err == sql.ErrNoRows { + _, err := sv.DB.Queries.CreateUser(c.Request().Context(), userName) + if err != nil { + return err + } + } else if err != nil { + return err + } + + t, err := template.ParseFiles("views/base.html", "views/public/login_success.html") + if err != nil { + return err + } + + return t.Execute(c.Response().Writer, nil) +} + +func PanelHandler(c echo.Context) error { + _, err := c.Cookie("sdvxreplay_session") + if err != nil { + return c.Redirect(http.StatusBadRequest, "/") + } + t, err := template.ParseFiles("views/base.html", "views/public/panel.html") + if err != nil { + return err } return t.Execute(c.Response().Writer, nil) diff --git a/main.go b/main.go index 703e852..d288022 100644 --- a/main.go +++ b/main.go @@ -1,42 +1,62 @@ package main import ( - "log" - - "github.com/andreykaipov/goobs" - "github.com/andreykaipov/goobs/api/events" + "github.com/charmbracelet/log" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "github.com/radiden/obs-replay-server/handlers" + "github.com/radiden/obs-replay-server/services" + + _ "embed" + + _ "github.com/mattn/go-sqlite3" ) -func main() { - // init obs connection - client, err := goobs.New("localhost:4455", goobs.WithPassword("ThpNdGXBwneYn9X7")) - if err != nil { - log.Fatal(err) - } - defer client.Disconnect() +//go:embed db/schema.sql +var initialSchema string - isRecording, err := client.Outputs.GetReplayBufferStatus() +var sv *services.Services + +func main() { + sv = services.InitServices(initialSchema) + handlers.InitHandlers(sv) + + isRecording, err := sv.OBS.Outputs.GetReplayBufferStatus() if err != nil { - panic(err) + log.Fatal("couldn't get replay buffer status", "error", err) } if !isRecording.OutputActive { - client.Outputs.StartReplayBuffer() + sv.OBS.Outputs.StartReplayBuffer() } - // init echo + // start echo e := echo.New() + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + if err == nil { + log.Info("[request]", "uri", v.URI, "status", v.Status) + } else { + log.Error("[request]", "uri", v.URI, "status", v.Status, "err", v.Error) + } + return nil + }, + })) + e.Static("/assets", "assets") e.GET("/", handlers.IndexHandler) + e.POST("/auth", handlers.AuthHandler) + e.GET("/panel", handlers.PanelHandler) e.Logger.Fatal(e.Start(":1323")) } -func getSavedPath(client *goobs.Client) string { - for { - msg := <-client.IncomingEvents - switch m := msg.(type) { - case *events.ReplayBufferSaved: - return m.SavedReplayPath - } - } -} +// func getSavedPath(client *goobs.Client) string { +// for { +// msg := <-client.IncomingEvents +// switch m := msg.(type) { +// case *events.ReplayBufferSaved: +// return m.SavedReplayPath +// } +// } +// } diff --git a/models/db.go b/models/db.go new file mode 100644 index 0000000..d276956 --- /dev/null +++ b/models/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 + +package models + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..d2b1d29 --- /dev/null +++ b/models/models.go @@ -0,0 +1,18 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 + +package models + +import () + +type Replay struct { + ID int64 + FilePath string + Owner int64 +} + +type User struct { + ID int64 + Name string +} diff --git a/models/query.sql.go b/models/query.sql.go new file mode 100644 index 0000000..494af13 --- /dev/null +++ b/models/query.sql.go @@ -0,0 +1,89 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 +// source: query.sql + +package models + +import ( + "context" +) + +const addReplay = `-- name: AddReplay :one +INSERT INTO replays (file_path, owner) VALUES (?, ?) RETURNING id, file_path, owner +` + +type AddReplayParams struct { + FilePath string + Owner int64 +} + +func (q *Queries) AddReplay(ctx context.Context, arg AddReplayParams) (Replay, error) { + row := q.db.QueryRowContext(ctx, addReplay, arg.FilePath, arg.Owner) + var i Replay + err := row.Scan(&i.ID, &i.FilePath, &i.Owner) + return i, err +} + +const createUser = `-- name: CreateUser :one +INSERT INTO users (name) VALUES (?) RETURNING id, name +` + +func (q *Queries) CreateUser(ctx context.Context, name string) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, name) + var i User + err := row.Scan(&i.ID, &i.Name) + return i, err +} + +const replaysForUser = `-- name: ReplaysForUser :many +SELECT replays.id, file_path, owner, users.id, name FROM replays JOIN users ON replays.owner = users.id WHERE users.name = ? +` + +type ReplaysForUserRow struct { + ID int64 + FilePath string + Owner int64 + ID_2 int64 + Name string +} + +func (q *Queries) ReplaysForUser(ctx context.Context, name string) ([]ReplaysForUserRow, error) { + rows, err := q.db.QueryContext(ctx, replaysForUser, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReplaysForUserRow + for rows.Next() { + var i ReplaysForUserRow + if err := rows.Scan( + &i.ID, + &i.FilePath, + &i.Owner, + &i.ID_2, + &i.Name, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const userByName = `-- name: UserByName :one +SELECT id, name FROM users WHERE name = ? LIMIT 1 +` + +func (q *Queries) UserByName(ctx context.Context, name string) (User, error) { + row := q.db.QueryRowContext(ctx, userByName, name) + var i User + err := row.Scan(&i.ID, &i.Name) + return i, err +} diff --git a/services/services.go b/services/services.go new file mode 100644 index 0000000..46b313e --- /dev/null +++ b/services/services.go @@ -0,0 +1,58 @@ +package services + +import ( + "context" + "database/sql" + _ "embed" + + "github.com/andreykaipov/goobs" + "github.com/charmbracelet/log" + "github.com/radiden/obs-replay-server/models" +) + +type Database struct { + Client *sql.DB + Queries *models.Queries +} + +type Services struct { + OBS *goobs.Client + DB *Database +} + +func InitServices(initialSchema string) *Services { + db, err := initDB(initialSchema) + if err != nil { + log.Fatal("couldn't initialize db", "error", err) + } + + obs, err := initOBS() + if err != nil { + log.Fatal("couldn't connect to obs", "error", err) + } + return &Services{ + DB: db, + OBS: obs, + } +} + +func initDB(initialSchema string) (*Database, error) { + ctx := context.Background() + + db, err := sql.Open("sqlite3", "file:db.sqlite") + if err != nil { + return nil, err + } + + db.ExecContext(ctx, initialSchema) + + return &Database{ + Client: db, + Queries: models.New(db), + }, nil +} + +func initOBS() (*goobs.Client, error) { + obs, err := goobs.New("localhost:4455", goobs.WithPassword("ThpNdGXBwneYn9X7")) + return obs, err +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..4585c36 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,9 @@ +version: 2 +sql: + - engine: "sqlite" + schema: "db/schema.sql" + queries: "db/query.sql" + gen: + go: + package: "models" + out: "models" diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..a61cafe Binary files /dev/null and b/tmp/main differ diff --git a/views/base.html b/views/base.html index fb52848..ab906a3 100644 --- a/views/base.html +++ b/views/base.html @@ -6,6 +6,7 @@ {{ template "title" . }} + diff --git a/views/public/index.html b/views/public/index.html index 9849f0c..3435532 100644 --- a/views/public/index.html +++ b/views/public/index.html @@ -2,7 +2,7 @@ {{define "content"}}

SDVX Replay Server

Wpisz swój nick aby kontynuować

-
+
diff --git a/views/public/login_success.html b/views/public/login_success.html new file mode 100644 index 0000000..961d39d --- /dev/null +++ b/views/public/login_success.html @@ -0,0 +1,9 @@ +{{define "title"}}Pomyślnie zalogowano{{end}} +{{define "content"}} + +

+ Pomyślnie zalogowano! Wkrótce nastąpi przekierowanie... +

+{{end}} \ No newline at end of file diff --git a/views/public/panel.html b/views/public/panel.html new file mode 100644 index 0000000..c4b1d74 --- /dev/null +++ b/views/public/panel.html @@ -0,0 +1,9 @@ +{{define "title"}}SDVX Replay - panel{{end}} +{{define "content"}} +
+ +
+ +

Moje powtórki

+{{end}} \ No newline at end of file