implement like all the boilerplate

This commit is contained in:
radiden 2023-06-14 13:56:31 +02:00
parent c61f730395
commit 82cf7c8145
Signed by: rai
GPG Key ID: C32B641C9E69E395
19 changed files with 415 additions and 36 deletions

44
.air.toml Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
db.sqlite
obs-replay-server

15
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

11
db/query.sql Normal file
View File

@ -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 *;

12
db/schema.sql Normal file
View File

@ -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)
);

9
go.mod
View File

@ -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

14
go.sum
View File

@ -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=

View File

@ -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)

68
main.go
View File

@ -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
// }
// }
// }

31
models/db.go Normal file
View File

@ -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,
}
}

18
models/models.go Normal file
View File

@ -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
}

89
models/query.sql.go Normal file
View File

@ -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
}

58
services/services.go Normal file
View File

@ -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
}

9
sqlc.yaml Normal file
View File

@ -0,0 +1,9 @@
version: 2
sql:
- engine: "sqlite"
schema: "db/schema.sql"
queries: "db/query.sql"
gen:
go:
package: "models"
out: "models"

BIN
tmp/main Executable file

Binary file not shown.

View File

@ -6,6 +6,7 @@
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>{{ template "title" . }}</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/kimeiga/bahunya/dist/bahunya.min.css">
</head>

View File

@ -2,7 +2,7 @@
{{define "content"}}
<h1>SDVX Replay Server</h1>
<h2>Wpisz swój nick aby kontynuować</h2>
<form action="">
<form action="/auth" method="POST">
<input type="text" name="username" placeholder="Nick">
<input type="submit" value="Kontynuuj">
</form>

View File

@ -0,0 +1,9 @@
{{define "title"}}Pomyślnie zalogowano{{end}}
{{define "content"}}
<script>
setInterval(window.location = "/panel", 1000)
</script>
<h1>
Pomyślnie zalogowano! Wkrótce nastąpi przekierowanie...
</h1>
{{end}}

9
views/public/panel.html Normal file
View File

@ -0,0 +1,9 @@
{{define "title"}}SDVX Replay - panel{{end}}
{{define "content"}}
<form action="/save" method="GET">
<button style="font-size: 24pt; padding: .5em; background-color: #599e34; color: #0e1117;" type="submit">Zapisz
powtórkę</button>
</form>
<h1>Moje powtórki</h1>
{{end}}