diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ffa3938 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +github: hewiefreeman +patreon: hewiefreeman +custom: paypal.me/hewiefreeman diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..91b29ba --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '17 4 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md new file mode 100644 index 0000000..6610fda --- /dev/null +++ b/CHANGE_LOG.md @@ -0,0 +1,22 @@ +# Change Log +This is where all important changes and bug fixes will be described in detail. Each entry is labeled with a patch version number, and that is the version in which the changes and/or fixes were made. All prior versions will lack the described changes and/or fixes described. + +> If you are experiencing any bugs or errors because of an update, please report them by [opening an issue](https://github.com/hewiefreeman/GopherGameServer/issues/new/choose)! + +### Key + :wrench: : Bug fix + + :warning: : Code-breaking change + + :newspaper: : New feature + + :monorail: : Optimization +

+## v1.0-BETA.2 + - :newspaper: Added a `version` macro to display current running server version + - :monorail: :warning: ([commit](https://github.com/hewiefreeman/GopherGameServer/commit/941c558bfe44f237f150918187785cceb8aafecd)) Restoring logic has been simplified, but any previous version restore files will fail to restore! + +## v1.0-ALPHA.5 + - :monorail: :warning: ([commit](https://github.com/hewiefreeman/GopherGameServer/commit/a5edd57bcc61fc6f5d10b194f4a433e7a5ed51da)) Merged the `rooms` and `users` packages into one `core` package. **Requires refactoring your server code)** + +> Version **1.0-ALPHA.5** merged the packages `users` and `rooms` into a single package `core`. If you've updated your server from **1.0-ALPHA.4** or below, you will need to edit your code and replace any instance of `rooms` or `users` with `core`. This also changes how [Rooms](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-creating--deleting-rooms) are **created** and **retrieved** ([core.GetRoom](https://godoc.org/github.com/hewiefreeman/GopherGameServer/core#GetRoom)), and how [Users](https://godoc.org/github.com/hewiefreeman/GopherGameServer/core#GetUser) are **retrieved**. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b1e3e68..7616198 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at dominiquedebergue@gmail.com. All +reported by contacting the project team at tbd@gmail.com . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0611485 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +## Ways to contribute + +1) See if there is an Issue that you can take up to fix or implement +2) Help expand and clarify documentation and/or usage section +3) Have a good idea? Open and submit a detailed Issue + +## Guidlines + +1) First, check to see if a similar issue has already been opened +2) Follow the [Code of Conduct](https://github.com/hewiefreeman/GopherGameServer/blob/master/CODE_OF_CONDUCT.md) +3) BE DETAILED in your Pull Requests +4) Do not introduce any further 3rd-party package dependencies unless provided a valid reason +5) Do not submit content that does not pass golint, or has not been formatted with `gofmt` +6) Do not submit content that has little to no documentation on exported functions, variables, etc diff --git a/LICENSE b/LICENSE index c05e850..d9a10c0 100644 --- a/LICENSE +++ b/LICENSE @@ -174,28 +174,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [2018] [Dominique Claude Debergue] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index d7817cc..451c4f7 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,45 @@ -# Gopher Game Server +

+

