From a16edb9b61a2ab9b66812d6c019543a7106ef65d Mon Sep 17 00:00:00 2001 From: lbenedar Date: Fri, 3 Apr 2026 18:59:15 +0300 Subject: [PATCH] add db connection, refactor code, add db migration files --- Makefile | 14 +- README.md | 2 +- cmd/api/handlers.go | 9 +- cmd/api/logger.go | 10 + cmd/api/main.go | 141 ++++++++----- .../000001_create_foundry_state.down.sql | 2 + .../000001_create_foundry_state.up.sql | 15 ++ db/migrations/000002_create_modules.down.sql | 3 + db/migrations/000002_create_modules.up.sql | 31 +++ db/migrations/000003_create_systems.down.sql | 2 + db/migrations/000003_create_systems.up.sql | 20 ++ db/migrations/000004_create_worlds.down.sql | 2 + db/migrations/000004_create_worlds.up.sql | 23 +++ db/migrations/000005_create_users.down.sql | 3 + db/migrations/000005_create_users.up.sql | 32 +++ go.mod | 1 + go.sum | 2 + internal/foundry/errors.go | 5 +- internal/foundry/foundry.go | 194 ++++++++---------- internal/foundry/get_requests.go | 64 ------ internal/foundry/post_requests.go | 32 --- internal/foundry/requests/config.go | 59 ++++++ internal/foundry/requests/get_requests.go | 64 ++++++ internal/foundry/requests/post_requests.go | 38 ++++ internal/foundry/{ => types}/ws_channels.go | 16 +- internal/foundry/types/ws_message.go | 17 ++ internal/foundry/websocket.go | 114 +++++----- internal/foundry/ws_message.go | 17 -- 28 files changed, 587 insertions(+), 345 deletions(-) create mode 100644 cmd/api/logger.go create mode 100644 db/migrations/000001_create_foundry_state.down.sql create mode 100644 db/migrations/000001_create_foundry_state.up.sql create mode 100644 db/migrations/000002_create_modules.down.sql create mode 100644 db/migrations/000002_create_modules.up.sql create mode 100644 db/migrations/000003_create_systems.down.sql create mode 100644 db/migrations/000003_create_systems.up.sql create mode 100644 db/migrations/000004_create_worlds.down.sql create mode 100644 db/migrations/000004_create_worlds.up.sql create mode 100644 db/migrations/000005_create_users.down.sql create mode 100644 db/migrations/000005_create_users.up.sql delete mode 100644 internal/foundry/get_requests.go delete mode 100644 internal/foundry/post_requests.go create mode 100644 internal/foundry/requests/config.go create mode 100644 internal/foundry/requests/get_requests.go create mode 100644 internal/foundry/requests/post_requests.go rename internal/foundry/{ => types}/ws_channels.go (55%) create mode 100644 internal/foundry/types/ws_message.go delete mode 100644 internal/foundry/ws_message.go diff --git a/Makefile b/Makefile index 01ee2af..4c73fcf 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,17 @@ -include .env api/run: - go run ./cmd/api -foundry_pass=${FOUNDRY_PASS} -foundry_host=${FOUNDRY_HOST} -port=${LISTEN_PORT} + go run ./cmd/api -foundry_pass=${FOUNDRY_PASS} -foundry_host=${FOUNDRY_HOST} -port=${LISTEN_PORT} -log_level=${LOG_LEVEL} -db-dsn=${DB_DSN} + +db/migration/new: + @echo 'Creating migration files for ${name}...' + migrate create -seq -ext=.sql -dir=./migrations ${name} + +db/migrations/up: + @echo 'Running up migrations...' + migrate -path ./db/migrations -database ${DB_DSN} up help: - go run ./cmd/api -help \ No newline at end of file + go run ./cmd/api -help + +.PHONY: run/api db/migration/new \ No newline at end of file diff --git a/README.md b/README.md index 31b3fe0..ed0ea26 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# foundry_helper_service +# Foundry-Scrapping-API diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index accd193..a1aaa92 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -1,15 +1,13 @@ package main import ( - "fmt" "net/http" "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models" ) func (app *application) ShowText(w http.ResponseWriter, r *http.Request) { - msg := app.foundryApp.CreateWSMessageByPage("/join") - data, err := app.foundryApp.HandleWebsocketRequest(msg) + data, err := app.foundryApp.HandleWSRequest("/join") if err != nil { app.slogger.Error("", "error", err) w.Write([]byte(err.Error())) @@ -29,8 +27,7 @@ func (app *application) ShowText(w http.ResponseWriter, r *http.Request) { } func (app *application) WhenNextSession(w http.ResponseWriter, r *http.Request) { - msg := app.foundryApp.CreateWSMessageByPage("/setup") - data, err := app.foundryApp.HandleWebsocketRequest(msg) + data, err := app.foundryApp.HandleWSRequest("/setup") if err != nil { app.slogger.Error("", "error", err) w.Write([]byte(err.Error())) @@ -50,7 +47,7 @@ func (app *application) WhenNextSession(w http.ResponseWriter, r *http.Request) w.Write([]byte(err.Error())) return } - fmt.Printf("JSON msg: %s\n", nextSession) + app.slogger.Info("Next session data is ready to send", "sessionTime", nextSession) w.Write([]byte(nextSession.Local().String())) } diff --git a/cmd/api/logger.go b/cmd/api/logger.go new file mode 100644 index 0000000..66175d6 --- /dev/null +++ b/cmd/api/logger.go @@ -0,0 +1,10 @@ +package main + +import "log/slog" + +var StringToLogLevel = map[string]slog.Leveler{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, +} diff --git a/cmd/api/main.go b/cmd/api/main.go index bab98d3..48faddc 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,8 +1,9 @@ package main import ( + "context" + "database/sql" "flag" - "fmt" "log" "log/slog" "net/http" @@ -10,6 +11,9 @@ import ( "time" "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry" + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests" + + _ "github.com/mattn/go-sqlite3" ) type service_mode string @@ -21,10 +25,15 @@ const ( ) type config struct { - mode service_mode - port string - foundryHost string - foundryPass string + mode service_mode + port string + + db struct { + dsn string + maxOpenConns int + maxIdleConns int + maxIdleTime string + } } type application struct { @@ -34,84 +43,120 @@ type application struct { logger *log.Logger } +func openDB(cfg config) (*sql.DB, error) { + db, err := sql.Open("sqlite3", cfg.db.dsn) + if err != nil { + return nil, err + } + + db.SetMaxIdleConns(cfg.db.maxIdleConns) + db.SetMaxOpenConns(cfg.db.maxOpenConns) + + duration, err := time.ParseDuration(cfg.db.maxIdleTime) + if err != nil { + return nil, err + } + db.SetConnMaxIdleTime(duration) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err = db.PingContext(ctx) + if err != nil { + return nil, err + } + return db, nil +} + // TODO: change default values func (app *application) parseFlags() { - var mode string - - flag.StringVar(&mode, "service_mode", "api", "Type of service mode(api|discord|tg)") flag.StringVar(&app.cfg.port, "port", ":9090", "Service port") - flag.StringVar(&app.cfg.foundryHost, "foundry_host", "127.0.0.1", "Address to connect to Foundry") - flag.StringVar(&app.cfg.foundryPass, "foundry_pass", "", "Password to connect to Foundry") + + flag.StringVar(&app.cfg.db.dsn, "db-dsn", "file:db/default.db?cache=shared", "PostgreSQL DSN") + flag.IntVar(&app.cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") + flag.IntVar(&app.cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") + flag.StringVar(&app.cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") + + var mode string + flag.StringVar(&mode, "service_mode", "api", "Type of service mode(api|discord|tg)") + + foundryConfig := requests.Config{SessionID: ""} + flag.StringVar(&foundryConfig.Host, "foundry_host", "127.0.0.1", "Address to connect to Foundry") + flag.StringVar(&foundryConfig.Password, "foundry_pass", "", "Password to connect to Foundry") + + var logLevelStr string + flag.StringVar(&logLevelStr, "log_level", "info", "Password to connect to Foundry(debug|info|warn|error)") + flag.Parse() app.cfg.mode = service_mode(mode) + app.foundryApp.SetConfig(&foundryConfig) + + logLevel, ok := StringToLogLevel[logLevelStr] + if !ok { + logLevel = slog.LevelInfo + } + + app.slogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})) + if !ok { + app.slogger.Warn("Wrong log level. Started with log_level=info", "received", logLevelStr) + } } -func (app *application) ConnectToFoundry(foundryHost, foundryPass string) error { +// TODO: Handle application exit if wrong data +func (app *application) StartListenFoundry() { var err error - if app.foundryApp == nil { - app.foundryApp, err = foundry.NewFoundry(foundryHost) - if err != nil { - return err - } - } - - fmt.Printf("Foundry1: %v\n", app.foundryApp) - - ok, err := app.foundryApp.CheckSessionToken(foundryHost) - if err != nil { - return err - } - - fmt.Printf("Foundry2: %v\n", app.foundryApp) - - if foundryPass != "" && !ok { - err := app.foundryApp.Authenticate(foundryPass) - if err != nil { - return err - } - } - - fmt.Printf("Foundry3: %v\n", app.foundryApp) - return nil -} - -func (app *application) StartListenFoundry() { for { - err := app.ConnectToFoundry(app.cfg.foundryHost, app.cfg.foundryPass) + if !app.foundryApp.HasSessionId() { + err = app.foundryApp.SetUpSessionId() + if err != nil { + app.slogger.Error("Error raised", "err", err.Error()) + return + } + } + + err = app.foundryApp.ConnectToFoundry() if err != nil { - app.logger.Println(err) + app.slogger.Error("Error raised", "err", err.Error()) return } - err = foundry.ConnectToWebSocket(app.foundryApp) + err = app.foundryApp.ConnectToWebSocket() if err != nil { - app.logger.Println(err) + app.slogger.Error("Error raised", "err", err.Error()) return } err = app.foundryApp.StartListen() if err != nil && err != foundry.ListenIsDone { - app.logger.Println(err) + app.slogger.Error("Error raised", "err", err.Error()) return } } } func main() { - app := application{foundryApp: nil} + app := application{foundryApp: foundry.NewFoundry()} app.parseFlags() - app.slogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) - app.logger = slog.NewLogLogger(app.slogger.Handler(), slog.LevelInfo) + app.foundryApp.SetLogger(app.slogger) + + db, err := openDB(app.cfg) + if err != nil { + app.slogger.Error("Error when opening database connection", "err", err) + return + } + defer db.Close() + app.slogger.Info("Database connection pool established") go app.StartListenFoundry() + errLog := slog.NewLogLogger(app.slogger.Handler(), slog.LevelError) server := &http.Server{ Addr: app.cfg.port, Handler: app.routes(), - ErrorLog: app.logger, + ErrorLog: errLog, // TLSConfig: tlsConfig, IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, diff --git a/db/migrations/000001_create_foundry_state.down.sql b/db/migrations/000001_create_foundry_state.down.sql new file mode 100644 index 0000000..3552615 --- /dev/null +++ b/db/migrations/000001_create_foundry_state.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS options; +DROP TABLE IF EXISTS foundry_state; \ No newline at end of file diff --git a/db/migrations/000001_create_foundry_state.up.sql b/db/migrations/000001_create_foundry_state.up.sql new file mode 100644 index 0000000..5a9ad28 --- /dev/null +++ b/db/migrations/000001_create_foundry_state.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS foundry_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + is_setup BOOLEAN NOT NULL DEFAULT TRUE, + state_type INTEGER NOT NULL, + + created_at DATETIME NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS options ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + state_id INTEGER UNIQUE, + lang TEXT NOT NULL, + FOREIGN KEY (state_id) REFERENCES foundry_state(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/migrations/000002_create_modules.down.sql b/db/migrations/000002_create_modules.down.sql new file mode 100644 index 0000000..dfe693f --- /dev/null +++ b/db/migrations/000002_create_modules.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS modules_languages; +DROP TABLE IF EXISTS modules_compatibility; +DROP TABLE IF EXISTS modules; diff --git a/db/migrations/000002_create_modules.up.sql b/db/migrations/000002_create_modules.up.sql new file mode 100644 index 0000000..f83d451 --- /dev/null +++ b/db/migrations/000002_create_modules.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS modules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_id VARCHAR(256) NOT NULL, + title VARCHAR(256) NOT NULL, + description TEXT NOT NULL, + url VARCHAR(256) NOT NULL, + version VARCHAR(64) NOT NULL, + availability INTEGER NOT NULL, + + created_at DATETIME NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS modules_compatibility ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + model_id INTEGER UNIQUE, + minimum VARCHAR(64) NOT NULL, + verified VARCHAR(64) NOT NULL, + maximum VARCHAR(64) NOT NULL, + + FOREIGN KEY (model_id) REFERENCES modules(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS modules_languages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + model_id INTEGER, + language VARCHAR(256) NOT NULL, + name VARCHAR(256) NOT NULL, + path VARCHAR(256) NOT NULL, + + FOREIGN KEY (model_id) REFERENCES modules(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/migrations/000003_create_systems.down.sql b/db/migrations/000003_create_systems.down.sql new file mode 100644 index 0000000..f4bf689 --- /dev/null +++ b/db/migrations/000003_create_systems.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS systems_compatibility; +DROP TABLE IF EXISTS systems; diff --git a/db/migrations/000003_create_systems.up.sql b/db/migrations/000003_create_systems.up.sql new file mode 100644 index 0000000..e8cc274 --- /dev/null +++ b/db/migrations/000003_create_systems.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS systems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_id VARCHAR(256) NOT NULL, + title VARCHAR(256) NOT NULL, + description TEXT NOT NULL, + url VARCHAR(256) NOT NULL, + download VARCHAR(256) NOT NULL, + + created_at DATETIME NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS systems_compatibility ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + system_id INTEGER UNIQUE, + minimum VARCHAR(64) NOT NULL, + verified VARCHAR(64) NOT NULL, + maximum VARCHAR(64) NOT NULL, + + FOREIGN KEY (system_id) REFERENCES systems(id) ON DELETE CASCADE +); diff --git a/db/migrations/000004_create_worlds.down.sql b/db/migrations/000004_create_worlds.down.sql new file mode 100644 index 0000000..022ec85 --- /dev/null +++ b/db/migrations/000004_create_worlds.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS worlds_compatibility; +DROP TABLE IF EXISTS worlds; diff --git a/db/migrations/000004_create_worlds.up.sql b/db/migrations/000004_create_worlds.up.sql new file mode 100644 index 0000000..0da2ae2 --- /dev/null +++ b/db/migrations/000004_create_worlds.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS worlds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_id VARCHAR(256) NOT NULL, + title VARCHAR(256) NOT NULL, + description TEXT NOT NULL, + system VARCHAR(256) NOT NULL, + core_version VARCHAR(256) NOT NULL, + system_version VARCHAR(256) NOT NULL, + playtime INTEGER NOT NULL, + next_session DATETIME NOT NULL, + + created_at DATETIME NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS worlds_compatibility ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + world_id INTEGER UNIQUE, + minimum VARCHAR(64) NOT NULL, + verified VARCHAR(64) NOT NULL, + maximum VARCHAR(64) NOT NULL, + + FOREIGN KEY (world_id) REFERENCES worlds(id) ON DELETE CASCADE +); diff --git a/db/migrations/000005_create_users.down.sql b/db/migrations/000005_create_users.down.sql new file mode 100644 index 0000000..96aea92 --- /dev/null +++ b/db/migrations/000005_create_users.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS users_hotbar; +DROP TABLE IF EXISTS users_stats; \ No newline at end of file diff --git a/db/migrations/000005_create_users.up.sql b/db/migrations/000005_create_users.up.sql new file mode 100644 index 0000000..9564948 --- /dev/null +++ b/db/migrations/000005_create_users.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(256) NOT NULL, + role INTEGER NOT NULL, + character VARCHAR(128) NOT NULL, + color VARCHAR(128) NOT NULL, + pronouns VARCHAR(128) NOT NULL, + + created_at DATETIME NOT NULL DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS users_hotbar ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + key TEXT NOT NULL, + value TEXT NOT NULL, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS users_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + core_version TEXT NOT NULL, + system_id VARCHAR(64) NOT NULL, + system_version VARCHAR(64) NOT NULL, + created_time INTEGER NOT NULL, + modified_time INTEGER NOT NULL, + last_modified_by VARCHAR(256) NOT NULL, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/go.mod b/go.mod index f8950e4..99b16b5 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/klauspost/compress v1.18.3 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mattn/go-sqlite3 v1.14.39 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/moby/buildkit v0.27.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect diff --git a/go.sum b/go.sum index 0496036..5258397 100644 --- a/go.sum +++ b/go.sum @@ -211,6 +211,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.39 h1:sIwSjlJGOaRJjw44/HXaeTblZMjseqr6OOio1tz/+JI= +github.com/mattn/go-sqlite3 v1.14.39/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/moby/buildkit v0.27.1 h1:qlIWpnZzqCkrYiGkctM1gBD/YZPOJTjtUdRBlI0oBOU= diff --git a/internal/foundry/errors.go b/internal/foundry/errors.go index c0a86a0..02585d7 100644 --- a/internal/foundry/errors.go +++ b/internal/foundry/errors.go @@ -6,8 +6,7 @@ import ( ) var ( - ErrorSidWrongFormat = errors.New("Session id wrong format") - ErrorSidNotFound = errors.New("Session id didn't find in response header") + ErrorFoundryNotInit = errors.New("Foundry is not initialized") ErrorAuthPassWrong = errors.New("Authentication password is wrong") ErrorNotAuth = errors.New("Admin is not authenticated") ErrorIsNotReady = errors.New("Connection is not ready for communication") @@ -21,7 +20,7 @@ var ( type FoundryError struct { Err error - Type FoundryCode + Type TransportCode IsFatal bool } diff --git a/internal/foundry/foundry.go b/internal/foundry/foundry.go index dbf3829..d36a776 100644 --- a/internal/foundry/foundry.go +++ b/internal/foundry/foundry.go @@ -4,37 +4,18 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" + "net/url" "strconv" "strings" + + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests" + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types" + "github.com/gorilla/websocket" ) const ( - authPath = "/auth" - licensePath = "/license" - joinPath = "/join" - playersPath = "/players" - setupPath = "/setup" - updatePath = "/update" - - socketPath = "/socket.io" -) - -var WsTypeData = map[string]string{ - authPath: "getAuthData", - licensePath: "getAuthData", - joinPath: "getJoinData", - playersPath: "getPlayersData", - setupPath: "getSetupData", - updatePath: "getUpdateData", -} - -type FoundryCode int - -const ( - WriterCode = FoundryCode(0) - ReaderCode = FoundryCode(1) - RespSessionData = "0" RespPingCode = "2" RespSessionId = "40" @@ -62,17 +43,24 @@ var CodesRespToReq = map[string]string{ } type Foundry struct { - config foundry_config + //TODO: make check of admin's authentication + isAuth bool - sessionID string - isAuth bool - currPage string + logger *slog.Logger + // db db.DB - ws *webSocketUtil + config requests.Config + ws *webSocketUtil } -type foundry_config struct { - host string +func NewFoundry() *Foundry { + foundry := &Foundry{config: requests.Config{SessionID: ""}, isAuth: false, ws: NewWebSocketUtil()} + + // err := foundry.config.GetSessionId() + // if err != nil { + // return nil, err + // } + return foundry } func (foundry *Foundry) checkAuthRespAnswer(r io.Reader, respLength int) error { @@ -90,42 +78,15 @@ func (foundry *Foundry) checkAuthRespAnswer(r io.Reader, respLength int) error { return nil } -func (foundry *Foundry) getSessionTokenFromHeader(resp http.Header) (bool, error) { - setCookieHeader := resp.Get("Set-Cookie") - if setCookieHeader == "" { - return false, nil - } - - for value := range strings.SplitSeq(setCookieHeader, ";") { - if strings.Contains(value, "session") { - sessionCookie := strings.Split(value, "=") - if len(sessionCookie) != 2 { - return false, ErrorSidWrongFormat - } - - foundry.sessionID = sessionCookie[1] - return true, nil - } - } - - return false, nil -} - -func (foundry *Foundry) Authenticate(password string) error { - if foundry.sessionID == "" { - err := foundry.setUpSessionId() +func (foundry *Foundry) Authenticate() error { + if foundry.config.SessionID == "" { + err := foundry.config.GetSessionId() if err != nil { return err } } - req, err := createAuthRequest(foundry.config.host, foundry.sessionID, password) - if err != nil { - return err - } - - client := &http.Client{} - resp, err := client.Do(req) + resp, err := foundry.config.PostAuthenticationData() if err != nil { return err } @@ -140,7 +101,9 @@ func (foundry *Foundry) Authenticate(password string) error { } func (foundry *Foundry) StartListen() error { - wsChannels := foundry.ws.InitWsChannels() + foundry.ws.channels = *types.InitWsChannels() + + wsChannels := &foundry.ws.channels defer wsChannels.Close() go foundry.ListenAndServeWS() @@ -162,6 +125,54 @@ func (foundry *Foundry) StartListen() error { } } +func (foundry *Foundry) ConnectToFoundry() error { + var err error + + if foundry == nil { + return ErrorFoundryNotInit + } + ok, err := (*foundry).config.GetSessionToken() + if err != nil { + return err + } + + if (*foundry).config.Password != "" && !ok { + err := (*foundry).Authenticate() + if err != nil { + return err + } + } + + foundry.logger.Info("Successefully connected to Foundry", "host", foundry.config.Host) + return nil +} + +func (foundry *Foundry) ConnectToWebSocket() error { + if !foundry.isAuth { + return ErrorNotAuth + } + + query := url.Values{} + query.Add("session", foundry.config.SessionID) + query.Add("EIO", "4") + query.Add("transport", "websocket") + + u := url.URL{Scheme: "ws", Host: foundry.config.Host, Path: fmt.Sprintf("%s/", requests.SocketPath), RawQuery: query.Encode()} + + wsHeader := http.Header{} + wsHeader.Set("Cookie", fmt.Sprintf("session=%s", foundry.config.SessionID)) + + wsConn, _, err := websocket.DefaultDialer.Dial(u.String(), wsHeader) + if err != nil { + return err + } + + foundry.ws.wsConn = wsConn + foundry.ws.currWsId = 0 + foundry.logger.Info("Successefully connected to Foundry Websocket") + return nil +} + func (foundry *Foundry) CloseWebSocketConn() { foundry.ws.wsConn.Close() } @@ -171,26 +182,27 @@ func (foundry *Foundry) ListenAndServeWS() { foundry.ws.ServeWebSocket() } -func (foundry *Foundry) CreateWSMessageByPage(page string) *wsMessage { - msgToSend := &wsMessage{code: CodesRespToReq[RespCreateSessionCode], id: foundry.ws.currWsId, msgJson: fmt.Sprintf("[\"%s\"]", WsTypeData[page])} - foundry.ws.currWsId++ +func (foundry *Foundry) HandleWSRequest(msgType string) ([]byte, error) { + msg := foundry.ws.CreateWSMessageByPage("/setup") - fmt.Printf("Msg: %s\n", msgToSend.toString()) - return msgToSend -} - -func (foundry *Foundry) HandleWebsocketRequest(msg *wsMessage) ([]byte, error) { return foundry.ws.HandleWebsocketRequest(msg) } -func NewFoundry(host string) (*Foundry, error) { - foundry := &Foundry{config: foundry_config{host: host}, isAuth: false, currPage: authPath, ws: NewWebSocketUtil()} +func (foundry *Foundry) HasSessionId() bool { + return foundry.config.SessionID != "" +} - err := foundry.setUpSessionId() - if err != nil { - return nil, err - } - return foundry, nil +func (foundry *Foundry) SetUpSessionId() error { + return foundry.config.GetSessionId() +} + +func (foundry *Foundry) SetConfig(config *requests.Config) { + foundry.config = *config +} + +func (foundry *Foundry) SetLogger(slogger *slog.Logger) { + foundry.logger = slogger + foundry.ws.logger = slogger } // /** @@ -206,27 +218,3 @@ func NewFoundry(host string) (*Foundry, error) { // }).catch(() => {}); // this.#pollActive(); // }; - -// func (foundry *Foundry) SendPageDataRequest(page string) error { -// msgToSend := &wsComm{code: CodesRespToReq[RespCreateSessionCode], id: foundry.ws.currWsId, msgJson: fmt.Sprintf("[\"%s\"]", WsTypeData[page])} -// foundry.ws.currWsId++ -// log.Printf("send: %s\n", msgToSend.toString()) - -// return foundry.ws.wsConn.WriteMessage(websocket.TextMessage, msgToSend.toByteSlice()) -// } - -// func (foundry *Foundry) setUpWebSocketConnection() error { -// err := foundry.wsConn.WriteMessage(websocket.TextMessage, []byte("40")) -// if err != nil { -// return err -// } -// return nil -// } - -// func (foundry *Foundry) sendPageDataRequest(page string) error { -// msgToSend := &wsComm{code: CodesRespToReq[RespCreateSessionCode], id: foundry.ws.currWsId, msgJson: fmt.Sprintf("[\"%s\"]", WsTypeData[page])} -// foundry.ws.currWsId++ -// log.Printf("send: %s\n", msgToSend.toString()) - -// return foundry.ws.wsConn.WriteMessage(websocket.TextMessage, msgToSend.toByteSlice()) -// } diff --git a/internal/foundry/get_requests.go b/internal/foundry/get_requests.go deleted file mode 100644 index ca2a2de..0000000 --- a/internal/foundry/get_requests.go +++ /dev/null @@ -1,64 +0,0 @@ -package foundry - -import ( - "encoding/json" - "fmt" - "net/http" - - "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models" -) - -func (foundry *Foundry) setUpSessionId() error { - getResp, err := http.Get(fmt.Sprintf("http://%s%s", foundry.config.host, authPath)) - if err != nil { - return err - } - - _, err = foundry.getSessionTokenFromHeader(getResp.Header) - return err -} - -func (foundry *Foundry) CheckSessionToken(host string) (bool, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("http://%s%s", host, authPath), nil) - if err != nil { - return false, err - } - - header := http.Header{} - header.Set("Cookie", fmt.Sprintf("session=%s", foundry.sessionID)) - header.Set("Connection", "keep-alive") - header.Set("Host", host) - header.Set("Origin", fmt.Sprintf("http://%s", host)) - header.Set("Referer", fmt.Sprintf("http://%s%s", host, authPath)) - - req.Header = header - client := &http.Client{} - - resp, err := client.Do(req) - if err != nil { - return false, err - } - - return foundry.getSessionTokenFromHeader(resp.Header) -} - -func (foundry *Foundry) GetStatus() (*models.Status, error) { - getResp, err := http.Get(fmt.Sprintf("http://%s/api/status", foundry.config.host)) - if err != nil { - return nil, err - } - defer getResp.Body.Close() - - statusByte := make([]byte, 64) - _, err = getResp.Body.Read(statusByte) - if err != nil { - return nil, err - } - - var status models.Status - err = json.Unmarshal(statusByte, &status) - if err != nil { - return nil, err - } - return &status, nil -} diff --git a/internal/foundry/post_requests.go b/internal/foundry/post_requests.go deleted file mode 100644 index b864a2c..0000000 --- a/internal/foundry/post_requests.go +++ /dev/null @@ -1,32 +0,0 @@ -package foundry - -import ( - "bytes" - "fmt" - "net/http" - "net/url" -) - -func createAuthRequest(host, sessionID, password string) (*http.Request, error) { - authData := url.Values{} - authData.Add("adminPassword", password) - authData.Add("action", "adminAuth") - - req, err := http.NewRequest("POST", fmt.Sprintf("http://%s%s", host, authPath), bytes.NewBuffer([]byte(authData.Encode()))) - if err != nil { - return nil, err - } - - header := http.Header{} - header.Set("Cookie", fmt.Sprintf("session=%s", sessionID)) - header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - header.Set("Connection", "keep-alive") - header.Set("Content-Length", fmt.Sprintf("%d", len(authData.Encode()))) - header.Set("Content-Type", "application/x-www-form-urlencoded") - header.Set("Host", host) - header.Set("Origin", fmt.Sprintf("http://%s", host)) - header.Set("Referer", fmt.Sprintf("http://%s%s", host, authPath)) - - req.Header = header - return req, nil -} diff --git a/internal/foundry/requests/config.go b/internal/foundry/requests/config.go new file mode 100644 index 0000000..1900ad4 --- /dev/null +++ b/internal/foundry/requests/config.go @@ -0,0 +1,59 @@ +package requests + +import ( + "errors" + "net/http" + "strings" +) + +var ( + ErrorSidWrongFormat = errors.New("Session id wrong format") + ErrorSidNotFound = errors.New("Session id didn't find in response header") +) + +const ( + AuthPath = "/auth" + LicensePath = "/license" + JoinPath = "/join" + PlayersPath = "/players" + SetupPath = "/setup" + UpdatePath = "/update" + + SocketPath = "/socket.io" +) + +var WsTypeData = map[string]string{ + AuthPath: "getAuthData", + LicensePath: "getAuthData", + JoinPath: "getJoinData", + PlayersPath: "getPlayersData", + SetupPath: "getSetupData", + UpdatePath: "getUpdateData", +} + +type Config struct { + Host string + Password string + SessionID string +} + +func (conf *Config) SetSessionTokenFromHeader(resp http.Header) (bool, error) { + setCookieHeader := resp.Get("Set-Cookie") + if setCookieHeader == "" { + return false, nil + } + + for value := range strings.SplitSeq(setCookieHeader, ";") { + if strings.Contains(value, "session") { + sessionCookie := strings.Split(value, "=") + if len(sessionCookie) != 2 { + return false, ErrorSidWrongFormat + } + + conf.SessionID = sessionCookie[1] + return true, nil + } + } + + return false, nil +} diff --git a/internal/foundry/requests/get_requests.go b/internal/foundry/requests/get_requests.go new file mode 100644 index 0000000..97360d7 --- /dev/null +++ b/internal/foundry/requests/get_requests.go @@ -0,0 +1,64 @@ +package requests + +import ( + "encoding/json" + "fmt" + "net/http" + + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models" +) + +func (conf *Config) GetSessionId() error { + getResp, err := http.Get(fmt.Sprintf("http://%s%s", conf.Host, AuthPath)) + if err != nil { + return err + } + + _, err = conf.SetSessionTokenFromHeader(getResp.Header) + return err +} + +func (conf *Config) GetSessionToken() (bool, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s%s", conf.Host, AuthPath), nil) + if err != nil { + return false, err + } + + header := http.Header{} + header.Set("Cookie", fmt.Sprintf("session=%s", conf.SessionID)) + header.Set("Connection", "keep-alive") + header.Set("Host", conf.Host) + header.Set("Origin", fmt.Sprintf("http://%s", conf.Host)) + header.Set("Referer", fmt.Sprintf("http://%s%s", conf.Host, AuthPath)) + + req.Header = header + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + return conf.SetSessionTokenFromHeader(resp.Header) +} + +func (conf *Config) GetStatus() (*models.Status, error) { + getResp, err := http.Get(fmt.Sprintf("http://%s/api/status", conf.Host)) + if err != nil { + return nil, err + } + defer getResp.Body.Close() + + statusByte := make([]byte, 64) + _, err = getResp.Body.Read(statusByte) + if err != nil { + return nil, err + } + + var status models.Status + err = json.Unmarshal(statusByte, &status) + if err != nil { + return nil, err + } + return &status, nil +} diff --git a/internal/foundry/requests/post_requests.go b/internal/foundry/requests/post_requests.go new file mode 100644 index 0000000..dce15ab --- /dev/null +++ b/internal/foundry/requests/post_requests.go @@ -0,0 +1,38 @@ +package requests + +import ( + "bytes" + "fmt" + "net/http" + "net/url" +) + +func (conf *Config) PostAuthenticationData() (*http.Response, error) { + authData := url.Values{} + authData.Add("adminPassword", conf.Password) + authData.Add("action", "adminAuth") + + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s%s", conf.Host, AuthPath), bytes.NewBuffer([]byte(authData.Encode()))) + if err != nil { + return nil, err + } + + header := http.Header{} + header.Set("Cookie", fmt.Sprintf("session=%s", conf.SessionID)) + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + header.Set("Connection", "keep-alive") + header.Set("Content-Length", fmt.Sprintf("%d", len(authData.Encode()))) + header.Set("Content-Type", "application/x-www-form-urlencoded") + header.Set("Host", conf.Host) + header.Set("Origin", fmt.Sprintf("http://%s", conf.Host)) + header.Set("Referer", fmt.Sprintf("http://%s%s", conf.Host, AuthPath)) + + req.Header = header + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/foundry/ws_channels.go b/internal/foundry/types/ws_channels.go similarity index 55% rename from internal/foundry/ws_channels.go rename to internal/foundry/types/ws_channels.go index d2adbe4..b3b8072 100644 --- a/internal/foundry/ws_channels.go +++ b/internal/foundry/types/ws_channels.go @@ -1,8 +1,8 @@ -package foundry +package types type Channels struct { done chan struct{} - msg chan *wsMessage + msg chan *WsMessage err chan error } @@ -10,7 +10,7 @@ func (channels Channels) Err() chan error { return channels.err } -func (channels Channels) Msg() chan *wsMessage { +func (channels Channels) Msg() chan *WsMessage { return channels.msg } @@ -23,3 +23,13 @@ func (channels *Channels) Close() { close(channels.err) close(channels.msg) } + +func InitWsChannels() *Channels { + channels := Channels{ + done: make(chan struct{}), + err: make(chan error, 10), + msg: make(chan *WsMessage, 10), + } + + return &channels +} diff --git a/internal/foundry/types/ws_message.go b/internal/foundry/types/ws_message.go new file mode 100644 index 0000000..d203957 --- /dev/null +++ b/internal/foundry/types/ws_message.go @@ -0,0 +1,17 @@ +package types + +import "fmt" + +type WsMessage struct { + Code string + Id int + MsgJson string +} + +func (w WsMessage) ToString() string { + return fmt.Sprintf("%s%d%s", w.Code, w.Id, w.MsgJson) +} + +func (w WsMessage) ToByteSlice() []byte { + return fmt.Appendf([]byte{}, "%s%d%s", w.Code, w.Id, w.MsgJson) +} diff --git a/internal/foundry/websocket.go b/internal/foundry/websocket.go index 3f4f591..53deb77 100644 --- a/internal/foundry/websocket.go +++ b/internal/foundry/websocket.go @@ -3,26 +3,37 @@ package foundry import ( "context" "fmt" - "log" - "net/http" - "net/url" + "log/slog" "strconv" "strings" "sync" "time" + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests" + "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types" "github.com/gorilla/websocket" ) +type TransportCode int + +const ( + WriterCode = TransportCode(0) + ReaderCode = TransportCode(1) +) + type webSocketUtil struct { wsConn *websocket.Conn currWsId int isReadReady bool + logger *slog.Logger - msgMap map[int](chan []byte) + msgMap map[int](chan []byte) + chanMutex sync.Mutex + channels types.Channels +} - mutex sync.Mutex - channels Channels +func NewWebSocketUtil() *webSocketUtil { + return &webSocketUtil{currWsId: 0, isReadReady: false, msgMap: make(map[int]chan []byte)} } func parseCode(msg *string) string { @@ -54,53 +65,28 @@ func parseId(msg *string, start int) (int, int) { return msgId, j } -func parseWsRespMessage(msg string) (*wsMessage, error) { - data := &wsMessage{} +func parseWsRespMessage(msg string) (*types.WsMessage, error) { + data := &types.WsMessage{} - data.code = parseCode(&msg) - if data.code != RespDataCode { + data.Code = parseCode(&msg) + if data.Code != RespDataCode { return data, nil } - i := len(data.code) + i := len(data.Code) if i == 0 { return nil, ErrorMsgNotHaveNumber } - data.id, i = parseId(&msg, i) + data.Id, i = parseId(&msg, i) if i == 0 { return nil, ErrorMsgNotHaveNumber } - data.msgJson = msg[i:] + data.MsgJson = msg[i:] return data, nil } -func ConnectToWebSocket(foundry *Foundry) error { - if !foundry.isAuth { - return ErrorNotAuth - } - - query := url.Values{} - query.Add("session", foundry.sessionID) - query.Add("EIO", "4") - query.Add("transport", "websocket") - - u := url.URL{Scheme: "ws", Host: foundry.config.host, Path: fmt.Sprintf("%s/", socketPath), RawQuery: query.Encode()} - - wsHeader := http.Header{} - wsHeader.Set("Cookie", fmt.Sprintf("session=%s", foundry.sessionID)) - - wsConn, _, err := websocket.DefaultDialer.Dial(u.String(), wsHeader) - if err != nil { - return err - } - - foundry.ws.wsConn = wsConn - foundry.ws.currWsId = 0 - return nil -} - // TODO: Lookup timeout func (ws *webSocketUtil) ReceiveMessage(id int) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -130,8 +116,8 @@ func (ws *webSocketUtil) ReceiveMessage(id int) ([]byte, error) { func (ws *webSocketUtil) closeMsgChannel(id int, timeout time.Duration) bool { time.Sleep(timeout) - ws.mutex.Lock() - defer ws.mutex.Unlock() + ws.chanMutex.Lock() + defer ws.chanMutex.Unlock() ok := true if _, ok = ws.msgMap[id]; !ok { @@ -147,21 +133,22 @@ func (ws *webSocketUtil) closeMsgChannel(id int, timeout time.Duration) bool { close(ws.msgMap[id]) delete(ws.msgMap, id) } - fmt.Printf("Channel has been closed(id=%d, timeout=%d)\n", id, timeout) + + ws.logger.Debug("WS: Channel has been closed\n", "id", id, "timeout", timeout.String()) return ok } -func (ws *webSocketUtil) HandleWebsocketRequest(msg *wsMessage) ([]byte, error) { +func (ws *webSocketUtil) HandleWebsocketRequest(msg *types.WsMessage) ([]byte, error) { if !ws.IsReadReady() { return nil, ErrorIsNotReady } - err := ws.wsConn.WriteMessage(websocket.TextMessage, msg.toByteSlice()) + err := ws.wsConn.WriteMessage(websocket.TextMessage, msg.ToByteSlice()) if err != nil { return nil, err } - data, err := ws.ReceiveMessage(msg.id) + data, err := ws.ReceiveMessage(msg.Id) if err != nil { return nil, err } @@ -169,10 +156,9 @@ func (ws *webSocketUtil) HandleWebsocketRequest(msg *wsMessage) ([]byte, error) } func (ws *webSocketUtil) sendOnlyCodeRequest(code string) error { - msgToSend := []byte(CodesRespToReq[code]) - log.Printf("send: %s\n", msgToSend) + ws.logger.Debug("WS: Data has been send\n", "msg", CodesRespToReq[code]) - return ws.wsConn.WriteMessage(websocket.TextMessage, msgToSend) + return ws.wsConn.WriteMessage(websocket.TextMessage, []byte(CodesRespToReq[code])) } func (ws *webSocketUtil) ListenWebSocket() { @@ -195,22 +181,22 @@ func (ws *webSocketUtil) ServeWebSocket() { for { select { case message := <-ws.channels.Msg(): - log.Println("recv code:", message.code) + ws.logger.Debug("WS: Data has been received\n", "msgCode", message.Code) - switch message.code { + switch message.Code { case RespPingCode, RespSessionData: - err := ws.sendOnlyCodeRequest(message.code) + err := ws.sendOnlyCodeRequest(message.Code) if err != nil { ws.isReadReady = false - ws.channels.err <- &FoundryError{Type: WriterCode, Err: err} + ws.channels.Err() <- &FoundryError{Type: WriterCode, Err: err} continue } case RespCreateSessionCode: ws.isReadReady = true case RespDataCode: - ws.msgMap[message.id] = make(chan []byte, 1) - ws.msgMap[message.id] <- []byte(message.msgJson) - go ws.closeMsgChannel(message.id, 5*time.Second) + ws.msgMap[message.Id] = make(chan []byte, 1) + ws.msgMap[message.Id] <- []byte(message.MsgJson) + go ws.closeMsgChannel(message.Id, 5*time.Second) default: } case <-ws.channels.Done(): @@ -219,18 +205,14 @@ func (ws *webSocketUtil) ServeWebSocket() { } } -func (ws *webSocketUtil) InitWsChannels() *Channels { - ws.channels.done = make(chan struct{}) - ws.channels.err = make(chan error, 10) - ws.channels.msg = make(chan *wsMessage, 10) - - return &ws.channels -} - -func NewWebSocketUtil() *webSocketUtil { - return &webSocketUtil{currWsId: 0, isReadReady: false, msgMap: make(map[int]chan []byte), channels: Channels{}} -} - func (ws *webSocketUtil) IsReadReady() bool { return ws.isReadReady } + +func (ws *webSocketUtil) CreateWSMessageByPage(page string) *types.WsMessage { + msgToSend := &types.WsMessage{Code: CodesRespToReq[RespCreateSessionCode], Id: ws.currWsId, MsgJson: fmt.Sprintf("[\"%s\"]", requests.WsTypeData[page])} + ws.currWsId++ + + ws.logger.Debug("WS: Data to send\n", "msgToSend", msgToSend.ToString()) + return msgToSend +} diff --git a/internal/foundry/ws_message.go b/internal/foundry/ws_message.go deleted file mode 100644 index 40321ab..0000000 --- a/internal/foundry/ws_message.go +++ /dev/null @@ -1,17 +0,0 @@ -package foundry - -import "fmt" - -type wsMessage struct { - code string - id int - msgJson string -} - -func (w wsMessage) toString() string { - return fmt.Sprintf("%s%d%s", w.code, w.id, w.msgJson) -} - -func (w wsMessage) toByteSlice() []byte { - return fmt.Appendf([]byte{}, "%s%d%s", w.code, w.id, w.msgJson) -}