diff --git a/cmd/api/main.go b/cmd/api/main.go index cb0ac94..2024b26 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -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) diff --git a/internal/foundry/actions/errors.go b/internal/foundry/actions/errors.go new file mode 100644 index 0000000..baaf677 --- /dev/null +++ b/internal/foundry/actions/errors.go @@ -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") +) diff --git a/internal/foundry/actions/model.go b/internal/foundry/actions/model.go new file mode 100644 index 0000000..69faf37 --- /dev/null +++ b/internal/foundry/actions/model.go @@ -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 +} diff --git a/internal/foundry/actions/progress.go b/internal/foundry/actions/progress.go new file mode 100644 index 0000000..93c75fc --- /dev/null +++ b/internal/foundry/actions/progress.go @@ -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 +} diff --git a/internal/foundry/actions/session.go b/internal/foundry/actions/session.go new file mode 100644 index 0000000..fd2e99a --- /dev/null +++ b/internal/foundry/actions/session.go @@ -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 +} diff --git a/internal/foundry/actions/shutdown.go b/internal/foundry/actions/shutdown.go new file mode 100644 index 0000000..0e63016 --- /dev/null +++ b/internal/foundry/actions/shutdown.go @@ -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 +} diff --git a/internal/foundry/actions/user_activity.go b/internal/foundry/actions/user_activity.go new file mode 100644 index 0000000..024126f --- /dev/null +++ b/internal/foundry/actions/user_activity.go @@ -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 +} diff --git a/internal/foundry/types/utils.go b/internal/foundry/actions/utils.go similarity index 91% rename from internal/foundry/types/utils.go rename to internal/foundry/actions/utils.go index 933ab26..04102b1 100644 --- a/internal/foundry/types/utils.go +++ b/internal/foundry/actions/utils.go @@ -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 { diff --git a/internal/foundry/errors.go b/internal/foundry/errors.go deleted file mode 100644 index 6efd861..0000000 --- a/internal/foundry/errors.go +++ /dev/null @@ -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 } diff --git a/internal/foundry/foundry.go b/internal/foundry/foundry.go index 84ec60a..7510c9e 100644 --- a/internal/foundry/foundry.go +++ b/internal/foundry/foundry.go @@ -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} -// */ -// 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(); -// }; diff --git a/internal/foundry/models/db/models.go b/internal/foundry/models/db/models.go index 85065e1..de557d2 100644 --- a/internal/foundry/models/db/models.go +++ b/internal/foundry/models/db/models.go @@ -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}, } } diff --git a/internal/foundry/models/json/status.go b/internal/foundry/models/json/status.go index 160277f..eb15649 100644 --- a/internal/foundry/models/json/status.go +++ b/internal/foundry/models/json/status.go @@ -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 +} diff --git a/internal/foundry/requests/config.go b/internal/foundry/requests/config.go index 9bccbc5..aaab9f1 100644 --- a/internal/foundry/requests/config.go +++ b/internal/foundry/requests/config.go @@ -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") diff --git a/internal/foundry/requests/get_requests.go b/internal/foundry/requests/get_requests.go index 853b9e4..aab393d 100644 --- a/internal/foundry/requests/get_requests.go +++ b/internal/foundry/requests/get_requests.go @@ -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 } diff --git a/internal/foundry/requests/post_requests.go b/internal/foundry/requests/post_requests.go index f6ae878..eeb2e59 100644 --- a/internal/foundry/requests/post_requests.go +++ b/internal/foundry/requests/post_requests.go @@ -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") diff --git a/internal/foundry/transport/db.go b/internal/foundry/transport/db.go new file mode 100644 index 0000000..b215dc6 --- /dev/null +++ b/internal/foundry/transport/db.go @@ -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 +} diff --git a/internal/foundry/transport/errors.go b/internal/foundry/transport/errors.go new file mode 100644 index 0000000..7c4dd9d --- /dev/null +++ b/internal/foundry/transport/errors.go @@ -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") +) diff --git a/internal/foundry/transport/http.go b/internal/foundry/transport/http.go new file mode 100644 index 0000000..d54b070 --- /dev/null +++ b/internal/foundry/transport/http.go @@ -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() +} diff --git a/internal/foundry/transport/transport.go b/internal/foundry/transport/transport.go new file mode 100644 index 0000000..767bd02 --- /dev/null +++ b/internal/foundry/transport/transport.go @@ -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 +} diff --git a/internal/foundry/transport/websocket.go b/internal/foundry/transport/websocket.go new file mode 100644 index 0000000..f376bce --- /dev/null +++ b/internal/foundry/transport/websocket.go @@ -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() +} diff --git a/internal/foundry/types/codes.go b/internal/foundry/types/codes.go new file mode 100644 index 0000000..63be2e2 --- /dev/null +++ b/internal/foundry/types/codes.go @@ -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, +} diff --git a/internal/foundry/types/error.go b/internal/foundry/types/error.go new file mode 100644 index 0000000..4a98a75 --- /dev/null +++ b/internal/foundry/types/error.go @@ -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} +// } diff --git a/internal/foundry/types/exchange_channels.go b/internal/foundry/types/exchange_channels.go deleted file mode 100644 index a2c7cf1..0000000 --- a/internal/foundry/types/exchange_channels.go +++ /dev/null @@ -1,9 +0,0 @@ -package types - -import "sync" - -type ExchangeChannels struct { - Msgs map[int](chan []byte) - ProgressMsg map[string](chan struct{}) - ProgressMutex sync.Mutex -} diff --git a/internal/foundry/types/status.go b/internal/foundry/types/status.go new file mode 100644 index 0000000..26cff32 --- /dev/null +++ b/internal/foundry/types/status.go @@ -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 +} diff --git a/internal/foundry/types/ws_action.go b/internal/foundry/types/ws_action.go deleted file mode 100644 index 3ad8b14..0000000 --- a/internal/foundry/types/ws_action.go +++ /dev/null @@ -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 -} diff --git a/internal/foundry/types/ws_channels.go b/internal/foundry/types/ws_channels.go index 11bb6b9..432d898 100644 --- a/internal/foundry/types/ws_channels.go +++ b/internal/foundry/types/ws_channels.go @@ -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 } diff --git a/internal/foundry/types/ws_message.go b/internal/foundry/types/ws_message.go index d203957..4fccab4 100644 --- a/internal/foundry/types/ws_message.go +++ b/internal/foundry/types/ws_message.go @@ -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])} +} diff --git a/internal/foundry/websocket.go b/internal/foundry/websocket.go deleted file mode 100644 index ea61b78..0000000 --- a/internal/foundry/websocket.go +++ /dev/null @@ -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 -}