-[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/hewiefreeman/GopherGameServer?status.svg)](https://godoc.org/github.com/hewiefreeman/GopherGameServer) [![Go Report Card](https://goreportcard.com/badge/github.com/hewiefreeman/GopherGameServer)](https://goreportcard.com/report/github.com/hewiefreeman/GopherGameServer) +
-Gopher Game Server is a full featured, super fast, and fully customizable game server written in Go. Comes with a client API for JavaScript (and eventually Java, C++, and C). +Gopher Game Server provides a flexible and diverse set of tools that greatly ease developments of any type of online multiplayer game, or real-time application. GGS does all the heavy lifting for you, ensuring you never need to worry about synchronizing or data type conversions. --**PROJECT IN DEVELOPMENT**- +Moreover, Gopher has a built-in, fully customizable SQL client authentication mechanism that creates and manages users' accounts for you. It even ties in a friending tool, so users can befriend and invite one another to groups, check each other's status, and more. All components are easily configurable and customizable for any specific project's needs. -Gopher Game Server is designed to provide all necessary tools to ease developments of any type of online game (or any real-time app/chat). Gopher will handle all server-side synchronizing and data type conversions, therefore, client actions receiving, variable setting, message sending, and other functionalities are unproblematic. +### :star: Main features -Gopher uses WebSockets and JSON to pass messages between the clients and the server. The JSON enabled server is designed to pass any types of data between client and server without cares of type conversions on either end. WebSockets increases efficiency of the server as far as possible on the network, since the WebSocket protocol is newer and doesn't send as much header and meta data that standard HTTP and most other protocols require. + - Super easy APIs for server, database, and client coding + - Chat, private messaging, and voice chat + - Customizable client authentication (\***1**) + - Built-in friending mechanism (\***1**) + - Supports multiple connections on the same User + - Server saves state on shut-down and restores on reboot (\***2**) -Moreover, Gopher has a built-in, fully customizable MySQL client authentication mechanism that manages users' accounts for you. It even ties in a friending tool, so users can befriend and invite one another to groups, check each other's status, and more. All components are easily configurable and customizable for specific project's needs. +> (\***1**) A MySQL (or similar SQL) database is required for the authentication/friending feature, but is an optional (like most) feature that can be enabled or disabled to use your own implementations. -### Main features: +> (\***2**) When updating and restarting your server, you might need to be able to recover any rooms that were in the middle of a game. This enables you to do so with minimal effort. - - Super easy and flexible APIs for server, database, and client coding - - Chat, private messaging, and voice chat - - Customizable client authentication (**\***) - - Built-in Friending (**\***) - - Supports multiple connections on the same User - - Server saves state on shut-down and restores on reboot - - Tools provided for administrating server while running +### Upcoming features -(**\***) A MySQL (or similar SQL) database is required for the authentication/friending feature, but is an optional (like most) feature that can be enabled or disabled to use your own implementations. + - Distributed load balancer and server coordinator + - Distributed server broadcasts + - GUI for administrating and monitoring servers + - Integration with [GopherDB](https://github.com/hewiefreeman/GopherDB) when stable (\***1**) -# Client APIs +> (\***1**) MySQL isn't very scalable on it's own, and the SQL implementation for storing friend info is probably not the most efficient. Hence, it is recommended to put the friends table into a separate database cluster. GopherDB, on the other hand, is a very promising database project that will greatly increase server efficiency, and could possibly even outperform MySQL overall. It has a built-in authentication table type, which takes a substantial load off the game servers, and further secures your users' private information. It also supports nested values which are deep-validated through table schemas, so you can store complex information using a wide variety of data types and rules. You can follow the project and get more info with the link above! - - JavaScript: [Gopher Client JS](https://github.com/hewiefreeman/GopherClientJS) +### Change Log +[CHANGE_LOG.md](https://github.com/hewiefreeman/GopherGameServer/blob/master/CHANGE_LOG.md) +


+# :video_game: Client APIs -The Java, C++, and C (possibly more with some help) client APIs will be made after completing version 1.0 and the JavaScript client API. + - JavaScript: [GopherClientJS](https://github.com/hewiefreeman/GopherClientJS) + + > If you want to make a client API in an unsupported language and want to know where to start and/or have any questions, feel free to open a new issue! -# Installing -Gopher Game Server requires at least **Go v1.8+**, and **MySQL v5.7+** for the authentication and friending features. +# :file_folder: Installing +Gopher Game Server requires at least **Go v1.8+** (and **MySQL v5.7+** for the authentication and friending features). First, install the dependencies: @@ -43,22 +51,69 @@ Then install the server: go get github.com/hewiefreeman/GopherGameServer -# Documentation +# :books: Usage + +[:bookmark: Wiki Home](https://github.com/hewiefreeman/GopherGameServer/wiki) + +### Table of Contents + +1) [**Getting Started**](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started) + - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-set-up) + - [Core Server Settings](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-core-server-settings) + - [Server Callbacks](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-server-callbacks) + - [Macro Commands](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-macro-commands) +2) [**Rooms**](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms) + - [Room Types](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-types) + - [Room Broadcasts](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-broadcasts) + - [Room Callbacks](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-callbacks) + - [Creating & Deleting Rooms](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-creating--deleting-rooms) + - [Room Variables](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-variables) + - [Messaging](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-messaging) +3) [**Users**](https://github.com/hewiefreeman/GopherGameServer/wiki/Users) + - [Login & Logout](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-login-and-logout) + - [Joining & Leaving Rooms](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-joining--leaving-rooms) + - [User Variables](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-user-variables) + - [Initiating and Revoking Room Invites](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-initiating-and-revoking-room-invites) + - [User Status](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-user-status) + - [Messaging](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-messaging) +4) [**Custom Client Actions**](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions) + - [Creating a Custom Client Action](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions#blue_book-creating-a-custom-client-action) + - [Responding to a Custom Client Action](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions#blue_book-responding-to-a-custom-client-action) +6) [**Saving & Restoring**](https://github.com/hewiefreeman/GopherGameServer/wiki/Saving-&-Restoring) + - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/Saving-&-Restoring#blue_book-set-up) +5) [**SQL Features**](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features) + - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-set-up) + - [Authenticating Clients](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-authenticating-clients) + - [Custom Account Info](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-custom-account-info) + - [Customizing Authentication Features](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-customizing-authentication-features) + - [Auto-Login (Remember Me)](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-auto-login-remember-me) + - [Friending](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-friending) + +# :scroll: Documentation [Package gopher](https://godoc.org/github.com/hewiefreeman/GopherGameServer) - Main server package for startup and settings -[Package rooms](https://godoc.org/github.com/hewiefreeman/GopherGameServer/rooms) - Package for using the Room, RoomType, and RoomUser types - -[Package users](https://godoc.org/github.com/hewiefreeman/GopherGameServer/users) - Package for using the User type +[Package core](https://godoc.org/github.com/hewiefreeman/GopherGameServer/core) - Package for all User and Room functionality [Package actions](https://godoc.org/github.com/hewiefreeman/GopherGameServer/actions) - Package for making custom client actions [Package database](https://godoc.org/github.com/hewiefreeman/GopherGameServer/database) - Package for customizing your database -# Usage +# :milky_way: Contributions +Contributions are open and welcomed! Help is needed for everything from documentation, cleaning up code, performance enhancements, client APIs and more. Don't forget to show your support with a :star:! + +If you want to make a client API in an unsupported language and want to know where to start and/or have any questions, feel free to open a new issue! + +Please read the following articles before submitting any contributions or filing an Issue: + + - [Contribution Guidlines](https://github.com/hewiefreeman/GopherGameServer/blob/master/CONTRIBUTING.md) + - [Code of Conduct](https://github.com/hewiefreeman/GopherGameServer/blob/master/CODE_OF_CONDUCT.md) + +
-(Coming soon...) +
GopherGameServer and all of it's contents Copyright 2022 Dominique Debergue +
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: -# Contributions + `http://www.apache.org/licenses/LICENSE-2.0` -**Gopher Game Server will be open for contributions as soon as version 1.0 is finished. At that time, contribution information will be posted here.** +
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
diff --git a/actions/actions.go b/actions/actions.go index 9465c68..ab80bd0 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -4,8 +4,8 @@ package actions import ( "errors" "github.com/gorilla/websocket" + "github.com/hewiefreeman/GopherGameServer/core" "github.com/hewiefreeman/GopherGameServer/helpers" - "github.com/hewiefreeman/GopherGameServer/users" ) // CustomClientAction is an action that you can handle on the server from @@ -16,7 +16,7 @@ import ( type CustomClientAction struct { dataType int - callback func(interface{}, Client) + callback func(interface{}, *Client) } // Client objects are created and sent along with your CustomClientAction callback function when a @@ -24,7 +24,7 @@ type CustomClientAction struct { type Client struct { action string - user *users.User + user *core.User connID string socket *websocket.Conn @@ -47,8 +47,8 @@ var ( // Default `ClientError`s const ( - ErrorMismatchedTypes = iota + 1001 // Client didn't pass the right data type for the given action - ErrorUnrecognizedAction // The custom action has not been defined + ErrorMismatchedTypes = iota + 1001 // Client didn't pass the right data type for the given action + ErrorUnrecognizedAction // The custom action has not been defined ) // These are the accepted data types that a client can send with a CustomClientMessage. You must use one @@ -75,7 +75,7 @@ const ( // // (*)Callback function format: // -// func yourFunction(actionData interface{}, client Client) { +// func yourFunction(actionData interface{}, client *Client) { // //... // // // optional client response @@ -88,13 +88,14 @@ const ( // // // Note: This function can only be called BEFORE starting the server. -func New(actionType string, dataType int, callback func(interface{}, Client)) error { +func New(actionType string, dataType int, callback func(interface{}, *Client)) error { if serverStarted { return errors.New("Cannot make a new CustomClientAction once the server has started") } customClientActions[actionType] = CustomClientAction{ dataType: dataType, - callback: callback} + callback: callback, + } return nil } @@ -116,7 +117,7 @@ func NoError() ClientError { // // WARNING: This is only meant for internal Gopher Game Server mechanics. Your CustomClientAction callbacks are called // from this function. This could spawn errors and/or memory leaks. -func HandleCustomClientAction(action string, data interface{}, user *users.User, conn *websocket.Conn, connID string) { +func HandleCustomClientAction(action string, data interface{}, user *core.User, conn *websocket.Conn, connID string) { client := Client{user: user, action: action, socket: conn, connID: connID, responded: false} // CHECK IF ACTION EXISTS if customAction, ok := customClientActions[action]; ok { @@ -126,7 +127,7 @@ func HandleCustomClientAction(action string, data interface{}, user *users.User, return } //EXECUTE CALLBACK - customAction.callback(data, client) + customAction.callback(data, &client) } else { client.Respond(nil, NewError("Unrecognized action", ErrorUnrecognizedAction)) } @@ -198,8 +199,8 @@ func typesMatch(data interface{}, theType int) bool { // is needed. // // NOTE: A response can only be sent once to a Client. Any more calls to Respond() on the same Client will not send a response, -// nor do anything at all. If you want to send a stream of messages to the Client, first get their User object with users.Get() using -// the Client's Name(). Then you can send Data messages directly to the User with the *User.DataMessage() function. +// nor do anything at all. If you want to send a stream of messages to the Client, first get their User object with *Client.User(), +// then you can send data messages directly to the User with the *User.DataMessage() function. func (c *Client) Respond(response interface{}, err ClientError) { //YOU CAN ONLY RESPOND ONCE if (*c).responded { @@ -207,17 +208,19 @@ func (c *Client) Respond(response interface{}, err ClientError) { } (*c).responded = true //CONSTRUCT MESSAGE - r := make(map[string]interface{}) - r[helpers.ServerActionCustomClientActionResponse] = make(map[string]interface{}) - r[helpers.ServerActionCustomClientActionResponse].(map[string]interface{})["a"] = (*c).action + r := map[string]map[string]interface{}{ + helpers.ServerActionCustomClientActionResponse: { + "a": (*c).action, + }, + } if err.id != -1 { - r[helpers.ServerActionCustomClientActionResponse].(map[string]interface{})["e"] = make(map[string]interface{}) - r[helpers.ServerActionCustomClientActionResponse].(map[string]interface{})["e"].(map[string]interface{})["m"] = err.message - r[helpers.ServerActionCustomClientActionResponse].(map[string]interface{})["e"].(map[string]interface{})["id"] = err.id + r[helpers.ServerActionCustomClientActionResponse]["e"] = map[string]interface{}{ + "m": err.message, + "id": err.id, + } } else { - r[helpers.ServerActionCustomClientActionResponse].(map[string]interface{})["r"] = response + r[helpers.ServerActionCustomClientActionResponse]["r"] = response } - //SEND MESSAGE TO CLIENT (*c).socket.WriteJSON(r) } @@ -227,7 +230,7 @@ func (c *Client) Respond(response interface{}, err ClientError) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // User gets the *User of the Client. -func (c *Client) User() *users.User { +func (c *Client) User() *core.User { return c.user } @@ -262,14 +265,12 @@ func SetServerStarted(val bool) { func Pause() { if !serverPaused { serverPaused = true - serverStarted = false } } // Resume is only for internal Gopher Game Server mechanics. func Resume() { if serverPaused { - serverStarted = true serverPaused = false } } diff --git a/callbacks.go b/callbacks.go index 2b140c0..16d1a83 100644 --- a/callbacks.go +++ b/callbacks.go @@ -2,14 +2,17 @@ package gopher import ( "errors" + "github.com/hewiefreeman/GopherGameServer/core" "github.com/hewiefreeman/GopherGameServer/database" - "github.com/hewiefreeman/GopherGameServer/users" "net/http" ) const ( + // ErrorIncorrectFunction is thrown when function input or return parameters don't match with the callback ErrorIncorrectFunction = "Incorrect function parameters or return parameters" - ErrorServerRunning = "Cannot call when the server is running." + // ErrorServerRunning is thrown when an action cannot be taken because the server is running. Pausing the server + // will enable you to run the command. + ErrorServerRunning = "Cannot call when the server is running." ) // SetStartCallback sets the callback that triggers when the server first starts up. The @@ -63,13 +66,13 @@ func SetResumeCallback(cb interface{}) error { return nil } -// SetStopCallback sets the callback that triggers when the server is stopped. The +// SetShutDownCallback sets the callback that triggers when the server is shut down. The // function passed must have the same parameter types as the following example: // // func serverStopped(){ // //code... // } -func SetStopCallback(cb interface{}) error { +func SetShutDownCallback(cb interface{}) error { if serverStarted { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func()); ok { @@ -88,16 +91,15 @@ func SetStopCallback(cb interface{}) error { // } // // The function returns a boolean. If false is returned, the client will receive an HTTP error `http.StatusForbidden` and -// will be rejected from the server. This can be used to, for instance, make a black/white list for your server. +// will be rejected from the server. This can be used to, for instance, make a black/white list or implement client sessions. func SetClientConnectCallback(cb interface{}) error { if serverStarted { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(*http.ResponseWriter, *http.Request) bool); ok { clientConnectCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetLoginCallback sets the callback that triggers when a client logs in as a User. The @@ -123,12 +125,11 @@ func SetLoginCallback(cb interface{}) error { if (*settings).EnableSqlFeatures { database.LoginCallback = callback } else { - users.LoginCallback = callback + core.LoginCallback = callback } - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetLogoutCallback sets the callback that triggers when a client logs out from a User. The @@ -143,11 +144,10 @@ func SetLogoutCallback(cb interface{}) error { if serverStarted { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(string, int)); ok { - users.LogoutCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + core.LogoutCallback = callback + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetSignupCallback sets the callback that triggers when a client makes an account. The @@ -167,10 +167,9 @@ func SetSignupCallback(cb interface{}) error { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(string, map[string]interface{}) bool); ok { database.SignUpCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetDeleteAccountCallback sets the callback that triggers when a client deletes their account. The @@ -194,10 +193,9 @@ func SetDeleteAccountCallback(cb interface{}) error { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { database.DeleteAccountCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetAccountInfoChangeCallback sets the callback that triggers when a client changes an `AccountInfoColumn`. The @@ -221,10 +219,9 @@ func SetAccountInfoChangeCallback(cb interface{}) error { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { database.AccountInfoChangeCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } // SetPasswordChangeCallback sets the callback that triggers when a client changes their password. The @@ -248,8 +245,7 @@ func SetPasswordChangeCallback(cb interface{}) error { return errors.New(ErrorServerRunning) } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { database.PasswordChangeCallback = callback - } else { - return errors.New(ErrorIncorrectFunction) + return nil } - return nil + return errors.New(ErrorIncorrectFunction) } diff --git a/clientActions.go b/clientActions.go index 3cf575d..242d434 100644 --- a/clientActions.go +++ b/clientActions.go @@ -3,10 +3,9 @@ package gopher import ( "github.com/gorilla/websocket" "github.com/hewiefreeman/GopherGameServer/actions" + "github.com/hewiefreeman/GopherGameServer/core" "github.com/hewiefreeman/GopherGameServer/database" "github.com/hewiefreeman/GopherGameServer/helpers" - "github.com/hewiefreeman/GopherGameServer/rooms" - "github.com/hewiefreeman/GopherGameServer/users" "sync" ) @@ -14,7 +13,7 @@ const ( errorInvalidAction = "Invalid action" errorLoggedIn = "You must be logged out" errorNotLoggedIn = "You must be logged in" - errorFeatureDisabled = "Server feature is disabled" + errorFeatureDisabled = "Server feature not enabled" errorRoomControl = "Clients cannot control rooms" errorServerRoom = "Clients cannot control that room type" errorNotOwner = "You are not the owner of the room" @@ -34,44 +33,44 @@ const ( errorIncorrectFormatVarKey = "Incorrect data format for variable key" ) -func clientActionHandler(action clientAction, user **users.User, conn *websocket.Conn, +func clientActionHandler(action clientAction, user **core.User, conn *websocket.Conn, deviceTag *string, devicePass *string, deviceUserID *int, connID *string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { - switch _action := action.A; _action { + switch action.A { - // HIGH LOOK-UP PRIORITY ITEMS + // Custom actions and voice streams case helpers.ClientActionCustomAction: return clientCustomAction(action.P, user, conn, *connID, clientMux) case helpers.ClientActionVoiceStream: return clientActionVoiceStream(action.P, user, conn, *connID, clientMux) - // USER VARIABLES + // User variables case helpers.ClientActionSetVariable: return clientActionSetVariable(action.P, user, *connID, clientMux) case helpers.ClientActionSetVariables: return clientActionSetVariables(action.P, user, *connID, clientMux) - // CHAT + // Chat case helpers.ClientActionChatMessage: return clientActionChatMessage(action.P, user, *connID, clientMux) case helpers.ClientActionPrivateMessage: return clientActionPrivateMessage(action.P, user, *connID, clientMux) - // CHANGE STATUS + // Change user status case helpers.ClientActionChangeStatus: return clientActionChangeStatus(action.P, user, clientMux) - // LOGIN/LOGOUT + // Log in/out case helpers.ClientActionLogin: return clientActionLogin(action.P, user, deviceTag, devicePass, deviceUserID, conn, connID, clientMux) case helpers.ClientActionLogout: return clientActionLogout(user, deviceTag, devicePass, deviceUserID, connID, clientMux) - // ROOM ACTIONS + // Room actions case helpers.ClientActionJoinRoom: return clientActionJoinRoom(action.P, user, *connID, clientMux) @@ -86,7 +85,7 @@ func clientActionHandler(action clientAction, user **users.User, conn *websocket case helpers.ClientActionRevokeInvite: return clientActionRevokeInvite(action.P, user, *connID, clientMux) - // FRIENDING + // Friending case helpers.ClientActionFriendRequest: return clientActionFriendRequest(action.P, user, clientMux) @@ -97,7 +96,7 @@ func clientActionHandler(action clientAction, user **users.User, conn *websocket case helpers.ClientActionRemoveFriend: return clientActionRemoveFriend(action.P, user, clientMux) - // DATABASE + // Database case helpers.ClientActionSignup: return clientActionSignup(action.P, user, clientMux) @@ -108,7 +107,7 @@ func clientActionHandler(action clientAction, user **users.User, conn *websocket case helpers.ClientActionChangeAccountInfo: return clientActionChangeAccountInfo(action.P, user, clientMux) - // INVALID CLIENT ACTION + // Invalid client action default: return nil, true, helpers.NewError(errorInvalidAction, helpers.ErrorGopherInvalidAction) @@ -119,7 +118,7 @@ func clientActionHandler(action clientAction, user **users.User, conn *websocket // CUSTOM CLIENT ACTIONS ///////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientCustomAction(params interface{}, user **users.User, conn *websocket.Conn, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientCustomAction(params interface{}, user **core.User, conn *websocket.Conn, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { var ok bool var pMap map[string]interface{} var action string @@ -133,14 +132,14 @@ func clientCustomAction(params interface{}, user **users.User, conn *websocket.C userRef := *user (*clientMux).Unlock() actions.HandleCustomClientAction(action, pMap["d"], userRef, conn, connID) - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // CHANGE USER STATUS //////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionChangeStatus(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionChangeStatus(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -148,7 +147,7 @@ func clientActionChangeStatus(params interface{}, user **users.User, clientMux * } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get param map and extract values var ok bool var statusF float64 if statusF, ok = params.(float64); !ok { @@ -158,14 +157,14 @@ func clientActionChangeStatus(params interface{}, user **users.User, clientMux * // userRef.SetStatus(status) // - return status, true, helpers.NewError("", 0) + return status, true, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // ACCOUNT/DATABASE ACTIONS ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionSignup(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionSignup(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user != nil { (*clientMux).Unlock() @@ -175,7 +174,7 @@ func clientActionSignup(params interface{}, user **users.User, clientMux *sync.M return nil, true, helpers.NewError(errorFeatureDisabled, helpers.ErrorGopherFeatureDisabled) } (*clientMux).Unlock() - //GET ITEMS FROM PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var customCols map[string]interface{} @@ -195,17 +194,17 @@ func clientActionSignup(params interface{}, user **users.User, clientMux *sync.M if pass, ok = pMap["p"].(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormatPass, helpers.ErrorGopherPasswordFormat) } - //SIGN CLIENT UP + // Sign client up signupErr := database.SignUpClient(userName, pass, customCols) if signupErr.ID != 0 { return nil, true, signupErr } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } -func clientActionDeleteAccount(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionDeleteAccount(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user != nil { (*clientMux).Unlock() @@ -215,7 +214,7 @@ func clientActionDeleteAccount(params interface{}, user **users.User, clientMux return nil, true, helpers.NewError(errorFeatureDisabled, helpers.ErrorGopherFeatureDisabled) } (*clientMux).Unlock() - //GET ITEMS FROM PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var customCols map[string]interface{} @@ -236,22 +235,22 @@ func clientActionDeleteAccount(params interface{}, user **users.User, clientMux return nil, true, helpers.NewError(errorIncorrectFormatPass, helpers.ErrorGopherPasswordFormat) } - //CHECK IF USER IS ONLINE - _, err := users.Get(userName) + // Check if online + _, err := core.GetUser(userName) if err == nil { return nil, true, helpers.NewError(err.Error(), helpers.ErrorGopherLoggedIn) } - //DELETE ACCOUNT + // Delete account deleteErr := database.DeleteAccount(userName, pass, customCols) if deleteErr.ID != 0 { return nil, true, deleteErr } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } -func clientActionChangePassword(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionChangePassword(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -262,7 +261,7 @@ func clientActionChangePassword(params interface{}, user **users.User, clientMux } userRef := *user (*clientMux).Unlock() - //GET ITEMS FROM PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var customCols map[string]interface{} @@ -282,17 +281,17 @@ func clientActionChangePassword(params interface{}, user **users.User, clientMux if newPass, ok = pMap["n"].(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormatNewPass, helpers.ErrorGopherNewPasswordFormat) } - //CHANGE PASSWORD + // Change password changeErr := database.ChangePassword(userRef.Name(), pass, newPass, customCols) if changeErr.ID != 0 { return nil, true, changeErr } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } -func clientActionChangeAccountInfo(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionChangeAccountInfo(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -303,7 +302,7 @@ func clientActionChangeAccountInfo(params interface{}, user **users.User, client } userRef := *user (*clientMux).Unlock() - //GET ITEMS FROM PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var customCols map[string]interface{} @@ -319,29 +318,29 @@ func clientActionChangeAccountInfo(params interface{}, user **users.User, client if pass, ok = pMap["p"].(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormatPass, helpers.ErrorGopherPasswordFormat) } - //CHANGE ACCOUNT INFO + // Change account info changeErr := database.ChangeAccountInfo(userRef.Name(), pass, customCols) if changeErr.ID != 0 { return nil, true, changeErr } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // LOGIN+LOGOUT ACTIONS ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionLogin(params interface{}, user **users.User, deviceTag *string, devicePass *string, deviceUserID *int, conn *websocket.Conn, +func clientActionLogin(params interface{}, user **core.User, deviceTag *string, devicePass *string, deviceUserID *int, conn *websocket.Conn, connID *string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user != nil { - defer (*clientMux).Unlock() + (*clientMux).Unlock() return nil, true, helpers.NewError(errorLoggedIn, helpers.ErrorGopherLoggedIn) } (*clientMux).Unlock() - //MAKE A MAP FROM PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var name string @@ -355,11 +354,11 @@ func clientActionLogin(params interface{}, user **users.User, deviceTag *string, if name, ok = pMap["n"].(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormatName, helpers.ErrorGopherNameFormat) } - if (*settings).EnableSqlFeatures { + if settings.EnableSqlFeatures { if pass, ok = pMap["p"].(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormatPass, helpers.ErrorGopherPasswordFormat) } - if (*settings).RememberMe { + if settings.RememberMe { if remMe, ok = pMap["r"].(bool); !ok { return nil, true, helpers.NewError(errorIncorrectFormatRemember, helpers.ErrorGopherRememberFormat) } @@ -375,34 +374,46 @@ func clientActionLogin(params interface{}, user **users.User, deviceTag *string, return nil, true, helpers.NewError(errorIncorrectFormatCols, helpers.ErrorGopherColumnsFormat) } } - //LOG IN + // Log socket in var dbIndex int - var uName string var dPass string var cID string var err helpers.GopherError - if (*settings).EnableSqlFeatures { - uName, dbIndex, dPass, err = database.LoginClient(name, pass, *deviceTag, remMe, customCols) - if err.ID != 0 { - return nil, true, err - } - cID, err = users.Login(uName, dbIndex, dPass, guest, remMe, conn, user, clientMux) - } else { - cID, err = users.Login(name, -1, "", guest, false, conn, user, clientMux) - } - if err.ID != 0 { - return nil, true, err + if dbIndex, dPass, cID, err = loginClient(settings, guest, name, pass, *deviceTag, remMe, customCols, user, + conn, clientMux); err.ID != 0 { + return nil, false, err } - //CHANGE SOCKET'S userName + + // Update socket *devicePass = dPass *deviceUserID = dbIndex *connID = cID // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() +} + +func loginClient(s *ServerSettings, guest bool, name string, pass string, deviceTag string, remMe bool, + customCols map[string]interface{}, user **core.User, conn *websocket.Conn, clientMux *sync.Mutex) (int, string, string, helpers.GopherError) { + var dbIndex int + var dPass string + var cID string + var err helpers.GopherError + if s.EnableSqlFeatures && !guest { + var uName string + uName, dbIndex, dPass, err = database.LoginClient(name, pass, deviceTag, remMe, customCols) + if err.ID != 0 { + return 0, "", "", err + } + cID, err = core.Login(uName, dbIndex, dPass, guest, remMe, conn, user, clientMux) + } else { + cID, err = core.Login(name, -1, "", guest, false, conn, user, clientMux) + } + + return dbIndex, dPass, cID, err } -func clientActionLogout(user **users.User, deviceTag *string, devicePass *string, deviceUserID *int, connID *string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionLogout(user **core.User, deviceTag *string, devicePass *string, deviceUserID *int, connID *string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -410,26 +421,26 @@ func clientActionLogout(user **users.User, deviceTag *string, devicePass *string } userRef := *user (*clientMux).Unlock() - //LOG User OUT AND RESET + // Log user out userRef.Logout(*connID) - //REMOVE AUTO-LOG IF ANY + // Remove any auto-logins for this device tag if (*settings).EnableSqlFeatures && (*settings).RememberMe { database.RemoveAutoLog(*deviceUserID, *deviceTag) } - // + // Update socket *devicePass = "" *deviceUserID = 0 *connID = "" // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // ROOM ACTIONS ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionJoinRoom(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionJoinRoom(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -437,28 +448,28 @@ func clientActionJoinRoom(params interface{}, user **users.User, connID string, } userRef := *user (*clientMux).Unlock() - //GET ROOM NAME FROM PARAMS + // Get room name from params var ok bool var roomName string if roomName, ok = params.(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormat, helpers.ErrorGopherIncorrectFormat) } - //GET ROOM - room, roomErr := rooms.Get(roomName) + // Get room + room, roomErr := core.GetRoom(roomName) if roomErr != nil { return nil, true, helpers.NewError(roomErr.Error(), helpers.ErrorGopherJoin) } - //MAKE User JOIN THE Room + // Make user join the room joinErr := userRef.Join(room, connID) if joinErr != nil { return nil, true, helpers.NewError(joinErr.Error(), helpers.ErrorGopherJoin) } // - return nil, true, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionLeaveRoom(user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionLeaveRoom(user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -466,17 +477,17 @@ func clientActionLeaveRoom(user **users.User, connID string, clientMux *sync.Mut } userRef := *user (*clientMux).Unlock() - //MAKE USER LEAVE ROOM + // Make user leave room leaveErr := userRef.Leave(connID) if leaveErr != nil { return nil, true, helpers.NewError(leaveErr.Error(), helpers.ErrorGopherLeave) } // - return nil, true, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionCreateRoom(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionCreateRoom(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -487,7 +498,7 @@ func clientActionCreateRoom(params interface{}, user **users.User, connID string } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var roomName string @@ -510,33 +521,28 @@ func clientActionCreateRoom(params interface{}, user **users.User, connID string return nil, true, helpers.NewError(errorIncorrectFormatMaxRoomUsers, helpers.ErrorGopherMaxRoomFormat) } maxUsers := int(maxUsersF) - // - if rType, ok := rooms.GetRoomTypes()[roomType]; !ok { + // Verify type + if rType, ok := core.GetRoomTypes()[roomType]; !ok { return nil, true, helpers.NewError(errorRoomType, helpers.ErrorGopherMaxRoomFormat) } else if rType.ServerOnly() { return nil, true, helpers.NewError(errorServerRoom, helpers.ErrorGopherServerRoom) } - //CHECK IF USER IS IN A ROOM - /*currRoom := user.RoomIn() - if currRoom != nil && currRoom.Name() != "" { - return nil, true, errors.New("You must leave your current room to create a room") - }*/ - //MAKE THE Room - room, roomErr := rooms.New(roomName, roomType, private, maxUsers, userRef.Name()) + // Make the room + room, roomErr := core.NewRoom(roomName, roomType, private, maxUsers, userRef.Name()) if roomErr != nil { return nil, true, helpers.NewError(roomErr.Error(), helpers.ErrorGopherCreateRoom) } - //ADD THE User TO THE ROOM + // Add user to the new room joinErr := userRef.Join(room, connID) if joinErr != nil { return nil, true, helpers.NewError(joinErr.Error(), helpers.ErrorGopherJoin) } // - return roomName, true, helpers.NewError("", 0) + return roomName, true, helpers.NoError() } -func clientActionDeleteRoom(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionDeleteRoom(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -547,34 +553,34 @@ func clientActionDeleteRoom(params interface{}, user **users.User, clientMux *sy } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get room name from params var ok bool var roomName string if roomName, ok = params.(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormat, helpers.ErrorGopherIncorrectFormat) } - //GET ROOM - room, roomErr := rooms.Get(roomName) + // Get room + room, roomErr := core.GetRoom(roomName) if roomErr != nil { return nil, true, helpers.NewError(roomErr.Error(), helpers.ErrorGopherDeleteRoom) } else if room.Owner() != userRef.Name() { return nil, true, helpers.NewError(errorNotOwner, helpers.ErrorGopherNotOwner) } // - rType := rooms.GetRoomTypes()[room.Type()] + rType := core.GetRoomTypes()[room.Type()] if rType.ServerOnly() { return nil, true, helpers.NewError(errorServerRoom, helpers.ErrorGopherServerRoom) } - //DELETE ROOM + // Delete the room deleteErr := room.Delete() if deleteErr != nil { return nil, true, helpers.NewError(deleteErr.Error(), helpers.ErrorGopherDeleteRoom) } - return nil, true, helpers.NewError("", 0) + return roomName, true, helpers.NoError() } -func clientActionRoomInvite(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionRoomInvite(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -585,27 +591,27 @@ func clientActionRoomInvite(params interface{}, user **users.User, connID string } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get name from params var ok bool var name string if name, ok = params.(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormat, helpers.ErrorGopherIncorrectFormat) } - //GET INVITED USER - invUser, invUserErr := users.Get(name) + // Get invited user + invUser, invUserErr := core.GetUser(name) if invUserErr != nil { return nil, true, helpers.NewError(invUserErr.Error(), helpers.ErrorGopherInvite) } - //INVITE + // Invite user invUserErr = userRef.Invite(invUser, connID) if invUserErr != nil { return nil, true, helpers.NewError(invUserErr.Error(), helpers.ErrorGopherInvite) } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } -func clientActionRevokeInvite(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionRevokeInvite(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -616,152 +622,147 @@ func clientActionRevokeInvite(params interface{}, user **users.User, connID stri } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get name from params var ok bool var name string if name, ok = params.(string); !ok { return nil, true, helpers.NewError(errorIncorrectFormat, helpers.ErrorGopherIncorrectFormat) } - //REVOKE INVITE + // Revoke invite revokeErr := userRef.RevokeInvite(name, connID) if revokeErr != nil { return nil, true, helpers.NewError(revokeErr.Error(), helpers.ErrorGopherRevokeInvite) } // - return nil, true, helpers.NewError("", 0) + return nil, true, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // CHAT+VOICE ACTIONS //////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionVoiceStream(params interface{}, user **users.User, conn *websocket.Conn, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionVoiceStream(params interface{}, user **core.User, conn *websocket.Conn, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } userRef := *user (*clientMux).Unlock() - //GET CURRENT ROOM + // Get current room currRoom := userRef.RoomIn(connID) if currRoom == nil || currRoom.Name() == "" { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } - //CHECK IF VOICE CHAT ROOM - rType := rooms.GetRoomTypes()[currRoom.Type()] + // Check for voice chat + rType := core.GetRoomTypes()[currRoom.Type()] if !rType.VoiceChatEnabled() { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } - //SEND VOICE STREAM + // Send voice stream currRoom.VoiceStream(userRef.Name(), conn, params) // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionChatMessage(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionChatMessage(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } userRef := *user (*clientMux).Unlock() - //GET CURRENT ROOM + // Get current room currRoom := userRef.RoomIn(connID) if currRoom == nil || currRoom.Name() == "" { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } - //SEND CHAT MESSAGE + // Send chat message currRoom.ChatMessage(userRef.Name(), params) // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionPrivateMessage(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionPrivateMessage(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var userName string if pMap, ok = params.(map[string]interface{}); !ok { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } if userName, ok = pMap["u"].(string); !ok { - return nil, false, helpers.NewError("", 0) - } - //GET CURRENT ROOM - currRoom := userRef.RoomIn(connID) - if currRoom == nil || currRoom.Name() == "" { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } - //SEND CHAT MESSAGE + // Send chat message userRef.PrivateMessage(userName, pMap["m"]) // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // USER VARIABLES //////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionSetVariable(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionSetVariable(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get param map and extract values var ok bool var pMap map[string]interface{} var varKey string var varVal interface{} if pMap, ok = params.(map[string]interface{}); !ok { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } if varKey, ok = pMap["k"].(string); !ok { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } varVal = pMap["v"] - //SET THE VARIABLE + // Set the variable userRef.SetVariable(varKey, varVal, connID) // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionSetVariables(params interface{}, user **users.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionSetVariables(params interface{}, user **core.User, connID string, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } userRef := *user (*clientMux).Unlock() - //GET PARAMS + // Get variables map from params var ok bool var pMap map[string]interface{} if pMap, ok = params.(map[string]interface{}); !ok { - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } //SET THE VARIABLES userRef.SetVariables(pMap, connID) // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// // FRIENDING ACTIONS ///////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -func clientActionFriendRequest(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionFriendRequest(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -772,7 +773,7 @@ func clientActionFriendRequest(params interface{}, user **users.User, clientMux } userRef := *user (*clientMux).Unlock() - //GET PARAMS AS A MAP + // Get name from params var ok bool var friendName string @@ -786,10 +787,10 @@ func clientActionFriendRequest(params interface{}, user **users.User, clientMux } // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionAcceptFriend(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionAcceptFriend(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -800,7 +801,7 @@ func clientActionAcceptFriend(params interface{}, user **users.User, clientMux * } userRef := *user (*clientMux).Unlock() - //GET PARAMS AS A MAP + // Get name from params var ok bool var friendName string @@ -814,10 +815,10 @@ func clientActionAcceptFriend(params interface{}, user **users.User, clientMux * } // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionDeclineFriend(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionDeclineFriend(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -828,7 +829,7 @@ func clientActionDeclineFriend(params interface{}, user **users.User, clientMux } userRef := *user (*clientMux).Unlock() - //GET PARAMS AS A MAP + // Get name from params var ok bool var friendName string @@ -842,10 +843,10 @@ func clientActionDeclineFriend(params interface{}, user **users.User, clientMux } // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } -func clientActionRemoveFriend(params interface{}, user **users.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { +func clientActionRemoveFriend(params interface{}, user **core.User, clientMux *sync.Mutex) (interface{}, bool, helpers.GopherError) { (*clientMux).Lock() if *user == nil { (*clientMux).Unlock() @@ -856,7 +857,7 @@ func clientActionRemoveFriend(params interface{}, user **users.User, clientMux * } userRef := *user (*clientMux).Unlock() - //GET PARAMS AS A MAP + // Get name from params var ok bool var friendName string @@ -870,5 +871,5 @@ func clientActionRemoveFriend(params interface{}, user **users.User, clientMux * } // - return nil, false, helpers.NewError("", 0) + return nil, false, helpers.NoError() } diff --git a/core/core.go b/core/core.go new file mode 100644 index 0000000..8b6210f --- /dev/null +++ b/core/core.go @@ -0,0 +1,143 @@ +// Package core contains all the tools to make and work with Users and Rooms. +// +// A User is a client who has successfully logged into the server. You can think of clients who are not attached to a User +// as, for instance, someone in the login screen, but are still connected to the server. A client doesn't +// have to be a User to be able to call your CustomClientActions, so keep that in mind when making them (Refer to the Usage for CustomClientActions). +// +// Users have their own variables which can be accessed and changed anytime. A User variable can +// be anything compatible with interface{}, so pretty much anything. +// +// A Room represents a place on the server where a User can join other Users. Rooms can either be public or private. Private Rooms must be assigned an "owner", which is the name of a User, or the ServerName +// from ServerSettings. The server's name that will be used for ownership of private Rooms can be set with the ServerSettings +// option ServerName when starting the server. Though keep in mind, setting the ServerName in ServerSettings will prevent a User who wants to go by that name +// from logging in. Public Rooms will accept a join request from any User, and private Rooms will only +// accept a join request from someone who is on it's invite list. Only the owner of the Room or the server itself can invite +// Users to a private Room. But remember, just because a User owns a private room doesn't mean the server cannot also invite +// to the room via *Room.AddInvite() function. +// +// Rooms have their own variables which can be accessed and changed anytime. Like User variables, a Room variable can +// be anything compatible with interface{}. +package core + +import ( + "github.com/hewiefreeman/GopherGameServer/helpers" +) + +var ( + serverStarted bool + serverPaused bool + + serverName string + kickOnLogin bool + sqlFeatures bool + rememberMe bool + multiConnect bool + maxUserConns uint8 + deleteRoomOnLeave bool = true +) + +// RoomRecoveryState is used internally for persisting room states on shutdown. +type RoomRecoveryState struct { + T string // rType + P bool // private + O string // owner + M int // maxUsers + I []string // inviteList + V map[string]interface{} // vars +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// SERVER STARTUP FUNCTIONS ////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + +// SetServerStarted is for Gopher Game Server internal mechanics only. +func SetServerStarted(val bool) { + if !serverStarted { + serverStarted = val + } +} + +// SettingsSet is for Gopher Game Server internal mechanics only. +func SettingsSet(kickDups bool, name string, deleteOnLeave bool, sqlFeat bool, remMe bool, multiConn bool, maxConns uint8) { + if !serverStarted { + kickOnLogin = kickDups + serverName = name + sqlFeatures = sqlFeat + rememberMe = remMe + multiConnect = multiConn + maxUserConns = maxConns + deleteRoomOnLeave = deleteOnLeave + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Pause is only for internal Gopher Game Server mechanics. +func Pause() { + if !serverPaused { + serverPaused = true + + // + clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) + usersMux.Lock() + for _, user := range users { + user.mux.Lock() + for connID, conn := range user.conns { + //REMOVE CONNECTION FROM THEIR ROOM + currRoom := conn.room + if currRoom != nil && currRoom.Name() != "" { + user.mux.Unlock() + currRoom.RemoveUser(user, connID) + user.mux.Lock() + } + + //LOG CONNECTION OUT + conn.clientMux.Lock() + if *(conn.user) != nil { + *(conn.user) = nil + } + conn.clientMux.Unlock() + + //SEND LOG OUT MESSAGE + conn.socket.WriteJSON(clientResp) + } + user.mux.Unlock() + } + users = make(map[string]*User) + usersMux.Unlock() + } +} + +// Resume is only for internal Gopher Game Server mechanics. +func Resume() { + if serverPaused { + serverPaused = false + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// GET STATES FOR GENERATING RECOVERY FILE //////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// GetRoomsState is only for internal Gopher Game Server mechanics. +func GetRoomsState() map[string]RoomRecoveryState { + state := make(map[string]RoomRecoveryState) + roomsMux.Lock() + for _, room := range rooms { + room.mux.Lock() + state[room.name] = RoomRecoveryState{ + T: room.rType, + P: room.private, + O: room.owner, + M: room.maxUsers, + I: room.inviteList, + V: room.vars, + } + room.mux.Unlock() + } + roomsMux.Unlock() + // + return state +} diff --git a/users/friending.go b/core/friending.go similarity index 80% rename from users/friending.go rename to core/friending.go index 32f6c7b..9c63023 100644 --- a/users/friending.go +++ b/core/friending.go @@ -1,4 +1,4 @@ -package users +package core import ( "errors" @@ -16,7 +16,7 @@ func (u *User) FriendRequest(friendName string) error { return errors.New("The user '" + friendName + "' cannot be requested as a friend") } //CHECK IF FRIEND IS ONLINE & GET DATABASE ID - friend, friendErr := Get(friendName) + friend, friendErr := GetUser(friendName) var friendOnline bool = false var friendID int if friendErr != nil { @@ -50,9 +50,11 @@ func (u *User) FriendRequest(friendName string) error { //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE if friendOnline { - message := make(map[string]interface{}) - message[helpers.ServerActionFriendRequest] = make(map[string]interface{}) - message[helpers.ServerActionFriendRequest].(map[string]interface{})["n"] = u.name + message := map[string]map[string]interface{}{ + helpers.ServerActionFriendRequest: { + "n": u.name, + }, + } friend.mux.Lock() for _, conn := range friend.conns { (*conn).socket.WriteJSON(message) @@ -61,7 +63,7 @@ func (u *User) FriendRequest(friendName string) error { } //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionFriendRequest, friendName, helpers.NewError("", 0)) + clientResp := helpers.MakeClientResponse(helpers.ClientActionFriendRequest, friendName, helpers.NoError()) u.mux.Lock() for _, conn := range u.conns { (*conn).socket.WriteJSON(clientResp) @@ -84,7 +86,7 @@ func (u *User) AcceptFriendRequest(friendName string) error { return errors.New("The user '" + friendName + "' cannot be accepted as a friend") } //CHECK IF FRIEND IS ONLINE & GET DATABASE ID - friend, friendErr := Get(friendName) + friend, friendErr := GetUser(friendName) var friendOnline bool = false var friendID int if friendErr != nil { @@ -114,28 +116,31 @@ func (u *User) AcceptFriendRequest(friendName string) error { return errors.New("Unexpected friend error") } - //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE - var status int = StatusOffline + //SEND ACCEPT MESSAGE TO THE USER IF THEY ARE ONLINE + var fStatus int = StatusOffline if friendOnline { - message := make(map[string]interface{}) - message[helpers.ServerActionFriendAccept] = make(map[string]interface{}) - message[helpers.ServerActionFriendAccept].(map[string]interface{})["n"] = u.name - message[helpers.ServerActionFriendAccept].(map[string]interface{})["s"] = u.status + message := map[string]map[string]interface{}{ + helpers.ServerActionFriendAccept: { + "n": u.name, + "s": u.status, + }, + } friend.mux.Lock() for _, conn := range friend.conns { (*conn).socket.WriteJSON(message) } - status = friend.status + fStatus = friend.status friend.mux.Unlock() } //MAKE RESPONSE - responseMap := make(map[string]interface{}) - responseMap["n"] = friendName - responseMap["s"] = status + responseMap := map[string]interface{}{ + "n": friendName, + "s": fStatus, + } - //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionAcceptFriend, responseMap, helpers.NewError("", 0)) + //SEND RESPONSE TO ALL CLIENT CONNECTIONS + clientResp := helpers.MakeClientResponse(helpers.ClientActionAcceptFriend, responseMap, helpers.NoError()) u.mux.Lock() for _, conn := range u.conns { (*conn).socket.WriteJSON(clientResp) @@ -147,7 +152,7 @@ func (u *User) AcceptFriendRequest(friendName string) error { } ////////////////////////////////////////////////////////////////////////////////////////////////////// -// DECLINE A FRIEND REQUEST ////////////////////////////////////////////////////////////////////// +// Decline friend request //////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// // DeclineFriendRequest declines a friend request from another User by their name. @@ -158,7 +163,7 @@ func (u *User) DeclineFriendRequest(friendName string) error { return errors.New("The user '" + friendName + "' cannot be declined as a friend") } //CHECK IF FRIEND IS ONLINE & GET DATABASE ID - friend, friendErr := Get(friendName) + friend, friendErr := GetUser(friendName) var friendOnline bool = false var friendID int if friendErr != nil { @@ -191,9 +196,11 @@ func (u *User) DeclineFriendRequest(friendName string) error { //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE if friendOnline { - message := make(map[string]interface{}) - message[helpers.ServerActionFriendRemove] = make(map[string]interface{}) - message[helpers.ServerActionFriendRemove].(map[string]interface{})["n"] = u.name + message := map[string]map[string]interface{}{ + helpers.ServerActionFriendRemove: { + "n": u.name, + }, + } friend.mux.Lock() for _, conn := range friend.conns { (*conn).socket.WriteJSON(message) @@ -202,7 +209,7 @@ func (u *User) DeclineFriendRequest(friendName string) error { } //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionDeclineFriend, friendName, helpers.NewError("", 0)) + clientResp := helpers.MakeClientResponse(helpers.ClientActionDeclineFriend, friendName, helpers.NoError()) u.mux.Lock() for _, conn := range u.conns { (*conn).socket.WriteJSON(clientResp) @@ -214,7 +221,7 @@ func (u *User) DeclineFriendRequest(friendName string) error { } ////////////////////////////////////////////////////////////////////////////////////////////////////// -// REMOVE A FRIEND /////////////////////////////////////////////////////////////////////////////// +// Remove a friend /////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// // RemoveFriend removes a friend from this this User and this User from the friend's Friend list. @@ -225,7 +232,7 @@ func (u *User) RemoveFriend(friendName string) error { return errors.New("The user '" + friendName + "' cannot be removed as a friend") } //CHECK IF FRIEND IS ONLINE & GET DATABASE ID - friend, friendErr := Get(friendName) + friend, friendErr := GetUser(friendName) var friendOnline bool = false var friendID int if friendErr != nil { @@ -258,9 +265,11 @@ func (u *User) RemoveFriend(friendName string) error { //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE if friendOnline { - message := make(map[string]interface{}) - message[helpers.ServerActionFriendRemove] = make(map[string]interface{}) - message[helpers.ServerActionFriendRemove].(map[string]interface{})["n"] = u.name + message := map[string]map[string]interface{}{ + helpers.ServerActionFriendRemove: { + "n": u.name, + }, + } friend.mux.Lock() for _, conn := range friend.conns { (*conn).socket.WriteJSON(message) @@ -269,7 +278,7 @@ func (u *User) RemoveFriend(friendName string) error { } //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionRemoveFriend, friendName, helpers.NewError("", 0)) + clientResp := helpers.MakeClientResponse(helpers.ClientActionRemoveFriend, friendName, helpers.NoError()) u.mux.Lock() for _, conn := range u.conns { (*conn).socket.WriteJSON(clientResp) @@ -279,3 +288,22 @@ func (u *User) RemoveFriend(friendName string) error { // return nil } + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Send message to all friends /////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + +func (u *User) sendToFriends(message interface{}) { + for key, val := range u.friends { + if val.RequestStatus() == database.FriendStatusAccepted { + friend, friendErr := GetUser(key) + if friendErr == nil { + friend.mux.Lock() + for _, friendConn := range friend.conns { + (*friendConn).socket.WriteJSON(message) + } + friend.mux.Unlock() + } + } + } +} \ No newline at end of file diff --git a/core/messaging.go b/core/messaging.go new file mode 100644 index 0000000..102d09e --- /dev/null +++ b/core/messaging.go @@ -0,0 +1,297 @@ +package core + +import ( + "errors" + "github.com/gorilla/websocket" + "github.com/hewiefreeman/GopherGameServer/helpers" +) + +// These represent the types of room messages the server sends. +const ( + MessageTypeChat = iota + MessageTypeServer +) + +// These are the sub-types that a MessageTypeServer will come with. Ordered by their visible priority for your UI. +const ( + ServerMessageGame = iota + ServerMessageNotice + ServerMessageImportant +) + +var ( + privateMessageCallback func(*User, *User, interface{}) + privateMessageCallbackSet bool + chatMessageCallback func(string, *Room, interface{}) + chatMessageCallbackSet bool + serverMessageCallback func(*Room, int, interface{}) + serverMessageCallbackSet bool +) + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Messaging Users /////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + +// PrivateMessage sends a private message to another User by name. +func (u *User) PrivateMessage(userName string, message interface{}) { + user, userErr := GetUser(userName) + if userErr != nil { + return + } + + //CONSTRUCT MESSAGE + theMessage := map[string]map[string]interface{}{ + helpers.ServerActionPrivateMessage: { + "f": u.name, // from + "t": user.name, // to + "m": message, + }, + } + + //SEND MESSAGES + user.mux.Lock() + for _, conn := range user.conns { + (*conn).socket.WriteJSON(theMessage) + } + user.mux.Unlock() + u.mux.Lock() + for _, conn := range u.conns { + (*conn).socket.WriteJSON(theMessage) + } + u.mux.Unlock() + + if privateMessageCallbackSet { + privateMessageCallback(u, user, message) + } + + return +} + +// DataMessage sends a data message directly to the User. +func (u *User) DataMessage(data interface{}, connID string) { + //CONSTRUCT MESSAGE + message := map[string]interface{}{ + helpers.ServerActionDataMessage: data, + } + + //SEND MESSAGE TO USER + u.mux.Lock() + if connID == "" { + for _, conn := range u.conns { + (*conn).socket.WriteJSON(message) + } + } else { + if conn, ok := u.conns[connID]; ok { + (*conn).socket.WriteJSON(message) + } + } + u.mux.Unlock() +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Messaging Rooms /////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + +// ServerMessage sends a server message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice +// of string. In which case, the server message will be sent to all Users in the Room. +func (r *Room) ServerMessage(message interface{}, messageType int, recipients []string) error { + if message == nil { + return errors.New("*Room.ServerMessage() requires a message") + } + + if serverMessageCallbackSet { + serverMessageCallback(r, messageType, message) + } + + return r.sendMessage(MessageTypeServer, messageType, recipients, "", message) +} + +// ChatMessage sends a chat message to all Users in the Room. +func (r *Room) ChatMessage(author string, message interface{}) error { + //REJECT INCORRECT INPUT + if len(author) == 0 { + return errors.New("*Room.ChatMessage() requires an author") + } else if message == nil { + return errors.New("*Room.ChatMessage() requires a message") + } + + if chatMessageCallbackSet { + chatMessageCallback(author, r, message) + } + + return r.sendMessage(MessageTypeChat, 0, nil, author, message) +} + +// DataMessage sends a data message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice +// of string. In which case, the data message will be sent to all Users in the Room. +func (r *Room) DataMessage(message interface{}, recipients []string) error { + //GET USER MAP + userMap, err := r.GetUserMap() + if err != nil { + return err + } + + //CONSTRUCT MESSAGE + theMessage := map[string]interface{}{ + helpers.ServerActionDataMessage: message, + } + + //SEND MESSAGE TO USERS + if recipients == nil || len(recipients) == 0 { + for _, u := range userMap { + u.mux.Lock() + for _, conn := range u.conns { + conn.socket.WriteJSON(theMessage) + } + u.mux.Unlock() + } + } else { + for i := 0; i < len(recipients); i++ { + if u, ok := userMap[recipients[i]]; ok { + u.mux.Lock() + for _, conn := range u.conns { + conn.socket.WriteJSON(theMessage) + } + u.mux.Unlock() + } + } + } + + // + return nil +} + +func (r *Room) sendMessage(mt int, st int, rec []string, a string, m interface{}) error { + //GET USER MAP + userMap, err := r.GetUserMap() + if err != nil { + return err + } + + //CONSTRUCT MESSAGE + message := map[string]map[string]interface{}{ + helpers.ServerActionRoomMessage: make(map[string]interface{}), + } + // Server messages come with a sub-type + if mt == MessageTypeServer { + message[helpers.ServerActionRoomMessage]["s"] = st + } + // Non-server messages have authors + if len(a) > 0 && mt != MessageTypeServer { + message[helpers.ServerActionRoomMessage]["a"] = a + } + // The message + message[helpers.ServerActionRoomMessage]["m"] = m + + //SEND MESSAGE TO USERS + if rec == nil || len(rec) == 0 { + for _, u := range userMap { + u.mux.Lock() + for _, conn := range u.conns { + conn.socket.WriteJSON(message) + } + u.mux.Unlock() + } + } else { + for i := 0; i < len(rec); i++ { + if u, ok := userMap[rec[i]]; ok { + u.mux.Lock() + for _, conn := range u.conns { + conn.socket.WriteJSON(message) + } + u.mux.Unlock() + } + } + } + + return nil +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// VOICE STREAMS ////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// VoiceStream sends a voice stream from the client API to all the users in the room besides the user who is speaking. +func (r *Room) VoiceStream(userName string, userSocket *websocket.Conn, stream interface{}) { + //GET USER MAP + userMap, err := r.GetUserMap() + if err != nil { + return + } + + //CONSTRUCT VOICE MESSAGE + theMessage := map[string]map[string]interface{}{ + helpers.ServerActionVoiceStream: { + "u": userName, + "d": stream, + }, + } + + //REMOVE SENDING USER FROM userMap + delete(userMap, userName) // COMMENT OUT FOR ECHO TESTS + + //SEND MESSAGE TO USERS + for _, u := range userMap { + for _, conn := range u.conns { + (*conn).socket.WriteJSON(theMessage) + } + } + + //CONSTRUCT PING MESSAGE + pingMessage := map[string]interface{}{ + helpers.ServerActionVoicePing: nil, + } + + //SEND PING MESSAGE TO SENDING USER + userSocket.WriteJSON(pingMessage) + + // + return +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback Setters /////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// SetPrivateMessageCallback sets the callback function for when a *User sends a private message to another *User. +// The function passed must have the same parameter types as the following example: +// +// func onPrivateMessage(from *core.User, to *core.User, message interface{}) { +// //code... +// } +func SetPrivateMessageCallback(cb func(*User, *User, interface{})) { + if !serverStarted { + privateMessageCallback = cb + privateMessageCallbackSet = true + } + +} + +// SetChatMessageCallback sets the callback function for when a *User sends a chat message to a *Room. +// The function passed must have the same parameter types as the following example: +// +// func onChatMessage(userName string, room *core.Room, message interface{}) { +// //code... +// } +func SetChatMessageCallback(cb func(string, *Room, interface{})) { + if !serverStarted { + chatMessageCallback = cb + chatMessageCallbackSet = true + } +} + +// SetServerMessageCallback sets the callback function for when the server sends a message to a *Room. +// The function passed must have the same parameter types as the following example: +// +// func onServerMessage(room *core.Room, messageType int, message interface{}) { +// //code... +// } +// +// The messageType value can be one of: core.ServerMessageGame, core.ServerMessageNotice, +// core.ServerMessageImportant, or a custom value you have set. +func SetServerMessageCallback(cb func(*Room, int, interface{})) { + if !serverStarted { + serverMessageCallback = cb + serverMessageCallbackSet = true + } +} diff --git a/rooms/roomTypes.go b/core/roomTypes.go similarity index 92% rename from rooms/roomTypes.go rename to core/roomTypes.go index f78e4f7..5a3572f 100644 --- a/rooms/roomTypes.go +++ b/core/roomTypes.go @@ -1,6 +1,4 @@ -package rooms - -import () +package core var ( roomTypes = make(map[string]*RoomType) @@ -17,10 +15,10 @@ type RoomType struct { broadcastUserEnter bool broadcastUserLeave bool - createCallback func(*Room) // roomCreated - deleteCallback func(*Room) // roomDeleted - userEnterCallback func(*Room, string) // roomFrom, userName - userLeaveCallback func(*Room, string) // roomFrom, userName + createCallback func(*Room) // roomCreated + deleteCallback func(*Room) // roomDeleted + userEnterCallback func(*Room, *RoomUser) // roomFrom, user + userLeaveCallback func(*Room, *RoomUser) // roomFrom, user } // NewRoomType Adds a RoomType to the server. A RoomType is used in conjunction with it's corresponding callbacks @@ -131,7 +129,7 @@ func (r *RoomType) SetDeleteCallback(callback func(*Room)) *RoomType { // and the string is the name of the User that entered. // // Note: You must call this BEFORE starting the server in order for it to take effect. -func (r *RoomType) SetUserEnterCallback(callback func(*Room, string)) *RoomType { +func (r *RoomType) SetUserEnterCallback(callback func(*Room, *RoomUser)) *RoomType { if serverStarted { return r } @@ -144,7 +142,7 @@ func (r *RoomType) SetUserEnterCallback(callback func(*Room, string)) *RoomType // and the string is the name of the User that left. // // Note: You must call this BEFORE starting the server in order for it to take effect. -func (r *RoomType) SetUserLeaveCallback(callback func(*Room, string)) *RoomType { +func (r *RoomType) SetUserLeaveCallback(callback func(*Room, *RoomUser)) *RoomType { if serverStarted { return r } @@ -153,7 +151,7 @@ func (r *RoomType) SetUserLeaveCallback(callback func(*Room, string)) *RoomType } ////////////////////////////////////////////////////////////////////////////////////////////////////// -// RoomType ATTRIBUTE READERS //////////////////////////////////////////////////////////////////// +// RoomType ATTRIBUTE & CALLBACK READERS ///////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// // ServerOnly returns true if the RoomType can only be manipulated by the server. @@ -197,7 +195,7 @@ func (r *RoomType) HasDeleteCallback() bool { } // UserEnterCallback returns the function that this RoomType calls when a User enters a Room of this RoomType. -func (r *RoomType) UserEnterCallback() func(*Room, string) { +func (r *RoomType) UserEnterCallback() func(*Room, *RoomUser) { return r.userEnterCallback } @@ -207,7 +205,7 @@ func (r *RoomType) HasUserEnterCallback() bool { } // UserLeaveCallback returns the function that this RoomType calls when a User leaves a Room of this RoomType. -func (r *RoomType) UserLeaveCallback() func(*Room, string) { +func (r *RoomType) UserLeaveCallback() func(*Room, *RoomUser) { return r.userLeaveCallback } diff --git a/rooms/rooms.go b/core/rooms.go similarity index 54% rename from rooms/rooms.go rename to core/rooms.go index f4cabe1..df933e2 100644 --- a/rooms/rooms.go +++ b/core/rooms.go @@ -1,32 +1,16 @@ -// Package rooms contains all the necessary tools to make and work with Rooms. A Room represents -// a place on the server where a User can join other Users. -// -// A Room can either be public or private. Private Rooms must be assigned an "owner", which is the name of a User, or the ServerName -// from ServerSettings. The server's name that will be used for ownership of private Rooms can be set with the ServerSettings -// option ServerName when starting the server. Though keep in mind, setting the ServerName in ServerSettings will prevent a User who wants to go by that name -// from logging in. Public Rooms will accept a join request from any User, and private Rooms will only -// accept a join request from someone who is on it's invite list. Only the owner of the Room or the server itself can invite -// Users to a private Room. But remember, just because a User owns a private room doesn't mean the server cannot also invite -// to the room via *Room.AddInvite() function. -// -// Rooms have their own variables which can be accessed and changed anytime. A Room variable can -// be anything compatible with interface{}, so pretty much anything. Room variables should mainly be used -// for things about the room itself that don't change very often (or, for instance, are absolutely needed for a joining -// User). -package rooms +package core import ( "errors" - "github.com/gorilla/websocket" "github.com/hewiefreeman/GopherGameServer/helpers" "sync" ) -// Room represents a room on the server that Users can join and leave. Use rooms.New() to make a new Room. +// Room represents a room on the server that Users can join and leave. Use core.NewRoom() to make a new Room. // -// WARNING: When you use a *User object in your code, DO NOT dereference it. Instead, there are -// many methods for *User for retrieving any information about them you could possibly need. -// Dereferencing them could cause data races in the User fields that get locked by mutexes. +// WARNING: When you use a *Room object in your code, DO NOT dereference it. Instead, there are +// many methods for *Room for maniupulating and retrieving any information about them you could possibly need. +// Dereferencing them could cause data races in the Room fields that get locked by mutexes. type Room struct { name string rType string @@ -41,45 +25,24 @@ type Room struct { vars map[string]interface{} } -// RoomUser is the representation of a User in a Room. Note: These -// are not the Users themselves. There are RoomUser methods that provide the same -// functionality as the matching User methods, but if you really need to get a User type from one of these, use -// users.Get() with the RoomUser's Name() function. +// RoomUser represents a User inside of a Room. Use the *RoomUser.User() function to get a *User from a *RoomUser type RoomUser struct { - name string - isGuest bool - dbID int - - //mux LOCKS ALL FIELDS BELOW - mux *sync.Mutex // Pointer to the User's mux - conns map[string]*roomUserConn // User's conns info + user *User -} - -type roomUserConn struct { - socket *websocket.Conn - roomIn **Room // Pointer to the User's Room pointer - vars *map[string]interface{} // Pointer to the User's variables + mux sync.Mutex + conns map[string]*userConn } var ( - //THE Rooms AND Rooms MUTEX rooms map[string]*Room = make(map[string]*Room) - roomsMux sync.Mutex //LOCKS rooms - - //SERVER SETTINGS - serverStarted bool = false - serverPaused bool = false - serverName string - deleteRoomOnLeave bool = true - multiConnect bool = false + roomsMux sync.Mutex ) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MAKE A NEW ROOM //////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// New adds a new room to the server. This can be called before or after starting the server. +// NewRoom adds a new room to the server. This can be called before or after starting the server. // Parameters: // // - name (string): Name of the Room @@ -91,10 +54,10 @@ var ( // - maxUsers (int): Maximum User capacity (Note: 0 means no limit) // // - owner (string): The owner of the room. If provided a blank string, will set the owner to the ServerName from ServerSettings -func New(name string, rType string, isPrivate bool, maxUsers int, owner string) (*Room, error) { +func NewRoom(name string, rType string, isPrivate bool, maxUsers int, owner string) (*Room, error) { //REJECT INCORRECT INPUT if len(name) == 0 { - return &Room{}, errors.New("rooms.New() requires a name") + return &Room{}, errors.New("core.NewRoom() requires a name") } else if maxUsers < 0 { maxUsers = 0 } else if owner == "" { @@ -113,21 +76,17 @@ func New(name string, rType string, isPrivate bool, maxUsers int, owner string) roomsMux.Unlock() return &Room{}, errors.New("A Room with the name '" + name + "' already exists") } - userMap := make(map[string]*RoomUser) - roomVars := make(map[string]interface{}) - invList := []string{} - theRoom := Room{name: name, private: isPrivate, inviteList: invList, usersMap: userMap, maxUsers: maxUsers, vars: roomVars, - owner: owner, rType: rType} + theRoom := Room{name: name, private: isPrivate, inviteList: []string{}, usersMap: make(map[string]*RoomUser), maxUsers: maxUsers, + vars: make(map[string]interface{}), owner: owner, rType: rType} rooms[name] = &theRoom - room := rooms[name] roomsMux.Unlock() //CALLBACK if roomType.HasCreateCallback() { - roomType.CreateCallback()(room) + roomType.CreateCallback()(&theRoom) } - return room, nil + return &theRoom, nil } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -144,17 +103,19 @@ func (r *Room) Delete() error { } // MAKE LEAVE MESSAGE - leaveMessage := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, nil, helpers.NewError("", 0)) + leaveMessage := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, nil, helpers.NoError()) // GO THROUGH ALL Users IN ROOM - for _, v := range r.usersMap { + for _, u := range r.usersMap { //CHANGE User's room POINTER TO nil & SEND MESSAGES - v.mux.Lock() - for key := range v.conns { - (*v.conns[key]).socket.WriteJSON(leaveMessage) - *((*v.conns[key]).roomIn) = nil + u.mux.Lock() + for key := range u.conns { + (*u.conns[key]).socket.WriteJSON(leaveMessage) + u.user.mux.Lock() + (*u.conns[key]).room = nil + u.user.mux.Unlock() } - v.mux.Unlock() + u.mux.Unlock() } r.usersMap = nil @@ -179,11 +140,11 @@ func (r *Room) Delete() error { // GET A ROOM ///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Get finds a Room on the server. If the room does not exit, an error will be returned. -func Get(roomName string) (*Room, error) { +// GetRoom finds a Room on the server. If the room does not exit, an error will be returned. +func GetRoom(roomName string) (*Room, error) { //REJECT INCORRECT INPUT if len(roomName) == 0 { - return &Room{}, errors.New("rooms.Get() requires a room name") + return &Room{}, errors.New("core.GetRoom() requires a room name") } var room *Room @@ -204,23 +165,19 @@ func Get(roomName string) (*Room, error) { // ADD A USER ///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// AddUser adds a User to the Room. -// -// WARNING: This is only meant for internal Gopher Game Server mechanics. If you want to make a User join a Room, use -// *User.Join() instead. Using this improperly will break server mechanics and cause errors and/or memory leaks. -func (r *Room) AddUser(userName string, dbID int, isGuest bool, socket *websocket.Conn, roomIn **Room, userVars *map[string]interface{}, - userStatus *int, userMux *sync.Mutex, connID string) error { +// AddUser adds a User to the Room. If you are using MultiConnect in ServerSettings, the connID +// parameter is the connection ID associated with one of the connections attached to that User. This must +// be provided when adding a User to a Room with MultiConnect enabled. Otherwise, an empty string can be used. +func (r *Room) AddUser(user *User, connID string) error { + userName := user.Name() // REJECT INCORRECT INPUT - if len(userName) == 0 { - return errors.New("*Room.AddUser() requires a user name") - } else if socket == nil { - return errors.New("*Room.AddUser() requires a user socket") + if user == nil { + return errors.New("*Room.AddUser() requires a valid User") } else if multiConnect && len(connID) == 0 { return errors.New("Must provide a connID when MultiConnect is enabled") } else if !multiConnect { connID = "1" } - r.mux.Lock() if r.usersMap == nil { r.mux.Unlock() @@ -229,10 +186,9 @@ func (r *Room) AddUser(userName string, dbID int, isGuest bool, socket *websocke r.mux.Unlock() return errors.New("The room '" + r.name + "' is full") } - // CHECK IF THE ROOM IS PRIVATE, OWNER JOINS FREELY if r.private && userName != r.owner { - // IF SO, CHECK IF THIS USER IS ON THE INVITE LIST + // IF PRIVATE AND NOT OWNER, CHECK IF THIS USER IS ON THE INVITE LIST if len(r.inviteList) > 0 { for i := 0; i < len(r.inviteList); i++ { if (r.inviteList)[i] == userName { @@ -251,64 +207,73 @@ func (r *Room) AddUser(userName string, dbID int, isGuest bool, socket *websocke } // CHECK IF USER IS ALREADY IN THE ROOM - var roomUserExists bool = false - if rUser, ok := r.usersMap[userName]; ok { - roomUserExists = true + var ru *RoomUser + var ok bool + if ru, ok = r.usersMap[userName]; ok { if !multiConnect { r.mux.Unlock() return errors.New("User '" + userName + "' is already in room '" + r.name + "'") } - (*rUser.mux).Lock() - if _, ok := rUser.conns[connID]; ok { + ru.mux.Lock() + if _, ok := ru.conns[connID]; ok { r.mux.Unlock() - (*rUser.mux).Unlock() + ru.mux.Unlock() return errors.New("User '" + userName + "' is already in room '" + r.name + "'") } - (*rUser.mux).Unlock() + ru.mux.Unlock() } - userList := r.usersMap - newConn := roomUserConn{socket: socket, roomIn: roomIn, vars: userVars} // ADD User TO ROOM - if roomUserExists { - (*r.usersMap[userName]).conns[connID] = &newConn + user.mux.Lock() + c := user.conns[connID] + if c == nil { + r.mux.Unlock() + return errors.New("Invalid connection ID") + } + if ru != nil { + (*r.usersMap[userName]).mux.Lock() + (*r.usersMap[userName]).conns[connID] = c + (*r.usersMap[userName]).mux.Unlock() } else { - conns := make(map[string]*roomUserConn) - conns[connID] = &newConn - newUser := RoomUser{name: userName, isGuest: isGuest, dbID: dbID, mux: userMux, conns: conns} + conns := make(map[string]*userConn) + conns[connID] = c + newUser := RoomUser{user: user, conns: conns} r.usersMap[userName] = &newUser + ru = r.usersMap[userName] } - r.mux.Unlock() - // CHANGE USER'S ROOM - (*userMux).Lock() - *roomIn = r - (*userMux).Unlock() + c.room = r + + user.mux.Unlock() + r.mux.Unlock() // roomType := roomTypes[r.rType] if roomType.BroadcastUserEnter() { //BROADCAST ENTER TO USERS IN ROOM - message := make(map[string]interface{}) - message[helpers.ServerActionUserEnter] = make(map[string]interface{}) - message[helpers.ServerActionUserEnter].(map[string]interface{})["u"] = userName - message[helpers.ServerActionUserEnter].(map[string]interface{})["g"] = isGuest - for _, u := range userList { + message := map[string]map[string]interface{}{ + helpers.ServerActionUserEnter: { + "u": userName, + "g": user.isGuest, + }, + } + for _, u := range r.usersMap { u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(message) + if u.user.Name() != userName { + for _, conn := range u.conns { + (*conn).socket.WriteJSON(message) + } } u.mux.Unlock() } } - // CALLBACK if roomType.HasUserEnterCallback() { - roomType.UserEnterCallback()(r, userName) + roomType.UserEnterCallback()(r, ru) } // SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionJoinRoom, r.Name(), helpers.NewError("", 0)) - socket.WriteJSON(clientResp) + clientResp := helpers.MakeClientResponse(helpers.ClientActionJoinRoom, r.Name(), helpers.NoError()) + c.socket.WriteJSON(clientResp) // return nil @@ -321,10 +286,10 @@ func (r *Room) AddUser(userName string, dbID int, isGuest bool, socket *websocke // RemoveUser removes a User from the room. If you are using MultiConnect in ServerSettings, the connID // parameter is the connection ID associated with one of the connections attached to that User. This must // be provided when removing a User from a Room with MultiConnect enabled. Otherwise, an empty string can be used. -func (r *Room) RemoveUser(userName string, connID string) error { +func (r *Room) RemoveUser(user *User, connID string) error { //REJECT INCORRECT INPUT - if len(userName) == 0 { - return errors.New("*Room.RemoveUser() requires a user name") + if user == nil || len(user.name) == 0 { + return errors.New("*Room.RemoveUser() requires a valid *User") } else if multiConnect && len(connID) == 0 { return errors.New("Must provide a connID when MultiConnect is enabled") } else if !multiConnect { @@ -336,36 +301,43 @@ func (r *Room) RemoveUser(userName string, connID string) error { r.mux.Unlock() return errors.New("The room '" + r.name + "' does not exist") } - if _, ok := r.usersMap[userName]; !ok { + var ok bool + var ru *RoomUser + if ru, ok = r.usersMap[user.name]; !ok { r.mux.Unlock() - return errors.New("User '" + userName + "' is not in room '" + r.name + "'") + return errors.New("User '" + user.name + "' is not in room '" + r.name + "'") } - if _, ok := (*r.usersMap[userName]).conns[connID]; !ok { + ru.mux.Lock() + var uConn *userConn + if uConn, ok = ru.conns[connID]; !ok { r.mux.Unlock() + ru.mux.Unlock() return errors.New("Invalid connID") } - uConn := *((*r.usersMap[userName]).conns[connID]) - uMux := (*r.usersMap[userName]).mux - delete((*r.usersMap[userName]).conns, connID) - if len((*r.usersMap[userName]).conns) == 0 { - delete(r.usersMap, userName) + delete(ru.conns, connID) + // Remove user when no conns are left in room + if len(ru.conns) == 0 { + delete(r.usersMap, user.name) } + ru.mux.Unlock() userList := r.usersMap r.mux.Unlock() // roomType := roomTypes[r.rType] //DELETE THE ROOM IF THE OWNER LEFT AND UserRoomControl IS ENABLED - if deleteRoomOnLeave && userName == r.owner { + if deleteRoomOnLeave && user.name == r.owner { deleteErr := r.Delete() if deleteErr != nil { return deleteErr } } else if roomType.BroadcastUserLeave() { //CONSTRUCT LEAVE MESSAGE - message := make(map[string]interface{}) - message[helpers.ServerActionUserLeave] = make(map[string]interface{}) - message[helpers.ServerActionUserLeave].(map[string]interface{})["u"] = userName + message := map[string]map[string]interface{}{ + helpers.ServerActionUserLeave: { + "u": user.name, + }, + } //SEND MESSAGE TO USERS for _, u := range userList { @@ -378,17 +350,17 @@ func (r *Room) RemoveUser(userName string, connID string) error { } // CHANGE USER'S ROOM - uMux.Lock() - *(uConn.roomIn) = nil - uMux.Unlock() + user.mux.Lock() + uConn.room = nil + user.mux.Unlock() //CALLBACK if roomType.HasUserLeaveCallback() { - roomType.UserLeaveCallback()(r, userName) + roomType.UserLeaveCallback()(r, ru) } //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, nil, helpers.NewError("", 0)) + clientResp := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, r.Name(), helpers.NoError()) uConn.socket.WriteJSON(clientResp) // @@ -402,10 +374,10 @@ func (r *Room) RemoveUser(userName string, connID string) error { // AddInvite adds a User to a private Room's invite list. This is only meant for internal Gopher Game Server mechanics. // If you want a User to invite someone to a private room, use the *User.Invite() function instead. // -// NOTE: You can use this function safely, but remember that private rooms are designed to have an "owner", +// NOTE: Remember that private rooms are designed to have an "owner", // and only the owner should be able to send an invite and revoke an invitation for their Rooms. Also, *User.Invite() -// will send an invite message to the invited User that the client API can easily receive. Though if you wish to make -// your own implementations for this, don't hesitate! +// will send an invite notification message to the invited User that the client API can easily receive. Though if you wish to make +// your own implementations for sending and receiving these notifications, this function is safe to use. func (r *Room) AddInvite(userName string) error { if !r.private { return errors.New("Room is not private") @@ -435,14 +407,12 @@ func (r *Room) AddInvite(userName string) error { // REMOVE FROM inviteList ///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// RemoveInvite removes a User from a private Room's invite list. +// RemoveInvite removes a User from a private Room's invite list. To make a User remove someone from their room themselves, +// use the *User.RevokeInvite() function. // // NOTE: You can use this function safely, but remember that private rooms are designed to have an "owner", // and only the owner should be able to send an invite and revoke an invitation for their Rooms. But if you find the // need to break the rules here, by all means do so! -// -// WARNING: This is only meant for internal Gopher Game Server mechanics. If you want a User to remove someone from the room's -// private invite list, use the *User.RevokeInvite() function instead. func (r *Room) RemoveInvite(userName string) error { if !r.private { return errors.New("Room is not private") @@ -451,13 +421,15 @@ func (r *Room) RemoveInvite(userName string) error { } r.mux.Lock() + if r.usersMap == nil { r.mux.Unlock() return errors.New("The room '" + r.name + "' does not exist") } for i := 0; i < len(r.inviteList); i++ { if r.inviteList[i] == userName { - r.inviteList = append(r.inviteList[:i], r.inviteList[i+1:]...) + r.inviteList[i] = r.inviteList[len(r.inviteList)-1] + r.inviteList = r.inviteList[:len(r.inviteList)-1] break } if i == len(r.inviteList)-1 { @@ -471,43 +443,6 @@ func (r *Room) RemoveInvite(userName string) error { return nil } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// GET A ROOM's inviteList //////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// InviteList gets a private Room's invite list. -func (r *Room) InviteList() ([]string, error) { - r.mux.Lock() - if r.usersMap == nil { - r.mux.Unlock() - return []string{}, errors.New("The room '" + r.name + "' does not exist") - } - list := r.inviteList - r.mux.Unlock() - // - return list, nil -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// GET A Room's usersMap ////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// GetUserMap retrieves all the RoomUsers as a map[string]*RoomUser. -func (r *Room) GetUserMap() (map[string]*RoomUser, error) { - var err error - var userMap map[string]*RoomUser - - r.mux.Lock() - if r.usersMap == nil { - err = errors.New("The room '" + r.name + "' does not exist") - } else { - userMap = r.usersMap - } - r.mux.Unlock() - - return userMap, err -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Room ATTRIBUTE READERS ///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -546,81 +481,62 @@ func (r *Room) NumUsers() int { return len(m) } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// RoomUser ATTRIBUTE READERS ///////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Name gets the name of the RoomUser. -func (u *RoomUser) Name() string { - return u.name -} - -// IsGuest returns true if the RoomUser is a guest. -func (u *RoomUser) IsGuest() bool { - return u.isGuest -} - -// DatabaseID gets the database index of the RoomUser. -func (u *RoomUser) DatabaseID() int { - return u.dbID +// InviteList gets a private Room's invite list. +func (r *Room) InviteList() ([]string, error) { + r.mux.Lock() + if r.usersMap == nil { + r.mux.Unlock() + return []string{}, errors.New("The room '" + r.name + "' does not exist") + } + list := r.inviteList + r.mux.Unlock() + // + return list, nil } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER STARTUP FUNCTIONS /////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// GetUserMap retrieves all the RoomUsers as a map[string]*RoomUser. +func (r *Room) GetUserMap() (map[string]*RoomUser, error) { + var err error + var userMap map[string]*RoomUser -// SetServerStarted is for Gopher Game Server internal mechanics. -func SetServerStarted(val bool) { - if !serverStarted { - serverStarted = val + r.mux.Lock() + if r.usersMap == nil { + err = errors.New("The room '" + r.name + "' does not exist") + } else { + userMap = r.usersMap } -} + r.mux.Unlock() -// SettingsSet is for Gopher Game Server internal mechanics. -func SettingsSet(name string, deleteOnLeave bool, multiConn bool) { - if !serverStarted { - serverName = name - deleteRoomOnLeave = deleteOnLeave - multiConnect = multiConn - } + return userMap, err } -////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Pause is only for internal Gopher Game Server mechanics. -func Pause() { - if !serverPaused { - serverPaused = true - serverStarted = false - } +// RoomCount returns the number of Rooms created on the server. +func RoomCount() int { + roomsMux.Lock() + length := len(rooms) + roomsMux.Unlock() + return length } -// Resume is only for internal Gopher Game Server mechanics. -func Resume() { - if serverPaused { - serverStarted = true - serverPaused = false - } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// RoomUser ATTRIBUTE READERS ///////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// User gets the *User object of a *RoomUser. +func (u *RoomUser) User() *User { + return u.user } -// Resume is only for internal Gopher Game Server mechanics. -func GetState() map[string]interface{} { - state := make(map[string]interface{}) - roomsMux.Lock() - for _, room := range rooms { - room.mux.Lock() - state[room.name] = make(map[string]interface{}) - state[room.name].(map[string]interface{})["t"] = room.rType - state[room.name].(map[string]interface{})["p"] = room.private - state[room.name].(map[string]interface{})["o"] = room.owner - state[room.name].(map[string]interface{})["m"] = room.maxUsers - state[room.name].(map[string]interface{})["i"] = room.inviteList - state[room.name].(map[string]interface{})["v"] = room.vars - room.mux.Unlock() - } - roomsMux.Unlock() - // - return state +// ConnectionIDs returns a []string of all the RoomUser's connection IDs. With MultiConnect in ServerSettings enabled, +// this will give you all the connections for this User that are currently in the Room. Otherwise, if you want +// all the User's connection IDs (not just the connections in the specified Room), use *User.ConnectionIDs() after getting +// the *User object with the *RoomUser.User() function. +func (u *RoomUser) ConnectionIDs() []string { + u.mux.Lock() + ids := make([]string, 0, len(u.conns)) + for id := range u.conns { + ids = append(ids, id) + } + u.mux.Unlock() + return ids } diff --git a/users/users.go b/core/users.go similarity index 65% rename from users/users.go rename to core/users.go index 846eeea..11114f8 100644 --- a/users/users.go +++ b/core/users.go @@ -1,15 +1,10 @@ -// Package users contains all the necessary tools to make and working with Users. A User is a client -// who has successfully logged into the server. You can think of clients who are not attached to a User -// as, for instance, someone in the login screen, but are still connected to the server. A client doesn't -// have to be a User to be able to call your CustomClientActions, so keep that in mind when making them! -package users +package core import ( "errors" "github.com/gorilla/websocket" "github.com/hewiefreeman/GopherGameServer/database" "github.com/hewiefreeman/GopherGameServer/helpers" - "github.com/hewiefreeman/GopherGameServer/rooms" "sync" ) @@ -30,7 +25,7 @@ type User struct { databaseID int isGuest bool - //mux LOCKS ALL FIELDS BELOW + //mux lock all items below mux sync.Mutex status int friends map[string]*database.Friend @@ -38,31 +33,25 @@ type User struct { } type userConn struct { - //MUST LOCK clientMux WHEN GETTING/SETTING *user + // Must lock *clientMux when using *user clientMux *sync.Mutex user **User socket *websocket.Conn - room *rooms.Room - vars map[string]interface{} + + //Must lock user's mux to use below items + room *Room + vars map[string]interface{} } var ( users map[string]*User = make(map[string]*User) usersMux sync.Mutex - serverStarted bool = false - serverPaused bool = false - - serverName string - kickOnLogin bool = false - sqlFeatures bool = false - rememberMe bool = false - multiConnect bool = false - - - LoginCallback func(string, int, map[string]interface{}, map[string]interface{}) bool // LoginCallback is only for internal Gopher Game Server mechanics. - LogoutCallback func(string, int) // LogoutCallback is only for internal Gopher Game Server mechanics. + // LoginCallback is only for internal Gopher Game Server mechanics. + LoginCallback func(string, int, map[string]interface{}, map[string]interface{}) bool + // LogoutCallback is only for internal Gopher Game Server mechanics. + LogoutCallback func(string, int) ) // These represent the four statuses a User could be. @@ -92,7 +81,7 @@ const ( // Login logs a User in to the service. func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bool, socket *websocket.Conn, connUser **User, clientMux *sync.Mutex) (string, helpers.GopherError) { - //REJECT INCORRECT INPUT + // Verify input if serverPaused { return "", helpers.NewError(errorServerPaused, helpers.ErrorServerPaused) } else if len(userName) == 0 { @@ -105,18 +94,18 @@ func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bo return "", helpers.NewError(errorRequiredSocket, helpers.ErrorAuthRequiredSocket) } - //ALWAYS SET A GUEST'S id TO -1 + // Guests always have -1 databaseID databaseID := dbID if isGuest { databaseID = -1 } - //RUN CALLBACK + // Callback if LoginCallback != nil && !LoginCallback(userName, dbID, nil, nil) { return "", helpers.NewError(errorDenied, helpers.ErrorActionDenied) } - //MAKE *User IN users MAP & MAKE connID + // Make *User in users & make connID var connID string var connErr error var userExists bool = false @@ -126,32 +115,32 @@ func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bo if userOnline, ok := users[userName]; ok { userExists = true if kickOnLogin { - //REMOVE USER FROM THEIR CURRENT ROOM IF ANY + // Kick user & remove from room userOnline.mux.Lock() for connKey, conn := range userOnline.conns { userRoom := (*conn).room if userRoom != nil && userRoom.Name() != "" { userOnline.mux.Unlock() - userRoom.RemoveUser(userOnline.name, connKey) + userRoom.RemoveUser(userOnline, connKey) userOnline.mux.Lock() } (*(*conn).clientMux).Lock() *((*conn).user) = nil (*(*conn).clientMux).Unlock() - //SEND LOGOUT MESSAGE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NewError("", 0)) + // Send logout message to client + clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) (*conn).socket.WriteJSON(clientResp) } userOnline.mux.Unlock() - //DELETE FROM users MAP + // Remove user from users map delete(users, userName) - //MAKE connID + // Make connID connID = "1" userExists = false } else if multiConnect { - //MAKE UNIQUE connID + // Make a unique connID for { connID, connErr = helpers.GenerateSecureString(5) if connErr != nil { @@ -170,71 +159,47 @@ func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bo return "", helpers.NewError(errorAlreadyLogged, helpers.ErrorAuthAlreadyLogged) } } else if multiConnect { - //MAKE UNIQUE connID + // Make connID connID, connErr = helpers.GenerateSecureString(5) if connErr != nil { usersMux.Unlock() return "", helpers.NewError(errorUnexpected, helpers.ErrorAuthUnexpected) } } else { - //MAKE connID + // Make connID connID = "1" } - //MAKE THE userConn + // Make the userConn vars := make(map[string]interface{}) conn := userConn{socket: socket, room: nil, vars: vars, user: connUser, clientMux: clientMux} - //FRIENDS OBJECTS - var friends []map[string]interface{} = []map[string]interface{}{} + // Make friends objects + var u *User + var friends []map[string]interface{} var friendsMap map[string]*database.Friend - //ADD THE userConn TO THE User OR MAKE THE User + // Add the userConn to the User or make new User if userExists { (*users[userName]).mux.Lock() (*users[userName]).conns[connID] = &conn friendsMap = (*users[userName]).friends (*users[userName]).mux.Unlock() - //MAKE FRINDS LIST FOR SERVER RESPONSE - for _, val := range friendsMap { - friendEntry := make(map[string]interface{}) - friendEntry["n"] = val.Name() - friendEntry["rs"] = val.RequestStatus() - if val.RequestStatus() == database.FriendStatusAccepted { - //GET THE User STATUS - if friend, ok := users[val.Name()]; ok { - friendEntry["s"] = friend.Status() - } else { - friendEntry["s"] = StatusOffline - } - } - friends = append(friends, friendEntry) - } + // Make friends list for response + friends = makeFriendsResponse(friendsMap) } else { - //GET User's Friend LIST FROM DATABASE + // Get friend list from database if dbID != -1 && sqlFeatures { var friendsErr error - friendsMap, friendsErr = database.GetFriends(dbID) // map[string]Friend - if friendsErr == nil { - //MAKE FRINDS LIST FOR SERVER RESPONSE - for _, val := range friendsMap { - friendEntry := make(map[string]interface{}) - friendEntry["n"] = val.Name() - friendEntry["rs"] = val.RequestStatus() - if val.RequestStatus() == database.FriendStatusAccepted { - //GET THE User STATUS - if friend, ok := users[val.Name()]; ok { - friendEntry["s"] = friend.Status() - } else { - friendEntry["s"] = StatusOffline - } - } - friends = append(friends, friendEntry) - } + if friendsMap, friendsErr = database.GetFriends(dbID); friendsErr == nil { + // Make friends list for response + friends = makeFriendsResponse(friendsMap) } } - conns := make(map[string]*userConn) - conns[connID] = &conn + conns := map[string]*userConn{ + connID: &conn, + } newUser := User{name: userName, databaseID: databaseID, isGuest: isGuest, status: 0, friends: friendsMap, conns: conns} - users[userName] = &newUser + u = &newUser + users[userName] = u } (*conn.clientMux).Lock() *(conn.user) = users[userName] @@ -242,37 +207,58 @@ func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bo // usersMux.Unlock() - //SEND ONLINE MESSAGE TO FRIENDS - statusMessage := make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange] = make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["n"] = userName - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["s"] = 0 - for key, val := range friendsMap { - if val.RequestStatus() == database.FriendStatusAccepted { - friend, friendErr := Get(key) - if friendErr == nil { - friend.mux.Lock() - for _, friendConn := range friend.conns { - friendConn.socket.WriteJSON(statusMessage) - } - friend.mux.Unlock() - } - } + // Send online message to friends + statusMessage := map[string]map[string]interface{}{ + helpers.ServerActionFriendStatusChange: { + "n": userName, + "s": 0, + }, } + u.sendToFriends(statusMessage) - //SUCCESS, SEND RESPONSE TO CLIENT - responseVal := make(map[string]interface{}) - responseVal["n"] = userName - responseVal["f"] = friends + // Login success, send response to client + var responseVal map[string]interface{} if rememberMe && len(autologPass) > 0 && remMe { - responseVal["ai"] = dbID - responseVal["ap"] = autologPass + responseVal = map[string]interface{}{ + "n": userName, + "f": friends, + "ai": dbID, + "ap": autologPass, + } + } else { + responseVal = map[string]interface{}{ + "n": userName, + "f": friends, + } } - clientResp := helpers.MakeClientResponse(helpers.ClientActionLogin, responseVal, helpers.NewError("", 0)) + clientResp := helpers.MakeClientResponse(helpers.ClientActionLogin, responseVal, helpers.NoError()) socket.WriteJSON(clientResp) // - return connID, helpers.NewError("", 0) + return connID, helpers.NoError() +} + +func makeFriendsResponse(friendsMap map[string]*database.Friend) []map[string]interface{} { + friends := make([]map[string]interface{}, len(friendsMap), len(friendsMap)) + i := 0; + for _, val := range friendsMap { + frs := val.RequestStatus() + friendEntry := map[string]interface{}{ + "n": val.Name(), + "rs": frs, + } + if frs == database.FriendStatusAccepted { + // Get the friend's status + if friend, ok := users[val.Name()]; ok { + friendEntry["s"] = friend.Status() + } else { + friendEntry["s"] = StatusOffline + } + } + friends[i] = friendEntry + i++; + } + return friends } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -289,18 +275,18 @@ func AutoLogIn(tag string, pass string, newPass string, dbID int, conn *websocke return "", helpers.NewError(errorServerPaused, helpers.ErrorServerPaused) } - //VERIFY AND GET USER NAME FROM DATABASE + // Verify and get user name from database userName, autoLogErr := database.AutoLoginClient(tag, pass, newPass, dbID) if autoLogErr.ID != 0 { return "", autoLogErr } - // + // Log user in connID, userErr := Login(userName, dbID, newPass, false, true, conn, connUser, clientMux) if userErr.ID != 0 { return "", userErr } - // - return connID, helpers.NewError("", 0) + + return connID, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -317,7 +303,7 @@ func (u *User) Logout(connID string) { connID = "1" } - //REMOVE USER FROM THEIR ROOM + // Remove user from their room u.mux.Lock() if _, ok := u.conns[connID]; !ok { u.mux.Unlock() @@ -326,30 +312,21 @@ func (u *User) Logout(connID string) { currRoom := (*u.conns[connID]).room if currRoom != nil && currRoom.Name() != "" { u.mux.Unlock() - currRoom.RemoveUser(u.name, connID) + currRoom.RemoveUser(u, connID) u.mux.Lock() } if len(u.conns) == 1 { - //SEND STATUS CHANGE TO FRIENDS - statusMessage := make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange] = make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["n"] = u.name - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["s"] = StatusOffline - for key, val := range u.friends { - if val.RequestStatus() == database.FriendStatusAccepted { - friend, friendErr := Get(key) - if friendErr == nil { - friend.mux.Lock() - for _, friendConn := range friend.conns { - (*friendConn).socket.WriteJSON(statusMessage) - } - friend.mux.Unlock() - } - } + // Send status change to friends + statusMessage := map[string]map[string]interface{}{ + helpers.ServerActionFriendStatusChange: { + "n": u.name, + "s": StatusOffline, + }, } + u.sendToFriends(statusMessage) } - //LOG USER OUT + // Log user out (*u.conns[connID]).clientMux.Lock() if *((*u.conns[connID]).user) != nil { *((*u.conns[connID]).user) = nil @@ -358,7 +335,7 @@ func (u *User) Logout(connID string) { socket := (*u.conns[connID]).socket delete(u.conns, connID) if len(u.conns) == 0 { - // DELETE THE USER IF THERE ARE NO MORE CONNS + // Delete user if there are no more conns u.mux.Unlock() usersMux.Lock() delete(users, u.name) @@ -367,11 +344,11 @@ func (u *User) Logout(connID string) { u.mux.Unlock() } - //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NewError("", 0)) + // Send response + clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) socket.WriteJSON(clientResp) - //RUN CALLBACK + // Run callback if LogoutCallback != nil { LogoutCallback(u.Name(), u.DatabaseID()) } @@ -381,56 +358,47 @@ func (u *User) Logout(connID string) { func (u *User) Kick() { u.mux.Lock() - //SEND STATUS CHANGE TO FRIENDS - statusMessage := make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange] = make(map[string]interface{}) - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["n"] = u.name - statusMessage[helpers.ServerActionFriendStatusChange].(map[string]interface{})["s"] = StatusOffline - for key, val := range u.friends { - if val.RequestStatus() == database.FriendStatusAccepted { - friend, friendErr := Get(key) - if friendErr == nil { - friend.mux.Lock() - for _, friendConn := range friend.conns { - (*friendConn).socket.WriteJSON(statusMessage) - } - friend.mux.Unlock() - } - } + // Send status change message to friends + statusMessage := map[string]map[string]interface{}{ + helpers.ServerActionFriendStatusChange: { + "n": u.name, + "s": StatusOffline, + }, } + u.sendToFriends(statusMessage) - //MAKE CLIENT RESPONSE - clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NewError("", 0)) + // Make response + clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) - //LOOP THROUGH CONNECTIONS + // Go through all connections for connID, conn := range u.conns { - //REMOVE CONNECTION FROM THEIR ROOM + // Remove from room currRoom := (*conn).room if currRoom != nil && currRoom.Name() != "" { u.mux.Unlock() - currRoom.RemoveUser(u.name, connID) + currRoom.RemoveUser(u, connID) u.mux.Lock() } - //LOG CONNECTION OUT + // Log connection out (*conn).clientMux.Lock() if *((*conn).user) != nil { *((*conn).user) = nil } (*conn).clientMux.Unlock() - //SEND RESPONSE + // Send response (*conn).socket.WriteJSON(clientResp) } u.mux.Unlock() - //REMOVE USER FROM users + // Remove from users usersMux.Lock() delete(users, u.name) usersMux.Unlock() - //RUN CALLBACK + // Run callback if LogoutCallback != nil { LogoutCallback(u.Name(), u.DatabaseID()) } @@ -440,9 +408,9 @@ func (u *User) Kick() { // GET A USER //////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// -// Get finds a logged in User by their name. Returns an error if the User is not online. -func Get(userName string) (*User, error) { - //REJECT INCORRECT INPUT +// GetUser finds a logged in User by their name. Returns an error if the User is not online. +func GetUser(userName string) (*User, error) { + // Verify input if len(userName) == 0 { return &User{}, errors.New("users.Get() requires a user name") } else if serverPaused { @@ -470,7 +438,7 @@ func Get(userName string) (*User, error) { // Join makes a User join a Room. If you are using MultiConnect in ServerSettings, the connID // parameter is the connection ID associated with one of the connections attached to that User. This must // be provided when making a User join a Room with MultiConnect enabled. Otherwise, an empty string can be used. -func (u *User) Join(r *rooms.Room, connID string) error { +func (u *User) Join(r *Room, connID string) error { if multiConnect && len(connID) == 0 { return errors.New("Must provide a connID when MultiConnect is enabled") } else if !multiConnect { @@ -486,19 +454,15 @@ func (u *User) Join(r *rooms.Room, connID string) error { u.mux.Unlock() return errors.New("User '" + u.name + "' is already in room '" + r.Name() + "'") } else if currRoom != nil && currRoom.Name() != "" { - //LEAVE USER'S CURRENT ROOM + // Leave current room u.mux.Unlock() u.Leave(connID) u.mux.Lock() } - roomPointer := &((*u.conns[connID]).room) - varsPointer := &((*u.conns[connID]).vars) - socketPointer := ((*u.conns[connID]).socket) - statusPointer := &u.status u.mux.Unlock() - //ADD USER TO DESIGNATED ROOM - addErr := r.AddUser(u.name, u.databaseID, u.isGuest, socketPointer, roomPointer, varsPointer, statusPointer, &u.mux, connID) + // Add user to room + addErr := r.AddUser(u, connID) if addErr != nil { return addErr } @@ -525,7 +489,7 @@ func (u *User) Leave(connID string) error { currRoom := (*u.conns[connID]).room u.mux.Unlock() if currRoom != nil && currRoom.Name() != "" { - removeErr := currRoom.RemoveUser(u.name, connID) + removeErr := currRoom.RemoveUser(u, connID) if removeErr != nil { return removeErr } @@ -543,29 +507,18 @@ func (u *User) Leave(connID string) error { // SetStatus sets the status of a User. Also sends a notification to all the User's Friends (with the request // status "accepted") that they changed their status. func (u *User) SetStatus(status int) { - u.mux.Lock() - friends := u.friends u.status = status u.mux.Unlock() - //SEND STATUS CHANGE MESSAGE TO User's FRIENDS WHOM ARE "ACCEPTED" - message := make(map[string]interface{}) - message[helpers.ServerActionFriendStatusChange] = make(map[string]interface{}) - message[helpers.ServerActionFriendStatusChange].(map[string]interface{})["n"] = u.name - message[helpers.ServerActionFriendStatusChange].(map[string]interface{})["s"] = status - for key, val := range friends { - if val.RequestStatus() == database.FriendStatusAccepted { - friend, friendErr := Get(key) - if friendErr == nil { - friend.mux.Lock() - for _, conn := range friend.conns { - (*conn).socket.WriteJSON(message) - } - friend.mux.Unlock() - } - } + // Send status to friends + message := map[string]map[string]interface{}{ + helpers.ServerActionFriendStatusChange: { + "n": u.name, + "s": status, + }, } + u.sendToFriends(message) } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -590,7 +543,7 @@ func (u *User) Invite(invUser *User, connID string) error { } currRoom := (*u.conns[connID]).room u.mux.Unlock() - rType := rooms.GetRoomTypes()[currRoom.Type()] + rType := GetRoomTypes()[currRoom.Type()] if currRoom == nil || currRoom.Name() == "" { return errors.New("The user '" + u.name + "' is not in a room") } else if !currRoom.IsPrivate() { @@ -601,19 +554,21 @@ func (u *User) Invite(invUser *User, connID string) error { return errors.New("Only the server can manipulate that type of room") } - //ADD TO INVITE LIST + // Add to invite list addErr := currRoom.AddInvite(invUser.name) if addErr != nil { return addErr } - //MAKE INVITE MESSAGE - invMessage := make(map[string]interface{}) - invMessage[helpers.ServerActionRoomInvite] = make(map[string]interface{}) - invMessage[helpers.ServerActionRoomInvite].(map[string]interface{})["u"] = u.name - invMessage[helpers.ServerActionRoomInvite].(map[string]interface{})["r"] = currRoom.Name() + // Make response message + invMessage := map[string]map[string]interface{}{ + helpers.ServerActionRoomInvite: { + "u": u.name, + "r": currRoom.Name(), + }, + } - //SEND MESSAGE + // Send response to all connections invUser.mux.Lock() for _, conn := range invUser.conns { (*conn).socket.WriteJSON(invMessage) @@ -646,7 +601,7 @@ func (u *User) RevokeInvite(revokeUser string, connID string) error { } currRoom := (*u.conns[connID]).room u.mux.Unlock() - rType := rooms.GetRoomTypes()[currRoom.Type()] + rType := GetRoomTypes()[currRoom.Type()] if currRoom == nil || currRoom.Name() == "" { return errors.New("The user '" + u.name + "' is not in a room") } else if !currRoom.IsPrivate() { @@ -657,7 +612,7 @@ func (u *User) RevokeInvite(revokeUser string, connID string) error { return errors.New("Only the server can manipulate that type of room") } - //REMOVE FROM INVITE LIST + // Remove from invite list removeErr := currRoom.RemoveInvite(revokeUser) if removeErr != nil { return removeErr @@ -671,6 +626,14 @@ func (u *User) RevokeInvite(revokeUser string, connID string) error { // User ATTRIBUTE READERS //////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// +// UserCount returns the number of Users logged into the server. +func UserCount() int { + usersMux.Lock() + length := len(users) + usersMux.Unlock() + return length +} + // Name gets the name of the User. func (u *User) Name() string { return u.name @@ -696,7 +659,7 @@ func (u *User) Friends() map[string]database.Friend { // RoomIn gets the Room that the User is currently in. A nil Room pointer means the User is not in a Room. If you are using MultiConnect in ServerSettings, the connID // parameter is the connection ID associated with one of the connections attached to that User. This must // be provided when getting a User's Room with MultiConnect enabled. Otherwise, an empty string can be used. -func (u *User) RoomIn(connID string) *rooms.Room { +func (u *User) RoomIn(connID string) *Room { if multiConnect && len(connID) == 0 { return nil } else if !multiConnect { @@ -738,76 +701,13 @@ func (u *User) IsGuest() bool { return u.isGuest } -////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER STARTUP FUNCTIONS ////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////////////////////////// - -// SetServerStarted is for Gopher Game Server internal mechanics only. -func SetServerStarted(val bool) { - if !serverStarted { - serverStarted = val - } -} - -// SettingsSet is for Gopher Game Server internal mechanics only. -func SettingsSet(kickDups bool, name string, deleteOnLeave bool, sqlFeat bool, remMe bool, multiConn bool) { - if !serverStarted { - kickOnLogin = kickDups - serverName = name - sqlFeatures = sqlFeat - rememberMe = remMe - multiConnect = multiConn - rooms.SettingsSet(name, deleteOnLeave, multiConn) - } -} - -////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Pause is only for internal Gopher Game Server mechanics. -func Pause() { - if !serverPaused { - serverPaused = true - - // - clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NewError("", 0)) - usersMux.Lock() - for _, user := range users { - (*user).mux.Lock() - for connID, conn := range (*user).conns { - //REMOVE CONNECTION FROM THEIR ROOM - currRoom := (*conn).room - if currRoom != nil && currRoom.Name() != "" { - (*user).mux.Unlock() - currRoom.RemoveUser((*user).name, connID) - (*user).mux.Lock() - } - - //LOG CONNECTION OUT - (*conn).clientMux.Lock() - if *((*conn).user) != nil { - *((*conn).user) = nil - } - (*conn).clientMux.Unlock() - - //SEND LOG OUT MESSAGE - (*conn).socket.WriteJSON(clientResp) - } - (*user).mux.Unlock() - } - users = make(map[string]*User) - usersMux.Unlock() - - // - serverStarted = false - } -} - -// Resume is only for internal Gopher Game Server mechanics. -func Resume() { - if serverPaused { - serverStarted = true - serverPaused = false +// ConnectionIDs returns a []string of all the User's connection IDs +func (u *User) ConnectionIDs() []string { + u.mux.Lock() + ids := make([]string, 0, len(u.conns)) + for id := range u.conns { + ids = append(ids, id) } -} + u.mux.Unlock() + return ids +} \ No newline at end of file diff --git a/users/vars.go b/core/vars.go similarity index 61% rename from users/vars.go rename to core/vars.go index 8ee6022..dd97e11 100644 --- a/users/vars.go +++ b/core/vars.go @@ -1,6 +1,7 @@ -package users +package core import ( + "errors" "github.com/hewiefreeman/GopherGameServer/helpers" ) @@ -20,21 +21,26 @@ func (u *User) SetVariable(key string, value interface{}, connID string) { } else if !multiConnect { connID = "1" } - //MAKE CLIENT MESSAGE - resp := make(map[string]interface{}) - resp["k"] = key - resp["v"] = value + // Set the variable u.mux.Lock() if _, ok := u.conns[connID]; !ok { u.mux.Unlock() return } (*u.conns[connID]).vars[key] = value - //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariable, resp, helpers.NewError("", 0)) - (*u.conns[connID]).socket.WriteJSON(clientResp) + socket := (*u.conns[connID]).socket u.mux.Unlock() + + //MAKE CLIENT MESSAGE + resp := map[string]interface{}{ + "k": key, + "v": value, + } + clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariable, resp, helpers.NoError()) + + //SEND RESPONSE TO CLIENT + socket.WriteJSON(clientResp) } // SetVariables sets all the specified User variables at once. The client API of the User will also receive these changes. If you are using MultiConnect in ServerSettings, the connID @@ -49,6 +55,8 @@ func (u *User) SetVariables(values map[string]interface{}, connID string) { } else if !multiConnect { connID = "1" } + + // Set the variables u.mux.Lock() if _, ok := u.conns[connID]; !ok { u.mux.Unlock() @@ -57,10 +65,13 @@ func (u *User) SetVariables(values map[string]interface{}, connID string) { for key, val := range values { (*u.conns[connID]).vars[key] = val } - //SEND RESPONSE TO CLIENT - clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariables, values, helpers.NewError("", 0)) - (*u.conns[connID]).socket.WriteJSON(clientResp) + socket := (*u.conns[connID]).socket u.mux.Unlock() + + //SEND RESPONSE TO CLIENT + clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariables, values, helpers.NoError()) + socket.WriteJSON(clientResp) + } // GetVariable gets one of the User's variables by it's key. If you are using MultiConnect in ServerSettings, the connID @@ -71,6 +82,7 @@ func (u *User) GetVariable(key string, connID string) interface{} { if len(key) == 0 { return nil } + u.mux.Lock() val := (*u.conns[connID]).vars[key] u.mux.Unlock() @@ -99,3 +111,82 @@ func (u *User) GetVariables(keys []string, connID string) map[string]interface{} // return value } + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ROOM VARIABLES ///////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// SetVariable sets a Room variable. +func (r *Room) SetVariable(key string, value interface{}) { + //REJECT INCORRECT INPUT + if len(key) == 0 { + return + } + + r.mux.Lock() + if r.usersMap == nil { + r.mux.Unlock() + return + } + r.vars[key] = value + r.mux.Unlock() + + // + return +} + +// SetVariables sets all the specified Room variables at once. +func (r *Room) SetVariables(values map[string]interface{}) { + r.mux.Lock() + if r.usersMap == nil { + r.mux.Unlock() + return + } + for key, val := range values { + r.vars[key] = val + } + r.mux.Unlock() + + // + return +} + +// GetVariable gets one of the Room's variables. +func (r *Room) GetVariable(key string) (interface{}, error) { + //REJECT INCORRECT INPUT + if len(key) == 0 { + return nil, errors.New("*Room.GetVariable() requires a key") + } + + r.mux.Lock() + if r.usersMap == nil { + r.mux.Unlock() + return nil, errors.New("Room '" + r.name + "' does not exist") + } + value := r.vars[key] + r.mux.Unlock() + + // + return value, nil +} + +// GetVariables gets all the specified (or all if not) Room variables as a map[string]interface{}. +func (r *Room) GetVariables(keys []string) (map[string]interface{}, error) { + var value map[string]interface{} = make(map[string]interface{}) + r.mux.Lock() + if r.usersMap == nil { + r.mux.Unlock() + return nil, errors.New("Room '" + r.name + "' does not exist") + } + if keys == nil || len(keys) == 0 { + value = r.vars + } else { + for i := 0; i < len(keys); i++ { + value[keys[i]] = r.vars[keys[i]] + } + } + r.mux.Unlock() + + // + return value, nil +} diff --git a/database/authentication.go b/database/authentication.go index 8c306d8..11db159 100644 --- a/database/authentication.go +++ b/database/authentication.go @@ -16,11 +16,16 @@ var ( customAccountInfoChangeRequirements map[string]struct{} = make(map[string]struct{}) customDeleteAccountRequirements map[string]struct{} = make(map[string]struct{}) - SignUpCallback func(string, map[string]interface{}) bool // SignUpCallback is only for internal Gopher Game Server mechanics. - LoginCallback func(string, int, map[string]interface{}, map[string]interface{}) bool // LoginCallback is only for internal Gopher Game Server mechanics. - DeleteAccountCallback func(string, int, map[string]interface{}, map[string]interface{}) bool // DeleteAccountCallback is only for internal Gopher Game Server mechanics. - AccountInfoChangeCallback func(string, int, map[string]interface{}, map[string]interface{}) bool // AccountInfoChangeCallback is only for internal Gopher Game Server mechanics. - PasswordChangeCallback func(string, int, map[string]interface{}, map[string]interface{}) bool // PasswordChangeCallback is only for internal Gopher Game Server mechanics. + // SignUpCallback is only for internal Gopher Game Server mechanics. + SignUpCallback func(string, map[string]interface{}) bool + // LoginCallback is only for internal Gopher Game Server mechanics. + LoginCallback func(string, int, map[string]interface{}, map[string]interface{}) bool + // DeleteAccountCallback is only for internal Gopher Game Server mechanics. + DeleteAccountCallback func(string, int, map[string]interface{}, map[string]interface{}) bool + // AccountInfoChangeCallback is only for internal Gopher Game Server mechanics. + AccountInfoChangeCallback func(string, int, map[string]interface{}, map[string]interface{}) bool + // PasswordChangeCallback is only for internal Gopher Game Server mechanics. + PasswordChangeCallback func(string, int, map[string]interface{}, map[string]interface{}) bool ) // Authentication error messages @@ -34,6 +39,7 @@ const ( errorInsufficientCols = "Insufficient custom column data" errorIncorrectLogin = "Incorrect login or password" errorInvalidAutoLog = "Invalid auto-login data" + errorNoShardFound = "Could not find a master or healthy replica database" ) ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -146,6 +152,10 @@ func checkCustomRequirements(customCols map[string]interface{}, requirements map return true } +////////////////////////////////////////////////////////////////////////////////////////////////////// +// QUERY HELPERS ///////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// // SIGN A USER UP //////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -176,11 +186,13 @@ func SignUpClient(userName string, password string, customCols map[string]interf return helpers.NewError(hashErr.Error(), helpers.ErrorAuthEncryption) } - var vals []interface{} = []interface{}{} + var vals []interface{} //CREATE PART 1 OF QUERY queryPart1 := "INSERT INTO " + tableUsers + " (" + usersColumnName + ", " + usersColumnPassword + ", " + if customCols != nil { + vals = make([]interface{}, 0, len(customCols)) if customLoginColumn != "" { if _, ok := customCols[customLoginColumn]; !ok { return helpers.NewError(errorInsufficientCols, helpers.ErrorAuthInsufficientCols) @@ -227,7 +239,7 @@ func SignUpClient(userName string, password string, customCols map[string]interf } // - return helpers.NewError("", 0) + return helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -252,23 +264,33 @@ func LoginClient(userName string, password string, deviceTag string, remMe bool, } //FIRST THREE ARE id, password, name IN THAT ORDER - var vals []interface{} = []interface{}{new(int), new([]byte), new(string)} + var vals []interface{} //CONSTRUCT SELECT QUERY selectQuery := "Select " + usersColumnID + ", " + usersColumnPassword + ", " + usersColumnName + ", " + if customCols != nil { + vals = make([]interface{}, 0, len(customCols)+3) + vals = append(vals, new(int), new([]byte), new(string)) for key := range customCols { selectQuery = selectQuery + key + ", " //MAINTAIN THE ORDER IN WHICH THE COLUMNS WERE DECLARED VIA A SLICE vals = append(vals, new(interface{})) } + } else { + vals = make([]interface{}, 0, 3) + vals = append(vals, new(int), new([]byte), new(string)) } + + // GET LOGIN COLUMN AND TABLE NAME + var loginCol string = usersColumnName + var tableName string = tableUsers if len(customLoginColumn) > 0 { - selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableUsers + " WHERE " + customLoginColumn + "=\"" + userName + "\" LIMIT 1;" - } else { - selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableUsers + " WHERE " + usersColumnName + "=\"" + userName + "\" LIMIT 1;" + loginCol = customLoginColumn } + selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableName + " WHERE " + loginCol + "=\"" + userName + "\" LIMIT 1;" + //EXECUTE SELECT QUERY checkRows, err := database.Query(selectQuery) if err != nil { @@ -318,13 +340,18 @@ func LoginClient(userName string, password string, deviceTag string, remMe bool, //MAKE AUTO-LOG ENTRY devicePass, devicePassErr = helpers.GenerateSecureString(32) if devicePassErr == nil { - database.Exec("INSERT INTO " + tableAutologs + " (" + autologsColumnID + ", " + autologsColumnDeviceTag + ", " + autologsColumnDevicePass + + _, exErr := database.Exec("INSERT INTO " + tableAutologs + " (" + autologsColumnID + ", " + autologsColumnDeviceTag + ", " + autologsColumnDevicePass + ") VALUES (" + strconv.Itoa(*dbIndex) + ", \"" + deviceTag + "\", \"" + devicePass + "\");") + if exErr != nil { + ////// LOG ERROR!!!!!! + } + } else { + ////// LOG ERROR!!!!!! } } // - return *uName, *dbIndex, devicePass, helpers.NewError("", 0) + return *uName, *dbIndex, devicePass, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -339,11 +366,14 @@ func AutoLoginClient(tag string, pass string, newPass string, dbID int) (string, if checkStringSQLInjection(tag) { return "", helpers.NewError(errorMaliciousChars, helpers.ErrorAuthMaliciousChars) } + //EXECUTE SELECT QUERY var dPass string - checkRows, err := database.Query("Select " + autologsColumnDevicePass + " FROM " + tableAutologs + " WHERE " + autologsColumnID + "=" + strconv.Itoa(dbID) + " AND " + + db := database + tableName := tableAutologs + checkRows, checkErr := db.Query("Select " + autologsColumnDevicePass + " FROM " + tableName + " WHERE " + autologsColumnID + "=" + strconv.Itoa(dbID) + " AND " + autologsColumnDeviceTag + "=\"" + tag + "\" LIMIT 1;") - if err != nil { + if checkErr != nil { return "", helpers.NewError(errorInvalidAutoLog, helpers.ErrorDatabaseInvalidAutolog) } // @@ -358,19 +388,21 @@ func AutoLoginClient(tag string, pass string, newPass string, dbID int) (string, if pass != dPass { //SOMEONE TRIED TO COMPROMISE THIS KEY PAIR. DELETE IT NOW. RemoveAutoLog(dbID, tag) + return "", helpers.NewError(errorInvalidAutoLog, helpers.ErrorDatabaseInvalidAutolog) } //UPDATE TO NEW PASS - _, updateErr := database.Exec("UPDATE " + tableAutologs + " SET " + autologsColumnDevicePass + "=\"" + newPass + "\" WHERE " + autologsColumnID + "=" + strconv.Itoa(dbID) + " AND " + + _, updateErr := db.Exec("UPDATE " + tableName + " SET " + autologsColumnDevicePass + "=\"" + newPass + "\" WHERE " + autologsColumnID + "=" + strconv.Itoa(dbID) + " AND " + autologsColumnDeviceTag + "=\"" + tag + "\" LIMIT 1;") if updateErr != nil { return "", helpers.NewError(errorInvalidAutoLog, helpers.ErrorDatabaseInvalidAutolog) } //EVERYTHING WENT WELL, GET THE User's NAME + tableName = tableUsers var userName string - userRows, err := database.Query("Select " + usersColumnName + " FROM " + tableUsers + " WHERE " + usersColumnID + "=" + strconv.Itoa(dbID) + " LIMIT 1;") - if err != nil { + userRows, usrErr := db.Query("Select " + usersColumnName + " FROM " + tableName + " WHERE " + usersColumnID + "=" + strconv.Itoa(dbID) + " LIMIT 1;") + if usrErr != nil { return "", helpers.NewError(errorInvalidAutoLog, helpers.ErrorDatabaseInvalidAutolog) } // @@ -387,7 +419,7 @@ func AutoLoginClient(tag string, pass string, newPass string, dbID int) (string, } // - return userName, helpers.NewError("", 0) + return userName, helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -427,18 +459,24 @@ func ChangePassword(userName string, password string, newPassword string, custom } //FIRST TWO ARE id, password IN THAT ORDER - var vals []interface{} = []interface{}{new(int), new([]byte)} - var valsList []interface{} = []interface{}{} + var vals []interface{} + var valsList []interface{} //CONSTRUCT SELECT QUERY selectQuery := "Select " + usersColumnID + ", " + usersColumnPassword + ", " if customCols != nil { + vals = make([]interface{}, 0, len(customCols)+2) + vals = append(vals, new(int), new([]byte)) + valsList = make([]interface{}, 0, len(customCols)) for key, val := range customCols { selectQuery = selectQuery + key + ", " //MAINTAIN THE ORDER IN WHICH THE COLUMNS WERE DECLARED VIA A SLICE vals = append(vals, new(interface{})) valsList = append(valsList, []interface{}{val, customAccountInfo[key].dataType, key}) } + } else { + vals = make([]interface{}, 0, 2) + vals = append(vals, new(int), new([]byte)) } selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableUsers + " WHERE " + usersColumnName + "=\"" + userName + "\" LIMIT 1;" @@ -495,7 +533,7 @@ func ChangePassword(userName string, password string, newPassword string, custom } // - return helpers.NewError("", 0) + return helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -520,18 +558,24 @@ func ChangeAccountInfo(userName string, password string, customCols map[string]i } //FIRST TWO ARE id, password IN THAT ORDER - var vals []interface{} = []interface{}{new(int), new([]byte)} - var valsList []interface{} = []interface{}{} + var vals []interface{} + var valsList []interface{} //CONSTRUCT SELECT QUERY selectQuery := "Select " + usersColumnID + ", " + usersColumnPassword + ", " if customCols != nil { + vals = make([]interface{}, 0, len(customCols)+2) + vals = append(vals, new(int), new([]byte)) + valsList = make([]interface{}, 0, len(customCols)) for key, val := range customCols { selectQuery = selectQuery + key + ", " //MAINTAIN THE ORDER IN WHICH THE COLUMNS WERE DECLARED VIA A SLICE vals = append(vals, new(interface{})) valsList = append(valsList, []interface{}{val, customAccountInfo[key].dataType, key}) } + } else { + vals = make([]interface{}, 0, 2) + vals = append(vals, new(int), new([]byte)) } selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableUsers + " WHERE " + usersColumnName + "=\"" + userName + "\" LIMIT 1;" @@ -596,7 +640,7 @@ func ChangeAccountInfo(userName string, password string, customCols map[string]i } // - return helpers.NewError("", 0) + return helpers.NoError() } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -619,16 +663,21 @@ func DeleteAccount(userName string, password string, customCols map[string]inter } //FIRST TWO ARE id, password IN THAT ORDER - var vals []interface{} = []interface{}{new(int), new([]byte)} + var vals []interface{} //CONSTRUCT SELECT QUERY selectQuery := "Select " + usersColumnID + ", " + usersColumnPassword + ", " if customCols != nil { + vals = make([]interface{}, 0, len(customCols)+2) + vals = append(vals, new(int), new([]byte)) for key := range customCols { selectQuery = selectQuery + key + ", " //MAINTAIN THE ORDER IN WHICH THE COLUMNS WERE DECLARED VIA A SLICE vals = append(vals, new(interface{})) } + } else { + vals = make([]interface{}, 0, 2) + vals = append(vals, new(int), new([]byte)) } selectQuery = selectQuery[0:len(selectQuery)-2] + " FROM " + tableUsers + " WHERE " + usersColumnName + "=\"" + userName + "\" LIMIT 1;" @@ -682,5 +731,5 @@ func DeleteAccount(userName string, password string, customCols map[string]inter } // - return helpers.NewError("", 0) + return helpers.NoError() } diff --git a/database/database.go b/database/database.go index 2b4fc18..396513f 100644 --- a/database/database.go +++ b/database/database.go @@ -17,11 +17,11 @@ var ( database *sql.DB //SERVER SETTINGS - serverStarted bool = false - serverPaused bool = false - rememberMe bool = false - databaseName string - inited bool = false + serverStarted bool = false + serverPaused bool = false + rememberMe bool = false + databaseName string = "gopherDB" + inited bool = false ) //TABLE & COLUMN NAMES @@ -77,24 +77,25 @@ func Init(userName string, password string, dbName string, protocol string, ip s var err error //OPEN THE DATABASE - var openErr error - database, openErr = sql.Open("mysql", userName+":"+password+"@"+protocol+"("+ip+":"+strconv.Itoa(port)+")/"+dbName) + database, err = sql.Open("mysql", userName+":"+password+"@"+protocol+"("+ip+":"+strconv.Itoa(port)+")/"+dbName) if err != nil { - return openErr + return err } //NOTE: Open doesn't open a connection. //MUST PING TO CHECK IF FOUND DATABASE - pingErr := database.Ping() - if pingErr != nil { + err = database.Ping() + if err != nil { return errors.New("Could not connect to database!") } - databaseName = dbName + if len(dbName) != 0 { + databaseName = dbName + } //CONFIGURE DATABASE - setupErr := setUp() - if setupErr != nil { - return setupErr + err = setUp() + if err != nil { + return err } // diff --git a/database/friending.go b/database/friending.go index 7e25447..d431f91 100644 --- a/database/friending.go +++ b/database/friending.go @@ -99,30 +99,34 @@ func GetFriends(userIndex int) (map[string]*Friend, error) { //EXECUTE SELECT QUERY friendRows, friendRowsErr := database.Query("Select " + friendsColumnFriend + ", " + friendsColumnStatus + " FROM " + tableFriends + " WHERE " + friendsColumnUser + "=" + strconv.Itoa(userIndex) + ";") if friendRowsErr != nil { - return friends, friendRowsErr + return nil, friendRowsErr } - defer friendRows.Close() // for friendRows.Next() { var friendName string var friendID int var friendStatus int if scanErr := friendRows.Scan(&friendID, &friendStatus); scanErr != nil { - return friends, scanErr + friendRows.Close() + return nil, scanErr } // friendInfoRows, friendInfoErr := database.Query("Select " + usersColumnName + " FROM " + tableUsers + " WHERE " + usersColumnID + "=" + strconv.Itoa(friendID) + " LIMIT 1;") if friendInfoErr != nil { - return friends, friendInfoErr + friendRows.Close() + return nil, friendInfoErr } - defer friendInfoRows.Close() friendInfoRows.Next() if scanErr := friendInfoRows.Scan(&friendName); scanErr != nil { - return friends, scanErr + friendRows.Close() + friendInfoRows.Close() + return nil, scanErr } + friendInfoRows.Close() aFriend := Friend{name: friendName, dbID: friendID, status: friendStatus} friends[friendName] = &aFriend } + friendRows.Close() // return friends, nil } diff --git a/database/setup.go b/database/setup.go index 242e544..5c37215 100644 --- a/database/setup.go +++ b/database/setup.go @@ -5,148 +5,160 @@ import ( "strconv" ) -// CONFIGURES THE DATABASE FOR Gopher Game Server USAGE +// Configures the SQL database for Gopher Game Server func setUp() error { - //CHECK IF THE TABLE users HAS BEEN MADE + // Check if the users table has been created _, checkErr := database.Exec("SELECT " + usersColumnName + " FROM " + tableUsers + " WHERE " + usersColumnID + "=1;") if checkErr != nil { - fmt.Println("Making all tables...") - //MAKE THE users TABLE QUERY - createQuery := "CREATE TABLE " + tableUsers + " (" + - usersColumnID + " INTEGER NOT NULL AUTO_INCREMENT, " + - usersColumnName + " VARCHAR(255) UNIQUE NOT NULL, " + - usersColumnPassword + " VARCHAR(255) NOT NULL, " - - //APPEND custom AccountInfoColumn ITEMS - for key, val := range customAccountInfo { - createQuery = createQuery + key + " " + dataTypes[val.dataType] - //CHECK IF NEEDS maxSize/precision - if isSizeDataType(val.dataType) { - createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ")" - } else if isPrecisionDataType(val.dataType) { - createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" - } - //CHECK IF UNIQUE - if val.unique { - createQuery = createQuery + " UNIQUE" - } - //CHECK IF NOT NULL - if val.notNull { - createQuery = createQuery + " NOT NULL, " - } else { - createQuery = createQuery + ", " + fmt.Println("Creating \"" + tableUsers + "\" table...") + // Make the users table + if cErr := createUserTableSQL(); cErr != nil { + return cErr + } + } + // Check for new custom AccountInfoColumn items + if newItemsErr := addNewCustomItemsSQL(); newItemsErr != nil { + return newItemsErr + } + + if rememberMe { + // Check if autologs table has been made + _, checkErr := database.Exec("SELECT " + autologsColumnID + " FROM " + tableAutologs + " WHERE " + autologsColumnID + "=1;") + if checkErr != nil { + fmt.Println("Making autologs table...") + if cErr := createAutologsTableSQL(); cErr != nil { + return cErr } } + } + // Make sure customLoginColumn is unique if it is set + if len(customLoginColumn) > 0 { + _, alterErr := database.Exec("ALTER TABLE " + tableUsers + " ADD UNIQUE (" + customLoginColumn + ");") + if alterErr != nil { + return alterErr + } + } - createQuery = createQuery + "PRIMARY KEY (" + usersColumnID + "));" + // + return nil +} - //EXECUTE users TABLE QUERY - _, createErr := database.Exec(createQuery) - if createErr != nil { - return createErr - } +func createUserTableSQL() error { + createQuery := "CREATE TABLE " + tableUsers + " (" + + usersColumnID + " INTEGER NOT NULL AUTO_INCREMENT, " + + usersColumnName + " VARCHAR(255) UNIQUE NOT NULL, " + + usersColumnPassword + " VARCHAR(255) NOT NULL, " - //ADJUST AUTO_INCREMENT TO 1 - _, adjustErr := database.Exec("ALTER TABLE " + tableUsers + " AUTO_INCREMENT=1;") - if adjustErr != nil { - return adjustErr + // Append custom AccountInfoColumn items + for key, val := range customAccountInfo { + createQuery = createQuery + key + " " + dataTypes[val.dataType] + // Check for maxSize/precision + if isSizeDataType(val.dataType) { + createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ")" + } else if isPrecisionDataType(val.dataType) { + createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" } - - //MAKE THE friends TABLE - _, friendsErr := database.Exec("CREATE TABLE " + tableFriends + " (" + - friendsColumnUser + " INTEGER NOT NULL, " + - friendsColumnFriend + " INTEGER NOT NULL, " + - friendsColumnStatus + " INTEGER NOT NULL, " + - "FOREIGN KEY(" + friendsColumnUser + ") REFERENCES " + tableUsers + "(" + usersColumnID + "), " + - "FOREIGN KEY(" + friendsColumnFriend + ") REFERENCES " + tableUsers + "(" + usersColumnID + ")" + - ");") - if friendsErr != nil { - return friendsErr + // Check for unique + if val.unique { + createQuery = createQuery + " UNIQUE" } + // Check for not-null + if val.notNull { + createQuery = createQuery + " NOT NULL, " + } else { + createQuery = createQuery + ", " + } + } - if rememberMe { - //MAKE THE autologs TABLE - _, friendsErr = database.Exec("CREATE TABLE " + tableAutologs + " (" + - autologsColumnID + " INTEGER NOT NULL, " + - autologsColumnDevicePass + " VARCHAR(255) NOT NULL, " + - autologsColumnDeviceTag + " VARCHAR(255) NOT NULL, " + - ");") - if friendsErr != nil { - return friendsErr - } + createQuery = createQuery + "PRIMARY KEY (" + usersColumnID + "));" + + // Execute users table query + _, createErr := database.Exec(createQuery) + if createErr != nil { + return createErr + } + + // Adjust auto-increment to 1 + _, adjustErr := database.Exec("ALTER TABLE " + tableUsers + " AUTO_INCREMENT=1;") + if adjustErr != nil { + return adjustErr + } + + // Make friends table + if _, friendsErr := database.Exec("CREATE TABLE " + tableFriends + " (" + + friendsColumnUser + " INTEGER NOT NULL, " + + friendsColumnFriend + " INTEGER NOT NULL, " + + friendsColumnStatus + " INTEGER NOT NULL" + + ");"); friendsErr != nil { + + return friendsErr + } + + if rememberMe { + if cErr := createAutologsTableSQL(); cErr != nil { + return cErr } + } + + return nil +} - } else { - //CHECK IF THERE ARE ANY NEW custom AccountInfoColumn ITEMS - query := "ALTER TABLE " + tableUsers + " " - var execQuery bool = false +func createAutologsTableSQL() error { + if _, aErr := database.Exec("CREATE TABLE " + tableAutologs + " (" + + autologsColumnID + " INTEGER NOT NULL, " + + autologsColumnDevicePass + " VARCHAR(255) NOT NULL, " + + autologsColumnDeviceTag + " VARCHAR(255) NOT NULL, " + + ");"); aErr != nil { + + return aErr + } + return nil +} + +func addNewCustomItemsSQL() error { + query := "ALTER TABLE " + tableUsers + " " + var execQuery bool + // + for key, val := range customAccountInfo { + // Check if item exists + checkRows, err := database.Query("SHOW COLUMNS FROM " + tableUsers + " LIKE '" + key + "';") + if err != nil { + return err + } // - for key, val := range customAccountInfo { - //CHECK IF COLUMN EXISTS - checkRows, err := database.Query("SHOW COLUMNS FROM " + tableUsers + " LIKE '" + key + "';") - if err != nil { - return err - } - // - checkRows.Next() - _, colsErr := checkRows.Columns() - if colsErr != nil { - fmt.Println("Adding AccountInfoColumn '" + key + "'...") - //THIS customAccountInfo COLUMN DOES NOT EXIST... YET, MY NERD. - query = query + "ADD COLUMN " + key + " " + dataTypes[val.dataType] - if isSizeDataType(val.dataType) { - query = query + "(" + strconv.Itoa(val.maxSize) + ")" - } else if isPrecisionDataType(val.dataType) { - query = query + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" - } - //CHECK IF UNIQUE - if val.unique { - query = query + " UNIQUE" - } - //CHECK IF NOT NULL - if val.notNull { - query = query + " NOT NULL, " - } else { - query = query + ", " - } - execQuery = true + checkRows.Next() + _, colsErr := checkRows.Columns() + if colsErr != nil { + // The item doesn't exist yet... + fmt.Println("Adding AccountInfoColumn '" + key + "'...") + query = query + "ADD COLUMN " + key + " " + dataTypes[val.dataType] + if isSizeDataType(val.dataType) { + query = query + "(" + strconv.Itoa(val.maxSize) + ")" + } else if isPrecisionDataType(val.dataType) { + query = query + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" } - checkRows.Close() - } - if execQuery { - //MAKE THE NEW COLUMNS - query = query[0:len(query)-2] + ";" - _, colsErr := database.Exec(query) - if colsErr != nil { - return colsErr + // Unique check + if val.unique { + query = query + " UNIQUE" } - } - - if rememberMe { - //CHECK IF THE autologs TABLE HAS BEEN MADE - _, checkErr := database.Exec("SELECT " + autologsColumnID + " FROM " + tableAutologs + " WHERE " + autologsColumnID + "=1;") - if checkErr != nil { - fmt.Println("Making autologs table...") - //MAKE THE autologs TABLE - _, friendsErr := database.Exec("CREATE TABLE " + tableAutologs + " (" + - autologsColumnID + " INTEGER NOT NULL, " + - autologsColumnDevicePass + " VARCHAR(255) NOT NULL, " + - autologsColumnDeviceTag + " VARCHAR(255) NOT NULL" + - ");") - if friendsErr != nil { - return friendsErr - } + // Not-null check + if val.notNull { + query = query + " NOT NULL, " + } else { + query = query + ", " } + execQuery = true } + checkRows.Close() } - //MAKE SURE THE customLoginColumn IS UNIQUE IF SET - if len(customLoginColumn) > 0 { - _, alterErr := database.Exec("ALTER TABLE " + tableUsers + " ADD UNIQUE (" + customLoginColumn + ");") - if alterErr != nil { - return alterErr + if execQuery { + // Make new columns + query = query[0:len(query)-2] + ";" + _, colsErr := database.Exec(query) + if colsErr != nil { + return colsErr } } - // return nil -} +} \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/docs/css/nav.css b/docs/css/nav.css new file mode 100644 index 0000000..8fbdd1b --- /dev/null +++ b/docs/css/nav.css @@ -0,0 +1,41 @@ +.nav { + display: block; + position: absolute; + + top: 0px; + right: 0px; + + height: 40px; + min-width: 320px; + + text-align: right; + + margin-right: 10px; + + overflow: hidden; +} + +.navButton { + display: inline-block; + position: relative; + + height: 30px; + + line-height: 30px; + text-align: center; + vertical-align: center; + align-self: right; + + padding-left: 5px; + padding-right: 5px; + margin-right: 3px; + + color: #ffffff; + background: #3f3f3f; + box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4); + + border-bottom: 3px solid #85d681; + border-radius: 0px 0px 2px 2px; + + cursor: pointer; +} \ No newline at end of file diff --git a/docs/css/shell.css b/docs/css/shell.css new file mode 100644 index 0000000..c59189c --- /dev/null +++ b/docs/css/shell.css @@ -0,0 +1,279 @@ +body { + font-family: 'Open Sans', sans-serif; + font-weight: 600; + font-size: 16px; + color: #ffffff; + + background: #e3e3e3; + + margin: 0px; + padding: 0px; + + overflow: scroll; +} + +br { + margin: 0px; + padding: 0px; +} + +p { + margin: 0px; + padding: 0px; +} + +a { + color: #ffffff; + text-decoration: none; +} + +a:link { + color: #ffffff; + text-decoration: none; +} + +a:hover { + color: #dedede; + text-decoration: underline; +} + +a:active { + color: #ffffff; + text-decoration: none; +} + +h1 { + font-family: 'Open Sans', sans-serif; + font-weight: 700; + font-size: 36px; + line-height: 36px; + + margin: 0px; + padding: 0px; + + color: #85d681; +} + +h2 { + font-family: 'Open Sans', sans-serif; + font-weight: 600; + font-size: 30px; + + margin: 0px; + padding: 0px; +} + +h3 { + font-family: 'Open Sans', sans-serif; + font-weight: 650; + font-size: 24px; + line-height: 24px; + + margin-top: 5px; + margin-bottom: 0px; + padding: 0px; + + color: #3f3f3f; +} + +h4 { + font-family: 'Open Sans', sans-serif; + font-weight: 650; + font-size: 16px; + line-height: 16px; + + margin: 0px; + padding: 0px; + + color: #3f3f3f; +} + +h5 { + font-family: 'Open Sans', sans-serif; + font-weight: 600; + font-size: 12px; + line-height: 12px; + + margin-top: 5px; + margin-bottom: 0px; + padding: 0px; + + color: #ffffff; +} + +.wrapper { + display: block; + position: absolute; + + text-align: center; + + top: 0px; + left: 0px; + + min-width: 310px; + width: 100%; + + background: rgba(255,255,255,0); + + z-index: 2; +} + +.shadowWrapper { + display: block; + position: fixed; + + text-align: center; + + top: 0px; + left: 0px; + + width: 100%; + height: 100%; + + overflow: hidden; + + z-index: 1; +} + +.shadowObj { + display: block; + + width: 100%; + max-width: 1000px; + height: 100%; + + background: #ECFFEB; + + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.4); +} + +.innerWrapper { + display: block; + + width: 100%; + max-width: 1000px; + + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: stretch; +} + +.topBar { + display: block; + position: relative; + + height: 200px; + width: 100%; + + /*background-image: url("../images/bg.jpg");*/ + background-color: #7a7a7a; + + border-bottom: 3px solid #85d681; + + z-index: 6; +} + +.content { + display: block; + position: relative; + + width: 100%; + min-height: 100px; + height: 10px; + + background: #ECFFEB; + + overflow: hidden; +} + +.logoHolder { + display: block; + position: absolute; + + bottom: -30px; + left: 0px; + + width: 230px; + height: 230px; + + background-image: url('../images/icon_large.png'); + background-repeat: no-repeat; + background-position: 0px -30px; + background-size: 100%; +} + +.titleHolder { + display: block; + position: absolute; + + bottom: 0px; + left: 220px; + + margin-top: 10px; + + width: 180px; + height: 160px; + + vertical-align: center; + text-align: center; +} + +.titleGhub { + display: block; + position: relative; + + height: 50px; + + margin-top: 10px; +} + +.headerBtnsHolder { + display: block; + position: absolute; + + vertical-align: center; + line-height: 155px; + + bottom: 0px; + right: 0px; + left: 410px; + + height: 140px; + + white-space: nowrap; +} + +.headerBtn { + display: inline-block; + position: relative; + + width: 100px; + height: 140px; + + margin-right: 20px; + + align-self: center; +} + +.headerBtnImage { + display: block; + position: relative; + + width: 100px; + height: 100px; + + background-repeat: no-repeat; + background-size: 100%; +} + +.filler { + display: block; + position: absolute; + + top: 0px; + left: 0px; + + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/docs/images/GitHub_Dark.png b/docs/images/GitHub_Dark.png new file mode 100644 index 0000000..ea6ff54 Binary files /dev/null and b/docs/images/GitHub_Dark.png differ diff --git a/docs/images/GitHub_Light.png b/docs/images/GitHub_Light.png new file mode 100644 index 0000000..192846a Binary files /dev/null and b/docs/images/GitHub_Light.png differ diff --git a/docs/images/Gopher Title.png b/docs/images/Gopher Title.png new file mode 100644 index 0000000..f59ceb9 Binary files /dev/null and b/docs/images/Gopher Title.png differ diff --git a/docs/images/icon.png b/docs/images/icon.png new file mode 100644 index 0000000..46b83c7 Binary files /dev/null and b/docs/images/icon.png differ diff --git a/Server Gopher.png b/docs/images/icon_large.png similarity index 100% rename from Server Gopher.png rename to docs/images/icon_large.png diff --git a/docs/images/minimal_code_ex.png b/docs/images/minimal_code_ex.png new file mode 100644 index 0000000..a0268c6 Binary files /dev/null and b/docs/images/minimal_code_ex.png differ diff --git a/docs/images/running_ex.png b/docs/images/running_ex.png new file mode 100644 index 0000000..ab152fb Binary files /dev/null and b/docs/images/running_ex.png differ diff --git a/docs/images/wiki_img.png b/docs/images/wiki_img.png new file mode 100644 index 0000000..2819403 Binary files /dev/null and b/docs/images/wiki_img.png differ diff --git a/docs/images/zip_file.png b/docs/images/zip_file.png new file mode 100644 index 0000000..5143a05 Binary files /dev/null and b/docs/images/zip_file.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..cded8e9 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,61 @@ + + + Gopher Game Server + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+

Gopher

+

Game Server

+
+

Source on GitHub

+
+ +
+ + +
+
+
+ +
+
+
+
+ + + + diff --git a/docs/js/init.js b/docs/js/init.js new file mode 100644 index 0000000..f5d75d6 --- /dev/null +++ b/docs/js/init.js @@ -0,0 +1,20 @@ +window.addEventListener("load", init); +var download_nav, + download_btn, + wiki_nav, + wiki_btn, + logo, + title; + +function init() { + download_nav = document.getElementById("downloadNav"); + download_btn = document.getElementById("downloadBtn"); + wiki_nav = document.getElementById("wikiNav"); + wiki_btn = document.getElementById("wikiBtn"); + logo = document.getElementById("logo"); + title = document.getElementById("title"); + + window.addEventListener("resize", resized); + // + resize_init(); +} \ No newline at end of file diff --git a/docs/js/resize.js b/docs/js/resize.js new file mode 100644 index 0000000..fc2d9be --- /dev/null +++ b/docs/js/resize.js @@ -0,0 +1,88 @@ +var download_btn_hidden = false, + download_btn_switch = 520, + wiki_btn_hidden = false, + wiki_btn_switch = 640, + logo_resize = false, + logo_switch = 350; + +function resized(e) { + // Resize and replace download button + if (document.documentElement.clientWidth <= download_btn_switch && !download_btn_hidden) { + download_nav.style.display = "inline-block"; + download_btn.style.display = "none"; + + download_btn_hidden = true; + } else if (document.documentElement.clientWidth > download_btn_switch && download_btn_hidden) { + download_nav.style.display = "none"; + download_btn.style.display = "inline-block"; + + download_btn_hidden = false; + } + + // Resize and replace wiki button + if (document.documentElement.clientWidth <= wiki_btn_switch && !wiki_btn_hidden) { + wiki_nav.style.display = "inline-block"; + wiki_btn.style.display = "none"; + + wiki_btn_hidden = true; + } else if (document.documentElement.clientWidth > wiki_btn_switch && wiki_btn_hidden) { + wiki_nav.style.display = "none"; + wiki_btn.style.display = "inline-block"; + + wiki_btn_hidden = false; + } + + // Resize logo + if (document.documentElement.clientWidth <= download_btn_switch) { + logo_resize = true; + logo.style.width = (document.documentElement.clientWidth/2)+"px"; + logo.style.height = (document.documentElement.clientWidth/2)+"px"; + title.style.left = ((document.documentElement.clientWidth/2)-30)+"px"; + } else if (document.documentElement.clientWidth > download_btn_switch && logo_resize) { + logo.style.width = "250px"; + logo.style.height = "250px"; + title.style.left = "220px"; + logo_resize = false; + } +} + +function resize_init() { + // Resize and replace download button + if (document.documentElement.clientWidth <= download_btn_switch) { + download_nav.style.display = "inline-block"; + download_btn.style.display = "none"; + + download_btn_hidden = true; + } else if (document.documentElement.clientWidth > download_btn_switch) { + download_nav.style.display = "none"; + download_btn.style.display = "inline-block"; + + download_btn_hidden = false; + } + + // Resize and replace wiki button + if (document.documentElement.clientWidth <= wiki_btn_switch) { + wiki_nav.style.display = "inline-block"; + wiki_btn.style.display = "none"; + + wiki_btn_hidden = true; + } else if (document.documentElement.clientWidth > wiki_btn_switch) { + wiki_nav.style.display = "none"; + wiki_btn.style.display = "inline-block"; + + wiki_btn_hidden = false; + } + + // Resize logo + if (document.documentElement.clientWidth <= download_btn_switch) { + logo_resize = true; + logo.style.width = (document.documentElement.clientWidth/2)+"px"; + logo.style.height = (document.documentElement.clientWidth/2)+"px"; + title.style.left = ((document.documentElement.clientWidth/2)-30)+"px"; + } else if (document.documentElement.clientWidth > download_btn_switch) { + logo.style.width = "250px"; + logo.style.height = "250px"; + title.style.left = "220px"; + logo_resize = false; + } +} \ No newline at end of file diff --git a/helpers/clientResponses.go b/helpers/clientResponses.go index b14ed93..5d0ea18 100644 --- a/helpers/clientResponses.go +++ b/helpers/clientResponses.go @@ -1,7 +1,5 @@ package helpers -import () - //BUILT-IN CLIENT ACTION/RESPONSE MESSAGE TYPES const ( ClientActionSignup = "s" @@ -50,21 +48,29 @@ const ( ServerActionSetAutoLoginPass = "ap" ServerActionAutoLoginFailed = "af" ServerActionAutoLoginNotFiled = "ai" + ServerActionWebRTCOffer = "wo" ) // MakeClientResponse is used for Gopher Game Server inner mechanics only. -func MakeClientResponse(action string, responseVal interface{}, err GopherError) map[string]interface{} { - var response map[string]interface{} = make(map[string]interface{}) +func MakeClientResponse(action string, responseVal interface{}, err GopherError) map[string]map[string]interface{} { + var response map[string]map[string]interface{} if err.ID != 0 { - response[ServerActionClientActionResponse] = make(map[string]interface{}) - response[ServerActionClientActionResponse].(map[string]interface{})["a"] = action - response[ServerActionClientActionResponse].(map[string]interface{})["e"] = make(map[string]interface{}) - response[ServerActionClientActionResponse].(map[string]interface{})["e"].(map[string]interface{})["m"] = err.Message - response[ServerActionClientActionResponse].(map[string]interface{})["e"].(map[string]interface{})["id"] = err.ID + response = map[string]map[string]interface{}{ + ServerActionClientActionResponse: { + "a": action, + "e": map[string]interface{}{ + "m": err.Message, + "id": err.ID, + }, + }, + } } else { - response[ServerActionClientActionResponse] = make(map[string]interface{}) - response[ServerActionClientActionResponse].(map[string]interface{})["a"] = action - response[ServerActionClientActionResponse].(map[string]interface{})["r"] = responseVal + response = map[string]map[string]interface{}{ + ServerActionClientActionResponse: { + "a": action, + "r": responseVal, + }, + } } // diff --git a/helpers/errors.go b/helpers/errors.go index b96f7c2..6938e07 100644 --- a/helpers/errors.go +++ b/helpers/errors.go @@ -1,7 +1,5 @@ package helpers -import () - // GopherError is used when sending an error message to the client API. type GopherError struct { Message string @@ -10,63 +8,68 @@ type GopherError struct { // Client response message error IDs const ( - ErrorGopherInvalidAction = iota + 1001 // Invalid client action - ErrorGopherIncorrectFormat // The client's data is not a map/object - ErrorGopherIncorrectCustomAction // Incorrect custom client action type - ErrorGopherNotLoggedIn // The client must be logged in to take action - ErrorGopherLoggedIn // The client must be logged out to take action - ErrorGopherStatusChange // Error while changing User's status - ErrorGopherFeatureDisabled // The feature required for the action is disabled - ErrorGopherColumnsFormat // The client's custom columns data is not a map/object - ErrorGopherNameFormat // The client's user name data is not a string - ErrorGopherPasswordFormat // The client's password data is not a string - ErrorGopherRememberFormat // The client's remember-me data is not a boolean - ErrorGopherGuestFormat // The client's guest data is not a boolean - ErrorGopherNewPasswordFormat - ErrorGopherRoomNameFormat - ErrorGopherRoomTypeFormat - ErrorGopherPrivateFormat - ErrorGopherMaxRoomFormat - ErrorGopherRoomControl - ErrorGopherServerRoom - ErrorGopherNotOwner - ErrorGopherLogin - ErrorGopherSignUp - ErrorGopherJoin - ErrorGopherLeave - ErrorGopherCreateRoom - ErrorGopherDeleteRoom - ErrorGopherInvite - ErrorGopherRevokeInvite - ErrorGopherFriendRequest - ErrorGopherFriendAccept - ErrorGopherFriendDecline - ErrorGopherFriendRemove + ErrorGopherInvalidAction = iota + 1001 // 1001. Invalid client action + ErrorGopherIncorrectFormat // 1002. The client's data is not a map/object + ErrorGopherIncorrectCustomAction // 1003. Incorrect custom client action type + ErrorGopherNotLoggedIn // 1004. The client must be logged in to take action + ErrorGopherLoggedIn // 1005. The client must be logged out to take action + ErrorGopherStatusChange // 1006. Error while changing User's status + ErrorGopherFeatureDisabled // 1007. A server feature must be explicitly enabled to take action + ErrorGopherColumnsFormat // 1008. The client's custom columns data is not a map/object + ErrorGopherNameFormat // 1009. The client's user name data is not a string + ErrorGopherPasswordFormat // 1010. The client's password data is not a string + ErrorGopherRememberFormat // 1011. The client's remember-me data is not a boolean + ErrorGopherGuestFormat // 1012. The client's guest data is not a boolean + ErrorGopherNewPasswordFormat // 1013. The client's new password data is not a string + ErrorGopherRoomNameFormat // 1014. The client's room name data is not a string + ErrorGopherRoomTypeFormat // 1015. The client's room type data is not a string + ErrorGopherPrivateFormat // 1016. The client's private room data is not a boolean + ErrorGopherMaxRoomFormat // 1017. The client's maximum room capacity data is not an integer + ErrorGopherRoomControl // 1018. Clients do not have the ability to control rooms + ErrorGopherServerRoom // 1019. The room type specified can only be made by the server + ErrorGopherNotOwner // 1020. The client must be the owner of the room to take action + ErrorGopherLogin // 1021. There was an error logging in + ErrorGopherSignUp // 1022. There was an error signing up + ErrorGopherJoin // 1023. There was an error joining a room + ErrorGopherLeave // 1024. There was an error leaving a room + ErrorGopherCreateRoom // 1025. There was an error creating a room + ErrorGopherDeleteRoom // 1026. There was an error deleting a room + ErrorGopherInvite // 1027. There was an error inviting User to a room + ErrorGopherRevokeInvite // 1028. There was an error revoking a User's invitation to a room + ErrorGopherFriendRequest // 1029. There was an error sending a friend request + ErrorGopherFriendAccept // 1030. There was an error accepting a friend request + ErrorGopherFriendDecline // 1031. There was an error declining a friend request + ErrorGopherFriendRemove // 1032. There was an error removing a friend // Authentication - ErrorAuthUnexpected - ErrorAuthAlreadyLogged - ErrorAuthRequiredName - ErrorAuthRequiredPass - ErrorAuthRequiredNewPass - ErrorAuthRequiredID - ErrorAuthRequiredSocket - ErrorAuthNameUnavail - ErrorAuthMaliciousChars - ErrorAuthIncorrectCols - ErrorAuthInsufficientCols - ErrorAuthEncryption - ErrorAuthQuery - ErrorAuthIncorrectLogin - ErrorDatabaseInvalidAutolog - ErrorAuthConversion + ErrorAuthUnexpected // 1033. There was an unexpected authorization error + ErrorAuthAlreadyLogged // 1034. The client is already logged in + ErrorAuthRequiredName // 1035. A user name is required + ErrorAuthRequiredPass // 1036. A password is required + ErrorAuthRequiredNewPass // 1037. A new password is required + ErrorAuthRequiredID // 1038. An account id is required + ErrorAuthRequiredSocket // 1039. A client socket pointer is required + ErrorAuthNameUnavail // 1040. The user name is unavailable + ErrorAuthMaliciousChars // 1041. There are malicious characters in the client's request variables + ErrorAuthIncorrectCols // 1042. The client supplied incorrect custom account info column data + ErrorAuthInsufficientCols // 1043. The client supplied an insufficient amount of custom account info columns + ErrorAuthEncryption // 1044. There was an error while encrypting data + ErrorAuthQuery // 1045. There was an error while querying the database + ErrorAuthIncorrectLogin // 1046. The client supplied an incorrect login or password + ErrorDatabaseInvalidAutolog // 1047. The client supplied incorrect auto-login (remember me) data + ErrorAuthConversion // 1048. There was an error while converting data to be stored on the database // Misc errors - ErrorActionDenied - ErrorServerPaused + ErrorActionDenied // 1049. A callback has denied the server action + ErrorServerPaused // 1050. The server is paused ) // NewError creates a new GopherError. func NewError(message string, id int) GopherError { return GopherError{Message: message, ID: id} } + +// NoError creates a new GopherError that represents a state in which no error occurred. +func NoError() GopherError { + return GopherError{} +} diff --git a/helpers/saveState.go b/helpers/saveState.go deleted file mode 100644 index 6a036b3..0000000 --- a/helpers/saveState.go +++ /dev/null @@ -1,23 +0,0 @@ -package helpers - -import ( - "encoding/json" - "io/ioutil" - "time" -) - -func SaveState(stateObj map[string]interface{}, saveFolder string) error { - //WRITE THE STATE - stateStr, err := json.Marshal(stateObj) - if err != nil { - return err - } - state := []byte(stateStr) - err = ioutil.WriteFile(saveFolder+"/Gopher Recovery - "+time.Now().Format("2006-01-02 15-04-05")+".grf", state, 0644) - if err != nil { - return err - } - - // - return nil -} diff --git a/helpers/saveState_test.go b/helpers/saveState_test.go deleted file mode 100644 index 6d3826d..0000000 --- a/helpers/saveState_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package helpers - -import ( - "fmt" - "testing" -) - -func TestSaveState(*testing.T) { - e := make(map[string]interface{}) - e["wtf"] = make(map[string]interface{}) - e["wtf"].(map[string]interface{})["innerWtf"] = "howdy do!" - err := saveState(e) - if err != nil { - fmt.Println(err) - } -} diff --git a/macros.go b/macros.go index a838ac9..ebb1bd5 100644 --- a/macros.go +++ b/macros.go @@ -3,19 +3,19 @@ package gopher import ( "bufio" "fmt" + "github.com/hewiefreeman/GopherGameServer/core" "os" - "strings" "strconv" - "github.com/hewiefreeman/GopherGameServer/users" - "github.com/hewiefreeman/GopherGameServer/rooms" + "strings" ) -func macroListener(){ +func macroListener() { for { reader := bufio.NewReader(os.Stdin) fmt.Print("[Gopher] Command: ") text, _ := reader.ReadString('\n') - stop := handleMacro(text[0:len(text)-2]) + text = strings.TrimSpace(text) + stop := handleMacro(text) if stop { return } @@ -30,26 +30,35 @@ func handleMacro(macro string) bool { } else if macro == "shutdown" { ShutDown() return true - } else if len(macro) >= 13 && macro[0:11] == "newroomtype" { - macroNewRoomType(macro) + } else if macro == "version" { + fmt.Println(version) + } else if macro == "roomcount" { + fmt.Println("Room count: ", core.RoomCount()) + } else if macro == "usercount" { + fmt.Println("User count: ", core.UserCount()) + } else if len(macro) >= 12 && macro[0:10] == "deleteroom" { + macroDeleteRoom(macro) } else if len(macro) >= 9 && macro[0:7] == "newroom" { macroNewRoom(macro) + } else if len(macro) >= 9 && macro[0:7] == "getuser" { + macroGetUser(macro) + } else if len(macro) >= 9 && macro[0:7] == "getroom" { + macroGetRoom(macro) } else if len(macro) >= 6 && macro[0:4] == "kick" { macroKick(macro) } return false } -// KICK A USER func macroKick(macro string) { - userName := macro[5:len(macro)] - user, userErr := users.Get(userName) + userName := macro[5:] + user, userErr := core.GetUser(userName) if userErr != nil { fmt.Println(userErr) return } user.Kick() - fmt.Println("Kicked user '"+userName+"'") + fmt.Println("Kicked user '" + userName + "'") } func macroNewRoom(macro string) { @@ -67,28 +76,83 @@ func macroNewRoom(macro string) { fmt.Println("maxUsers must be an integer") return } - _, roomErr := rooms.New(s[1], s[2], isPrivate, maxUsers, "") + _, roomErr := core.NewRoom(s[1], s[2], isPrivate, maxUsers, "") if roomErr != nil { fmt.Println(roomErr) return } - fmt.Println("Created room '"+s[1]+"'") + fmt.Println("Created room '" + s[1] + "'") } -func macroNewRoomType(macro string) { +func macroDeleteRoom(macro string) { s := strings.Split(macro, " ") - if len(s) != 3 { - fmt.Println("newroomtype expects 2 parameters (name string, serverOnly bool)") + if len(s) != 2 { + fmt.Println("deleteroom expects 1 parameter (name string)") + return + } + room, roomErr := core.GetRoom(s[1]) + if roomErr != nil { + fmt.Println(roomErr) + return + } + deleteErr := room.Delete() + if deleteErr != nil { + fmt.Println(deleteErr) + return + } + fmt.Println("Deleted room '" + s[1] + "'") +} + +func macroGetUser(macro string) { + s := strings.Split(macro, " ") + if len(s) != 2 { + fmt.Println("getuser expects 1 parameter (name string)") + return + } + + user, userErr := core.GetUser(s[1]) + if userErr != nil { + fmt.Println(userErr) return } - serverOnly := false - if s[2] == "true" || s[2] == "t" { - serverOnly = true + + fmt.Println("-- User '" + s[1] + "' --") + fmt.Println("Status:", user.Status()) + fmt.Println("Guest:", user.IsGuest()) + fmt.Println("Connections:") + conns := user.ConnectionIDs() + for i := 0; i < len(conns); i++ { + fmt.Println(" [ ID: '"+conns[i]+"', Room: '"+user.RoomIn(conns[i]).Name()+"', Vars:", user.GetVariables(nil, conns[i]), "]") } - rooms.NewRoomType(s[1], serverOnly) - if _, ok := rooms.GetRoomTypes()[s[1]]; !ok { - fmt.Println("The server must be paused to create a room type") + fmt.Println("Friends:", user.Friends()) + fmt.Println("Database ID:", user.DatabaseID()) +} + +func macroGetRoom(macro string) { + s := strings.Split(macro, " ") + if len(s) != 2 { + fmt.Println("getroom expects 1 parameter (name string)") return } - fmt.Println("Created room type '"+s[1]+"'") + + room, roomErr := core.GetRoom(s[1]) + if roomErr != nil { + fmt.Println(roomErr) + return + } + + invList, _ := room.InviteList() + usrMap, _ := room.GetUserMap() + + fmt.Println("-- Room '" + s[1] + "' --") + fmt.Println("Type:", room.Type()) + fmt.Println("Private:", room.IsPrivate()) + fmt.Println("Owner:", room.Owner()) + fmt.Println("Max Users:", room.MaxUsers()) + users := make([]string, 0, len(usrMap)) + for name := range usrMap { + users = append(users, name) + } + fmt.Println("Users:", "("+strconv.Itoa(room.NumUsers())+")", users) + fmt.Println("Invite List:", invList) } diff --git a/rooms/messages.go b/rooms/messages.go deleted file mode 100644 index 0bfeb90..0000000 --- a/rooms/messages.go +++ /dev/null @@ -1,137 +0,0 @@ -package rooms - -import ( - "errors" - "github.com/hewiefreeman/GopherGameServer/helpers" -) - -// These represent the types of room messages the server sends. -const ( - MessageTypeChat = iota - MessageTypeServer -) - -// These are the sub-types that a MessageTypeServer will come with. Ordered by their visible priority for your UI. -const ( - ServerMessageGame = iota - ServerMessageNotice - ServerMessageImportant -) - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// CHAT MESSAGES ////////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// ChatMessage sends a chat message to all Users in the Room. -func (r *Room) ChatMessage(author string, message interface{}) error { - //REJECT INCORRECT INPUT - if len(author) == 0 { - return errors.New("*Room.ChatMessage() requires an author") - } else if message == nil { - return errors.New("*Room.ChatMessage() requires a message") - } - - return r.sendMessage(MessageTypeChat, 0, nil, author, message) -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER MESSAGES //////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// ServerMessage sends a server message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice -// of string. In which case, the server message will be sent to all Users in the Room. -func (r *Room) ServerMessage(message interface{}, messageType int, recipients []string) error { - if message == nil { - return errors.New("*Room.ServerMessage() requires a message") - } - - return r.sendMessage(MessageTypeServer, messageType, recipients, "", message) -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// DATA MESSAGES ////////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// DataMessage sends a data message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice -// of string. In which case, the data message will be sent to all Users in the Room. -func (r *Room) DataMessage(message interface{}, recipients []string) error { - //GET USER MAP - userMap, err := r.GetUserMap() - if err != nil { - return err - } - - //CONSTRUCT MESSAGE - theMessage := make(map[string]interface{}) - theMessage[helpers.ServerActionDataMessage] = message - - //SEND MESSAGE TO USERS - if recipients == nil || len(recipients) == 0 { - for _, u := range userMap { - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(theMessage) - } - u.mux.Unlock() - } - } else { - for i := 0; i < len(recipients); i++ { - if u, ok := userMap[recipients[i]]; ok { - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(theMessage) - } - u.mux.Unlock() - } - } - } - - // - return nil -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// SENDING MESSAGES /////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -func (r *Room) sendMessage(mt int, st int, rec []string, a string, m interface{}) error { - //GET USER MAP - userMap, err := r.GetUserMap() - if err != nil { - return err - } - - //CONSTRUCT MESSAGE - message := make(map[string]interface{}) - message[helpers.ServerActionRoomMessage] = make(map[string]interface{}) - if mt == MessageTypeServer { - message[helpers.ServerActionRoomMessage].(map[string]interface{})["s"] = st - } // Server messages come with a sub-type - if len(a) > 0 && mt != MessageTypeServer { - message[helpers.ServerActionRoomMessage].(map[string]interface{})["a"] = a - } // Non-server messages have authors - message[helpers.ServerActionRoomMessage].(map[string]interface{})["m"] = m // The message - - //SEND MESSAGE TO USERS - if rec == nil || len(rec) == 0 { - for _, u := range userMap { - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(message) - } - u.mux.Unlock() - } - } else { - for i := 0; i < len(rec); i++ { - if u, ok := userMap[rec[i]]; ok { - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(message) - } - u.mux.Unlock() - } - } - } - - return nil -} diff --git a/rooms/vars.go b/rooms/vars.go deleted file mode 100644 index aa72ef3..0000000 --- a/rooms/vars.go +++ /dev/null @@ -1,182 +0,0 @@ -package rooms - -import ( - "errors" -) - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// ROOM VARIABLES ///////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// SetVariable sets a Room variable. -func (r *Room) SetVariable(key string, value interface{}) { - //REJECT INCORRECT INPUT - if len(key) == 0 { - return - } - - r.mux.Lock() - if r.usersMap == nil { - r.mux.Unlock() - return - } - r.vars[key] = value - r.mux.Unlock() - - // - return -} - -// SetVariables sets all the specified Room variables at once. -func (r *Room) SetVariables(values map[string]interface{}) { - r.mux.Lock() - if r.usersMap == nil { - r.mux.Unlock() - return - } - for key, val := range values { - r.vars[key] = val - } - r.mux.Unlock() - - // - return -} - -// GetVariable gets one of the Room's variables. -func (r *Room) GetVariable(key string) (interface{}, error) { - //REJECT INCORRECT INPUT - if len(key) == 0 { - return nil, errors.New("*Room.GetVariable() requires a key") - } - - r.mux.Lock() - if r.usersMap == nil { - r.mux.Unlock() - return nil, errors.New("Room '" + r.name + "' does not exist") - } - value := r.vars[key] - r.mux.Unlock() - - // - return value, nil -} - -// GetVariables gets all the specified (or all if not) Room variables as a map[string]interface{}. -func (r *Room) GetVariables(keys []string) (map[string]interface{}, error) { - var value map[string]interface{} = make(map[string]interface{}) - r.mux.Lock() - if r.usersMap == nil { - r.mux.Unlock() - return nil, errors.New("Room '" + r.name + "' does not exist") - } - if keys == nil || len(keys) == 0 { - value = r.vars - } else { - for i := 0; i < len(keys); i++ { - value[keys[i]] = r.vars[keys[i]] - } - } - r.mux.Unlock() - - // - return value, nil -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// USER VARIABLES ///////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// SetVariable sets a RoomUser's variable. If you are using MultiConnect in ServerSettings, the connID -// parameter is the connection ID associated with one of the connections attached to that User. This must -// be provided when manipulating a RoomUser's variables with MultiConnect enabled. Otherwise, an empty string can be used. -func (r *RoomUser) SetVariable(key string, value interface{}, connID string) { - //REJECT INCORRECT INPUT - if len(key) == 0 { - return - } else if multiConnect && len(connID) == 0 { - return - } else if !multiConnect { - connID = "1" - } - (*r.mux).Lock() - if conn, ok := r.conns[connID]; ok { - (*(*conn).vars)[key] = value - } - (*r.mux).Unlock() -} - -// SetVariables sets all the specified RoomUser's variables at once. If you are using MultiConnect in ServerSettings, the connID -// parameter is the connection ID associated with one of the connections attached to that User. This must -// be provided when manipulating a RoomUser's variables with MultiConnect enabled. Otherwise, an empty string can be used. -func (r *RoomUser) SetVariables(values map[string]interface{}, connID string) { - if multiConnect && len(connID) == 0 { - return - } else if !multiConnect { - connID = "1" - } - (*r.mux).Lock() - if conn, ok := r.conns[connID]; ok { - for key, val := range values { - (*(*conn).vars)[key] = val - } - } - (*r.mux).Unlock() -} - -// GetVariable gets a RoomUser's variable. If you are using MultiConnect in ServerSettings, the connID -// parameter is the connection ID associated with one of the connections attached to that User. This must -// be provided when manipulating a RoomUser's variables with MultiConnect enabled. Otherwise, an empty string can be used. -func (r *RoomUser) GetVariable(key string, connID string) interface{} { - //REJECT INCORRECT INPUT - if len(key) == 0 { - return nil - } else if multiConnect && len(connID) == 0 { - return nil - } else if !multiConnect { - connID = "1" - } - - var value interface{} - - (*r.mux).Lock() - if conn, ok := r.conns[connID]; ok { - value = (*(*conn).vars)[key] - } - (*r.mux).Unlock() - - // - return value -} - -// GetVariables gets all the specified (or all if not) RoomUser's variables as a map[string]interface{}. If you are using MultiConnect in ServerSettings, the connID -// parameter is the connection ID associated with one of the connections attached to that User. This must -// be provided when manipulating a RoomUser's variables with MultiConnect enabled. Otherwise, an empty string can be used. -func (r *RoomUser) GetVariables(keys []string, connID string) map[string]interface{} { - if multiConnect && len(connID) == 0 { - return nil - } else if !multiConnect { - connID = "1" - } - - var value map[string]interface{} = make(map[string]interface{}) - - if keys == nil || len(keys) == 0 { - (*r.mux).Lock() - value = *((*r.conns[connID]).vars) - (*r.mux).Unlock() - } else { - (*r.mux).Lock() - for i := 0; i < len(keys); i++ { - if conn, ok := r.conns[connID]; ok { - value[keys[i]] = (*(*conn).vars)[keys[i]] - } else { - value[keys[i]] = nil - } - } - (*r.mux).Unlock() - } - - // - return value -} diff --git a/rooms/voice.go b/rooms/voice.go deleted file mode 100644 index c8a79d6..0000000 --- a/rooms/voice.go +++ /dev/null @@ -1,45 +0,0 @@ -package rooms - -import ( - "github.com/gorilla/websocket" - "github.com/hewiefreeman/GopherGameServer/helpers" -) - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// VOICE STREAMS ////////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// VoiceStream sends a voice stream from the client API to all the users in the room besides the user who is speaking. -func (r *Room) VoiceStream(userName string, userSocket *websocket.Conn, stream interface{}) { - //GET USER MAP - userMap, err := r.GetUserMap() - if err != nil { - return - } - - //CONSTRUCT VOICE MESSAGE - theMessage := make(map[string]interface{}) - theMessage[helpers.ServerActionVoiceStream] = make(map[string]interface{}) - theMessage[helpers.ServerActionVoiceStream].(map[string]interface{})["u"] = userName - theMessage[helpers.ServerActionVoiceStream].(map[string]interface{})["d"] = stream - - //REMOVE SENDING USER FROM userMap - delete(userMap, userName) // COMMENT OUT FOR ECHO TESTS - - //SEND MESSAGE TO USERS - for _, u := range userMap { - for _, conn := range u.conns { - (*conn).socket.WriteJSON(theMessage) - } - } - - //CONSTRUCT PING MESSAGE - pingMessage := make(map[string]interface{}) - pingMessage[helpers.ServerActionVoicePing] = nil - - //SEND PING MESSAGE TO SENDING USER - userSocket.WriteJSON(pingMessage) - - // - return -} diff --git a/server.go b/server.go index 2d7ff34..6a50b9e 100644 --- a/server.go +++ b/server.go @@ -6,22 +6,22 @@ package gopher import ( "context" + "encoding/json" "fmt" "github.com/hewiefreeman/GopherGameServer/actions" + "github.com/hewiefreeman/GopherGameServer/core" "github.com/hewiefreeman/GopherGameServer/database" - "github.com/hewiefreeman/GopherGameServer/rooms" - "github.com/hewiefreeman/GopherGameServer/users" - "github.com/hewiefreeman/GopherGameServer/helpers" - "net/http" - "strconv" - "encoding/json" "io/ioutil" + "net/http" "os" + "strconv" + "time" ) /////////// TO DOs: +/////////// - Make authentication for GopherDB /////////// - Admin tools -/////////// - More useful command-line macros +/////////// - More useful command-line macros // ServerSettings are the core settings for the Gopher Game Server. You must fill one of these out to customize // the server's functionality to your liking. @@ -40,8 +40,9 @@ type ServerSettings struct { OriginOnly bool // When enabled, the server declines connections made from outside the origin server (Admin logins always check origin). IMPORTANT: Enable this for web apps and LAN servers. - MultiConnect bool // Enables multiple connections under the same User. When enabled, will override KickDupOnLogin's functionality. - KickDupOnLogin bool // When enabled, a logged in User will be disconnected from service when another User logs in with the same name. + MultiConnect bool // Enables multiple connections under the same User. When enabled, will override KickDupOnLogin's functionality. + MaxUserConns uint8 // Overrides the default (255) of maximum simultaneous connections on a single User + KickDupOnLogin bool // When enabled, a logged in User will be disconnected from service when another User logs in with the same name. UserRoomControl bool // Enables Users to create Rooms, invite/uninvite(AKA revoke) other Users to their owned private rooms, and destroy their owned rooms. RoomDeleteOnLeave bool // When enabled, Rooms created by a User will be deleted when the owner leaves. WARNING: If disabled, you must remember to at some point delete the rooms created by Users, or they will pile up endlessly! @@ -55,15 +56,17 @@ type ServerSettings struct { SqlDatabase string // SQL database name (Required for SQL features) EncryptionCost int // The amount of encryption iterations the server will run when storing and checking passwords. The higher the number, the longer encryptions take, but are more secure. Default is 4, range is 4-31. CustomLoginColumn string // The custom AccountInfoColumn you wish to use for logging in instead of the default name column. - RememberMe bool // Enables the "Remember Me" login feature. You can read more about this in project's "Usage" section. + RememberMe bool // Enables the "Remember Me" login feature. You can read more about this in project's wiki. EnableRecovery bool // Enables the recovery of all Rooms, their settings, and their variables on start-up after terminating the server. RecoveryLocation string // The folder location (starting from system's root folder) where you would like to store the recovery data. (Required for recovery) - EnableAdminTools bool // Enables the use of the Admin Tools - EnableRemoteAdmin bool // Enabled administration (only) from outside the origin server. When enabled, will override OriginOnly's functionality, but only for administrator connections. - AdminToolsLogin string // The login name for the Admin Tools (Required for Admin Tools) - AdminToolsPassword string // The password for the Admin Tools (Required for Admin Tools) + AdminLogin string // The login name for the Admin Tools (Required for Admin Tools) + AdminPassword string // The password for the Admin Tools (Required for Admin Tools) +} + +type serverRestore struct { + R map[string]core.RoomRecoveryState } var ( @@ -83,11 +86,11 @@ var ( clientConnectCallback func(*http.ResponseWriter, *http.Request) bool //SERVER VERSION NUMBER - version string = "1.0-ALPHA.3" + version string = "1.0-BETA.2" ) ////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER START-UP /////////////////////////////////////////////////////////////////////////////// +// Server start-up /////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// // Start will start the server. Call with a pointer to your `ServerSettings` (or nil for defaults) to start the server. The default @@ -102,54 +105,16 @@ func Start(s *ServerSettings) { return } serverStarted = true - fmt.Println(" _____ _ _____\n| __ \\ | | / ___|\n| | \\/ ___ _ __ | |__ ___ _ __\\ `--. ___ _ ____ _____ _ __\n| | __ / _ \\| '_ \\| '_ \\ / _ \\ '__|`--. \\/ _ \\ '__\\ \\ / / _ \\ '__|\n| |_\\ \\ (_) | |_) | | | | __/ | /\\__/ / __/ | \\ V / __/ |\n \\____/\\___/| .__/|_| |_|\\___|_| \\____/ \\___|_| \\_/ \\___|_|\n | |\n |_| v" + version + "\n\n") + fmt.Println(" _______ __\n | _ |.-----..-----.| |--..-----..----.\n |. |___||. _ ||. _ ||. ||. -__||. _|\n |. | ||:. . ||:. __||: |: ||: ||: |\n |: | |'-----'|: | '--'--''-----''--'\n |::.. . | '--' - Game Server -\n '-------'\n\n ") fmt.Println("Starting server...") - //SET SERVER SETTINGS + // Set server settings if s != nil { - settings = s - if settings.ServerName == "" { - fmt.Println("ServerName in ServerSettings is required. Shutting down...") - return - - } else if settings.HostName == "" || settings.IP == "" || settings.Port < 1 { - fmt.Println("HostName, IP, and Port in ServerSettings are required. Shutting down...") - return - - } else if settings.TLS == true && (settings.CertFile == "" || settings.PrivKeyFile == "") { - fmt.Println("CertFile and PrivKeyFile in ServerSettings are required for a TLS connection. Shutting down...") - return - - } else if settings.EnableSqlFeatures == true && (settings.SqlIP == "" || settings.SqlPort < 1 || settings.SqlProtocol == "" || - settings.SqlUser == "" || settings.SqlPassword == "" || settings.SqlDatabase == "") { - fmt.Println("SqlIP, SqlPort, SqlProtocol, SqlUser, SqlPassword, and SqlDatabase in ServerSettings are required for the SQL features. Shutting down...") + if !s.verify() { return - - } else if settings.EnableRecovery == true && settings.RecoveryLocation == "" { - fmt.Println("RecoveryLocation in ServerSettings is required for server recovery. Shutting down...") - return - - } else if settings.EnableRecovery { - //CHECK IF INVALID LOCATION - if _, err := os.Stat(settings.RecoveryLocation); err != nil { - fmt.Println("RecoveryLocation error:", err) - fmt.Println("Shutting down...") - return - } - var d []byte - if err := ioutil.WriteFile(settings.RecoveryLocation+"/test.txt", d, 0644); err != nil { - fmt.Println("RecoveryLocation error:", err) - fmt.Println("Shutting down...") - return - } - os.Remove(settings.RecoveryLocation+"/test.txt") - - } else if settings.EnableAdminTools == true && (settings.AdminToolsLogin == "" || settings.AdminToolsPassword == "") { - fmt.Println("AdminToolsLogin and AdminToolsPassword in ServerSettings are required for Administrator Tools. Shutting down...") - return - } + settings = s } else { - //DEFAULT localhost SETTINGS + // Default localhost settings fmt.Println("Using default settings...") settings = &ServerSettings{ ServerName: "!server!", @@ -186,23 +151,20 @@ func Start(s *ServerSettings) { EnableRecovery: false, RecoveryLocation: "C:/", - EnableAdminTools: true, - EnableRemoteAdmin: false, - AdminToolsLogin: "admin", - AdminToolsPassword: "password"} + AdminLogin: "admin", + AdminPassword: "password"} } - //UPDATE SETTINGS IN users PACKAGE, THEN users WILL UPDATE SETTINGS FOR rooms PACKAGE - users.SettingsSet((*settings).KickDupOnLogin, (*settings).ServerName, (*settings).RoomDeleteOnLeave, (*settings).EnableSqlFeatures, - (*settings).RememberMe, (*settings).MultiConnect) + // Update package settings + core.SettingsSet((*settings).KickDupOnLogin, (*settings).ServerName, (*settings).RoomDeleteOnLeave, (*settings).EnableSqlFeatures, + (*settings).RememberMe, (*settings).MultiConnect, (*settings).MaxUserConns) - //NOTIFY PACKAGES OF SERVER START - users.SetServerStarted(true) - rooms.SetServerStarted(true) + // Notify packages of server start + core.SetServerStarted(true) actions.SetServerStarted(true) database.SetServerStarted(true) - //START UP DATABASE + // Start database if (*settings).EnableSqlFeatures { fmt.Println("Initializing database...") dbErr := database.Init((*settings).SqlUser, (*settings).SqlPassword, (*settings).SqlDatabase, @@ -216,29 +178,29 @@ func Start(s *ServerSettings) { fmt.Println("Database initialized") } - //RECOVER PREVIOUS SERVER STATE + // Recover state if settings.EnableRecovery { recoverState() } - //START HTTP/SOCKET LISTENER + // Start socket listener if settings.TLS { httpServer = makeServer("/wss", settings.TLS) } else { httpServer = makeServer("/ws", settings.TLS) } - //RUN START CALLBACK + // Run callback if startCallback != nil { startCallback() } - //START MACRO COMMAND LISTENER + // Start macro listener go macroListener() fmt.Println("Startup complete") - //BLOCK UNTIL SERVER SHUT-DOWN + // Wait for server shutdown doneErr := <-serverEndChan if doneErr != http.ErrServerClosed { @@ -247,13 +209,12 @@ func Start(s *ServerSettings) { if !serverStopping { fmt.Println("Disconnecting users...") - //PAUSE SERVER - users.Pause() - rooms.Pause() + // Pause server + core.Pause() actions.Pause() database.Pause() - //SAVE STATE + // Save state if settings.EnableRecovery { saveState() } @@ -267,6 +228,51 @@ func Start(s *ServerSettings) { } } +func (settings *ServerSettings) verify() bool { + if settings.ServerName == "" { + fmt.Println("ServerName in ServerSettings is required. Shutting down...") + return false + + } else if settings.HostName == "" || settings.IP == "" || settings.Port < 1 { + fmt.Println("HostName, IP, and Port in ServerSettings are required. Shutting down...") + return false + + } else if settings.TLS == true && (settings.CertFile == "" || settings.PrivKeyFile == "") { + fmt.Println("CertFile and PrivKeyFile in ServerSettings are required for a TLS connection. Shutting down...") + return false + + } else if settings.EnableSqlFeatures == true && (settings.SqlIP == "" || settings.SqlPort < 1 || settings.SqlProtocol == "" || + settings.SqlUser == "" || settings.SqlPassword == "" || settings.SqlDatabase == "") { + fmt.Println("SqlIP, SqlPort, SqlProtocol, SqlUser, SqlPassword, and SqlDatabase in ServerSettings are required for the SQL features. Shutting down...") + return false + + } else if settings.EnableRecovery == true && settings.RecoveryLocation == "" { + fmt.Println("RecoveryLocation in ServerSettings is required for server recovery. Shutting down...") + return false + + } else if settings.EnableRecovery { + // Check if invalid file location + if _, err := os.Stat(settings.RecoveryLocation); err != nil { + fmt.Println("RecoveryLocation error:", err) + fmt.Println("Shutting down...") + return false + } + var d []byte + if err := ioutil.WriteFile(settings.RecoveryLocation+"/test.txt", d, 0644); err != nil { + fmt.Println("RecoveryLocation error:", err) + fmt.Println("Shutting down...") + return false + } + os.Remove(settings.RecoveryLocation + "/test.txt") + + } else if settings.AdminLogin == "" || settings.AdminPassword == "" { + fmt.Println("AdminLogin and AdminPassword in ServerSettings are required. Shutting down...") + return false + } + + return true +} + func makeServer(handleDir string, tls bool) *http.Server { server := &http.Server{Addr: settings.IP + ":" + strconv.Itoa(settings.Port)} http.HandleFunc(handleDir, socketInitializer) @@ -287,23 +293,22 @@ func makeServer(handleDir string, tls bool) *http.Server { } ////////////////////////////////////////////////////////////////////////////////////////////////////// -// SERVER ACTIONS //////////////////////////////////////////////////////////////////////////////// +// Server actions //////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// // Pause will log all Users off and prevent anyone from logging in. All rooms and their variables created by the server will remain in memory. -// Same goes for rooms created by Users unless `RoomDeleteOnLeave` in `ServerSettings` is set to true. +// Same goes for rooms created by Users unless RoomDeleteOnLeave in ServerSettings is set to true. func Pause() { if !serverPaused { serverPaused = true fmt.Println("Pausing server...") - users.Pause() - rooms.Pause() + core.Pause() actions.Pause() database.Pause() - //RUN CALLBACK + // Run callback if pauseCallback != nil { pauseCallback() } @@ -321,12 +326,11 @@ func Resume() { serverStarted = true fmt.Println("Resuming server...") - users.Resume() - rooms.Resume() + core.Resume() actions.Resume() database.Resume() - //RUN CALLBACK + // Run callback if resumeCallback != nil { resumeCallback() } @@ -337,24 +341,23 @@ func Resume() { } } -// Stop will log all Users off, save the state of the server if EnableRecovery in ServerSettings is set to true, then shut the server down. +// ShutDown will log all Users off, save the state of the server if EnableRecovery in ServerSettings is set to true, then shut the server down. func ShutDown() error { if !serverStopping { serverStopping = true fmt.Println("Disconnecting users...") - //PAUSE SERVER - users.Pause() - rooms.Pause() + // Pause server + core.Pause() actions.Pause() database.Pause() - //SAVE STATE + // Save state if settings.EnableRecovery { saveState() } - //SHUT DOWN SERVER + // Shut server down fmt.Println("Shutting server down...") shutdownErr := httpServer.Shutdown(context.Background()) if shutdownErr != http.ErrServerClosed { @@ -366,12 +369,12 @@ func ShutDown() error { } ////////////////////////////////////////////////////////////////////////////////////////////////////// -// SAVING AND RECOVERING STATE /////////////////////////////////////////////////////////////////// +// Saving and recovery /////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// func saveState() { fmt.Println("Saving server state...") - saveErr := helpers.SaveState(getState(), settings.RecoveryLocation) + saveErr := writeState(getState(), settings.RecoveryLocation) if saveErr != nil { fmt.Println("Error saving state:", saveErr) return @@ -379,27 +382,36 @@ func saveState() { fmt.Println("Save state successful") } -func getState() map[string]interface{} { - state := make(map[string]interface{}) +func writeState(stateObj serverRestore, saveFolder string) error { + state, err := json.Marshal(stateObj) + if err != nil { + return err + } + err = ioutil.WriteFile(saveFolder+"/Gopher Recovery - "+time.Now().Format("2006-01-02 15-04-05")+".grf", state, 0644) + if err != nil { + return err + } - //GET ROOM STATES - state["rooms"] = rooms.GetState() + return nil +} - // - return state +func getState() serverRestore { + return serverRestore{ + R: core.GetRoomsState(), + } } func recoverState() { fmt.Println("Recovering previous state...") - //GET LATEST RECOVERY FILE + // Get last recovery file files, fileErr := ioutil.ReadDir(settings.RecoveryLocation) if fileErr != nil { fmt.Println("Error recovering state:", fileErr) return } var newestFile string - var newestTime int64 = 0 + var newestTime int64 for _, f := range files { if len(f.Name()) < 19 || f.Name()[0:15] != "Gopher Recovery" { continue @@ -416,75 +428,39 @@ func recoverState() { } } - //READ RECOVERY FILE + // Read file r, err := ioutil.ReadFile(settings.RecoveryLocation + "/" + newestFile) if err != nil { fmt.Println("Error recovering state:", err) return } - //CONVERT JSON TO MAP - var recovery map[string]interface{} + // Convert JSON + var recovery serverRestore if err = json.Unmarshal(r, &recovery); err != nil { fmt.Println("Error recovering state:", err) return } - //RECOVER ROOMS - var roomsMap map[string]interface{} - var ok bool - if roomsMap, ok = recovery["rooms"].(map[string]interface{}); ok { - for name, val := range roomsMap { - var m map[string]interface{} - if m, ok = val.(map[string]interface{}); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect room format") - continue - } - var rType string - var isPrivate bool - var maxUsers float64 - var inviteList []interface{} - var owner string - if rType, ok = m["t"].(string); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect room type format") - continue - } - if isPrivate, ok = m["p"].(bool); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect private format") - continue - } - if maxUsers, ok = m["m"].(float64); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect maximum users format") - continue - } - if inviteList, ok = m["i"].([]interface{}); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect invite list format") - continue - } - if owner, ok = m["o"].(string); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect owner format") - continue - } - room, roomErr := rooms.New(name, rType, isPrivate, int(maxUsers), owner) - if roomErr != nil { - fmt.Println("Error recovering room '"+name+"':", roomErr) - continue - } - for i := 0; i < len(inviteList); i++ { - var userName string - if userName, ok = inviteList[i].(string); !ok { - fmt.Println("Error recovering room '"+name+"': Incorrect user name format in invite list") - continue - } - invErr := room.AddInvite(userName) - if invErr != nil { - fmt.Println("Error inviting '"+userName+"' to the room '"+name+"':", invErr) - } + if recovery.R == nil || len(recovery.R) == 0 { + fmt.Println("No rooms to restore!") + return + } + + // Recover rooms + for name, val := range recovery.R { + room, roomErr := core.NewRoom(name, val.T, val.P, val.M, val.O) + if roomErr != nil { + fmt.Println("Error recovering room '"+name+"':", roomErr) + continue + } + for _, userName := range val.I { + invErr := room.AddInvite(userName) + if invErr != nil { + fmt.Println("Error inviting '"+userName+"' to the room '"+name+"':", invErr) } } - } else { - fmt.Println("Error recovering state: Error interpreting JSON in recovery file") - return + room.SetVariables(val.V) } // diff --git a/sockets.go b/sockets.go index b33f465..b90c868 100644 --- a/sockets.go +++ b/sockets.go @@ -2,8 +2,8 @@ package gopher import ( "github.com/gorilla/websocket" + "github.com/hewiefreeman/GopherGameServer/core" "github.com/hewiefreeman/GopherGameServer/helpers" - "github.com/hewiefreeman/GopherGameServer/users" "net/http" "strconv" "sync" @@ -30,7 +30,7 @@ func socketInitializer(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") + ":" + strconv.Itoa(settings.Port) host := settings.HostName + ":" + strconv.Itoa(settings.Port) hostAlias := settings.HostAlias + ":" + strconv.Itoa(settings.Port) - if origin != host && (hostAlias != "" && origin != hostAlias) { + if origin != host && (settings.HostAlias != "" && origin != hostAlias) { http.Error(w, "Origin not allowed.", http.StatusForbidden) return } @@ -48,14 +48,14 @@ func socketInitializer(w http.ResponseWriter, r *http.Request) { return } - //UPGRADE CONNECTION + //UPGRADE CONNECTION PING-PONG conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024) if err != nil { http.Error(w, "Could not establish a connection.", http.StatusForbidden) return } - // + // START WEBSOCKET LOOP go clientActionListener(conn) } @@ -64,7 +64,7 @@ func clientActionListener(conn *websocket.Conn) { var action clientAction var clientMux sync.Mutex // LOCKS user AND connID - var user *users.User // THE CLIENT'S User OBJECT + var user *core.User // THE CLIENT'S User OBJECT var connID string // CLIENT SESSION ID // THE CLIENT'S AUTOLOG INFO @@ -73,32 +73,26 @@ func clientActionListener(conn *websocket.Conn) { var deviceUserID int if (*settings).RememberMe { - var sentTagRequest bool = false + //SEND TAG RETRIEVAL MESSAGE + tagMessage := map[string]interface{}{ + helpers.ServerActionRequestDeviceTag: nil, + } + writeErr := conn.WriteJSON(tagMessage) + if writeErr != nil { + closeSocket(conn) + return + } //PARAMS var ok bool var err error var gErr helpers.GopherError - var pMap map[string]interface{} var oldPass string //PING-PONG FOR TAGGING DEVICE - BREAKS WHEN THE DEVICE HAS BEEN PROPERLY TAGGED OR AUTHENTICATED. for { - if !sentTagRequest { - //SEND TAG RETRIEVAL MESSAGE - tagMessage := make(map[string]interface{}) - tagMessage[helpers.ServerActionRequestDeviceTag] = nil - writeErr := conn.WriteJSON(tagMessage) - if writeErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() - return - } - sentTagRequest = true - } //READ INPUT BUFFER readErr := conn.ReadJSON(&action) if readErr != nil || action.A == "" { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } @@ -107,17 +101,16 @@ func clientActionListener(conn *websocket.Conn) { //NO DEVICE TAG. MAKE ONE AND SEND IT. newDeviceTag, newDeviceTagErr := helpers.GenerateSecureString(32) if newDeviceTagErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } deviceTag = string(newDeviceTag) - tagMessage := make(map[string]interface{}) - tagMessage[helpers.ServerActionSetDeviceTag] = deviceTag + tagMessage := map[string]interface{}{ + helpers.ServerActionSetDeviceTag: deviceTag, + } writeErr := conn.WriteJSON(tagMessage) if writeErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } } else if action.A == "1" { @@ -125,22 +118,20 @@ func clientActionListener(conn *websocket.Conn) { if sentDeviceTag, ohK := action.P.(string); ohK { if len(deviceTag) > 0 && sentDeviceTag != deviceTag { //CLIENT DIDN'T USE THE PROVIDED DEVICE CODE FROM THE SERVER - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } //SEND AUTO-LOG NOT FILED MESSAGE - notFiledMessage := make(map[string]interface{}) - notFiledMessage[helpers.ServerActionAutoLoginNotFiled] = nil + notFiledMessage := map[string]interface{}{ + helpers.ServerActionAutoLoginNotFiled: nil, + } writeErr := conn.WriteJSON(notFiledMessage) if writeErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } } else { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } @@ -149,80 +140,74 @@ func clientActionListener(conn *websocket.Conn) { } else if action.A == "2" { //THE CLIENT HAS A LOGIN KEY PAIR - MAKE A NEW PASS FOR THEM + var pMap map[string]interface{} devicePass, err = helpers.GenerateSecureString(32) if err != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } //GET PARAMS if pMap, ok = action.P.(map[string]interface{}); !ok { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } if deviceTag, ok = pMap["dt"].(string); !ok { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } if oldPass, ok = pMap["da"].(string); !ok { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } var deviceUserIDStr string if deviceUserIDStr, ok = pMap["di"].(string); !ok { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } //CONVERT di TO INT deviceUserID, err = strconv.Atoi(deviceUserIDStr) if err != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } //CHANGE THE CLIENT'S PASS - newPassMessage := make(map[string]interface{}) - newPassMessage[helpers.ServerActionSetAutoLoginPass] = devicePass + newPassMessage := map[string]interface{}{ + helpers.ServerActionSetAutoLoginPass: devicePass, + } writeErr := conn.WriteJSON(newPassMessage) if writeErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } } else if action.A == "3" { if deviceTag == "" || oldPass == "" || deviceUserID == 0 || devicePass == "" { //IRRESPONSIBLE USAGE - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } //AUTO-LOG THE CLIENT - connID, gErr = users.AutoLogIn(deviceTag, oldPass, devicePass, deviceUserID, conn, &user, &clientMux) + connID, gErr = core.AutoLogIn(deviceTag, oldPass, devicePass, deviceUserID, conn, &user, &clientMux) if gErr.ID != 0 { - //ERROR AUTO-LOGGING - RUN AUTOLOGCOMPLETE AND DELETE KEYS FOR CLIENT, AND SILENTLY CHANGE DEVICE KEY + //ERROR AUTO-LOGGING - RUN AUTOLOGCOMPLETE AND DELETE KEYS FOR CLIENT, AND SILENTLY CHANGE DEVICE TAG newTag, newTagErr := helpers.GenerateSecureString(32) if newTagErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } - autologMessage := make(map[string]interface{}) - autologMessage[helpers.ServerActionAutoLoginFailed] = make(map[string]interface{}) - autologMessage[helpers.ServerActionAutoLoginFailed].(map[string]interface{})["dt"] = newTag - autologMessage[helpers.ServerActionAutoLoginFailed].(map[string]interface{})["e"] = make(map[string]interface{}) - autologMessage[helpers.ServerActionAutoLoginFailed].(map[string]interface{})["e"].(map[string]interface{})["m"] = gErr.Message - autologMessage[helpers.ServerActionAutoLoginFailed].(map[string]interface{})["e"].(map[string]interface{})["id"] = gErr.ID + autologMessage := map[string]map[string]interface{}{ + helpers.ServerActionAutoLoginFailed: { + "dt": newTag, + "e": map[string]interface{}{ + "m": gErr.Message, + "id": gErr.ID, + }, + }, + } writeErr := conn.WriteJSON(autologMessage) if writeErr != nil { - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } - oldPass = "" devicePass = "" deviceUserID = 0 deviceTag = newTag @@ -243,8 +228,7 @@ func clientActionListener(conn *websocket.Conn) { //DISCONNECT USER clientMux.Lock() sockedDropped(user, connID, &clientMux) - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } @@ -257,8 +241,7 @@ func clientActionListener(conn *websocket.Conn) { //DISCONNECT USER clientMux.Lock() sockedDropped(user, connID, &clientMux) - conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) - conn.Close() + closeSocket(conn) return } } @@ -268,26 +251,31 @@ func clientActionListener(conn *websocket.Conn) { } } -func sockedDropped(user *users.User, connID string, clientMux *sync.Mutex) { +func closeSocket(conn *websocket.Conn) { + conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) + conn.Close() + conns.subtract() +} + +func sockedDropped(user *core.User, connID string, clientMux *sync.Mutex) { if user != nil { //CLIENT WAS LOGGED IN. LOG THEM OUT (*clientMux).Unlock() user.Logout(connID) } - conns.subtract() - } /////////////////////// HELPERS FOR connections func (c *connections) add() bool { c.connsMux.Lock() - defer c.connsMux.Unlock() // if (*settings).MaxConnections != 0 && c.conns == (*settings).MaxConnections { + c.connsMux.Unlock() return false } c.conns++ + c.connsMux.Unlock() // return true } @@ -298,10 +286,11 @@ func (c *connections) subtract() { c.connsMux.Unlock() } -// ClientsConnected gets the number of clients connected to the server. Includes connections -// not logged in as a User. +// ClientsConnected returns the number of clients connected to the server. Includes connections +// not logged in as a User. To get the number of Users logged in, use the core.UserCount() function. func ClientsConnected() int { conns.connsMux.Lock() - defer conns.connsMux.Unlock() - return conns.conns + c := conns.conns + conns.connsMux.Unlock() + return c } diff --git a/startandstop_test.go b/startandstop_test.go new file mode 100644 index 0000000..f79089a --- /dev/null +++ b/startandstop_test.go @@ -0,0 +1,14 @@ +package gopher + +import ( + "testing" + "time" +) + +func TestStartAndStop(t *testing.T) { + go Start(nil) + time.Sleep(time.Second * 2) + if sdErr := ShutDown(); sdErr != nil { + t.Errorf(sdErr.Error()) + } +} diff --git a/users/messages.go b/users/messages.go deleted file mode 100644 index f8421c5..0000000 --- a/users/messages.go +++ /dev/null @@ -1,56 +0,0 @@ -package users - -import ( - "github.com/hewiefreeman/GopherGameServer/helpers" -) - -////////////////////////////////////////////////////////////////////////////////////////////////////// -// SEND A PRIVATE MESSAGE //////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////////////////////////// - -// PrivateMessage sends a private message to another User by name. -func (u *User) PrivateMessage(userName string, message interface{}) { - user, userErr := Get(userName) - if userErr != nil { - return - } - - //CONSTRUCT MESSAGE - theMessage := make(map[string]interface{}) - theMessage[helpers.ServerActionPrivateMessage] = make(map[string]interface{}) - theMessage[helpers.ServerActionPrivateMessage].(map[string]interface{})["f"] = u.name // from - theMessage[helpers.ServerActionPrivateMessage].(map[string]interface{})["t"] = user.name // to - theMessage[helpers.ServerActionPrivateMessage].(map[string]interface{})["m"] = message // message - - //SEND MESSAGES - user.mux.Lock() - for _, conn := range user.conns { - (*conn).socket.WriteJSON(theMessage) - } - user.mux.Unlock() - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(theMessage) - } - u.mux.Unlock() - - return -} - -////////////////////////////////////////////////////////////////////////////////////////////////////// -// SEND A DATA MESSAGE /////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////////////////////////// - -// DataMessage sends a data message directly to the User. -func (u *User) DataMessage(data interface{}) { - //CONSTRUCT MESSAGE - message := make(map[string]interface{}) - message[helpers.ServerActionDataMessage] = data - - //SEND MESSAGE TO USER - u.mux.Lock() - for _, conn := range u.conns { - (*conn).socket.WriteJSON(message) - } - u.mux.Unlock() -}