refactor foundry lib structure; add transport classes for db,http,websocket; add action interface instead of multiple function that accepts action classes; add changing foundry status on websocket responses

This commit is contained in:
lbenedar
2026-04-09 18:29:41 +03:00
parent 468e027418
commit fb4e371348
28 changed files with 900 additions and 791 deletions

View File

@@ -12,6 +12,7 @@ import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
_ "github.com/mattn/go-sqlite3"
)
@@ -38,7 +39,7 @@ type config struct {
type application struct {
cfg config
foundryApp *foundry.Foundry
foundryApp *foundry.FoundryApi
slogger *slog.Logger
logger *log.Logger
}
@@ -69,7 +70,7 @@ func openDB(cfg config) (*sql.DB, error) {
}
// TODO: change default values
func (app *application) parseFlags() {
func (app *application) parseFlags() *requests.FoundryHttpRequest {
flag.StringVar(&app.cfg.port, "port", ":9090", "Service port")
flag.StringVar(&app.cfg.db.dsn, "db-dsn", "file:db/default.db?cache=shared", "PostgreSQL DSN")
@@ -80,9 +81,9 @@ func (app *application) parseFlags() {
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")
foundryHttpData := requests.FoundryHttpRequest{SessionID: ""}
flag.StringVar(&foundryHttpData.Host, "foundry_host", "127.0.0.1", "Address to connect to Foundry")
flag.StringVar(&foundryHttpData.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)")
@@ -90,7 +91,7 @@ func (app *application) parseFlags() {
flag.Parse()
app.cfg.mode = service_mode(mode)
app.foundryApp.SetConfig(&foundryConfig)
// app.foundryApp.SetConfig(&foundryConfig)
logLevel, ok := StringToLogLevel[logLevelStr]
if !ok {
@@ -101,6 +102,7 @@ func (app *application) parseFlags() {
if !ok {
app.slogger.Warn("Wrong log level. Started with log_level=info", "received", logLevelStr)
}
return &foundryHttpData
}
// TODO: Handle application exit if wrong data
@@ -108,39 +110,18 @@ func (app *application) StartListenFoundry() {
var err error
for {
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.slogger.Error("Error raised", "err", err.Error())
return
}
err = app.foundryApp.ConnectToWebSocket()
if err != nil {
app.slogger.Error("Error raised", "err", err.Error())
return
}
err = app.foundryApp.StartListen()
if err != nil && err != foundry.ListenIsDone {
app.slogger.Error("Error raised", "err", err.Error())
return
}
}
}
func main() {
app := application{foundryApp: foundry.NewFoundry()}
app := application{foundryApp: &foundry.FoundryApi{}}
app.parseFlags()
app.foundryApp.SetLogger(app.slogger)
foundryHttpData := app.parseFlags()
app.slogger.Info("", "dsn", app.cfg.db.dsn)
@@ -152,8 +133,7 @@ func main() {
defer db.Close()
app.slogger.Info("Database connection pool established")
app.foundryApp.CreateNewDataModels(db)
app.foundryApp.Transport = transport.NewFoundryTransport(db, app.slogger, foundryHttpData)
go app.StartListenFoundry()
errLog := slog.NewLogLogger(app.slogger.Handler(), slog.LevelError)

View File

@@ -0,0 +1,9 @@
package actions
import "errors"
var (
ErrActionNotFound = errors.New("Action doesn't found")
ErrObjTypeNotMatch = errors.New("Provided value type didn't match obj field type")
ErrObjNotPointer = errors.New("Passed obj is not pointer")
)

View File

@@ -0,0 +1,69 @@
package actions
import (
"encoding/json"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
const (
WsSessionType = "session"
WsProgressType = "progress"
WsUserActivityType = "userActivity"
WsShutdownType = "shutdown"
)
type WsActions interface {
Action(tr *transport.FoundryTransport, status *types.FoundryStatus) error
}
var WsMsgActions = map[string]func() WsActions{
"session": func() WsActions { return &WsSessionMsg{} },
"progress": func() WsActions { return &WsProgressMsg{} },
// "userActivity": WsUserActivityCursor{},
"shutdown": func() WsActions { return &WsShutdownMsg{} },
}
func GetWsMsgAction(dataType string) WsActions {
if factory, ok := WsMsgActions[dataType]; ok {
return factory()
}
return nil
}
func SplitToTypeAndData(dataJson []byte) (WsActions, error) {
var rawItems []json.RawMessage
err := json.Unmarshal(dataJson, &rawItems)
if err != nil {
return nil, err
}
var dataType string
err = json.Unmarshal(rawItems[0], &dataType)
if err != nil {
return nil, err
}
if dataType == WsUserActivityType {
return nil, nil
}
var data map[string]any
err = json.Unmarshal(rawItems[1], &data)
if err != nil {
return nil, err
}
actionDataPtr := GetWsMsgAction(dataType)
if actionDataPtr == nil {
return nil, ErrActionNotFound
}
err = FillStruct(data, actionDataPtr)
if err != nil {
return nil, err
}
return actionDataPtr, nil
}

View File

@@ -0,0 +1,37 @@
package actions
import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
type WsProgressMsg struct {
Id string `json:"id"`
Message string `json:"message"`
Pct float64 `json:"pct"`
HasChanged bool `json:"hasChanged,omitempty"`
Act string `json:"action"`
Step string `json:"step"`
}
func (msg WsProgressMsg) Action(tr *transport.FoundryTransport, status *types.FoundryStatus) error {
if !msg.HasChanged {
tr.Logger.Debug("World has been launched", "world", msg.Id)
select {
case <-tr.ExchangeChan.ProgressMsg[msg.Id]:
tr.Logger.Warn("Channel for the world is closed", "world", msg.Id)
default:
close(tr.ExchangeChan.ProgressMsg[msg.Id])
}
if status.IsActive != true {
newStatus, err := tr.Http.GetStatus()
if err != nil {
return err
}
status.Update(newStatus)
tr.Logger.Info("Status has been changed", "status", status)
}
}
return nil
}

View File

@@ -0,0 +1,26 @@
package actions
import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
type WsSessionMsg struct {
SessionId string `json:"sessionId"`
UserId *string `json:"userId,omitempty"`
}
func (msg WsSessionMsg) Action(tr *transport.FoundryTransport, status *types.FoundryStatus) error {
if status.IsActive {
tr.Logger.Warn("World is started. Please, return to setup page to fully initialize database")
return nil
}
if tr.IsDbInit {
return nil
}
tr.IsDbInit = true
go tr.FillDBWithFoundryData()
return nil
}

View File

@@ -0,0 +1,30 @@
package actions
import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
type WsShutdownMsg struct {
World string `json:"world"`
UserId *string `json:"userId,omitempty"`
}
func (msg WsShutdownMsg) Action(tr *transport.FoundryTransport, status *types.FoundryStatus) error {
if status.IsActive != false {
newStatus, err := tr.Http.GetStatus()
if err != nil {
return err
}
status.Update(newStatus)
tr.Logger.Info("Status has been changed", "status", status)
}
if tr.IsDbInit {
return nil
}
tr.IsDbInit = true
go tr.FillDBWithFoundryData()
return nil
}

View File

@@ -0,0 +1,20 @@
package actions
import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
type WsUserActivityMsg struct {
Cursor WsUserActivityCursor `json:"cursor"`
}
type WsUserActivityCursor struct {
X int `json:"x"`
Y int `json:"y"`
}
func (msg WsUserActivityMsg) Action(tr *transport.FoundryTransport, status *types.FoundryStatus) error {
tr.Logger.Debug("WsUserActivityMsg")
return nil
}

View File

@@ -1,7 +1,6 @@
package types
package actions
import (
"errors"
"fmt"
"reflect"
"strings"
@@ -37,7 +36,7 @@ func setField(obj any, name string, value any) error {
structFieldType := structFieldValue.Type()
val := reflect.ValueOf(value)
if structFieldType != val.Type() {
return errors.New("Provided value type didn't match obj field type")
return ErrObjTypeNotMatch
}
if value == nil {
@@ -59,7 +58,7 @@ func isPointer(obj any) bool {
func FillStruct(m map[string]any, obj any) error {
if !isPointer(obj) {
return errors.New("Passed obj is not pointer")
return ErrObjNotPointer
}
for k, v := range m {

View File

@@ -1,32 +0,0 @@
package foundry
import (
"errors"
"fmt"
)
var (
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")
ErrorStateTypeNotExist = errors.New("State type does not exists")
ErrorMsgNotHaveNumber = errors.New("Message doesn't have dataCode and id")
ErrorTimeout = errors.New("Answer has not been received after timeout")
ErrorReadChannel = errors.New("Error when reading from msg channel")
ListenIsDone = errors.New("Listen for websocket data in foundry is stopped")
)
type FoundryError struct {
Err error
Type TransportCode
IsFatal bool
}
func (e *FoundryError) Error() string {
return fmt.Sprintf("Received from %d: %s", e.Type, e.Err.Error())
}
func (e *FoundryError) Unwrap() error { return e.Err }

View File

@@ -1,81 +1,35 @@
package foundry
import (
"database/sql"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/actions"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/transport"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
"github.com/gorilla/websocket"
)
type Foundry struct {
var (
ListenIsDone = errors.New("Listen for websocket data in foundry is stopped")
)
type FoundryApi struct {
//TODO: make check of admin's authentication
isAuth bool
Transport *transport.FoundryTransport
IsReadReady bool
logger *slog.Logger
ws *webSocketUtil
Status types.FoundryStatus
}
func NewFoundry() *Foundry {
foundry := &Foundry{isAuth: false, ws: NewWebSocketUtil()}
// err := foundry.config.GetSessionId()
// if err != nil {
// return nil, err
// }
return foundry
func NewFoundry() *FoundryApi {
return &FoundryApi{}
}
func (foundry *Foundry) checkAuthRespAnswer(r io.Reader, respLength int) error {
authBody := make([]byte, respLength)
func (foundry *FoundryApi) StartListen() error {
foundry.Transport.ReadChan = *types.InitWsChannels()
_, err := r.Read(authBody)
if err != nil && err != io.EOF {
return err
}
if !strings.Contains(string(authBody), "Admin authentication successful") {
return ErrorAuthPassWrong
}
foundry.isAuth = true
return nil
}
func (foundry *Foundry) Authenticate() error {
if foundry.ws.config.SessionID == "" {
err := foundry.ws.config.GetSessionId()
if err != nil {
return err
}
}
resp, err := foundry.ws.config.PostAuthenticationData()
if err != nil {
return err
}
defer resp.Body.Close()
contentLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
if err != nil {
return err
}
return foundry.checkAuthRespAnswer(resp.Body, contentLen)
}
func (foundry *Foundry) StartListen() error {
foundry.ws.readChan = *types.InitWsChannels()
wsChannels := &foundry.ws.readChan
defer wsChannels.Close()
wsChannels := &foundry.Transport.ReadChan
defer foundry.Transport.ReadChan.Close()
go foundry.ListenAndServeWS()
var err error
@@ -93,115 +47,108 @@ func (foundry *Foundry) StartListen() error {
// }
// foundry.models.FoundryState.Insert(foundryState)
case err = <-wsChannels.Err():
var foundryErr *FoundryError
var foundryErr *types.FoundryError
if errors.As(err, &foundryErr) {
if foundryErr.IsFatal {
foundry.CloseWebSocketConn()
foundry.Transport.CloseWebSocketConn()
return foundryErr
}
foundry.logger.Warn("Got error when listening or served websocket", "err", err.Error())
foundry.Transport.Logger.Warn("Got error when listening or served", "err", err.Error(), "type", foundryErr.Type, "direction", foundryErr.Direction)
}
case <-wsChannels.Done():
foundry.CloseWebSocketConn()
foundry.Transport.CloseWebSocketConn()
return ListenIsDone
}
}
}
func (foundry *Foundry) ConnectToFoundry() error {
func (foundry *FoundryApi) ServeWebSocket() {
for {
select {
case message, ok := <-foundry.Transport.ReadChan.Msg():
if !ok {
foundry.Transport.Logger.Debug("Read channel is closed", "type", types.WebSocketCode)
return
}
foundry.Transport.Logger.Debug("Data has been received\n", "msg", message.Code, "type", types.WebSocketCode) //, "msg", message.MsgJson)
switch message.Code {
case types.RespPingCode, types.RespSessionDataCode:
err := foundry.Transport.SendOnlyCodeRequest(message.Code)
if err != nil {
foundry.IsReadReady = false
foundry.Transport.ReadChan.Err() <- &types.FoundryError{Direction: types.WriterCode, Err: err, Type: types.WebSocketCode}
continue
}
case types.RespServerChangeCode:
foundry.IsReadReady = true
data, err := actions.SplitToTypeAndData([]byte(message.MsgJson))
if err != nil {
foundry.Transport.ReadChan.Err() <- &types.FoundryError{Direction: types.ReaderCode, Err: err, Type: types.WebSocketCode}
continue
}
if data == nil {
continue
}
err = data.Action(foundry.Transport, &foundry.Status)
if err != nil {
foundry.Transport.ReadChan.Err() <- &types.FoundryError{Direction: types.ReaderCode, Err: err, Type: types.WebSocketCode}
continue
}
case types.RespDataCode:
foundry.Transport.ExchangeChan.Msgs[message.Id] = make(chan []byte, 1)
foundry.Transport.ExchangeChan.Msgs[message.Id] <- []byte(message.MsgJson)
go foundry.Transport.CloseMsgChannel(message.Id, 5*time.Second)
default:
}
case <-foundry.Transport.ReadChan.Done():
return
}
}
}
func (foundry *FoundryApi) ListenAndServeWS() {
go foundry.Transport.ListenWebSocket()
foundry.ServeWebSocket()
}
func (foundry *FoundryApi) HandleWSRequest(msgType string) ([]byte, error) {
msg := types.NewWsMessageByPage(msgType, foundry.Transport.CurrWsId)
return foundry.Transport.HandleWebsocketRequest(msg)
}
func (foundry *FoundryApi) ConnectToWebSocket() error {
var err error
if foundry == nil {
return ErrorFoundryNotInit
}
ok, err := (*foundry).ws.config.GetSessionToken()
if err != nil {
return err
}
if (*foundry).ws.config.Password != "" && !ok {
err := (*foundry).Authenticate()
if !foundry.Transport.HasSessionId() {
err = foundry.Transport.InitSessionId()
if err != nil {
return err
}
}
foundry.logger.Info("Successefully connected to Foundry", "host", foundry.ws.config.Host)
return nil
}
func (foundry *Foundry) ConnectToWebSocket() error {
if !foundry.isAuth {
return ErrorNotAuth
}
query := url.Values{}
query.Add("session", foundry.ws.config.SessionID)
query.Add("EIO", "4")
query.Add("transport", "websocket")
u := url.URL{Scheme: "ws", Host: foundry.ws.config.Host, Path: fmt.Sprintf("%s/", requests.SocketPath), RawQuery: query.Encode()}
wsHeader := http.Header{}
wsHeader.Set("Cookie", fmt.Sprintf("session=%s", foundry.ws.config.SessionID))
wsConn, _, err := websocket.DefaultDialer.Dial(u.String(), wsHeader)
err = foundry.Transport.ConnectToFoundry()
if err != nil {
return err
}
foundry.ws.wsConn = wsConn
foundry.ws.currWsId = 0
foundry.logger.Info("Successefully connected to Foundry Websocket")
err = foundry.Transport.InitWebSocketConnection()
if err != nil {
return err
}
status, err := foundry.Transport.Http.GetStatus()
if err != nil {
return err
}
foundry.Status = *types.NewFoundryStatus(status)
err = foundry.StartListen()
if err != nil && err != ListenIsDone {
return err
}
return nil
}
func (foundry *Foundry) CloseWebSocketConn() {
foundry.ws.wsConn.Close()
}
func (foundry *Foundry) ListenAndServeWS() {
go foundry.ws.ListenWebSocket()
foundry.ws.ServeWebSocket()
}
func (foundry *Foundry) HandleWSRequest(msgType string) ([]byte, error) {
msg := foundry.ws.CreateWSMessageByPage(msgType)
return foundry.ws.HandleWebsocketRequest(msg)
}
func (foundry *Foundry) HasSessionId() bool {
return foundry.ws.config.SessionID != ""
}
func (foundry *Foundry) SetUpSessionId() error {
return foundry.ws.config.GetSessionId()
}
func (foundry *Foundry) SetConfig(config *requests.Config) {
foundry.ws.config = *config
}
func (foundry *Foundry) SetLogger(slogger *slog.Logger) {
foundry.logger = slogger
foundry.ws.logger = slogger
}
func (foundry *Foundry) CreateNewDataModels(db *sql.DB) {
foundry.ws.CreateNewDataModels(db)
}
// /**
// * Poll the server to see if a World has become active, and automatically refresh if so.
// * @returns {Promise<void>}
// */
// async #pollActive() {
// const poll = () => {
// const status = foundry.utils.getRoute("/api/status");
// this.#activePollState.last = Date.now();
// this.#activePollState.polling = foundry.utils.fetchJsonWithTimeout(status, {}, {
// timeoutMs: 10000
// }).catch(() => {});
// this.#pollActive();
// };

View File

@@ -21,8 +21,8 @@ type Models struct {
// }
// }
func NewModels(db *sql.DB) Models {
return Models{
func NewModels(db *sql.DB) *Models {
return &Models{
FoundryState: FoundryStateModel{DB: db},
}
}

View File

@@ -9,3 +9,13 @@ type Status struct {
Users int `json:"users,omitempty"`
Uptime int64 `json:"uptime,omitempty"`
}
func (s *Status) Copy(copyStatus *Status) {
s.Active = copyStatus.Active
s.System = copyStatus.System
s.SystemVersion = copyStatus.SystemVersion
s.Uptime = copyStatus.Uptime
s.Users = copyStatus.Users
s.Version = copyStatus.Version
s.World = copyStatus.World
}

View File

@@ -42,17 +42,17 @@ var PathToSetupState = map[string]db.StateType{
}
var PathToWorldState = map[string]db.StateType{
JoinPath: db.JoinState,
PlayersPath: db.PlayersState,
JoinPath: db.JoinState,
// PlayersPath: db.PlayersState,
}
type Config struct {
type FoundryHttpRequest struct {
Host string
Password string
SessionID string
}
func (conf *Config) SetSessionTokenFromHeader(resp http.Header) (bool, error) {
func (conf *FoundryHttpRequest) SetSessionTokenFromHeader(resp http.Header) (bool, error) {
setCookieHeader := resp.Get("Set-Cookie")
if setCookieHeader == "" {
return false, nil
@@ -73,7 +73,7 @@ func (conf *Config) SetSessionTokenFromHeader(resp http.Header) (bool, error) {
return false, nil
}
func (conf *Config) GetRequestHeader() *http.Header {
func (conf *FoundryHttpRequest) GetRequestHeader() *http.Header {
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")

View File

@@ -1,14 +1,16 @@
package requests
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
json_model "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/json"
)
func (conf *Config) GetSessionId() error {
func (conf *FoundryHttpRequest) GetSessionId() error {
getResp, err := http.Get(fmt.Sprintf("http://%s%s", conf.Host, AuthPath))
if err != nil {
return err
@@ -18,7 +20,7 @@ func (conf *Config) GetSessionId() error {
return err
}
func (conf *Config) GetSessionToken() (bool, error) {
func (conf *FoundryHttpRequest) GetSessionToken() (bool, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s%s", conf.Host, AuthPath), nil)
if err != nil {
return false, err
@@ -42,21 +44,21 @@ func (conf *Config) GetSessionToken() (bool, error) {
return conf.SetSessionTokenFromHeader(resp.Header)
}
func (conf *Config) GetStatus() (*json_model.Status, error) {
func (conf *FoundryHttpRequest) GetStatus() (*json_model.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 {
statusByte := bytes.Buffer{}
_, err = io.Copy(&statusByte, getResp.Body)
if err != nil && err != io.EOF {
return nil, err
}
var status json_model.Status
err = json.Unmarshal(statusByte, &status)
err = json.Unmarshal(statusByte.Bytes(), &status)
if err != nil {
return nil, err
}

View File

@@ -7,7 +7,7 @@ import (
"net/url"
)
func (conf *Config) PostAuthenticationData() (*http.Response, error) {
func (conf *FoundryHttpRequest) PostAuthenticationData() (*http.Response, error) {
authData := url.Values{}
authData.Add("adminPassword", conf.Password)
authData.Add("action", "adminAuth")
@@ -30,7 +30,7 @@ func (conf *Config) PostAuthenticationData() (*http.Response, error) {
return resp, nil
}
func (conf *Config) PostLaunchWorld(world string) (*http.Response, error) {
func (conf *FoundryHttpRequest) PostLaunchWorld(world string) (*http.Response, error) {
launchData := url.Values{}
launchData.Add("world", world)
launchData.Add("action", "launchWorld")
@@ -53,7 +53,7 @@ func (conf *Config) PostLaunchWorld(world string) (*http.Response, error) {
return resp, nil
}
func (conf *Config) PostReturnToSetup() (*http.Response, error) {
func (conf *FoundryHttpRequest) PostReturnToSetup() (*http.Response, error) {
launchData := url.Values{}
launchData.Add("action", "shutdown")

View File

@@ -0,0 +1,98 @@
package transport
import (
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/db"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/json"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
)
func (tr *FoundryTransport) FillDBWithFoundryData() {
err := tr.InitSetupData()
if err != nil {
tr.ReadChan.Err() <- &types.FoundryError{Direction: types.WriterCode, Err: err, Type: types.DbCode}
return
}
idState, err := tr.Models.FoundryState.GetIdByType(db.SetupState)
if err != nil {
tr.ReadChan.Err() <- &types.FoundryError{Direction: types.WriterCode, Err: err, Type: types.DbCode}
return
}
err = tr.InitWorldsData(idState)
if err != nil {
tr.ReadChan.Err() <- &types.FoundryError{Direction: types.WriterCode, Err: err, Type: types.DbCode}
return
}
}
func (tr *FoundryTransport) InitSetupData() error {
tr.Models.FoundryState.DeleteAll()
tr.Models.FoundryState.DeleteAllSeq()
for k := range requests.PathToSetupState {
err := tr.InsertJsonDataToDB(k)
if err != nil {
return err
}
}
return nil
}
func (tr *FoundryTransport) InsertJsonDataToDB(statePath string) error {
_, ok1 := requests.PathToSetupState[statePath]
_, ok2 := requests.PathToWorldState[statePath]
if !ok1 && !ok2 {
return ErrorStateTypeNotExist
}
msgJson, err := tr.GetJsonData(statePath)
if err != nil {
return err
}
foundryStateJson, err := json.ParseSetupModel(msgJson)
if err != nil {
return err
}
foundryStateDb := foundryStateJson.GetFoundryStateDB(requests.PathToSetupState[statePath])
err = tr.Models.FoundryState.Insert(foundryStateDb)
if err != nil {
return err
}
return nil
}
func (tr *FoundryTransport) InitWorldsData(idState int64) error {
worlds, err := tr.Models.FoundryState.GetWorlds(idState)
if err != nil {
return err
}
tr.Logger.Debug("Worlds", "len", len(worlds))
for i := range worlds {
worldName := worlds[i].TextId
tr.InitWorldData(worldName)
_, err = tr.Http.PostReturnToSetup()
if err != nil {
return err
}
}
return nil
}
func (tr *FoundryTransport) InitWorldData(worldName string) error {
tr.Logger.Debug("World name", "name", worldName)
err := tr.LaunchWorld(worldName)
if err != nil {
return err
}
for k := range requests.PathToWorldState {
err := tr.InsertJsonDataToDB(k)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,11 @@
package transport
import "errors"
var (
ErrorNotAuth = errors.New("Admin is not authenticated")
ErrorIsNotReady = errors.New("Connection is not ready for communication")
ErrorTimeout = errors.New("Answer has not been received after timeout")
ErrorStateTypeNotExist = errors.New("State type does not exists")
ErrorAuthPassWrong = errors.New("Authentication password is wrong")
)

View File

@@ -0,0 +1,101 @@
package transport
import (
"context"
"errors"
"io"
"strconv"
"strings"
"time"
)
func (tr *FoundryTransport) LaunchWorld(worldName string) error {
resp, err := tr.Http.PostLaunchWorld(worldName)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New("World is not found")
}
tr.ExchangeChan.ProgressMsg[worldName] = make(chan struct{})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
select {
case <-tr.ExchangeChan.ProgressMsg[worldName]:
tr.Logger.Debug("World has been started(from GetInitialData)", "world", worldName)
case <-ctx.Done():
close(tr.ExchangeChan.ProgressMsg[worldName])
err := ctx.Err()
if err != nil {
return err
}
return ErrorTimeout
}
return nil
}
func (tr *FoundryTransport) ConnectToFoundry() error {
var err error
ok, err := tr.Http.GetSessionToken()
if err != nil {
return err
}
if tr.Http.Password != "" && !ok {
err := tr.Authenticate()
if err != nil {
return err
}
}
tr.Logger.Info("Successefully connected to Foundry", "host", tr.Http.Host)
return nil
}
func (tr *FoundryTransport) Authenticate() error {
if tr.Http.SessionID == "" {
err := tr.Http.GetSessionId()
if err != nil {
return err
}
}
resp, err := tr.Http.PostAuthenticationData()
if err != nil {
return err
}
defer resp.Body.Close()
contentLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
if err != nil {
return err
}
return tr.CheckAuthRespAnswer(resp.Body, contentLen)
}
func (tr *FoundryTransport) CheckAuthRespAnswer(r io.Reader, respLength int) error {
authBody := make([]byte, respLength)
_, err := r.Read(authBody)
if err != nil && err != io.EOF {
return err
}
if !strings.Contains(string(authBody), "Admin authentication successful") {
return ErrorAuthPassWrong
}
tr.IsAuth = true
return nil
}
func (tr *FoundryTransport) HasSessionId() bool {
return tr.Http.SessionID != ""
}
func (tr *FoundryTransport) InitSessionId() error {
return tr.Http.GetSessionId()
}

View File

@@ -0,0 +1,71 @@
package transport
import (
"database/sql"
"log/slog"
"sync"
"time"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/db"
"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 FoundryTransport struct {
WsConn *websocket.Conn
CurrWsId int
ChanMutex sync.Mutex
ReadChan types.ReadChannels
ExchangeChan types.ExchangeChannels
Http requests.FoundryHttpRequest
IsAuth bool
Models *db.Models
IsDbInit bool
Logger *slog.Logger
}
func NewFoundryTransport(dbConn *sql.DB, logger *slog.Logger, httpConfig *requests.FoundryHttpRequest) *FoundryTransport {
return &FoundryTransport{
CurrWsId: 0,
ExchangeChan: types.ExchangeChannels{
Msgs: make(map[int]chan []byte),
ProgressMsg: map[string]chan struct{}{},
},
Http: *httpConfig,
IsAuth: false,
Models: db.NewModels(dbConn),
IsDbInit: false,
Logger: logger,
}
}
func (t *FoundryTransport) CloseMsgChannel(id int, timeout time.Duration) bool {
time.Sleep(timeout)
t.ChanMutex.Lock()
defer t.ChanMutex.Unlock()
ok := true
if _, ok = t.ExchangeChan.Msgs[id]; !ok {
return false
}
select {
case _, ok = <-t.ExchangeChan.Msgs[id]:
if ok {
close(t.ExchangeChan.Msgs[id])
delete(t.ExchangeChan.Msgs, id)
}
default:
close(t.ExchangeChan.Msgs[id])
delete(t.ExchangeChan.Msgs, id)
}
return ok
}

View File

@@ -0,0 +1,111 @@
package transport
import (
"context"
"fmt"
"net/http"
"net/url"
"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"
)
func (tr *FoundryTransport) InitWebSocketConnection() error {
if !tr.IsAuth {
return ErrorNotAuth
}
query := url.Values{}
query.Add("session", tr.Http.SessionID)
query.Add("EIO", "4")
query.Add("transport", "websocket")
u := url.URL{Scheme: "ws", Host: tr.Http.Host, Path: fmt.Sprintf("%s/", requests.SocketPath), RawQuery: query.Encode()}
wsHeader := http.Header{}
wsHeader.Set("Cookie", fmt.Sprintf("session=%s", tr.Http.SessionID))
wsConn, _, err := websocket.DefaultDialer.Dial(u.String(), wsHeader)
if err != nil {
return err
}
tr.WsConn = wsConn
tr.CurrWsId = 0
tr.Logger.Info("Successefully connected to Foundry Websocket")
return nil
}
func (tr *FoundryTransport) HandleWebsocketRequest(msg *types.WsMessage) ([]byte, error) {
err := tr.WsConn.WriteMessage(websocket.TextMessage, msg.ToByteSlice())
if err != nil {
return nil, err
}
data, err := tr.ReceiveMessage(msg.Id)
if err != nil {
return nil, err
}
return data, nil
}
func (tr *FoundryTransport) ReceiveMessage(id int) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for {
select {
case msg, ok := <-tr.ExchangeChan.Msgs[id]:
if !ok {
time.Sleep(5 * time.Microsecond)
continue
}
tr.CloseMsgChannel(id, 0)
return msg, nil
case <-ctx.Done():
err := ctx.Err()
if err != nil {
return nil, err
}
return nil, ErrorTimeout
default:
time.Sleep(5 * time.Microsecond)
}
}
}
func (tr *FoundryTransport) ListenWebSocket() {
for {
_, message, err := tr.WsConn.ReadMessage()
if err != nil {
tr.ReadChan.Err() <- &types.FoundryError{Direction: types.ReaderCode, Err: err, Type: types.WebSocketCode}
return
}
data, err := types.ParseWsRespMessage(string(message), types.RequestCodes)
if err != nil {
tr.ReadChan.Err() <- &types.FoundryError{Direction: types.ReaderCode, Err: err, Type: types.WebSocketCode}
continue
}
tr.ReadChan.Msg() <- data
}
}
func (tr *FoundryTransport) SendOnlyCodeRequest(code string) error {
tr.Logger.Debug("WS: Data has been send\n", "msg", types.CodesRespToReq[code])
return tr.WsConn.WriteMessage(websocket.TextMessage, []byte(types.CodesRespToReq[code]))
}
func (tr *FoundryTransport) GetJsonData(stateType string) ([]byte, error) {
msg := types.NewWsMessageByPage(stateType, tr.CurrWsId)
tr.CurrWsId++
return tr.HandleWebsocketRequest(msg)
}
func (tr *FoundryTransport) CloseWebSocketConn() {
tr.Logger.Debug("Websocket has been closed")
tr.WsConn.Close()
}

View File

@@ -0,0 +1,40 @@
package types
type DirectionCode int
type TransportCode int
const (
WriterCode = DirectionCode(0)
ReaderCode = DirectionCode(1)
DbCode = TransportCode(0)
HttpCode = TransportCode(1)
WebSocketCode = TransportCode(2)
)
const (
RespSessionDataCode = "0"
RespPingCode = "2"
RespSessionIdCode = "40"
RespServerChangeCode = "42"
RespDataCode = "43"
ReqPongCode = "3"
ReqCreateSessionCode = "40"
ReqDataCode = "42"
)
var RequestCodes = []string{
RespSessionDataCode,
RespPingCode,
RespSessionIdCode,
RespServerChangeCode,
RespDataCode,
}
var CodesRespToReq = map[string]string{
RespSessionDataCode: ReqCreateSessionCode,
RespPingCode: ReqPongCode,
RespServerChangeCode: ReqDataCode,
//ReqDataCode: RespServerChangeCode,
}

View File

@@ -0,0 +1,24 @@
package types
import "fmt"
type FoundryError struct {
Err error
Direction DirectionCode
Type TransportCode
IsFatal bool
}
func (e *FoundryError) Error() string {
return fmt.Sprintf("Received from %d: %s", e.Type, e.Err.Error())
}
func (e *FoundryError) Unwrap() error { return e.Err }
// func NewWriterError(err error, isFatal bool) *FoundryError {
// return &FoundryError{Direction: WriterCode, Err: err, IsFatal: isFatal}
// }
// func NewReaderError(err error, isFatal bool) *FoundryError {
// return &FoundryError{Type: ReaderCode, Err: err, IsFatal: isFatal}
// }

View File

@@ -1,9 +0,0 @@
package types
import "sync"
type ExchangeChannels struct {
Msgs map[int](chan []byte)
ProgressMsg map[string](chan struct{})
ProgressMutex sync.Mutex
}

View File

@@ -0,0 +1,35 @@
package types
import "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/json"
type FoundryStatus struct {
IsActive bool
Version string
World string
System string
SystemVersion string
Users int
Uptime int64
}
func NewFoundryStatus(status *json.Status) *FoundryStatus {
return &FoundryStatus{
IsActive: status.Active,
Version: status.Version,
World: status.World,
System: status.System,
SystemVersion: status.SystemVersion,
Users: status.Users,
Uptime: status.Uptime,
}
}
func (fs *FoundryStatus) Update(status *json.Status) {
fs.IsActive = status.Active
fs.Version = status.Version
fs.World = status.World
fs.System = status.System
fs.SystemVersion = status.SystemVersion
fs.Users = status.Users
fs.Uptime = status.Uptime
}

View File

@@ -1,72 +0,0 @@
package types
import "fmt"
const (
WsSessionType = "session"
WsProgressType = "progress"
WsUserActivityType = "userActivity"
WsShutdownType = "shutdown"
)
var WsMsgActions = map[string]func() any{
"session": func() any { return &WsSessionMsg{} },
"progress": func() any { return &WsProgressMsg{} },
// "userActivity": WsUserActivityCursor{},
"shutdown": func() any { return &WsShutdownMsg{} },
}
func GetWsMsgAction(dataType string) any {
if factory, ok := WsMsgActions[dataType]; ok {
return factory()
}
return nil
}
type WsSessionMsg struct {
SessionId string `json:"sessionId"`
UserId *string `json:"userId,omitempty"`
}
func (msg WsSessionMsg) Action() error {
fmt.Println("WsSessionMsg")
return nil
}
type WsProgressMsg struct {
Id string `json:"id"`
Message string `json:"message"`
Pct float64 `json:"pct"`
HasChanged bool `json:"hasChanged,omitempty"`
Act string `json:"action"`
Step string `json:"step"`
}
func (msg WsProgressMsg) Action() error {
fmt.Println("WsProgressMsg")
return nil
}
type WsUserActivityMsg struct {
Cursor WsUserActivityCursor `json:"cursor"`
}
type WsUserActivityCursor struct {
X int `json:"x"`
Y int `json:"y"`
}
func (msg WsUserActivityMsg) Action() error {
fmt.Println("WsUserActivityMsg")
return nil
}
type WsShutdownMsg struct {
World string `json:"world"`
UserId *string `json:"userId,omitempty"`
}
func (msg WsShutdownMsg) Action() error {
fmt.Println("WsShutdownMsg")
return nil
}

View File

@@ -1,5 +1,13 @@
package types
import "sync"
type ExchangeChannels struct {
Msgs map[int](chan []byte)
ProgressMsg map[string](chan struct{})
ProgressMutex sync.Mutex
}
type ReadChannels struct {
done chan struct{}
msg chan *WsMessage
@@ -25,11 +33,9 @@ func (channels *ReadChannels) Close() {
}
func InitWsChannels() *ReadChannels {
channels := ReadChannels{
return &ReadChannels{
done: make(chan struct{}),
err: make(chan error, 10),
msg: make(chan *WsMessage, 10),
}
return &channels
}

View File

@@ -1,6 +1,17 @@
package types
import "fmt"
import (
"errors"
"fmt"
"strconv"
"strings"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests"
)
var (
ErrorMsgNotHaveNumber = errors.New("Message doesn't have dataCode and id")
)
type WsMessage struct {
Code string
@@ -15,3 +26,58 @@ func (w WsMessage) ToString() string {
func (w WsMessage) ToByteSlice() []byte {
return fmt.Appendf([]byte{}, "%s%d%s", w.Code, w.Id, w.MsgJson)
}
func parseCode(msg *string, requestCodes []string) string {
for j := range requestCodes {
if !strings.HasPrefix(*msg, requestCodes[j]) {
continue
}
return requestCodes[j]
}
return ""
}
func parseId(msg *string, start int) (int, int) {
msgLen := len(*msg)
j := start
for ; j < msgLen; j++ {
if (*msg)[j] < '0' || (*msg)[j] > '9' {
break
}
}
if j >= msgLen || j <= 0 {
return 0, 0
}
msgId, err := strconv.Atoi((*msg)[start:j])
if err != nil {
return 0, 0
}
return msgId, j
}
func ParseWsRespMessage(msg string, requestCodes []string) (*WsMessage, error) {
data := &WsMessage{}
data.Code = parseCode(&msg, requestCodes)
i := len(data.Code)
if i == 0 {
return nil, ErrorMsgNotHaveNumber
}
data.Id, i = parseId(&msg, i)
if i == 0 {
i = len(data.Code)
}
// if i == 0 {
// return nil, ErrorMsgNotHaveNumber
// }
data.MsgJson = msg[i:]
return data, nil
}
func NewWsMessageByPage(page string, currWsId int) *WsMessage {
return &WsMessage{Code: CodesRespToReq[RespServerChangeCode], Id: currWsId, MsgJson: fmt.Sprintf("[\"%s\"]", requests.WsTypeData[page])}
}

View File

@@ -1,470 +0,0 @@
package foundry
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
"sync"
"time"
db_model "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/db"
json_model "gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/models/json"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/requests"
"gitea.local.lab/Lbenedar/foundry_helper_service/internal/foundry/types"
"github.com/gorilla/websocket"
)
//TODO: refactor all code
type TransportCode int
type FoundryMode int
const (
WriterCode = TransportCode(0)
ReaderCode = TransportCode(1)
)
// const (
// SetupMode = FoundryMode(0)
// WorldMode = FoundryMode(1)
// )
// var
const (
RespSessionData = "0"
RespPingCode = "2"
RespSessionId = "40"
RespServerChangeCode = "42"
RespDataCode = "43"
ReqPongCode = "3"
ReqCreateSessionCode = "40"
ReqDataCode = "42"
)
var RequestCodes = []string{
RespSessionData,
RespPingCode,
RespSessionId,
RespServerChangeCode,
RespDataCode,
}
var CodesRespToReq = map[string]string{
RespSessionData: ReqCreateSessionCode,
RespPingCode: ReqPongCode,
RespServerChangeCode: ReqDataCode,
//ReqDataCode: RespServerChangeCode,
}
type webSocketUtil struct {
wsConn *websocket.Conn
currWsId int
isReadReady bool
logger *slog.Logger
config requests.Config
chanMutex sync.Mutex
readChan types.ReadChannels
exchangeChan types.ExchangeChannels
models db_model.Models
}
func NewWebSocketUtil() *webSocketUtil {
return &webSocketUtil{
currWsId: 0,
isReadReady: false,
exchangeChan: types.ExchangeChannels{
Msgs: make(map[int]chan []byte),
ProgressMsg: map[string]chan struct{}{},
},
config: requests.Config{SessionID: ""}}
}
func parseCode(msg *string) string {
for j := range RequestCodes {
if !strings.HasPrefix(*msg, RequestCodes[j]) {
continue
}
return RequestCodes[j]
}
return ""
}
func parseId(msg *string, start int) (int, int) {
msgLen := len(*msg)
j := start
for ; j < msgLen; j++ {
if (*msg)[j] < '0' || (*msg)[j] > '9' {
break
}
}
if j >= msgLen || j <= 0 {
return 0, 0
}
msgId, err := strconv.Atoi((*msg)[start:j])
if err != nil {
return 0, 0
}
return msgId, j
}
func parseWsRespMessage(msg string) (*types.WsMessage, error) {
data := &types.WsMessage{}
data.Code = parseCode(&msg)
i := len(data.Code)
if i == 0 {
return nil, ErrorMsgNotHaveNumber
}
data.Id, i = parseId(&msg, i)
if i == 0 {
i = len(data.Code)
}
// if i == 0 {
// return nil, ErrorMsgNotHaveNumber
// }
data.MsgJson = msg[i:]
return data, nil
}
// TODO: Lookup timeout
func (ws *webSocketUtil) ReceiveMessage(id int) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for {
select {
case msg, ok := <-ws.exchangeChan.Msgs[id]:
if !ok {
time.Sleep(5 * time.Microsecond)
continue
}
ws.closeMsgChannel(id, 0)
return msg, nil
case <-ctx.Done():
err := ctx.Err()
if err != nil {
return nil, err
}
return nil, ErrorTimeout
default:
time.Sleep(5 * time.Microsecond)
}
}
}
func (ws *webSocketUtil) closeMsgChannel(id int, timeout time.Duration) bool {
time.Sleep(timeout)
ws.chanMutex.Lock()
defer ws.chanMutex.Unlock()
ok := true
if _, ok = ws.exchangeChan.Msgs[id]; !ok {
return false
}
select {
case _, ok = <-ws.exchangeChan.Msgs[id]:
if ok {
close(ws.exchangeChan.Msgs[id])
delete(ws.exchangeChan.Msgs, id)
}
default:
close(ws.exchangeChan.Msgs[id])
delete(ws.exchangeChan.Msgs, id)
}
ws.logger.Debug("WS: Channel has been closed\n", "id", id, "timeout", timeout.String())
return ok
}
func (ws *webSocketUtil) HandleWebsocketRequest(msg *types.WsMessage) ([]byte, error) {
if !ws.IsReadReady() {
return nil, ErrorIsNotReady
}
err := ws.wsConn.WriteMessage(websocket.TextMessage, msg.ToByteSlice())
if err != nil {
return nil, err
}
data, err := ws.ReceiveMessage(msg.Id)
if err != nil {
return nil, err
}
return data, nil
}
func (ws *webSocketUtil) sendOnlyCodeRequest(code string) error {
ws.logger.Debug("WS: Data has been send\n", "msg", CodesRespToReq[code])
return ws.wsConn.WriteMessage(websocket.TextMessage, []byte(CodesRespToReq[code]))
}
func (ws *webSocketUtil) ListenWebSocket() {
for {
_, message, err := ws.wsConn.ReadMessage()
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: ReaderCode, Err: err, IsFatal: true}
return
}
data, err := parseWsRespMessage(string(message))
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: ReaderCode, Err: err}
continue
}
ws.readChan.Msg() <- data
}
}
func (ws *webSocketUtil) ServeWebSocket() {
for {
select {
case message := <-ws.readChan.Msg():
ws.logger.Debug("WS: Data has been received\n", "msgCode", message.Code)
switch message.Code {
case RespPingCode, RespSessionData:
err := ws.sendOnlyCodeRequest(message.Code)
if err != nil {
ws.isReadReady = false
ws.readChan.Err() <- &FoundryError{Type: WriterCode, Err: err}
continue
}
case RespServerChangeCode:
ws.isReadReady = true
dataType, data, err := ws.SplitToTypeAndData([]byte(message.MsgJson))
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: WriterCode, Err: err}
continue
}
// ws.logger.Debug("Parsed server change message", "dataType", dataType, "data", data)
ws.ProcessAction(dataType, data)
case RespDataCode:
ws.exchangeChan.Msgs[message.Id] = make(chan []byte, 1)
ws.exchangeChan.Msgs[message.Id] <- []byte(message.MsgJson)
go ws.closeMsgChannel(message.Id, 5*time.Second)
default:
}
case <-ws.readChan.Done():
return
}
}
}
func (ws *webSocketUtil) IsReadReady() bool {
return ws.isReadReady
}
func (ws *webSocketUtil) GetJsonData(stateType string) ([]byte, error) {
msg := ws.CreateWSMessageByPage(stateType)
return ws.HandleWebsocketRequest(msg)
}
func (ws *webSocketUtil) InsertJsonDataToDB(statePath string) error {
_, ok1 := requests.PathToSetupState[statePath]
_, ok2 := requests.PathToWorldState[statePath]
if !ok1 && !ok2 {
return ErrorStateTypeNotExist
}
msgJson, err := ws.GetJsonData(statePath)
if err != nil {
return err
}
foundryStateJson, err := json_model.ParseSetupModel(msgJson)
if err != nil {
return err
}
foundryStateDb := foundryStateJson.GetFoundryStateDB(requests.PathToSetupState[statePath])
err = ws.models.FoundryState.Insert(foundryStateDb)
if err != nil {
return err
}
return nil
}
func (ws *webSocketUtil) GetInitialData() {
err := ws.InitSetupData()
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: WriterCode, Err: err}
return
}
idState, err := ws.models.FoundryState.GetIdByType(db_model.SetupState)
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: WriterCode, Err: err}
return
}
ws.logger.Debug("Id state", "id", idState)
err = ws.InitWorldsData(idState)
if err != nil {
ws.readChan.Err() <- &FoundryError{Type: WriterCode, Err: err}
return
}
}
func (ws *webSocketUtil) InitSetupData() error {
ws.models.FoundryState.DeleteAll()
ws.models.FoundryState.DeleteAllSeq()
for k := range requests.PathToSetupState {
err := ws.InsertJsonDataToDB(k)
if err != nil {
return err
}
}
return nil
}
func (ws *webSocketUtil) InitWorldsData(idState int64) error {
worlds, err := ws.models.FoundryState.GetWorlds(idState)
if err != nil {
return err
}
ws.logger.Debug("Worlds", "len", len(worlds))
for i := range worlds {
worldName := worlds[i].TextId
ws.InitWorldData(worldName)
_, err = ws.config.PostReturnToSetup()
if err != nil {
return err
}
}
return nil
}
func (ws *webSocketUtil) InitWorldData(worldName string) error {
ws.logger.Debug("World name", "name", worldName)
err := ws.LaunchWorld(worldName)
if err != nil {
return err
}
for k := range requests.PathToWorldState {
err := ws.InsertJsonDataToDB(k)
if err != nil {
return err
}
}
return nil
}
func (ws *webSocketUtil) LaunchWorld(worldName string) error {
resp, err := ws.config.PostLaunchWorld(worldName)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New("World is not found")
}
ws.exchangeChan.ProgressMsg[worldName] = make(chan struct{})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
select {
case <-ws.exchangeChan.ProgressMsg[worldName]:
ws.logger.Debug("World has been started(from GetInitialData)", "world", worldName)
case <-ctx.Done():
close(ws.exchangeChan.ProgressMsg[worldName])
err := ctx.Err()
if err != nil {
return err
}
return ErrorTimeout
}
return nil
}
func (ws *webSocketUtil) CreateNewDataModels(db *sql.DB) {
ws.models = db_model.NewModels(db)
}
func (ws *webSocketUtil) SplitToTypeAndData(dataJson []byte) (string, any, error) {
var rawItems []json.RawMessage
err := json.Unmarshal(dataJson, &rawItems)
if err != nil {
return "", nil, err
}
var dataType string
err = json.Unmarshal(rawItems[0], &dataType)
if err != nil {
return "", nil, err
}
var data map[string]any
err = json.Unmarshal(rawItems[1], &data)
if err != nil {
return "", nil, err
}
actionDataPtr := types.GetWsMsgAction(dataType)
if actionDataPtr == nil {
return dataType, nil, nil
}
err = types.FillStruct(data, actionDataPtr)
if err != nil {
return "", nil, err
}
return dataType, actionDataPtr, nil
}
func (ws *webSocketUtil) ProcessAction(dataType string, data any) {
switch tp := data.(type) {
case *types.WsSessionMsg:
ws.ProcessSessionAction(tp)
case *types.WsProgressMsg:
ws.ProcessProgressAction(tp)
case *types.WsShutdownMsg:
ws.ProcessShutdownAction(tp)
default:
ws.logger.Debug("default")
}
}
func (ws *webSocketUtil) ProcessSessionAction(data *types.WsSessionMsg) {
ws.logger.Debug("ProcessSessionAction", "data", data)
go ws.GetInitialData()
}
func (ws *webSocketUtil) ProcessProgressAction(data *types.WsProgressMsg) {
if !data.HasChanged {
ws.logger.Debug("World has been launched", "world", data.Id)
select {
case <-ws.exchangeChan.ProgressMsg[data.Id]:
ws.logger.Warn("Channel for the world is closed", "world", data.Id)
default:
close(ws.exchangeChan.ProgressMsg[data.Id])
}
}
}
func (ws *webSocketUtil) ProcessShutdownAction(data *types.WsShutdownMsg) {
ws.logger.Debug("ProcessShutdownAction", "data", data)
}
func (ws *webSocketUtil) CreateWSMessageByPage(page string) *types.WsMessage {
msgToSend := &types.WsMessage{Code: CodesRespToReq[RespServerChangeCode], 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
}