├── docs ├── _config.yml ├── images │ ├── icon.png │ ├── wiki_img.png │ ├── zip_file.png │ ├── icon_large.png │ ├── running_ex.png │ ├── GitHub_Dark.png │ ├── GitHub_Light.png │ ├── Gopher Title.png │ └── minimal_code_ex.png ├── js │ ├── init.js │ └── resize.js ├── css │ ├── nav.css │ └── shell.css └── index.html ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql-analysis.yml ├── startandstop_test.go ├── CONTRIBUTING.md ├── helpers ├── crypto.go ├── clientResponses.go └── errors.go ├── CHANGE_LOG.md ├── CODE_OF_CONDUCT.md ├── macros.go ├── database ├── setup.go ├── database.go ├── accountInfoColumn.go └── friending.go ├── core ├── core.go ├── vars.go ├── roomTypes.go ├── messaging.go ├── friending.go ├── rooms.go └── users.go ├── sockets.go ├── README.md ├── actions └── actions.go ├── LICENSE ├── callbacks.go └── server.go /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/icon.png -------------------------------------------------------------------------------- /docs/images/wiki_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/wiki_img.png -------------------------------------------------------------------------------- /docs/images/zip_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/zip_file.png -------------------------------------------------------------------------------- /docs/images/icon_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/icon_large.png -------------------------------------------------------------------------------- /docs/images/running_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/running_ex.png -------------------------------------------------------------------------------- /docs/images/GitHub_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/GitHub_Dark.png -------------------------------------------------------------------------------- /docs/images/GitHub_Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/GitHub_Light.png -------------------------------------------------------------------------------- /docs/images/Gopher Title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/Gopher Title.png -------------------------------------------------------------------------------- /docs/images/minimal_code_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hewiefreeman/GopherGameServer/HEAD/docs/images/minimal_code_ex.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: hewiefreeman 4 | patreon: hewiefreeman 5 | custom: paypal.me/hewiefreeman 6 | -------------------------------------------------------------------------------- /startandstop_test.go: -------------------------------------------------------------------------------- 1 | package gopher 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStartAndStop(t *testing.T) { 9 | go Start(nil) 10 | time.Sleep(time.Second * 2) 11 | if sdErr := ShutDown(); sdErr != nil { 12 | t.Errorf(sdErr.Error()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/js/init.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", init); 2 | var download_nav, 3 | download_btn, 4 | wiki_nav, 5 | wiki_btn, 6 | logo, 7 | title; 8 | 9 | function init() { 10 | download_nav = document.getElementById("downloadNav"); 11 | download_btn = document.getElementById("downloadBtn"); 12 | wiki_nav = document.getElementById("wikiNav"); 13 | wiki_btn = document.getElementById("wikiBtn"); 14 | logo = document.getElementById("logo"); 15 | title = document.getElementById("title"); 16 | 17 | window.addEventListener("resize", resized); 18 | // 19 | resize_init(); 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: REQUEST 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is: 12 | 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen: 16 | 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered: 20 | 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here: 24 | -------------------------------------------------------------------------------- /docs/css/nav.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: block; 3 | position: absolute; 4 | 5 | top: 0px; 6 | right: 0px; 7 | 8 | height: 40px; 9 | min-width: 320px; 10 | 11 | text-align: right; 12 | 13 | margin-right: 10px; 14 | 15 | overflow: hidden; 16 | } 17 | 18 | .navButton { 19 | display: inline-block; 20 | position: relative; 21 | 22 | height: 30px; 23 | 24 | line-height: 30px; 25 | text-align: center; 26 | vertical-align: center; 27 | align-self: right; 28 | 29 | padding-left: 5px; 30 | padding-right: 5px; 31 | margin-right: 3px; 32 | 33 | color: #ffffff; 34 | background: #3f3f3f; 35 | box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4); 36 | 37 | border-bottom: 3px solid #85d681; 38 | border-radius: 0px 0px 2px 2px; 39 | 40 | cursor: pointer; 41 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Ways to contribute 2 | 3 | 1) See if there is an Issue that you can take up to fix or implement 4 | 2) Help expand and clarify documentation and/or usage section 5 | 3) Have a good idea? Open and submit a detailed Issue 6 | 7 | ## Guidlines 8 | 9 | 1) First, check to see if a similar issue has already been opened 10 | 2) Follow the [Code of Conduct](https://github.com/hewiefreeman/GopherGameServer/blob/master/CODE_OF_CONDUCT.md) 11 | 3) BE DETAILED in your Pull Requests 12 | 4) Do not introduce any further 3rd-party package dependencies unless provided a valid reason 13 | 5) Do not submit content that does not pass golint, or has not been formatted with `gofmt` 14 | 6) Do not submit content that has little to no documentation on exported functions, variables, etc 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /helpers/crypto.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // GenerateRandomBytes uses the `crypto/rand` library to create a secure random `[]byte` at a given size `n`. 10 | func GenerateRandomBytes(n int) ([]byte, error) { 11 | b := make([]byte, n) 12 | _, err := rand.Read(b) 13 | // Note that err == nil only if we read len(b) bytes. 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return b, nil 19 | } 20 | 21 | // GenerateSecureString uses the `crypto/rand` and `encoding/base64` libraries to create a random `string` 22 | // of given length `n`. 23 | func GenerateSecureString(n int) (string, error) { 24 | b, err := GenerateRandomBytes(n) 25 | return base64.URLEncoding.EncodeToString(b), err 26 | } 27 | 28 | // EncryptString encrypts a `string` with the `golang.org/x/crypto/bcrypt` library at a given cost. 29 | func EncryptString(str string, cost int) (string, error) { 30 | bytes, err := bcrypt.GenerateFromPassword([]byte(str), cost) 31 | return string(bytes), err 32 | } 33 | 34 | // CompareEncryptedData uses the `golang.org/x/crypto/bcrypt` library to compare a `string` to an 35 | // encrypted `[]byte`. Returns true if the `string` matches the encrypted `[]byte`. 36 | func CompareEncryptedData(str string, hash []byte) bool { 37 | err := bcrypt.CompareHashAndPassword(hash, []byte(str)) 38 | return err == nil 39 | } 40 | -------------------------------------------------------------------------------- /CHANGE_LOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 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. 3 | 4 | > 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)! 5 | 6 | ### Key 7 | :wrench: : Bug fix 8 | 9 | :warning: : Code-breaking change 10 | 11 | :newspaper: : New feature 12 | 13 | :monorail: : Optimization 14 |

15 | ## v1.0-BETA.2 16 | - :newspaper: Added a `version` macro to display current running server version 17 | - :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! 18 | 19 | ## v1.0-ALPHA.5 20 | - :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)** 21 | 22 | > 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**. 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '17 4 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # 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 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /helpers/clientResponses.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | //BUILT-IN CLIENT ACTION/RESPONSE MESSAGE TYPES 4 | const ( 5 | ClientActionSignup = "s" 6 | ClientActionDeleteAccount = "d" 7 | ClientActionChangePassword = "pc" 8 | ClientActionChangeAccountInfo = "ic" 9 | ClientActionLogin = "li" 10 | ClientActionLogout = "lo" 11 | ClientActionJoinRoom = "j" 12 | ClientActionLeaveRoom = "lr" 13 | ClientActionCreateRoom = "r" 14 | ClientActionDeleteRoom = "rd" 15 | ClientActionRoomInvite = "i" 16 | ClientActionRevokeInvite = "ri" 17 | ClientActionChatMessage = "c" 18 | ClientActionPrivateMessage = "p" 19 | ClientActionVoiceStream = "v" 20 | ClientActionChangeStatus = "sc" 21 | ClientActionCustomAction = "a" 22 | ClientActionFriendRequest = "f" 23 | ClientActionAcceptFriend = "fa" 24 | ClientActionDeclineFriend = "fd" 25 | ClientActionRemoveFriend = "fr" 26 | ClientActionSetVariable = "vs" 27 | ClientActionSetVariables = "vx" 28 | ) 29 | 30 | //BUILT-IN SERVER ACTION RESPONSES 31 | const ( 32 | ServerActionClientActionResponse = "c" 33 | ServerActionCustomClientActionResponse = "a" 34 | ServerActionDataMessage = "d" 35 | ServerActionPrivateMessage = "p" 36 | ServerActionRoomMessage = "m" 37 | ServerActionUserEnter = "e" 38 | ServerActionUserLeave = "x" 39 | ServerActionVoiceStream = "v" 40 | ServerActionVoicePing = "vp" 41 | ServerActionRoomInvite = "i" 42 | ServerActionFriendRequest = "f" 43 | ServerActionFriendAccept = "fa" 44 | ServerActionFriendRemove = "fr" 45 | ServerActionFriendStatusChange = "fs" 46 | ServerActionRequestDeviceTag = "t" 47 | ServerActionSetDeviceTag = "ts" 48 | ServerActionSetAutoLoginPass = "ap" 49 | ServerActionAutoLoginFailed = "af" 50 | ServerActionAutoLoginNotFiled = "ai" 51 | ServerActionWebRTCOffer = "wo" 52 | ) 53 | 54 | // MakeClientResponse is used for Gopher Game Server inner mechanics only. 55 | func MakeClientResponse(action string, responseVal interface{}, err GopherError) map[string]map[string]interface{} { 56 | var response map[string]map[string]interface{} 57 | if err.ID != 0 { 58 | response = map[string]map[string]interface{}{ 59 | ServerActionClientActionResponse: { 60 | "a": action, 61 | "e": map[string]interface{}{ 62 | "m": err.Message, 63 | "id": err.ID, 64 | }, 65 | }, 66 | } 67 | } else { 68 | response = map[string]map[string]interface{}{ 69 | ServerActionClientActionResponse: { 70 | "a": action, 71 | "r": responseVal, 72 | }, 73 | } 74 | } 75 | 76 | // 77 | return response 78 | } 79 | -------------------------------------------------------------------------------- /docs/js/resize.js: -------------------------------------------------------------------------------- 1 | var download_btn_hidden = false, 2 | download_btn_switch = 520, 3 | wiki_btn_hidden = false, 4 | wiki_btn_switch = 640, 5 | logo_resize = false, 6 | logo_switch = 350; 7 | 8 | function resized(e) { 9 | // Resize and replace download button 10 | if (document.documentElement.clientWidth <= download_btn_switch && !download_btn_hidden) { 11 | download_nav.style.display = "inline-block"; 12 | download_btn.style.display = "none"; 13 | 14 | download_btn_hidden = true; 15 | } else if (document.documentElement.clientWidth > download_btn_switch && download_btn_hidden) { 16 | download_nav.style.display = "none"; 17 | download_btn.style.display = "inline-block"; 18 | 19 | download_btn_hidden = false; 20 | } 21 | 22 | // Resize and replace wiki button 23 | if (document.documentElement.clientWidth <= wiki_btn_switch && !wiki_btn_hidden) { 24 | wiki_nav.style.display = "inline-block"; 25 | wiki_btn.style.display = "none"; 26 | 27 | wiki_btn_hidden = true; 28 | } else if (document.documentElement.clientWidth > wiki_btn_switch && wiki_btn_hidden) { 29 | wiki_nav.style.display = "none"; 30 | wiki_btn.style.display = "inline-block"; 31 | 32 | wiki_btn_hidden = false; 33 | } 34 | 35 | // Resize logo 36 | if (document.documentElement.clientWidth <= download_btn_switch) { 37 | logo_resize = true; 38 | logo.style.width = (document.documentElement.clientWidth/2)+"px"; 39 | logo.style.height = (document.documentElement.clientWidth/2)+"px"; 40 | title.style.left = ((document.documentElement.clientWidth/2)-30)+"px"; 41 | } else if (document.documentElement.clientWidth > download_btn_switch && logo_resize) { 42 | logo.style.width = "250px"; 43 | logo.style.height = "250px"; 44 | title.style.left = "220px"; 45 | logo_resize = false; 46 | } 47 | } 48 | 49 | function resize_init() { 50 | // Resize and replace download button 51 | if (document.documentElement.clientWidth <= download_btn_switch) { 52 | download_nav.style.display = "inline-block"; 53 | download_btn.style.display = "none"; 54 | 55 | download_btn_hidden = true; 56 | } else if (document.documentElement.clientWidth > download_btn_switch) { 57 | download_nav.style.display = "none"; 58 | download_btn.style.display = "inline-block"; 59 | 60 | download_btn_hidden = false; 61 | } 62 | 63 | // Resize and replace wiki button 64 | if (document.documentElement.clientWidth <= wiki_btn_switch) { 65 | wiki_nav.style.display = "inline-block"; 66 | wiki_btn.style.display = "none"; 67 | 68 | wiki_btn_hidden = true; 69 | } else if (document.documentElement.clientWidth > wiki_btn_switch) { 70 | wiki_nav.style.display = "none"; 71 | wiki_btn.style.display = "inline-block"; 72 | 73 | wiki_btn_hidden = false; 74 | } 75 | 76 | // Resize logo 77 | if (document.documentElement.clientWidth <= download_btn_switch) { 78 | logo_resize = true; 79 | logo.style.width = (document.documentElement.clientWidth/2)+"px"; 80 | logo.style.height = (document.documentElement.clientWidth/2)+"px"; 81 | title.style.left = ((document.documentElement.clientWidth/2)-30)+"px"; 82 | } else if (document.documentElement.clientWidth > download_btn_switch) { 83 | logo.style.width = "250px"; 84 | logo.style.height = "250px"; 85 | title.style.left = "220px"; 86 | logo_resize = false; 87 | } 88 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gopher Game Server 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 35 |
36 |

Gopher

37 |

Game Server

38 |
39 |

Source on GitHub

40 |
41 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tbd@gmail.com . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /macros.go: -------------------------------------------------------------------------------- 1 | package gopher 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/hewiefreeman/GopherGameServer/core" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func macroListener() { 13 | for { 14 | reader := bufio.NewReader(os.Stdin) 15 | fmt.Print("[Gopher] Command: ") 16 | text, _ := reader.ReadString('\n') 17 | text = strings.TrimSpace(text) 18 | stop := handleMacro(text) 19 | if stop { 20 | return 21 | } 22 | } 23 | } 24 | 25 | func handleMacro(macro string) bool { 26 | if macro == "pause" { 27 | Pause() 28 | } else if macro == "resume" { 29 | Resume() 30 | } else if macro == "shutdown" { 31 | ShutDown() 32 | return true 33 | } else if macro == "version" { 34 | fmt.Println(version) 35 | } else if macro == "roomcount" { 36 | fmt.Println("Room count: ", core.RoomCount()) 37 | } else if macro == "usercount" { 38 | fmt.Println("User count: ", core.UserCount()) 39 | } else if len(macro) >= 12 && macro[0:10] == "deleteroom" { 40 | macroDeleteRoom(macro) 41 | } else if len(macro) >= 9 && macro[0:7] == "newroom" { 42 | macroNewRoom(macro) 43 | } else if len(macro) >= 9 && macro[0:7] == "getuser" { 44 | macroGetUser(macro) 45 | } else if len(macro) >= 9 && macro[0:7] == "getroom" { 46 | macroGetRoom(macro) 47 | } else if len(macro) >= 6 && macro[0:4] == "kick" { 48 | macroKick(macro) 49 | } 50 | return false 51 | } 52 | 53 | func macroKick(macro string) { 54 | userName := macro[5:] 55 | user, userErr := core.GetUser(userName) 56 | if userErr != nil { 57 | fmt.Println(userErr) 58 | return 59 | } 60 | user.Kick() 61 | fmt.Println("Kicked user '" + userName + "'") 62 | } 63 | 64 | func macroNewRoom(macro string) { 65 | s := strings.Split(macro, " ") 66 | if len(s) != 5 { 67 | fmt.Println("newroom expects 4 parameters (name string, rType string, isPrivate bool, maxUsers int)") 68 | return 69 | } 70 | isPrivate := false 71 | if s[3] == "true" || s[3] == "t" { 72 | isPrivate = true 73 | } 74 | maxUsers, err := strconv.Atoi(s[4]) 75 | if err != nil { 76 | fmt.Println("maxUsers must be an integer") 77 | return 78 | } 79 | _, roomErr := core.NewRoom(s[1], s[2], isPrivate, maxUsers, "") 80 | if roomErr != nil { 81 | fmt.Println(roomErr) 82 | return 83 | } 84 | fmt.Println("Created room '" + s[1] + "'") 85 | } 86 | 87 | func macroDeleteRoom(macro string) { 88 | s := strings.Split(macro, " ") 89 | if len(s) != 2 { 90 | fmt.Println("deleteroom expects 1 parameter (name string)") 91 | return 92 | } 93 | room, roomErr := core.GetRoom(s[1]) 94 | if roomErr != nil { 95 | fmt.Println(roomErr) 96 | return 97 | } 98 | deleteErr := room.Delete() 99 | if deleteErr != nil { 100 | fmt.Println(deleteErr) 101 | return 102 | } 103 | fmt.Println("Deleted room '" + s[1] + "'") 104 | } 105 | 106 | func macroGetUser(macro string) { 107 | s := strings.Split(macro, " ") 108 | if len(s) != 2 { 109 | fmt.Println("getuser expects 1 parameter (name string)") 110 | return 111 | } 112 | 113 | user, userErr := core.GetUser(s[1]) 114 | if userErr != nil { 115 | fmt.Println(userErr) 116 | return 117 | } 118 | 119 | fmt.Println("-- User '" + s[1] + "' --") 120 | fmt.Println("Status:", user.Status()) 121 | fmt.Println("Guest:", user.IsGuest()) 122 | fmt.Println("Connections:") 123 | conns := user.ConnectionIDs() 124 | for i := 0; i < len(conns); i++ { 125 | fmt.Println(" [ ID: '"+conns[i]+"', Room: '"+user.RoomIn(conns[i]).Name()+"', Vars:", user.GetVariables(nil, conns[i]), "]") 126 | } 127 | fmt.Println("Friends:", user.Friends()) 128 | fmt.Println("Database ID:", user.DatabaseID()) 129 | } 130 | 131 | func macroGetRoom(macro string) { 132 | s := strings.Split(macro, " ") 133 | if len(s) != 2 { 134 | fmt.Println("getroom expects 1 parameter (name string)") 135 | return 136 | } 137 | 138 | room, roomErr := core.GetRoom(s[1]) 139 | if roomErr != nil { 140 | fmt.Println(roomErr) 141 | return 142 | } 143 | 144 | invList, _ := room.InviteList() 145 | usrMap, _ := room.GetUserMap() 146 | 147 | fmt.Println("-- Room '" + s[1] + "' --") 148 | fmt.Println("Type:", room.Type()) 149 | fmt.Println("Private:", room.IsPrivate()) 150 | fmt.Println("Owner:", room.Owner()) 151 | fmt.Println("Max Users:", room.MaxUsers()) 152 | users := make([]string, 0, len(usrMap)) 153 | for name := range usrMap { 154 | users = append(users, name) 155 | } 156 | fmt.Println("Users:", "("+strconv.Itoa(room.NumUsers())+")", users) 157 | fmt.Println("Invite List:", invList) 158 | } 159 | -------------------------------------------------------------------------------- /docs/css/shell.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Open Sans', sans-serif; 3 | font-weight: 600; 4 | font-size: 16px; 5 | color: #ffffff; 6 | 7 | background: #e3e3e3; 8 | 9 | margin: 0px; 10 | padding: 0px; 11 | 12 | overflow: scroll; 13 | } 14 | 15 | br { 16 | margin: 0px; 17 | padding: 0px; 18 | } 19 | 20 | p { 21 | margin: 0px; 22 | padding: 0px; 23 | } 24 | 25 | a { 26 | color: #ffffff; 27 | text-decoration: none; 28 | } 29 | 30 | a:link { 31 | color: #ffffff; 32 | text-decoration: none; 33 | } 34 | 35 | a:hover { 36 | color: #dedede; 37 | text-decoration: underline; 38 | } 39 | 40 | a:active { 41 | color: #ffffff; 42 | text-decoration: none; 43 | } 44 | 45 | h1 { 46 | font-family: 'Open Sans', sans-serif; 47 | font-weight: 700; 48 | font-size: 36px; 49 | line-height: 36px; 50 | 51 | margin: 0px; 52 | padding: 0px; 53 | 54 | color: #85d681; 55 | } 56 | 57 | h2 { 58 | font-family: 'Open Sans', sans-serif; 59 | font-weight: 600; 60 | font-size: 30px; 61 | 62 | margin: 0px; 63 | padding: 0px; 64 | } 65 | 66 | h3 { 67 | font-family: 'Open Sans', sans-serif; 68 | font-weight: 650; 69 | font-size: 24px; 70 | line-height: 24px; 71 | 72 | margin-top: 5px; 73 | margin-bottom: 0px; 74 | padding: 0px; 75 | 76 | color: #3f3f3f; 77 | } 78 | 79 | h4 { 80 | font-family: 'Open Sans', sans-serif; 81 | font-weight: 650; 82 | font-size: 16px; 83 | line-height: 16px; 84 | 85 | margin: 0px; 86 | padding: 0px; 87 | 88 | color: #3f3f3f; 89 | } 90 | 91 | h5 { 92 | font-family: 'Open Sans', sans-serif; 93 | font-weight: 600; 94 | font-size: 12px; 95 | line-height: 12px; 96 | 97 | margin-top: 5px; 98 | margin-bottom: 0px; 99 | padding: 0px; 100 | 101 | color: #ffffff; 102 | } 103 | 104 | .wrapper { 105 | display: block; 106 | position: absolute; 107 | 108 | text-align: center; 109 | 110 | top: 0px; 111 | left: 0px; 112 | 113 | min-width: 310px; 114 | width: 100%; 115 | 116 | background: rgba(255,255,255,0); 117 | 118 | z-index: 2; 119 | } 120 | 121 | .shadowWrapper { 122 | display: block; 123 | position: fixed; 124 | 125 | text-align: center; 126 | 127 | top: 0px; 128 | left: 0px; 129 | 130 | width: 100%; 131 | height: 100%; 132 | 133 | overflow: hidden; 134 | 135 | z-index: 1; 136 | } 137 | 138 | .shadowObj { 139 | display: block; 140 | 141 | width: 100%; 142 | max-width: 1000px; 143 | height: 100%; 144 | 145 | background: #ECFFEB; 146 | 147 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.4); 148 | } 149 | 150 | .innerWrapper { 151 | display: block; 152 | 153 | width: 100%; 154 | max-width: 1000px; 155 | 156 | flex-direction: column; 157 | flex-wrap: nowrap; 158 | justify-content: flex-start; 159 | align-items: stretch; 160 | } 161 | 162 | .topBar { 163 | display: block; 164 | position: relative; 165 | 166 | height: 200px; 167 | width: 100%; 168 | 169 | /*background-image: url("../images/bg.jpg");*/ 170 | background-color: #7a7a7a; 171 | 172 | border-bottom: 3px solid #85d681; 173 | 174 | z-index: 6; 175 | } 176 | 177 | .content { 178 | display: block; 179 | position: relative; 180 | 181 | width: 100%; 182 | min-height: 100px; 183 | height: 10px; 184 | 185 | background: #ECFFEB; 186 | 187 | overflow: hidden; 188 | } 189 | 190 | .logoHolder { 191 | display: block; 192 | position: absolute; 193 | 194 | bottom: -30px; 195 | left: 0px; 196 | 197 | width: 230px; 198 | height: 230px; 199 | 200 | background-image: url('../images/icon_large.png'); 201 | background-repeat: no-repeat; 202 | background-position: 0px -30px; 203 | background-size: 100%; 204 | } 205 | 206 | .titleHolder { 207 | display: block; 208 | position: absolute; 209 | 210 | bottom: 0px; 211 | left: 220px; 212 | 213 | margin-top: 10px; 214 | 215 | width: 180px; 216 | height: 160px; 217 | 218 | vertical-align: center; 219 | text-align: center; 220 | } 221 | 222 | .titleGhub { 223 | display: block; 224 | position: relative; 225 | 226 | height: 50px; 227 | 228 | margin-top: 10px; 229 | } 230 | 231 | .headerBtnsHolder { 232 | display: block; 233 | position: absolute; 234 | 235 | vertical-align: center; 236 | line-height: 155px; 237 | 238 | bottom: 0px; 239 | right: 0px; 240 | left: 410px; 241 | 242 | height: 140px; 243 | 244 | white-space: nowrap; 245 | } 246 | 247 | .headerBtn { 248 | display: inline-block; 249 | position: relative; 250 | 251 | width: 100px; 252 | height: 140px; 253 | 254 | margin-right: 20px; 255 | 256 | align-self: center; 257 | } 258 | 259 | .headerBtnImage { 260 | display: block; 261 | position: relative; 262 | 263 | width: 100px; 264 | height: 100px; 265 | 266 | background-repeat: no-repeat; 267 | background-size: 100%; 268 | } 269 | 270 | .filler { 271 | display: block; 272 | position: absolute; 273 | 274 | top: 0px; 275 | left: 0px; 276 | 277 | width: 100%; 278 | height: 100%; 279 | } -------------------------------------------------------------------------------- /database/setup.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // Configures the SQL database for Gopher Game Server 9 | func setUp() error { 10 | // Check if the users table has been created 11 | _, checkErr := database.Exec("SELECT " + usersColumnName + " FROM " + tableUsers + " WHERE " + usersColumnID + "=1;") 12 | if checkErr != nil { 13 | fmt.Println("Creating \"" + tableUsers + "\" table...") 14 | // Make the users table 15 | if cErr := createUserTableSQL(); cErr != nil { 16 | return cErr 17 | } 18 | } 19 | // Check for new custom AccountInfoColumn items 20 | if newItemsErr := addNewCustomItemsSQL(); newItemsErr != nil { 21 | return newItemsErr 22 | } 23 | 24 | if rememberMe { 25 | // Check if autologs table has been made 26 | _, checkErr := database.Exec("SELECT " + autologsColumnID + " FROM " + tableAutologs + " WHERE " + autologsColumnID + "=1;") 27 | if checkErr != nil { 28 | fmt.Println("Making autologs table...") 29 | if cErr := createAutologsTableSQL(); cErr != nil { 30 | return cErr 31 | } 32 | } 33 | } 34 | // Make sure customLoginColumn is unique if it is set 35 | if len(customLoginColumn) > 0 { 36 | _, alterErr := database.Exec("ALTER TABLE " + tableUsers + " ADD UNIQUE (" + customLoginColumn + ");") 37 | if alterErr != nil { 38 | return alterErr 39 | } 40 | } 41 | 42 | // 43 | return nil 44 | } 45 | 46 | func createUserTableSQL() error { 47 | createQuery := "CREATE TABLE " + tableUsers + " (" + 48 | usersColumnID + " INTEGER NOT NULL AUTO_INCREMENT, " + 49 | usersColumnName + " VARCHAR(255) UNIQUE NOT NULL, " + 50 | usersColumnPassword + " VARCHAR(255) NOT NULL, " 51 | 52 | // Append custom AccountInfoColumn items 53 | for key, val := range customAccountInfo { 54 | createQuery = createQuery + key + " " + dataTypes[val.dataType] 55 | // Check for maxSize/precision 56 | if isSizeDataType(val.dataType) { 57 | createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ")" 58 | } else if isPrecisionDataType(val.dataType) { 59 | createQuery = createQuery + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" 60 | } 61 | // Check for unique 62 | if val.unique { 63 | createQuery = createQuery + " UNIQUE" 64 | } 65 | // Check for not-null 66 | if val.notNull { 67 | createQuery = createQuery + " NOT NULL, " 68 | } else { 69 | createQuery = createQuery + ", " 70 | } 71 | } 72 | 73 | createQuery = createQuery + "PRIMARY KEY (" + usersColumnID + "));" 74 | 75 | // Execute users table query 76 | _, createErr := database.Exec(createQuery) 77 | if createErr != nil { 78 | return createErr 79 | } 80 | 81 | // Adjust auto-increment to 1 82 | _, adjustErr := database.Exec("ALTER TABLE " + tableUsers + " AUTO_INCREMENT=1;") 83 | if adjustErr != nil { 84 | return adjustErr 85 | } 86 | 87 | // Make friends table 88 | if _, friendsErr := database.Exec("CREATE TABLE " + tableFriends + " (" + 89 | friendsColumnUser + " INTEGER NOT NULL, " + 90 | friendsColumnFriend + " INTEGER NOT NULL, " + 91 | friendsColumnStatus + " INTEGER NOT NULL" + 92 | ");"); friendsErr != nil { 93 | 94 | return friendsErr 95 | } 96 | 97 | if rememberMe { 98 | if cErr := createAutologsTableSQL(); cErr != nil { 99 | return cErr 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func createAutologsTableSQL() error { 107 | if _, aErr := database.Exec("CREATE TABLE " + tableAutologs + " (" + 108 | autologsColumnID + " INTEGER NOT NULL, " + 109 | autologsColumnDevicePass + " VARCHAR(255) NOT NULL, " + 110 | autologsColumnDeviceTag + " VARCHAR(255) NOT NULL, " + 111 | ");"); aErr != nil { 112 | 113 | return aErr 114 | } 115 | return nil 116 | } 117 | 118 | func addNewCustomItemsSQL() error { 119 | query := "ALTER TABLE " + tableUsers + " " 120 | var execQuery bool 121 | // 122 | for key, val := range customAccountInfo { 123 | // Check if item exists 124 | checkRows, err := database.Query("SHOW COLUMNS FROM " + tableUsers + " LIKE '" + key + "';") 125 | if err != nil { 126 | return err 127 | } 128 | // 129 | checkRows.Next() 130 | _, colsErr := checkRows.Columns() 131 | if colsErr != nil { 132 | // The item doesn't exist yet... 133 | fmt.Println("Adding AccountInfoColumn '" + key + "'...") 134 | query = query + "ADD COLUMN " + key + " " + dataTypes[val.dataType] 135 | if isSizeDataType(val.dataType) { 136 | query = query + "(" + strconv.Itoa(val.maxSize) + ")" 137 | } else if isPrecisionDataType(val.dataType) { 138 | query = query + "(" + strconv.Itoa(val.maxSize) + ", " + strconv.Itoa(val.precision) + ")" 139 | } 140 | // Unique check 141 | if val.unique { 142 | query = query + " UNIQUE" 143 | } 144 | // Not-null check 145 | if val.notNull { 146 | query = query + " NOT NULL, " 147 | } else { 148 | query = query + ", " 149 | } 150 | execQuery = true 151 | } 152 | checkRows.Close() 153 | } 154 | if execQuery { 155 | // Make new columns 156 | query = query[0:len(query)-2] + ";" 157 | _, colsErr := database.Exec(query) 158 | if colsErr != nil { 159 | return colsErr 160 | } 161 | } 162 | 163 | return nil 164 | } -------------------------------------------------------------------------------- /helpers/errors.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | // GopherError is used when sending an error message to the client API. 4 | type GopherError struct { 5 | Message string 6 | ID int 7 | } 8 | 9 | // Client response message error IDs 10 | const ( 11 | ErrorGopherInvalidAction = iota + 1001 // 1001. Invalid client action 12 | ErrorGopherIncorrectFormat // 1002. The client's data is not a map/object 13 | ErrorGopherIncorrectCustomAction // 1003. Incorrect custom client action type 14 | ErrorGopherNotLoggedIn // 1004. The client must be logged in to take action 15 | ErrorGopherLoggedIn // 1005. The client must be logged out to take action 16 | ErrorGopherStatusChange // 1006. Error while changing User's status 17 | ErrorGopherFeatureDisabled // 1007. A server feature must be explicitly enabled to take action 18 | ErrorGopherColumnsFormat // 1008. The client's custom columns data is not a map/object 19 | ErrorGopherNameFormat // 1009. The client's user name data is not a string 20 | ErrorGopherPasswordFormat // 1010. The client's password data is not a string 21 | ErrorGopherRememberFormat // 1011. The client's remember-me data is not a boolean 22 | ErrorGopherGuestFormat // 1012. The client's guest data is not a boolean 23 | ErrorGopherNewPasswordFormat // 1013. The client's new password data is not a string 24 | ErrorGopherRoomNameFormat // 1014. The client's room name data is not a string 25 | ErrorGopherRoomTypeFormat // 1015. The client's room type data is not a string 26 | ErrorGopherPrivateFormat // 1016. The client's private room data is not a boolean 27 | ErrorGopherMaxRoomFormat // 1017. The client's maximum room capacity data is not an integer 28 | ErrorGopherRoomControl // 1018. Clients do not have the ability to control rooms 29 | ErrorGopherServerRoom // 1019. The room type specified can only be made by the server 30 | ErrorGopherNotOwner // 1020. The client must be the owner of the room to take action 31 | ErrorGopherLogin // 1021. There was an error logging in 32 | ErrorGopherSignUp // 1022. There was an error signing up 33 | ErrorGopherJoin // 1023. There was an error joining a room 34 | ErrorGopherLeave // 1024. There was an error leaving a room 35 | ErrorGopherCreateRoom // 1025. There was an error creating a room 36 | ErrorGopherDeleteRoom // 1026. There was an error deleting a room 37 | ErrorGopherInvite // 1027. There was an error inviting User to a room 38 | ErrorGopherRevokeInvite // 1028. There was an error revoking a User's invitation to a room 39 | ErrorGopherFriendRequest // 1029. There was an error sending a friend request 40 | ErrorGopherFriendAccept // 1030. There was an error accepting a friend request 41 | ErrorGopherFriendDecline // 1031. There was an error declining a friend request 42 | ErrorGopherFriendRemove // 1032. There was an error removing a friend 43 | 44 | // Authentication 45 | ErrorAuthUnexpected // 1033. There was an unexpected authorization error 46 | ErrorAuthAlreadyLogged // 1034. The client is already logged in 47 | ErrorAuthRequiredName // 1035. A user name is required 48 | ErrorAuthRequiredPass // 1036. A password is required 49 | ErrorAuthRequiredNewPass // 1037. A new password is required 50 | ErrorAuthRequiredID // 1038. An account id is required 51 | ErrorAuthRequiredSocket // 1039. A client socket pointer is required 52 | ErrorAuthNameUnavail // 1040. The user name is unavailable 53 | ErrorAuthMaliciousChars // 1041. There are malicious characters in the client's request variables 54 | ErrorAuthIncorrectCols // 1042. The client supplied incorrect custom account info column data 55 | ErrorAuthInsufficientCols // 1043. The client supplied an insufficient amount of custom account info columns 56 | ErrorAuthEncryption // 1044. There was an error while encrypting data 57 | ErrorAuthQuery // 1045. There was an error while querying the database 58 | ErrorAuthIncorrectLogin // 1046. The client supplied an incorrect login or password 59 | ErrorDatabaseInvalidAutolog // 1047. The client supplied incorrect auto-login (remember me) data 60 | ErrorAuthConversion // 1048. There was an error while converting data to be stored on the database 61 | 62 | // Misc errors 63 | ErrorActionDenied // 1049. A callback has denied the server action 64 | ErrorServerPaused // 1050. The server is paused 65 | ) 66 | 67 | // NewError creates a new GopherError. 68 | func NewError(message string, id int) GopherError { 69 | return GopherError{Message: message, ID: id} 70 | } 71 | 72 | // NoError creates a new GopherError that represents a state in which no error occurred. 73 | func NoError() GopherError { 74 | return GopherError{} 75 | } 76 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | // Package database contains helpers for customizing your database with the SQL features enabled. 2 | // It mostly contains a bunch of mixed Gopher Server only functions and customizing methods. 3 | // It would probably be easier to take a look at the database usage section on the Github page 4 | // for the project before looking through here for more info. 5 | package database 6 | 7 | import ( 8 | "database/sql" 9 | "errors" 10 | "fmt" 11 | _ "github.com/go-sql-driver/mysql" // Github project page specifies to use blank import 12 | "strconv" 13 | ) 14 | 15 | var ( 16 | //THE DATABASE 17 | database *sql.DB 18 | 19 | //SERVER SETTINGS 20 | serverStarted bool = false 21 | serverPaused bool = false 22 | rememberMe bool = false 23 | databaseName string = "gopherDB" 24 | inited bool = false 25 | ) 26 | 27 | //TABLE & COLUMN NAMES 28 | const ( 29 | tableUsers = "users" 30 | tableFriends = "friends" 31 | tableAutologs = "autologs" 32 | 33 | //users TABLE COLUMNS 34 | usersColumnID = "_id" 35 | usersColumnName = "name" 36 | usersColumnPassword = "pass" 37 | 38 | //friends TABLE COLUMNS 39 | friendsColumnUser = "user" 40 | friendsColumnFriend = "friend" 41 | friendsColumnStatus = "status" 42 | 43 | //autologs TABLE COLUMNS 44 | autologsColumnID = "_id" 45 | autologsColumnDeviceTag = "dn" 46 | autologsColumnDevicePass = "da" 47 | ) 48 | 49 | // Init initializes the database connection and sets up the database according to your custom parameters. 50 | // 51 | // WARNING: This is only meant for internal Gopher Game Server mechanics. If you want to enable SQL authorization 52 | // and friending, use the EnableSqlFeatures and corresponding options in ServerSetting. 53 | func Init(userName string, password string, dbName string, protocol string, ip string, port int, encryptCost int, remMe bool, custLoginCol string) error { 54 | if inited { 55 | return errors.New("sql package is already initialized") 56 | } else if len(userName) == 0 { 57 | return errors.New("sql.Start() requires a user name") 58 | } else if len(password) == 0 { 59 | return errors.New("sql.Start() requires a password") 60 | } else if len(userName) == 0 { 61 | return errors.New("sql.Start() requires a database name") 62 | } else if len(custLoginCol) > 0 { 63 | if _, ok := customAccountInfo[custLoginCol]; !ok { 64 | return errors.New("The AccountInfoColumn '" + custLoginCol + "' does not exist. Use database.NewAccountInfoColumn() to make a column with that name.") 65 | } 66 | customLoginColumn = custLoginCol 67 | } 68 | 69 | if encryptCost >= 4 && encryptCost <= 31 { 70 | encryptionCost = encryptCost 71 | } else if encryptCost != 0 { 72 | fmt.Println("EncryptionCost must be a minimum of 4, and max of 31. Setting to default: 4") 73 | } 74 | 75 | rememberMe = remMe 76 | 77 | var err error 78 | 79 | //OPEN THE DATABASE 80 | database, err = sql.Open("mysql", userName+":"+password+"@"+protocol+"("+ip+":"+strconv.Itoa(port)+")/"+dbName) 81 | if err != nil { 82 | return err 83 | } 84 | //NOTE: Open doesn't open a connection. 85 | //MUST PING TO CHECK IF FOUND DATABASE 86 | err = database.Ping() 87 | if err != nil { 88 | return errors.New("Could not connect to database!") 89 | } 90 | 91 | if len(dbName) != 0 { 92 | databaseName = dbName 93 | } 94 | 95 | //CONFIGURE DATABASE 96 | err = setUp() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // 102 | inited = true 103 | 104 | // 105 | return nil 106 | } 107 | 108 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 109 | // GET User's DATABASE INDEX ///////////////////////////////////////////////////////////////////// 110 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 111 | 112 | // GetUserDatabaseIndex gets the database index of a User by their name. 113 | func GetUserDatabaseIndex(userName string) (int, error) { 114 | if checkStringSQLInjection(userName) { 115 | return 0, errors.New("Malicious characters detected") 116 | } 117 | var id int 118 | rows, err := database.Query("SELECT " + usersColumnID + " FROM " + tableUsers + " WHERE " + usersColumnName + "=\"" + userName + "\" LIMIT 1;") 119 | if err != nil { 120 | return 0, err 121 | } 122 | // 123 | rows.Next() 124 | if scanErr := rows.Scan(&id); scanErr != nil { 125 | rows.Close() 126 | return 0, scanErr 127 | } 128 | rows.Close() 129 | 130 | // 131 | return id, nil 132 | } 133 | 134 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 135 | // SERVER STARTUP FUNCTIONS /////////////////////////////////////////////////////////////////////////////////// 136 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 137 | 138 | // SetServerStarted is for Gopher Game Server internal mechanics only. 139 | func SetServerStarted(val bool) { 140 | if !serverStarted { 141 | serverStarted = val 142 | } 143 | } 144 | 145 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 146 | // SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// 147 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 148 | 149 | // Pause is only for internal Gopher Game Server mechanics. 150 | func Pause() { 151 | if !serverPaused { 152 | serverPaused = true 153 | serverStarted = false 154 | } 155 | } 156 | 157 | // Resume is only for internal Gopher Game Server mechanics. 158 | func Resume() { 159 | if serverPaused { 160 | serverStarted = true 161 | serverPaused = false 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | // Package core contains all the tools to make and work with Users and Rooms. 2 | // 3 | // A User is a client who has successfully logged into the server. You can think of clients who are not attached to a User 4 | // as, for instance, someone in the login screen, but are still connected to the server. A client doesn't 5 | // 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). 6 | // 7 | // Users have their own variables which can be accessed and changed anytime. A User variable can 8 | // be anything compatible with interface{}, so pretty much anything. 9 | // 10 | // 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 11 | // from ServerSettings. The server's name that will be used for ownership of private Rooms can be set with the ServerSettings 12 | // 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 13 | // from logging in. Public Rooms will accept a join request from any User, and private Rooms will only 14 | // 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 15 | // Users to a private Room. But remember, just because a User owns a private room doesn't mean the server cannot also invite 16 | // to the room via *Room.AddInvite() function. 17 | // 18 | // Rooms have their own variables which can be accessed and changed anytime. Like User variables, a Room variable can 19 | // be anything compatible with interface{}. 20 | package core 21 | 22 | import ( 23 | "github.com/hewiefreeman/GopherGameServer/helpers" 24 | ) 25 | 26 | var ( 27 | serverStarted bool 28 | serverPaused bool 29 | 30 | serverName string 31 | kickOnLogin bool 32 | sqlFeatures bool 33 | rememberMe bool 34 | multiConnect bool 35 | maxUserConns uint8 36 | deleteRoomOnLeave bool = true 37 | ) 38 | 39 | // RoomRecoveryState is used internally for persisting room states on shutdown. 40 | type RoomRecoveryState struct { 41 | T string // rType 42 | P bool // private 43 | O string // owner 44 | M int // maxUsers 45 | I []string // inviteList 46 | V map[string]interface{} // vars 47 | } 48 | 49 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 50 | // SERVER STARTUP FUNCTIONS ////////////////////////////////////////////////////////////////////// 51 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 52 | 53 | // SetServerStarted is for Gopher Game Server internal mechanics only. 54 | func SetServerStarted(val bool) { 55 | if !serverStarted { 56 | serverStarted = val 57 | } 58 | } 59 | 60 | // SettingsSet is for Gopher Game Server internal mechanics only. 61 | func SettingsSet(kickDups bool, name string, deleteOnLeave bool, sqlFeat bool, remMe bool, multiConn bool, maxConns uint8) { 62 | if !serverStarted { 63 | kickOnLogin = kickDups 64 | serverName = name 65 | sqlFeatures = sqlFeat 66 | rememberMe = remMe 67 | multiConnect = multiConn 68 | maxUserConns = maxConns 69 | deleteRoomOnLeave = deleteOnLeave 70 | } 71 | } 72 | 73 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 74 | // SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// 75 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 76 | 77 | // Pause is only for internal Gopher Game Server mechanics. 78 | func Pause() { 79 | if !serverPaused { 80 | serverPaused = true 81 | 82 | // 83 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) 84 | usersMux.Lock() 85 | for _, user := range users { 86 | user.mux.Lock() 87 | for connID, conn := range user.conns { 88 | //REMOVE CONNECTION FROM THEIR ROOM 89 | currRoom := conn.room 90 | if currRoom != nil && currRoom.Name() != "" { 91 | user.mux.Unlock() 92 | currRoom.RemoveUser(user, connID) 93 | user.mux.Lock() 94 | } 95 | 96 | //LOG CONNECTION OUT 97 | conn.clientMux.Lock() 98 | if *(conn.user) != nil { 99 | *(conn.user) = nil 100 | } 101 | conn.clientMux.Unlock() 102 | 103 | //SEND LOG OUT MESSAGE 104 | conn.socket.WriteJSON(clientResp) 105 | } 106 | user.mux.Unlock() 107 | } 108 | users = make(map[string]*User) 109 | usersMux.Unlock() 110 | } 111 | } 112 | 113 | // Resume is only for internal Gopher Game Server mechanics. 114 | func Resume() { 115 | if serverPaused { 116 | serverPaused = false 117 | } 118 | } 119 | 120 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 121 | // GET STATES FOR GENERATING RECOVERY FILE //////////////////////////////////////////////////////////////////// 122 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 123 | 124 | // GetRoomsState is only for internal Gopher Game Server mechanics. 125 | func GetRoomsState() map[string]RoomRecoveryState { 126 | state := make(map[string]RoomRecoveryState) 127 | roomsMux.Lock() 128 | for _, room := range rooms { 129 | room.mux.Lock() 130 | state[room.name] = RoomRecoveryState{ 131 | T: room.rType, 132 | P: room.private, 133 | O: room.owner, 134 | M: room.maxUsers, 135 | I: room.inviteList, 136 | V: room.vars, 137 | } 138 | room.mux.Unlock() 139 | } 140 | roomsMux.Unlock() 141 | // 142 | return state 143 | } 144 | -------------------------------------------------------------------------------- /core/vars.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/hewiefreeman/GopherGameServer/helpers" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 | // USER VARIABLES ///////////////////////////////////////////////////////////////////////////////////////////// 10 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | // SetVariable sets a User variable. The client API of the User will also receive these changes. If you are using MultiConnect in ServerSettings, the connID 13 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 14 | // be provided when setting a User's variables with MultiConnect enabled. Otherwise, an empty string can be used. 15 | func (u *User) SetVariable(key string, value interface{}, connID string) { 16 | //REJECT INCORRECT INPUT 17 | if len(key) == 0 { 18 | return 19 | } else if multiConnect && len(connID) == 0 { 20 | return 21 | } else if !multiConnect { 22 | connID = "1" 23 | } 24 | 25 | // Set the variable 26 | u.mux.Lock() 27 | if _, ok := u.conns[connID]; !ok { 28 | u.mux.Unlock() 29 | return 30 | } 31 | (*u.conns[connID]).vars[key] = value 32 | socket := (*u.conns[connID]).socket 33 | u.mux.Unlock() 34 | 35 | //MAKE CLIENT MESSAGE 36 | resp := map[string]interface{}{ 37 | "k": key, 38 | "v": value, 39 | } 40 | clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariable, resp, helpers.NoError()) 41 | 42 | //SEND RESPONSE TO CLIENT 43 | socket.WriteJSON(clientResp) 44 | } 45 | 46 | // 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 47 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 48 | // be provided when setting a User's variables with MultiConnect enabled. Otherwise, an empty string can be used. 49 | func (u *User) SetVariables(values map[string]interface{}, connID string) { 50 | //REJECT INCORRECT INPUT 51 | if values == nil || len(values) == 0 { 52 | return 53 | } else if multiConnect && len(connID) == 0 { 54 | return 55 | } else if !multiConnect { 56 | connID = "1" 57 | } 58 | 59 | // Set the variables 60 | u.mux.Lock() 61 | if _, ok := u.conns[connID]; !ok { 62 | u.mux.Unlock() 63 | return 64 | } 65 | for key, val := range values { 66 | (*u.conns[connID]).vars[key] = val 67 | } 68 | socket := (*u.conns[connID]).socket 69 | u.mux.Unlock() 70 | 71 | //SEND RESPONSE TO CLIENT 72 | clientResp := helpers.MakeClientResponse(helpers.ClientActionSetVariables, values, helpers.NoError()) 73 | socket.WriteJSON(clientResp) 74 | 75 | } 76 | 77 | // GetVariable gets one of the User's variables by it's key. If you are using MultiConnect in ServerSettings, the connID 78 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 79 | // be provided when getting a User's variables with MultiConnect enabled. Otherwise, an empty string can be used. 80 | func (u *User) GetVariable(key string, connID string) interface{} { 81 | //REJECT INCORRECT INPUT 82 | if len(key) == 0 { 83 | return nil 84 | } 85 | 86 | u.mux.Lock() 87 | val := (*u.conns[connID]).vars[key] 88 | u.mux.Unlock() 89 | 90 | // 91 | return val 92 | } 93 | 94 | // GetVariables gets the specified (or all if nil) User variables as a map[string]interface{}. If you are using MultiConnect in ServerSettings, the connID 95 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 96 | // be provided when getting a User's variables with MultiConnect enabled. Otherwise, an empty string can be used. 97 | func (u *User) GetVariables(keys []string, connID string) map[string]interface{} { 98 | var value map[string]interface{} = make(map[string]interface{}) 99 | if keys == nil || len(keys) == 0 { 100 | u.mux.Lock() 101 | value = (*u.conns[connID]).vars 102 | u.mux.Unlock() 103 | } else { 104 | u.mux.Lock() 105 | for i := 0; i < len(keys); i++ { 106 | value[keys[i]] = (*u.conns[connID]).vars[keys[i]] 107 | } 108 | u.mux.Unlock() 109 | } 110 | 111 | // 112 | return value 113 | } 114 | 115 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 116 | // ROOM VARIABLES ///////////////////////////////////////////////////////////////////////////////////////////// 117 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 118 | 119 | // SetVariable sets a Room variable. 120 | func (r *Room) SetVariable(key string, value interface{}) { 121 | //REJECT INCORRECT INPUT 122 | if len(key) == 0 { 123 | return 124 | } 125 | 126 | r.mux.Lock() 127 | if r.usersMap == nil { 128 | r.mux.Unlock() 129 | return 130 | } 131 | r.vars[key] = value 132 | r.mux.Unlock() 133 | 134 | // 135 | return 136 | } 137 | 138 | // SetVariables sets all the specified Room variables at once. 139 | func (r *Room) SetVariables(values map[string]interface{}) { 140 | r.mux.Lock() 141 | if r.usersMap == nil { 142 | r.mux.Unlock() 143 | return 144 | } 145 | for key, val := range values { 146 | r.vars[key] = val 147 | } 148 | r.mux.Unlock() 149 | 150 | // 151 | return 152 | } 153 | 154 | // GetVariable gets one of the Room's variables. 155 | func (r *Room) GetVariable(key string) (interface{}, error) { 156 | //REJECT INCORRECT INPUT 157 | if len(key) == 0 { 158 | return nil, errors.New("*Room.GetVariable() requires a key") 159 | } 160 | 161 | r.mux.Lock() 162 | if r.usersMap == nil { 163 | r.mux.Unlock() 164 | return nil, errors.New("Room '" + r.name + "' does not exist") 165 | } 166 | value := r.vars[key] 167 | r.mux.Unlock() 168 | 169 | // 170 | return value, nil 171 | } 172 | 173 | // GetVariables gets all the specified (or all if not) Room variables as a map[string]interface{}. 174 | func (r *Room) GetVariables(keys []string) (map[string]interface{}, error) { 175 | var value map[string]interface{} = make(map[string]interface{}) 176 | r.mux.Lock() 177 | if r.usersMap == nil { 178 | r.mux.Unlock() 179 | return nil, errors.New("Room '" + r.name + "' does not exist") 180 | } 181 | if keys == nil || len(keys) == 0 { 182 | value = r.vars 183 | } else { 184 | for i := 0; i < len(keys); i++ { 185 | value[keys[i]] = r.vars[keys[i]] 186 | } 187 | } 188 | r.mux.Unlock() 189 | 190 | // 191 | return value, nil 192 | } 193 | -------------------------------------------------------------------------------- /database/accountInfoColumn.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // AccountInfoColumn is the representation of an extra column on the users table that you can define. You can define as many 11 | // as you want. These work with the ServerCallbacks and client APIs to provide you with information on data retrieved from 12 | // the database when the corresponding callback is triggered. 13 | // 14 | // You can make an AccountInfoColumn unique, which means when someone tries to update or insert into a unique column, the server 15 | // will first check if any other row has that same value in that unique column. If a unique column cannot be updated because another 16 | // row has the same value, an error will be sent back to the client. Keep in mind, this is an expensive task and should be used lightly, 17 | // mainly for extra authentication. 18 | type AccountInfoColumn struct { 19 | dataType int 20 | maxSize int 21 | precision int 22 | notNull bool 23 | unique bool 24 | encrypt bool 25 | } 26 | 27 | var ( 28 | customAccountInfo map[string]AccountInfoColumn = make(map[string]AccountInfoColumn) 29 | ) 30 | 31 | // MySQL database data types. Use one of these when making a new AccountInfoColumn or 32 | // CustomTable's columns. The parentheses next to a type indicate it requires a maximum 33 | // size when making a column of that type. Two pairs of parentheses means it requires a 34 | // decimal precision number as well a max size. 35 | const ( 36 | //NUMERIC TYPES 37 | DataTypeTinyInt = iota // TINYINT() 38 | DataTypeSmallInt // SMALLINT() 39 | DataTypeMediumInt // MEDIUMINT() 40 | DataTypeInt // INTEGER() 41 | DataTypeFloat // FLOAT()() 42 | DataTypeDouble // DOUBLE()() 43 | DataTypeDecimal // DECIMAL()() 44 | DataTypeBigInt // BIGINT() 45 | 46 | //CHARACTER TYPES 47 | DataTypeChar // CHAR() 48 | DataTypeVarChar // VARCHAR() 49 | DataTypeNationalVarChar // NVARCHAR() 50 | DataTypeJSON // JSON 51 | 52 | //TEXT TYPES 53 | DataTypeTinyText // TINYTEXT 54 | DataTypeMediumText // MEDIUMTEXT 55 | DataTypeText // TEXT() 56 | DataTypeLongText // LONGTEXT 57 | 58 | //DATE TYPES 59 | DataTypeDate // DATE 60 | DataTypeDateTime // DATETIME() 61 | DataTypeTime // TIME() 62 | DataTypeTimeStamp // TIMESTAMP() 63 | DataTypeYear // YEAR() 64 | 65 | //BINARY TYPES 66 | DataTypeTinyBlob // TINYBLOB 67 | DataTypeMediumBlob // MEDIUMBLOB 68 | DataTypeBlob // BLOB() 69 | DataTypeLongBlob // LONGBLOB 70 | DataTypeBinary // BINARY() 71 | DataTypeVarBinary // VARBINARY() 72 | 73 | //OTHER TYPES 74 | DataTypeBit // BIT() 75 | DataTypeENUM // ENUM() 76 | DataTypeSet // SET() 77 | ) 78 | 79 | var ( 80 | //DATA TYPES THAT REQUIRE A SIZE 81 | dataTypesSize []string = []string{ 82 | "TINYINT", 83 | "SMALLINT", 84 | "MEDIUMINT", 85 | "INTEGER", 86 | "BIGINT", 87 | "CHAR", 88 | "VARCHAR", 89 | "NVARCHAR", 90 | "TEXT", 91 | "DATETIME", 92 | "TIME", 93 | "TIMESTAMP", 94 | "YEAR", 95 | "BLOB", 96 | "BINARY", 97 | "VARBINARY", 98 | "BIT", 99 | "ENUM", 100 | "SET"} 101 | 102 | //DATA TYPES THAT REQUIRE A SIZE AND PRECISION 103 | dataTypesPrecision []string = []string{ 104 | "FLOAT", 105 | "DOUBLE", 106 | "DECIMAL"} 107 | 108 | //DATA TYPE LITERAL NAME LIST 109 | dataTypes []string = []string{ 110 | "TINYINT", 111 | "SMALLINT", 112 | "MEDIUMINT", 113 | "INTEGER", 114 | "FLOAT", 115 | "DOUBLE", 116 | "DECIMAL", 117 | "BIG INT", 118 | "CHAR", 119 | "VARCHAR", 120 | "NVARCHAR", 121 | "JSON", 122 | "TINYTEXT", 123 | "MEDIUMTEXT", 124 | "TEXT", 125 | "LONGTEXT", 126 | "DATE", 127 | "DATETIME", 128 | "TIME", 129 | "TIMESTAMP", 130 | "YEAR", 131 | "TINYBLOB", 132 | "MEDIUMBLOB", 133 | "BLOB", 134 | "LONGBLOB", 135 | "BINARY", 136 | "VARBINARY", 137 | "BIT", 138 | "ENUM", 139 | "SET"} 140 | ) 141 | 142 | // NewAccountInfoColumn makes a new AccountInfoColumn. You can only make new AccountInfoColumns before starting the server. 143 | func NewAccountInfoColumn(name string, dataType int, maxSize int, precision int, notNull bool, unique bool, encrypt bool) error { 144 | if serverStarted { 145 | return errors.New("You can't make a new AccountInfoColumn after the server has started") 146 | } else if len(name) == 0 { 147 | return errors.New("database.NewAccountInfoColumn() requires a name") 148 | } else if dataType < 0 || dataType > len(dataTypes)-1 { 149 | return errors.New("Incorrect data type") 150 | } else if checkStringSQLInjection(name) { 151 | return errors.New("Malicious characters detected") 152 | } 153 | 154 | if isSizeDataType(dataType) && maxSize == 0 { 155 | return errors.New("The data type '" + dataTypesSize[dataType] + "' requires a max size") 156 | } else if isPrecisionDataType(dataType) && (maxSize == 0 || precision == 0) { 157 | return errors.New("The data type '" + dataTypesSize[dataType] + "' requires a max size and precision") 158 | } 159 | 160 | customAccountInfo[name] = AccountInfoColumn{dataType: dataType, maxSize: maxSize, precision: precision, notNull: notNull, unique: unique, encrypt: encrypt} 161 | 162 | // 163 | return nil 164 | } 165 | 166 | //CHECKS IF THE DATA TYPE REQUIRES A MAX SIZE 167 | func isSizeDataType(dataType int) bool { 168 | for i := 0; i < len(dataTypesSize); i++ { 169 | if dataTypes[dataType] == dataTypesSize[i] { 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | 176 | //CHECKS IF THE DATA TYPE REQUIRES A MAX SIZE 177 | func isPrecisionDataType(dataType int) bool { 178 | for i := 0; i < len(dataTypesPrecision); i++ { 179 | if dataTypes[dataType] == dataTypesPrecision[i] { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | 186 | //CONVERTS DATA TYPES TO STRING FOR SQL QUERIES 187 | func convertDataToString(dataType string, data interface{}) (string, error) { 188 | switch data.(type) { 189 | case int: 190 | if dataType != "INTEGER" && dataType != "TINYINT" && dataType != "MEDIUMINT" && dataType != "BIGINT" && dataType != "SMALLINT" { 191 | return "", errors.New("Mismatched data types") 192 | } 193 | return strconv.Itoa(data.(int)), nil 194 | 195 | case float32: 196 | if dataType != "REAL" && dataType != "FLOAT" && dataType != "DOUBLE" && dataType != "DECIMAL" { 197 | return "", errors.New("Mismatched data types") 198 | } 199 | return fmt.Sprintf("%f", data.(float32)), nil 200 | 201 | case float64: 202 | if dataType != "REAL" && dataType != "FLOAT" && dataType != "DOUBLE" && dataType != "DECIMAL" { 203 | return "", errors.New("Mismatched data types") 204 | } 205 | return strconv.FormatFloat(data.(float64), 'f', -1, 64), nil 206 | 207 | case string: 208 | if dataType != "CHAR" && dataType != "VARCHAR" && dataType != "NVARCHAR" && dataType != "JSON" && dataType != "TEXT" && 209 | dataType != "TINYTEXT" && dataType != "MEDIUMTEXT" && dataType != "LONGTEXT" && dataType != "DATE" && 210 | dataType != "DATETIME" && dataType != "TIME" && dataType != "TIMESTAMP" && dataType != "YEAR" { 211 | return "", errors.New("Mismatched data types") 212 | } else if checkStringSQLInjection(data.(string)) { 213 | return "", errors.New("Malicious characters detected") 214 | } 215 | return "\"" + data.(string) + "\"", nil 216 | 217 | default: 218 | return "", errors.New("Data type is not supported. You can open an issue on GitHub to request support for an unsupported SQL data type.") 219 | } 220 | } 221 | 222 | //CHECKS IF THERE ARE ANY MALICIOUS CHARACTERS IN A STRING 223 | func checkStringSQLInjection(inputStr string) bool { 224 | return (strings.Contains(inputStr, "\"") || strings.Contains(inputStr, ")") || strings.Contains(inputStr, "(") || strings.Contains(inputStr, ";")) 225 | } 226 | -------------------------------------------------------------------------------- /database/friending.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // Friend represents a client's friend. A friend has a User name, a database index reference, and a status. 8 | // Their status could be FriendStatusRequested, FriendStatusPending, or FriendStatusAccepted (0, 1, or 2). If a User has a Friend 9 | // with the status FriendStatusRequested, they need to accept the request. If a User has a Friend 10 | // with the status FriendStatusPending, that friend has not yet accepted their request. If a User has a Friend 11 | // with the status FriendStatusAccepted, that friend is indeed a friend. 12 | type Friend struct { 13 | name string 14 | dbID int 15 | status int 16 | } 17 | 18 | // The three statuses a Friend could be: requested, pending, or accepted (0, 1, and 2). If a User has a Friend 19 | // with the status FriendStatusRequested, they need to accept the request. If a User has a Friend 20 | // with the status FriendStatusPending, that friend has not yet accepted their request. If a User has a Friend 21 | // with the status FriendStatusAccepted, that friend is indeed a friend. 22 | const ( 23 | FriendStatusRequested = iota 24 | FriendStatusPending 25 | FriendStatusAccepted 26 | ) 27 | 28 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 29 | // SEND FRIEND REQUEST /////////////////////////////////////////////////////////////////////////// 30 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | // FriendRequest stores the data for a friend request onto the database. 33 | // 34 | // WARNING: This is only meant for internal Gopher Game Server mechanics. Use the client APIs to send a 35 | // friend request when using the SQL features. 36 | func FriendRequest(userIndex int, friendIndex int) error { 37 | _, insertErr := database.Exec("INSERT INTO " + tableFriends + " (" + friendsColumnUser + ", " + friendsColumnFriend + ", " + friendsColumnStatus + ") " + 38 | "VALUES (" + strconv.Itoa(userIndex) + ", " + strconv.Itoa(friendIndex) + ", " + strconv.Itoa(FriendStatusPending) + ");") 39 | if insertErr != nil { 40 | return insertErr 41 | } 42 | _, insertErr = database.Exec("INSERT INTO " + tableFriends + " (" + friendsColumnUser + ", " + friendsColumnFriend + ", " + friendsColumnStatus + ") " + 43 | "VALUES (" + strconv.Itoa(friendIndex) + ", " + strconv.Itoa(userIndex) + ", " + strconv.Itoa(FriendStatusRequested) + ");") 44 | if insertErr != nil { 45 | return insertErr 46 | } 47 | // 48 | return nil 49 | } 50 | 51 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 52 | // ACCEPT FRIEND REQUEST ///////////////////////////////////////////////////////////////////////// 53 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 54 | 55 | // FriendRequestAccepted stores the data for a friend accept onto the database. 56 | // 57 | // WARNING: This is only meant for internal Gopher Game Server mechanics. Use the client APIs to accept a 58 | // friend request when using the SQL features. 59 | func FriendRequestAccepted(userIndex int, friendIndex int) error { 60 | _, updateErr := database.Exec("UPDATE " + tableFriends + " SET " + friendsColumnStatus + "=" + strconv.Itoa(FriendStatusAccepted) + " WHERE (" + friendsColumnUser + "=" + strconv.Itoa(userIndex) + 61 | " AND " + friendsColumnFriend + "=" + strconv.Itoa(friendIndex) + ") OR (" + friendsColumnUser + "=" + strconv.Itoa(friendIndex) + 62 | " AND " + friendsColumnFriend + "=" + strconv.Itoa(userIndex) + ");") 63 | if updateErr != nil { 64 | return updateErr 65 | } 66 | // 67 | return nil 68 | } 69 | 70 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 71 | // REMOVE FRIEND ///////////////////////////////////////////////////////////////////////////////// 72 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 73 | 74 | // RemoveFriend removes the data for a friendship from database. 75 | // 76 | // WARNING: This is only meant for internal Gopher Game Server mechanics. Use the client APIs to remove a 77 | // friend when using the SQL features. 78 | func RemoveFriend(userIndex int, friendIndex int) error { 79 | _, updateErr := database.Exec("DELETE FROM " + tableFriends + " WHERE (" + friendsColumnUser + "=" + strconv.Itoa(userIndex) + " AND " + friendsColumnFriend + "=" + strconv.Itoa(friendIndex) + ") OR (" + 80 | friendsColumnUser + "=" + strconv.Itoa(friendIndex) + " AND " + friendsColumnFriend + "=" + strconv.Itoa(userIndex) + ");") 81 | if updateErr != nil { 82 | return updateErr 83 | } 84 | // 85 | return nil 86 | } 87 | 88 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 89 | // GET FRIENDS ///////////////////////////////////////////////////////////////////////////////// 90 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 91 | 92 | // GetFriends gets a User's frinds list from the database. 93 | // 94 | // WARNING: This is only meant for internal Gopher Game Server mechanics. Use the *User.Friends() function 95 | // instead to avoid errors when using the SQL features. 96 | func GetFriends(userIndex int) (map[string]*Friend, error) { 97 | var friends map[string]*Friend = make(map[string]*Friend) 98 | 99 | //EXECUTE SELECT QUERY 100 | friendRows, friendRowsErr := database.Query("Select " + friendsColumnFriend + ", " + friendsColumnStatus + " FROM " + tableFriends + " WHERE " + friendsColumnUser + "=" + strconv.Itoa(userIndex) + ";") 101 | if friendRowsErr != nil { 102 | return nil, friendRowsErr 103 | } 104 | // 105 | for friendRows.Next() { 106 | var friendName string 107 | var friendID int 108 | var friendStatus int 109 | if scanErr := friendRows.Scan(&friendID, &friendStatus); scanErr != nil { 110 | friendRows.Close() 111 | return nil, scanErr 112 | } 113 | // 114 | friendInfoRows, friendInfoErr := database.Query("Select " + usersColumnName + " FROM " + tableUsers + " WHERE " + usersColumnID + "=" + strconv.Itoa(friendID) + " LIMIT 1;") 115 | if friendInfoErr != nil { 116 | friendRows.Close() 117 | return nil, friendInfoErr 118 | } 119 | friendInfoRows.Next() 120 | if scanErr := friendInfoRows.Scan(&friendName); scanErr != nil { 121 | friendRows.Close() 122 | friendInfoRows.Close() 123 | return nil, scanErr 124 | } 125 | friendInfoRows.Close() 126 | aFriend := Friend{name: friendName, dbID: friendID, status: friendStatus} 127 | friends[friendName] = &aFriend 128 | } 129 | friendRows.Close() 130 | // 131 | return friends, nil 132 | } 133 | 134 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 135 | // MAKE A Friend FROM PARAMETERS ///////////////////////////////////////////////////////////////// 136 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 137 | 138 | // NewFriend makes a new Friend from given parameters. Used for Gopher Game Server inner mechanics only. 139 | func NewFriend(name string, dbID int, status int) *Friend { 140 | nFriend := Friend{name: name, dbID: dbID, status: status} 141 | return &nFriend 142 | } 143 | 144 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 145 | // Friend ATTRIBUTE READERS ////////////////////////////////////////////////////////////////////// 146 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 147 | 148 | // Name gets the User name of the Friend. 149 | func (f *Friend) Name() string { 150 | return f.name 151 | } 152 | 153 | // DatabaseID gets the database index of the Friend. 154 | func (f *Friend) DatabaseID() int { 155 | return f.dbID 156 | } 157 | 158 | // RequestStatus gets the request status of the Friend. Could be either friendStatusRequested or friendStatusAccepted (0 or 1). 159 | func (f *Friend) RequestStatus() int { 160 | return f.status 161 | } 162 | 163 | // SetStatus sets the request status of a Friend. 164 | // 165 | // WARNING: This is only meant for internal Gopher Game Server mechanics. 166 | func (f *Friend) SetStatus(status int) { 167 | f.status = status 168 | } 169 | -------------------------------------------------------------------------------- /core/roomTypes.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | var ( 4 | roomTypes = make(map[string]*RoomType) 5 | ) 6 | 7 | // RoomType represents a type of room a client or the server can make. You can only make and set 8 | // options for a RoomType before starting the server. Doing so at any other time will have no effect 9 | // at all. 10 | type RoomType struct { 11 | serverOnly bool 12 | 13 | voiceChat bool 14 | 15 | broadcastUserEnter bool 16 | broadcastUserLeave bool 17 | 18 | createCallback func(*Room) // roomCreated 19 | deleteCallback func(*Room) // roomDeleted 20 | userEnterCallback func(*Room, *RoomUser) // roomFrom, user 21 | userLeaveCallback func(*Room, *RoomUser) // roomFrom, user 22 | } 23 | 24 | // NewRoomType Adds a RoomType to the server. A RoomType is used in conjunction with it's corresponding callbacks 25 | // and options. You cannot make a Room on the server until you have at least one RoomType to set it to. 26 | // A RoomType requires at least a name and the serverOnly option, which when set to true will prevent 27 | // the client API from being able to create, destroy, invite or revoke an invitation with that RoomType. 28 | // Though you can always make a CustomClientAction to create a Room, initialize it, send requests, etc. 29 | // When making a new RoomType you can chain the broadcasts and callbacks you want for it like so: 30 | // 31 | // rooms.NewRoomType("lobby", true).EnableBroadcastUserEnter().EnableBroadcastUserLeave(). 32 | // .SetCreateCallback(yourFunc).SetDeleteCallback(anotherFunc) 33 | // 34 | func NewRoomType(name string, serverOnly bool) *RoomType { 35 | if len(name) == 0 { 36 | return &RoomType{} 37 | } else if serverStarted { 38 | return &RoomType{} 39 | } 40 | rt := RoomType{ 41 | serverOnly: serverOnly, 42 | 43 | voiceChat: false, 44 | 45 | broadcastUserEnter: false, 46 | broadcastUserLeave: false, 47 | 48 | createCallback: nil, 49 | deleteCallback: nil, 50 | userEnterCallback: nil, 51 | userLeaveCallback: nil} 52 | 53 | roomTypes[name] = &rt 54 | 55 | // 56 | return roomTypes[name] 57 | } 58 | 59 | // GetRoomTypes gets a map of all the RoomTypes. 60 | func GetRoomTypes() map[string]*RoomType { 61 | return roomTypes 62 | } 63 | 64 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | // RoomType SETTERS ////////////////////////////////////////////////////////////////////////////// 66 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 67 | 68 | // EnableVoiceChat Enables voice chat for this RoomType. 69 | // 70 | // Note: You must call this BEFORE starting the server in order for it to take effect. 71 | func (r *RoomType) EnableVoiceChat() *RoomType { 72 | if serverStarted { 73 | return r 74 | } 75 | (*r).voiceChat = true 76 | return r 77 | } 78 | 79 | // EnableBroadcastUserEnter sends an "entry" message to all Users in the Room when another 80 | // User enters the Room. You can capture these messages on the client side easily with the client APIs. 81 | // 82 | // Note: You must call this BEFORE starting the server in order for it to take effect. 83 | func (r *RoomType) EnableBroadcastUserEnter() *RoomType { 84 | if serverStarted { 85 | return r 86 | } 87 | (*r).broadcastUserEnter = true 88 | return r 89 | } 90 | 91 | // EnableBroadcastUserLeave sends a "left" message to all Users in the Room when another 92 | // User leaves the Room. You can capture these messages on the client side easily with the client APIs. 93 | // 94 | // Note: You must call this BEFORE starting the server in order for it to take effect. 95 | func (r *RoomType) EnableBroadcastUserLeave() *RoomType { 96 | if serverStarted { 97 | return r 98 | } 99 | (*r).broadcastUserLeave = true 100 | return r 101 | } 102 | 103 | // SetCreateCallback is executed when someone creates a Room of this RoomType by setting the creation 104 | // callback. Your function must take in a Room object as the parameter which is a reference of the created room. 105 | // 106 | // Note: You must call this BEFORE starting the server in order for it to take effect. 107 | func (r *RoomType) SetCreateCallback(callback func(*Room)) *RoomType { 108 | if serverStarted { 109 | return r 110 | } 111 | (*r).createCallback = callback 112 | return r 113 | } 114 | 115 | // SetDeleteCallback is executed when someone deletes a Room of this RoomType by setting the delete 116 | // callback. Your function must take in a Room object as the parameter which is a reference of the deleted room. 117 | // 118 | // Note: You must call this BEFORE starting the server in order for it to take effect. 119 | func (r *RoomType) SetDeleteCallback(callback func(*Room)) *RoomType { 120 | if serverStarted { 121 | return r 122 | } 123 | (*r).deleteCallback = callback 124 | return r 125 | } 126 | 127 | // SetUserEnterCallback is executed when a User enters a Room of this RoomType by setting the User enter callback. 128 | // Your function must take in a Room and a string as the parameters. The Room is the Room in which the User entered, 129 | // and the string is the name of the User that entered. 130 | // 131 | // Note: You must call this BEFORE starting the server in order for it to take effect. 132 | func (r *RoomType) SetUserEnterCallback(callback func(*Room, *RoomUser)) *RoomType { 133 | if serverStarted { 134 | return r 135 | } 136 | (*r).userEnterCallback = callback 137 | return r 138 | } 139 | 140 | // SetUserLeaveCallback is executed when a User leaves a Room of this RoomType by setting the User leave callback. 141 | // Your function must take in a Room and a string as the parameters. The Room is the Room in which the User left, 142 | // and the string is the name of the User that left. 143 | // 144 | // Note: You must call this BEFORE starting the server in order for it to take effect. 145 | func (r *RoomType) SetUserLeaveCallback(callback func(*Room, *RoomUser)) *RoomType { 146 | if serverStarted { 147 | return r 148 | } 149 | (*r).userLeaveCallback = callback 150 | return r 151 | } 152 | 153 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 154 | // RoomType ATTRIBUTE & CALLBACK READERS ///////////////////////////////////////////////////////// 155 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 156 | 157 | // ServerOnly returns true if the RoomType can only be manipulated by the server. 158 | func (r *RoomType) ServerOnly() bool { 159 | return r.serverOnly 160 | } 161 | 162 | // VoiceChatEnabled returns true if voice chat is enabled for this RoomType 163 | func (r *RoomType) VoiceChatEnabled() bool { 164 | return r.voiceChat 165 | } 166 | 167 | // BroadcastUserEnter returns true if this RoomType has a user entry broadcast 168 | func (r *RoomType) BroadcastUserEnter() bool { 169 | return r.broadcastUserEnter 170 | } 171 | 172 | // BroadcastUserLeave returns true if this RoomType has a user leave broadcast 173 | func (r *RoomType) BroadcastUserLeave() bool { 174 | return r.broadcastUserLeave 175 | } 176 | 177 | // CreateCallback returns the function that this RoomType calls when a Room of this RoomType is created. 178 | func (r *RoomType) CreateCallback() func(*Room) { 179 | return r.createCallback 180 | } 181 | 182 | // HasCreateCallback returns true if this RoomType has a creation callback. 183 | func (r *RoomType) HasCreateCallback() bool { 184 | return r.createCallback != nil 185 | } 186 | 187 | // DeleteCallback returns the function that this RoomType calls when a Room of this RoomType is deleted. 188 | func (r *RoomType) DeleteCallback() func(*Room) { 189 | return r.deleteCallback 190 | } 191 | 192 | // HasDeleteCallback returns true if this RoomType has a delete callback. 193 | func (r *RoomType) HasDeleteCallback() bool { 194 | return r.deleteCallback != nil 195 | } 196 | 197 | // UserEnterCallback returns the function that this RoomType calls when a User enters a Room of this RoomType. 198 | func (r *RoomType) UserEnterCallback() func(*Room, *RoomUser) { 199 | return r.userEnterCallback 200 | } 201 | 202 | // HasUserEnterCallback returns true if this RoomType has a user enter callback. 203 | func (r *RoomType) HasUserEnterCallback() bool { 204 | return r.userEnterCallback != nil 205 | } 206 | 207 | // UserLeaveCallback returns the function that this RoomType calls when a User leaves a Room of this RoomType. 208 | func (r *RoomType) UserLeaveCallback() func(*Room, *RoomUser) { 209 | return r.userLeaveCallback 210 | } 211 | 212 | // HasUserLeaveCallback returns true if this RoomType has a user leave callback. 213 | func (r *RoomType) HasUserLeaveCallback() bool { 214 | return r.userLeaveCallback != nil 215 | } 216 | -------------------------------------------------------------------------------- /sockets.go: -------------------------------------------------------------------------------- 1 | package gopher 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "github.com/hewiefreeman/GopherGameServer/core" 6 | "github.com/hewiefreeman/GopherGameServer/helpers" 7 | "net/http" 8 | "strconv" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ( 14 | conns connections = connections{} 15 | ) 16 | 17 | type connections struct { 18 | conns int 19 | connsMux sync.Mutex 20 | } 21 | 22 | type clientAction struct { 23 | A string // action 24 | P interface{} // parameters 25 | } 26 | 27 | func socketInitializer(w http.ResponseWriter, r *http.Request) { 28 | //DECLINE CONNECTIONS COMING FROM OUTSIDE THE ORIGIN SERVER 29 | if settings.OriginOnly { 30 | origin := r.Header.Get("Origin") + ":" + strconv.Itoa(settings.Port) 31 | host := settings.HostName + ":" + strconv.Itoa(settings.Port) 32 | hostAlias := settings.HostAlias + ":" + strconv.Itoa(settings.Port) 33 | if origin != host && (settings.HostAlias != "" && origin != hostAlias) { 34 | http.Error(w, "Origin not allowed.", http.StatusForbidden) 35 | return 36 | } 37 | } 38 | 39 | //REJECT IF SERVER IS FULL 40 | if !conns.add() { 41 | http.Error(w, "Server is full.", 413) 42 | return 43 | } 44 | 45 | // CLIENT CONNECT CALLBACK 46 | if clientConnectCallback != nil && !clientConnectCallback(&w, r) { 47 | http.Error(w, "Could not establish a connection.", http.StatusForbidden) 48 | return 49 | } 50 | 51 | //UPGRADE CONNECTION PING-PONG 52 | conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024) 53 | if err != nil { 54 | http.Error(w, "Could not establish a connection.", http.StatusForbidden) 55 | return 56 | } 57 | 58 | // START WEBSOCKET LOOP 59 | go clientActionListener(conn) 60 | } 61 | 62 | func clientActionListener(conn *websocket.Conn) { 63 | // CLIENT ACTION INPUT 64 | var action clientAction 65 | 66 | var clientMux sync.Mutex // LOCKS user AND connID 67 | var user *core.User // THE CLIENT'S User OBJECT 68 | var connID string // CLIENT SESSION ID 69 | 70 | // THE CLIENT'S AUTOLOG INFO 71 | var deviceTag string 72 | var devicePass string 73 | var deviceUserID int 74 | 75 | if (*settings).RememberMe { 76 | //SEND TAG RETRIEVAL MESSAGE 77 | tagMessage := map[string]interface{}{ 78 | helpers.ServerActionRequestDeviceTag: nil, 79 | } 80 | writeErr := conn.WriteJSON(tagMessage) 81 | if writeErr != nil { 82 | closeSocket(conn) 83 | return 84 | } 85 | //PARAMS 86 | var ok bool 87 | var err error 88 | var gErr helpers.GopherError 89 | var oldPass string 90 | //PING-PONG FOR TAGGING DEVICE - BREAKS WHEN THE DEVICE HAS BEEN PROPERLY TAGGED OR AUTHENTICATED. 91 | for { 92 | //READ INPUT BUFFER 93 | readErr := conn.ReadJSON(&action) 94 | if readErr != nil || action.A == "" { 95 | closeSocket(conn) 96 | return 97 | } 98 | 99 | //DETERMINE ACTION 100 | if action.A == "0" { 101 | //NO DEVICE TAG. MAKE ONE AND SEND IT. 102 | newDeviceTag, newDeviceTagErr := helpers.GenerateSecureString(32) 103 | if newDeviceTagErr != nil { 104 | closeSocket(conn) 105 | return 106 | } 107 | deviceTag = string(newDeviceTag) 108 | tagMessage := map[string]interface{}{ 109 | helpers.ServerActionSetDeviceTag: deviceTag, 110 | } 111 | writeErr := conn.WriteJSON(tagMessage) 112 | if writeErr != nil { 113 | closeSocket(conn) 114 | return 115 | } 116 | } else if action.A == "1" { 117 | //THE CLIENT ONLY HAS A DEVICE TAG, BREAK 118 | if sentDeviceTag, ohK := action.P.(string); ohK { 119 | if len(deviceTag) > 0 && sentDeviceTag != deviceTag { 120 | //CLIENT DIDN'T USE THE PROVIDED DEVICE CODE FROM THE SERVER 121 | closeSocket(conn) 122 | return 123 | } 124 | //SEND AUTO-LOG NOT FILED MESSAGE 125 | notFiledMessage := map[string]interface{}{ 126 | helpers.ServerActionAutoLoginNotFiled: nil, 127 | } 128 | writeErr := conn.WriteJSON(notFiledMessage) 129 | if writeErr != nil { 130 | closeSocket(conn) 131 | return 132 | } 133 | } else { 134 | closeSocket(conn) 135 | return 136 | } 137 | 138 | // 139 | break 140 | 141 | } else if action.A == "2" { 142 | //THE CLIENT HAS A LOGIN KEY PAIR - MAKE A NEW PASS FOR THEM 143 | var pMap map[string]interface{} 144 | devicePass, err = helpers.GenerateSecureString(32) 145 | if err != nil { 146 | closeSocket(conn) 147 | return 148 | } 149 | //GET PARAMS 150 | if pMap, ok = action.P.(map[string]interface{}); !ok { 151 | closeSocket(conn) 152 | return 153 | } 154 | if deviceTag, ok = pMap["dt"].(string); !ok { 155 | closeSocket(conn) 156 | return 157 | } 158 | if oldPass, ok = pMap["da"].(string); !ok { 159 | closeSocket(conn) 160 | return 161 | } 162 | var deviceUserIDStr string 163 | if deviceUserIDStr, ok = pMap["di"].(string); !ok { 164 | closeSocket(conn) 165 | return 166 | } 167 | //CONVERT di TO INT 168 | deviceUserID, err = strconv.Atoi(deviceUserIDStr) 169 | if err != nil { 170 | closeSocket(conn) 171 | return 172 | } 173 | //CHANGE THE CLIENT'S PASS 174 | newPassMessage := map[string]interface{}{ 175 | helpers.ServerActionSetAutoLoginPass: devicePass, 176 | } 177 | writeErr := conn.WriteJSON(newPassMessage) 178 | if writeErr != nil { 179 | closeSocket(conn) 180 | return 181 | } 182 | } else if action.A == "3" { 183 | if deviceTag == "" || oldPass == "" || deviceUserID == 0 || devicePass == "" { 184 | //IRRESPONSIBLE USAGE 185 | closeSocket(conn) 186 | return 187 | } 188 | //AUTO-LOG THE CLIENT 189 | connID, gErr = core.AutoLogIn(deviceTag, oldPass, devicePass, deviceUserID, conn, &user, &clientMux) 190 | if gErr.ID != 0 { 191 | //ERROR AUTO-LOGGING - RUN AUTOLOGCOMPLETE AND DELETE KEYS FOR CLIENT, AND SILENTLY CHANGE DEVICE TAG 192 | newTag, newTagErr := helpers.GenerateSecureString(32) 193 | if newTagErr != nil { 194 | closeSocket(conn) 195 | return 196 | } 197 | autologMessage := map[string]map[string]interface{}{ 198 | helpers.ServerActionAutoLoginFailed: { 199 | "dt": newTag, 200 | "e": map[string]interface{}{ 201 | "m": gErr.Message, 202 | "id": gErr.ID, 203 | }, 204 | }, 205 | } 206 | writeErr := conn.WriteJSON(autologMessage) 207 | if writeErr != nil { 208 | closeSocket(conn) 209 | return 210 | } 211 | devicePass = "" 212 | deviceUserID = 0 213 | deviceTag = newTag 214 | // 215 | break 216 | } 217 | // 218 | break 219 | } 220 | } 221 | } 222 | 223 | //STANDARD CONNECTION LOOP 224 | for { 225 | //READ INPUT BUFFER 226 | readErr := conn.ReadJSON(&action) 227 | if readErr != nil || action.A == "" { 228 | //DISCONNECT USER 229 | clientMux.Lock() 230 | sockedDropped(user, connID, &clientMux) 231 | closeSocket(conn) 232 | return 233 | } 234 | 235 | //TAKE ACTION 236 | responseVal, respond, actionErr := clientActionHandler(action, &user, conn, &deviceTag, &devicePass, &deviceUserID, &connID, &clientMux) 237 | 238 | if respond { 239 | //SEND RESPONSE 240 | if writeErr := conn.WriteJSON(helpers.MakeClientResponse(action.A, responseVal, actionErr)); writeErr != nil { 241 | //DISCONNECT USER 242 | clientMux.Lock() 243 | sockedDropped(user, connID, &clientMux) 244 | closeSocket(conn) 245 | return 246 | } 247 | } 248 | 249 | // 250 | action = clientAction{} 251 | } 252 | } 253 | 254 | func closeSocket(conn *websocket.Conn) { 255 | conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second*1)) 256 | conn.Close() 257 | conns.subtract() 258 | } 259 | 260 | func sockedDropped(user *core.User, connID string, clientMux *sync.Mutex) { 261 | if user != nil { 262 | //CLIENT WAS LOGGED IN. LOG THEM OUT 263 | (*clientMux).Unlock() 264 | user.Logout(connID) 265 | } 266 | } 267 | 268 | /////////////////////// HELPERS FOR connections 269 | 270 | func (c *connections) add() bool { 271 | c.connsMux.Lock() 272 | // 273 | if (*settings).MaxConnections != 0 && c.conns == (*settings).MaxConnections { 274 | c.connsMux.Unlock() 275 | return false 276 | } 277 | c.conns++ 278 | c.connsMux.Unlock() 279 | // 280 | return true 281 | } 282 | 283 | func (c *connections) subtract() { 284 | c.connsMux.Lock() 285 | c.conns-- 286 | c.connsMux.Unlock() 287 | } 288 | 289 | // ClientsConnected returns the number of clients connected to the server. Includes connections 290 | // not logged in as a User. To get the number of Users logged in, use the core.UserCount() function. 291 | func ClientsConnected() int { 292 | conns.connsMux.Lock() 293 | c := conns.conns 294 | conns.connsMux.Unlock() 295 | return c 296 | } 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 |
5 | 6 | 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. 7 | 8 | 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. 9 | 10 | ### :star: Main features 11 | 12 | - Super easy APIs for server, database, and client coding 13 | - Chat, private messaging, and voice chat 14 | - Customizable client authentication (\***1**) 15 | - Built-in friending mechanism (\***1**) 16 | - Supports multiple connections on the same User 17 | - Server saves state on shut-down and restores on reboot (\***2**) 18 | 19 | > (\***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. 20 | 21 | > (\***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. 22 | 23 | ### Upcoming features 24 | 25 | - Distributed load balancer and server coordinator 26 | - Distributed server broadcasts 27 | - GUI for administrating and monitoring servers 28 | - Integration with [GopherDB](https://github.com/hewiefreeman/GopherDB) when stable (\***1**) 29 | 30 | > (\***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! 31 | 32 | ### Change Log 33 | [CHANGE_LOG.md](https://github.com/hewiefreeman/GopherGameServer/blob/master/CHANGE_LOG.md) 34 |


35 | # :video_game: Client APIs 36 | 37 | - JavaScript: [GopherClientJS](https://github.com/hewiefreeman/GopherClientJS) 38 | 39 | > 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! 40 | 41 | # :file_folder: Installing 42 | Gopher Game Server requires at least **Go v1.8+** (and **MySQL v5.7+** for the authentication and friending features). 43 | 44 | First, install the dependencies: 45 | 46 | go get github.com/gorilla/websocket 47 | go get github.com/go-sql-driver/mysql 48 | go get golang.org/x/crypto/bcrypt 49 | 50 | Then install the server: 51 | 52 | go get github.com/hewiefreeman/GopherGameServer 53 | 54 | # :books: Usage 55 | 56 | [:bookmark: Wiki Home](https://github.com/hewiefreeman/GopherGameServer/wiki) 57 | 58 | ### Table of Contents 59 | 60 | 1) [**Getting Started**](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started) 61 | - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-set-up) 62 | - [Core Server Settings](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-core-server-settings) 63 | - [Server Callbacks](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-server-callbacks) 64 | - [Macro Commands](https://github.com/hewiefreeman/GopherGameServer/wiki/Getting-Started#blue_book-macro-commands) 65 | 2) [**Rooms**](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms) 66 | - [Room Types](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-types) 67 | - [Room Broadcasts](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-broadcasts) 68 | - [Room Callbacks](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-callbacks) 69 | - [Creating & Deleting Rooms](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-creating--deleting-rooms) 70 | - [Room Variables](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-room-variables) 71 | - [Messaging](https://github.com/hewiefreeman/GopherGameServer/wiki/Rooms#blue_book-messaging) 72 | 3) [**Users**](https://github.com/hewiefreeman/GopherGameServer/wiki/Users) 73 | - [Login & Logout](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-login-and-logout) 74 | - [Joining & Leaving Rooms](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-joining--leaving-rooms) 75 | - [User Variables](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-user-variables) 76 | - [Initiating and Revoking Room Invites](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-initiating-and-revoking-room-invites) 77 | - [User Status](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-user-status) 78 | - [Messaging](https://github.com/hewiefreeman/GopherGameServer/wiki/Users#blue_book-messaging) 79 | 4) [**Custom Client Actions**](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions) 80 | - [Creating a Custom Client Action](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions#blue_book-creating-a-custom-client-action) 81 | - [Responding to a Custom Client Action](https://github.com/hewiefreeman/GopherGameServer/wiki/Custom-Client-Actions#blue_book-responding-to-a-custom-client-action) 82 | 6) [**Saving & Restoring**](https://github.com/hewiefreeman/GopherGameServer/wiki/Saving-&-Restoring) 83 | - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/Saving-&-Restoring#blue_book-set-up) 84 | 5) [**SQL Features**](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features) 85 | - [Set-Up](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-set-up) 86 | - [Authenticating Clients](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-authenticating-clients) 87 | - [Custom Account Info](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-custom-account-info) 88 | - [Customizing Authentication Features](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-customizing-authentication-features) 89 | - [Auto-Login (Remember Me)](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-auto-login-remember-me) 90 | - [Friending](https://github.com/hewiefreeman/GopherGameServer/wiki/SQL-Features#blue_book-friending) 91 | 92 | # :scroll: Documentation 93 | 94 | [Package gopher](https://godoc.org/github.com/hewiefreeman/GopherGameServer) - Main server package for startup and settings 95 | 96 | [Package core](https://godoc.org/github.com/hewiefreeman/GopherGameServer/core) - Package for all User and Room functionality 97 | 98 | [Package actions](https://godoc.org/github.com/hewiefreeman/GopherGameServer/actions) - Package for making custom client actions 99 | 100 | [Package database](https://godoc.org/github.com/hewiefreeman/GopherGameServer/database) - Package for customizing your database 101 | 102 | # :milky_way: Contributions 103 | 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:! 104 | 105 | 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! 106 | 107 | Please read the following articles before submitting any contributions or filing an Issue: 108 | 109 | - [Contribution Guidlines](https://github.com/hewiefreeman/GopherGameServer/blob/master/CONTRIBUTING.md) 110 | - [Code of Conduct](https://github.com/hewiefreeman/GopherGameServer/blob/master/CODE_OF_CONDUCT.md) 111 | 112 |
113 | 114 |
GopherGameServer and all of it's contents Copyright 2022 Dominique Debergue 115 |
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: 116 | 117 | `http://www.apache.org/licenses/LICENSE-2.0` 118 | 119 |
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.
120 | -------------------------------------------------------------------------------- /core/messaging.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/gorilla/websocket" 6 | "github.com/hewiefreeman/GopherGameServer/helpers" 7 | ) 8 | 9 | // These represent the types of room messages the server sends. 10 | const ( 11 | MessageTypeChat = iota 12 | MessageTypeServer 13 | ) 14 | 15 | // These are the sub-types that a MessageTypeServer will come with. Ordered by their visible priority for your UI. 16 | const ( 17 | ServerMessageGame = iota 18 | ServerMessageNotice 19 | ServerMessageImportant 20 | ) 21 | 22 | var ( 23 | privateMessageCallback func(*User, *User, interface{}) 24 | privateMessageCallbackSet bool 25 | chatMessageCallback func(string, *Room, interface{}) 26 | chatMessageCallbackSet bool 27 | serverMessageCallback func(*Room, int, interface{}) 28 | serverMessageCallbackSet bool 29 | ) 30 | 31 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 32 | // Messaging Users /////////////////////////////////////////////////////////////////////////////// 33 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 34 | 35 | // PrivateMessage sends a private message to another User by name. 36 | func (u *User) PrivateMessage(userName string, message interface{}) { 37 | user, userErr := GetUser(userName) 38 | if userErr != nil { 39 | return 40 | } 41 | 42 | //CONSTRUCT MESSAGE 43 | theMessage := map[string]map[string]interface{}{ 44 | helpers.ServerActionPrivateMessage: { 45 | "f": u.name, // from 46 | "t": user.name, // to 47 | "m": message, 48 | }, 49 | } 50 | 51 | //SEND MESSAGES 52 | user.mux.Lock() 53 | for _, conn := range user.conns { 54 | (*conn).socket.WriteJSON(theMessage) 55 | } 56 | user.mux.Unlock() 57 | u.mux.Lock() 58 | for _, conn := range u.conns { 59 | (*conn).socket.WriteJSON(theMessage) 60 | } 61 | u.mux.Unlock() 62 | 63 | if privateMessageCallbackSet { 64 | privateMessageCallback(u, user, message) 65 | } 66 | 67 | return 68 | } 69 | 70 | // DataMessage sends a data message directly to the User. 71 | func (u *User) DataMessage(data interface{}, connID string) { 72 | //CONSTRUCT MESSAGE 73 | message := map[string]interface{}{ 74 | helpers.ServerActionDataMessage: data, 75 | } 76 | 77 | //SEND MESSAGE TO USER 78 | u.mux.Lock() 79 | if connID == "" { 80 | for _, conn := range u.conns { 81 | (*conn).socket.WriteJSON(message) 82 | } 83 | } else { 84 | if conn, ok := u.conns[connID]; ok { 85 | (*conn).socket.WriteJSON(message) 86 | } 87 | } 88 | u.mux.Unlock() 89 | } 90 | 91 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 92 | // Messaging Rooms /////////////////////////////////////////////////////////////////////////////// 93 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 94 | 95 | // ServerMessage sends a server message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice 96 | // of string. In which case, the server message will be sent to all Users in the Room. 97 | func (r *Room) ServerMessage(message interface{}, messageType int, recipients []string) error { 98 | if message == nil { 99 | return errors.New("*Room.ServerMessage() requires a message") 100 | } 101 | 102 | if serverMessageCallbackSet { 103 | serverMessageCallback(r, messageType, message) 104 | } 105 | 106 | return r.sendMessage(MessageTypeServer, messageType, recipients, "", message) 107 | } 108 | 109 | // ChatMessage sends a chat message to all Users in the Room. 110 | func (r *Room) ChatMessage(author string, message interface{}) error { 111 | //REJECT INCORRECT INPUT 112 | if len(author) == 0 { 113 | return errors.New("*Room.ChatMessage() requires an author") 114 | } else if message == nil { 115 | return errors.New("*Room.ChatMessage() requires a message") 116 | } 117 | 118 | if chatMessageCallbackSet { 119 | chatMessageCallback(author, r, message) 120 | } 121 | 122 | return r.sendMessage(MessageTypeChat, 0, nil, author, message) 123 | } 124 | 125 | // DataMessage sends a data message to the specified recipients in the Room. The parameter recipients can be nil or an empty slice 126 | // of string. In which case, the data message will be sent to all Users in the Room. 127 | func (r *Room) DataMessage(message interface{}, recipients []string) error { 128 | //GET USER MAP 129 | userMap, err := r.GetUserMap() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | //CONSTRUCT MESSAGE 135 | theMessage := map[string]interface{}{ 136 | helpers.ServerActionDataMessage: message, 137 | } 138 | 139 | //SEND MESSAGE TO USERS 140 | if recipients == nil || len(recipients) == 0 { 141 | for _, u := range userMap { 142 | u.mux.Lock() 143 | for _, conn := range u.conns { 144 | conn.socket.WriteJSON(theMessage) 145 | } 146 | u.mux.Unlock() 147 | } 148 | } else { 149 | for i := 0; i < len(recipients); i++ { 150 | if u, ok := userMap[recipients[i]]; ok { 151 | u.mux.Lock() 152 | for _, conn := range u.conns { 153 | conn.socket.WriteJSON(theMessage) 154 | } 155 | u.mux.Unlock() 156 | } 157 | } 158 | } 159 | 160 | // 161 | return nil 162 | } 163 | 164 | func (r *Room) sendMessage(mt int, st int, rec []string, a string, m interface{}) error { 165 | //GET USER MAP 166 | userMap, err := r.GetUserMap() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | //CONSTRUCT MESSAGE 172 | message := map[string]map[string]interface{}{ 173 | helpers.ServerActionRoomMessage: make(map[string]interface{}), 174 | } 175 | // Server messages come with a sub-type 176 | if mt == MessageTypeServer { 177 | message[helpers.ServerActionRoomMessage]["s"] = st 178 | } 179 | // Non-server messages have authors 180 | if len(a) > 0 && mt != MessageTypeServer { 181 | message[helpers.ServerActionRoomMessage]["a"] = a 182 | } 183 | // The message 184 | message[helpers.ServerActionRoomMessage]["m"] = m 185 | 186 | //SEND MESSAGE TO USERS 187 | if rec == nil || len(rec) == 0 { 188 | for _, u := range userMap { 189 | u.mux.Lock() 190 | for _, conn := range u.conns { 191 | conn.socket.WriteJSON(message) 192 | } 193 | u.mux.Unlock() 194 | } 195 | } else { 196 | for i := 0; i < len(rec); i++ { 197 | if u, ok := userMap[rec[i]]; ok { 198 | u.mux.Lock() 199 | for _, conn := range u.conns { 200 | conn.socket.WriteJSON(message) 201 | } 202 | u.mux.Unlock() 203 | } 204 | } 205 | } 206 | 207 | return nil 208 | } 209 | 210 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 211 | // VOICE STREAMS ////////////////////////////////////////////////////////////////////////////////////////////// 212 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 213 | 214 | // VoiceStream sends a voice stream from the client API to all the users in the room besides the user who is speaking. 215 | func (r *Room) VoiceStream(userName string, userSocket *websocket.Conn, stream interface{}) { 216 | //GET USER MAP 217 | userMap, err := r.GetUserMap() 218 | if err != nil { 219 | return 220 | } 221 | 222 | //CONSTRUCT VOICE MESSAGE 223 | theMessage := map[string]map[string]interface{}{ 224 | helpers.ServerActionVoiceStream: { 225 | "u": userName, 226 | "d": stream, 227 | }, 228 | } 229 | 230 | //REMOVE SENDING USER FROM userMap 231 | delete(userMap, userName) // COMMENT OUT FOR ECHO TESTS 232 | 233 | //SEND MESSAGE TO USERS 234 | for _, u := range userMap { 235 | for _, conn := range u.conns { 236 | (*conn).socket.WriteJSON(theMessage) 237 | } 238 | } 239 | 240 | //CONSTRUCT PING MESSAGE 241 | pingMessage := map[string]interface{}{ 242 | helpers.ServerActionVoicePing: nil, 243 | } 244 | 245 | //SEND PING MESSAGE TO SENDING USER 246 | userSocket.WriteJSON(pingMessage) 247 | 248 | // 249 | return 250 | } 251 | 252 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 253 | // Callback Setters /////////////////////////////////////////////////////////////////////////////////////////// 254 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 255 | 256 | // SetPrivateMessageCallback sets the callback function for when a *User sends a private message to another *User. 257 | // The function passed must have the same parameter types as the following example: 258 | // 259 | // func onPrivateMessage(from *core.User, to *core.User, message interface{}) { 260 | // //code... 261 | // } 262 | func SetPrivateMessageCallback(cb func(*User, *User, interface{})) { 263 | if !serverStarted { 264 | privateMessageCallback = cb 265 | privateMessageCallbackSet = true 266 | } 267 | 268 | } 269 | 270 | // SetChatMessageCallback sets the callback function for when a *User sends a chat message to a *Room. 271 | // The function passed must have the same parameter types as the following example: 272 | // 273 | // func onChatMessage(userName string, room *core.Room, message interface{}) { 274 | // //code... 275 | // } 276 | func SetChatMessageCallback(cb func(string, *Room, interface{})) { 277 | if !serverStarted { 278 | chatMessageCallback = cb 279 | chatMessageCallbackSet = true 280 | } 281 | } 282 | 283 | // SetServerMessageCallback sets the callback function for when the server sends a message to a *Room. 284 | // The function passed must have the same parameter types as the following example: 285 | // 286 | // func onServerMessage(room *core.Room, messageType int, message interface{}) { 287 | // //code... 288 | // } 289 | // 290 | // The messageType value can be one of: core.ServerMessageGame, core.ServerMessageNotice, 291 | // core.ServerMessageImportant, or a custom value you have set. 292 | func SetServerMessageCallback(cb func(*Room, int, interface{})) { 293 | if !serverStarted { 294 | serverMessageCallback = cb 295 | serverMessageCallbackSet = true 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /actions/actions.go: -------------------------------------------------------------------------------- 1 | // Package actions contains all the tools for making your custom client actions. 2 | package actions 3 | 4 | import ( 5 | "errors" 6 | "github.com/gorilla/websocket" 7 | "github.com/hewiefreeman/GopherGameServer/core" 8 | "github.com/hewiefreeman/GopherGameServer/helpers" 9 | ) 10 | 11 | // CustomClientAction is an action that you can handle on the server from 12 | // a connected client. For instance, a client can send to the server a 13 | // CustomClientAction type "setPosition" that comes with parameters as an object {x: 2, y: 3}. 14 | // You just need to make a callback function for the CustomClientAction type "setPosition", and as soon as the 15 | // action is received by the server, the callback function will be executed concurrently in a Goroutine. 16 | type CustomClientAction struct { 17 | dataType int 18 | 19 | callback func(interface{}, *Client) 20 | } 21 | 22 | // Client objects are created and sent along with your CustomClientAction callback function when a 23 | // client sends an action. 24 | type Client struct { 25 | action string 26 | 27 | user *core.User 28 | connID string 29 | socket *websocket.Conn 30 | 31 | responded bool 32 | } 33 | 34 | // ClientError is used when an error is thrown in your CustomClientAction. Use `actions.NewError()` to make a 35 | // ClientError object. When no error needs to be thrown, use `actions.NoError()` instead. 36 | type ClientError struct { 37 | message string 38 | id int 39 | } 40 | 41 | var ( 42 | customClientActions map[string]CustomClientAction = make(map[string]CustomClientAction) 43 | 44 | serverStarted = false 45 | serverPaused = false 46 | ) 47 | 48 | // Default `ClientError`s 49 | const ( 50 | ErrorMismatchedTypes = iota + 1001 // Client didn't pass the right data type for the given action 51 | ErrorUnrecognizedAction // The custom action has not been defined 52 | ) 53 | 54 | // These are the accepted data types that a client can send with a CustomClientMessage. You must use one 55 | // of these when making a new CustomClientAction, or it will not work. If a client tries to send a type of data that doesnt 56 | // match the type specified for that action, the CustomClientAction will send an error back to the client and skip 57 | // executing your callback function. 58 | const ( 59 | DataTypeBool = iota // Boolean data type 60 | DataTypeInt // int, int32, and int64 data types 61 | DataTypeFloat // float32 and float64 data types 62 | DataTypeString // string data type 63 | DataTypeArray // []interface{} data type 64 | DataTypeMap // map[string]interface{} data type 65 | DataTypeNil // nil data type 66 | ) 67 | 68 | // New creates a new `CustomClientAction` with the corresponding parameters: 69 | // 70 | // - actionType (string): The type of action 71 | // 72 | // - (*)callback (func(interface{},Client)): The function that will be executed when a client calls this `actionType` 73 | // 74 | // - dataType (int): The type of data this action accepts. Options are `DataTypeBool`, `DataTypeInt`, `DataTypeFloat`, `DataTypeString`, `DataTypeArray`, `DataTypeMap`, and `DataTypeNil` 75 | // 76 | // (*)Callback function format: 77 | // 78 | // func yourFunction(actionData interface{}, client *Client) { 79 | // //... 80 | // 81 | // // optional client response 82 | // client.Respond("example", nil); 83 | // } 84 | // 85 | // - actionData: The data the client sent along with the action 86 | // 87 | // - client: A `Client` object representing the client that sent the action 88 | // 89 | // 90 | // Note: This function can only be called BEFORE starting the server. 91 | func New(actionType string, dataType int, callback func(interface{}, *Client)) error { 92 | if serverStarted { 93 | return errors.New("Cannot make a new CustomClientAction once the server has started") 94 | } 95 | customClientActions[actionType] = CustomClientAction{ 96 | dataType: dataType, 97 | callback: callback, 98 | } 99 | return nil 100 | } 101 | 102 | // NewError creates a new error with a provided message and ID. 103 | func NewError(message string, id int) ClientError { 104 | return ClientError{message: message, id: id} 105 | } 106 | 107 | // NoError is used when no error needs to be thrown in your `CustomClientAction`. 108 | func NoError() ClientError { 109 | return ClientError{id: -1} 110 | } 111 | 112 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 113 | // SEND A CustomClientAction RESPONSE TO THE Client /////////////////////////////////////////////////////////// 114 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 115 | 116 | // HandleCustomClientAction handles your custom client actions. 117 | // 118 | // WARNING: This is only meant for internal Gopher Game Server mechanics. Your CustomClientAction callbacks are called 119 | // from this function. This could spawn errors and/or memory leaks. 120 | func HandleCustomClientAction(action string, data interface{}, user *core.User, conn *websocket.Conn, connID string) { 121 | client := Client{user: user, action: action, socket: conn, connID: connID, responded: false} 122 | // CHECK IF ACTION EXISTS 123 | if customAction, ok := customClientActions[action]; ok { 124 | // CHECK IF THE TYPE OF data MATCHES THE TYPE action SPECIFIES 125 | if !typesMatch(data, customAction.dataType) { 126 | client.Respond(nil, NewError("Mismatched data type", ErrorMismatchedTypes)) 127 | return 128 | } 129 | //EXECUTE CALLBACK 130 | customAction.callback(data, &client) 131 | } else { 132 | client.Respond(nil, NewError("Unrecognized action", ErrorUnrecognizedAction)) 133 | } 134 | } 135 | 136 | // 137 | func typesMatch(data interface{}, theType int) bool { 138 | switch data.(type) { 139 | case bool: 140 | if theType == DataTypeBool { 141 | return true 142 | } 143 | 144 | case int: 145 | if theType == DataTypeInt { 146 | return true 147 | } 148 | 149 | case int32: 150 | if theType == DataTypeInt { 151 | return true 152 | } 153 | 154 | case int64: 155 | if theType == DataTypeInt { 156 | return true 157 | } 158 | 159 | case float32: 160 | if theType == DataTypeFloat { 161 | return true 162 | } 163 | 164 | case float64: 165 | if theType == DataTypeFloat { 166 | return true 167 | } 168 | 169 | case string: 170 | if theType == DataTypeString { 171 | return true 172 | } 173 | 174 | case []interface{}: 175 | if theType == DataTypeArray { 176 | return true 177 | } 178 | 179 | case map[string]interface{}: 180 | if theType == DataTypeMap { 181 | return true 182 | } 183 | 184 | case nil: 185 | if theType == DataTypeNil { 186 | return true 187 | } 188 | } 189 | // 190 | return false 191 | } 192 | 193 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 194 | // SEND A CustomClientAction RESPONSE TO THE Client /////////////////////////////////////////////////////////// 195 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 196 | 197 | // Respond sends a CustomClientAction response to the client. If an error is provided, only the error mesage will be received 198 | // by the Client (the response parameter will not be sent as well). It's perfectly fine to not send back any response if none 199 | // is needed. 200 | // 201 | // 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, 202 | // 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(), 203 | // then you can send data messages directly to the User with the *User.DataMessage() function. 204 | func (c *Client) Respond(response interface{}, err ClientError) { 205 | //YOU CAN ONLY RESPOND ONCE 206 | if (*c).responded { 207 | return 208 | } 209 | (*c).responded = true 210 | //CONSTRUCT MESSAGE 211 | r := map[string]map[string]interface{}{ 212 | helpers.ServerActionCustomClientActionResponse: { 213 | "a": (*c).action, 214 | }, 215 | } 216 | if err.id != -1 { 217 | r[helpers.ServerActionCustomClientActionResponse]["e"] = map[string]interface{}{ 218 | "m": err.message, 219 | "id": err.id, 220 | } 221 | } else { 222 | r[helpers.ServerActionCustomClientActionResponse]["r"] = response 223 | } 224 | //SEND MESSAGE TO CLIENT 225 | (*c).socket.WriteJSON(r) 226 | } 227 | 228 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 229 | // Client ATTRIBUTE READERS /////////////////////////////////////////////////////////////////////////////////// 230 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 231 | 232 | // User gets the *User of the Client. 233 | func (c *Client) User() *core.User { 234 | return c.user 235 | } 236 | 237 | // ConnectionID gets the connection ID of the Client. This is only used if you have MultiConnect enabled in ServerSettings and 238 | // you need to, for instance, call *User functions with the Client's *User obtained with the client.User() function. If 239 | // you do, use client.ConnectionID() when calling any *User functions from a Client. 240 | func (c *Client) ConnectionID() string { 241 | return c.connID 242 | } 243 | 244 | // Action gets the type of action the Client sent. 245 | func (c *Client) Action() string { 246 | return c.action 247 | } 248 | 249 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 250 | // SERVER STARTUP FUNCTIONS /////////////////////////////////////////////////////////////////////////////////// 251 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 252 | 253 | // SetServerStarted is for Gopher Game Server internal mechanics only. 254 | func SetServerStarted(val bool) { 255 | if !serverStarted { 256 | serverStarted = val 257 | } 258 | } 259 | 260 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 261 | // SERVER PAUSE AND RESUME /////////////////////////////////////////////////////////////////////// 262 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 263 | 264 | // Pause is only for internal Gopher Game Server mechanics. 265 | func Pause() { 266 | if !serverPaused { 267 | serverPaused = true 268 | } 269 | } 270 | 271 | // Resume is only for internal Gopher Game Server mechanics. 272 | func Resume() { 273 | if serverPaused { 274 | serverPaused = false 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /core/friending.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/hewiefreeman/GopherGameServer/database" 6 | "github.com/hewiefreeman/GopherGameServer/helpers" 7 | ) 8 | 9 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | // SEND A FRIEND REQUEST ///////////////////////////////////////////////////////////////////////// 11 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | // FriendRequest sends a friend request to another User by their name. 14 | func (u *User) FriendRequest(friendName string) error { 15 | if _, ok := u.friends[friendName]; ok { 16 | return errors.New("The user '" + friendName + "' cannot be requested as a friend") 17 | } 18 | //CHECK IF FRIEND IS ONLINE & GET DATABASE ID 19 | friend, friendErr := GetUser(friendName) 20 | var friendOnline bool = false 21 | var friendID int 22 | if friendErr != nil { 23 | //GET FRIEND'S DATABASE ID FROM database PACKAGE 24 | friendID, friendErr = database.GetUserDatabaseIndex(friendName) 25 | if friendErr != nil { 26 | return errors.New("The user '" + friendName + "' does not exist") 27 | } 28 | } else { 29 | friendID = friend.databaseID 30 | friendOnline = true 31 | } 32 | 33 | //ADD REQUESTED FRIEND FOR USER 34 | u.mux.Lock() 35 | u.friends[friendName] = database.NewFriend(friendName, friendID, database.FriendStatusPending) 36 | u.mux.Unlock() 37 | 38 | //ADD REQUESTED FRIEND FOR FRIEND 39 | if friendOnline { 40 | friend.mux.Lock() 41 | friend.friends[u.name] = database.NewFriend(u.name, u.databaseID, database.FriendStatusRequested) 42 | friend.mux.Unlock() 43 | } 44 | 45 | //MAKE THE FRIEND REQUEST ON DATABASE 46 | friendingErr := database.FriendRequest(u.databaseID, friendID) 47 | if friendingErr != nil { 48 | return errors.New("Unexpected friend error") 49 | } 50 | 51 | //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE 52 | if friendOnline { 53 | message := map[string]map[string]interface{}{ 54 | helpers.ServerActionFriendRequest: { 55 | "n": u.name, 56 | }, 57 | } 58 | friend.mux.Lock() 59 | for _, conn := range friend.conns { 60 | (*conn).socket.WriteJSON(message) 61 | } 62 | friend.mux.Unlock() 63 | } 64 | 65 | //SEND RESPONSE TO CLIENT 66 | clientResp := helpers.MakeClientResponse(helpers.ClientActionFriendRequest, friendName, helpers.NoError()) 67 | u.mux.Lock() 68 | for _, conn := range u.conns { 69 | (*conn).socket.WriteJSON(clientResp) 70 | } 71 | u.mux.Unlock() 72 | 73 | // 74 | return nil 75 | } 76 | 77 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 78 | // ACCEPT A FRIEND REQUEST /////////////////////////////////////////////////////////////////////// 79 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 80 | 81 | // AcceptFriendRequest accepts a friend request from another User by their name. 82 | func (u *User) AcceptFriendRequest(friendName string) error { 83 | if _, ok := u.friends[friendName]; !ok { 84 | return errors.New("The user '" + friendName + "' has not requested you as a friend") 85 | } else if (u.friends[friendName]).RequestStatus() != database.FriendStatusRequested { 86 | return errors.New("The user '" + friendName + "' cannot be accepted as a friend") 87 | } 88 | //CHECK IF FRIEND IS ONLINE & GET DATABASE ID 89 | friend, friendErr := GetUser(friendName) 90 | var friendOnline bool = false 91 | var friendID int 92 | if friendErr != nil { 93 | //GET FRIEND'S DATABASE ID FROM database PACKAGE 94 | friendID, friendErr = database.GetUserDatabaseIndex(friendName) 95 | if friendErr != nil { 96 | return errors.New("The user '" + friendName + "' does not exist") 97 | } 98 | } else { 99 | friendID = friend.databaseID 100 | friendOnline = true 101 | } 102 | 103 | //ACCEPT FRIEND FOR USER 104 | u.mux.Lock() 105 | u.friends[friendName].SetStatus(database.FriendStatusAccepted) 106 | u.mux.Unlock() 107 | //ACCEPT FRIEND FOR FRIEND 108 | if friendOnline { 109 | friend.mux.Lock() 110 | friend.friends[u.name].SetStatus(database.FriendStatusAccepted) 111 | friend.mux.Unlock() 112 | } 113 | //UPDATE FRIENDS ON DATABASE 114 | friendingErr := database.FriendRequestAccepted(u.databaseID, friendID) 115 | if friendingErr != nil { 116 | return errors.New("Unexpected friend error") 117 | } 118 | 119 | //SEND ACCEPT MESSAGE TO THE USER IF THEY ARE ONLINE 120 | var fStatus int = StatusOffline 121 | if friendOnline { 122 | message := map[string]map[string]interface{}{ 123 | helpers.ServerActionFriendAccept: { 124 | "n": u.name, 125 | "s": u.status, 126 | }, 127 | } 128 | friend.mux.Lock() 129 | for _, conn := range friend.conns { 130 | (*conn).socket.WriteJSON(message) 131 | } 132 | fStatus = friend.status 133 | friend.mux.Unlock() 134 | } 135 | 136 | //MAKE RESPONSE 137 | responseMap := map[string]interface{}{ 138 | "n": friendName, 139 | "s": fStatus, 140 | } 141 | 142 | //SEND RESPONSE TO ALL CLIENT CONNECTIONS 143 | clientResp := helpers.MakeClientResponse(helpers.ClientActionAcceptFriend, responseMap, helpers.NoError()) 144 | u.mux.Lock() 145 | for _, conn := range u.conns { 146 | (*conn).socket.WriteJSON(clientResp) 147 | } 148 | u.mux.Unlock() 149 | 150 | // 151 | return nil 152 | } 153 | 154 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 155 | // Decline friend request //////////////////////////////////////////////////////////////////////// 156 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 157 | 158 | // DeclineFriendRequest declines a friend request from another User by their name. 159 | func (u *User) DeclineFriendRequest(friendName string) error { 160 | if _, ok := u.friends[friendName]; !ok { 161 | return errors.New("The user '" + friendName + "' has not requested you as a friend") 162 | } else if u.friends[friendName].RequestStatus() != database.FriendStatusRequested { 163 | return errors.New("The user '" + friendName + "' cannot be declined as a friend") 164 | } 165 | //CHECK IF FRIEND IS ONLINE & GET DATABASE ID 166 | friend, friendErr := GetUser(friendName) 167 | var friendOnline bool = false 168 | var friendID int 169 | if friendErr != nil { 170 | //GET FRIEND'S DATABASE ID FROM database PACKAGE 171 | friendID, friendErr = database.GetUserDatabaseIndex(friendName) 172 | if friendErr != nil { 173 | return errors.New("The user '" + friendName + "' does not exist") 174 | } 175 | } else { 176 | friendID = friend.databaseID 177 | friendOnline = true 178 | } 179 | 180 | //DELETE THE Users' Friends 181 | u.mux.Lock() 182 | delete(u.friends, friendName) 183 | u.mux.Unlock() 184 | //ACCEPT FRIEND FOR FRIEND 185 | if friendOnline { 186 | friend.mux.Lock() 187 | delete(friend.friends, u.name) 188 | friend.mux.Unlock() 189 | } 190 | 191 | //UPDATE FRIENDS ON DATABASE 192 | removeErr := database.RemoveFriend(u.databaseID, friendID) 193 | if removeErr != nil { 194 | return errors.New("Unexpected friend error") 195 | } 196 | 197 | //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE 198 | if friendOnline { 199 | message := map[string]map[string]interface{}{ 200 | helpers.ServerActionFriendRemove: { 201 | "n": u.name, 202 | }, 203 | } 204 | friend.mux.Lock() 205 | for _, conn := range friend.conns { 206 | (*conn).socket.WriteJSON(message) 207 | } 208 | friend.mux.Unlock() 209 | } 210 | 211 | //SEND RESPONSE TO CLIENT 212 | clientResp := helpers.MakeClientResponse(helpers.ClientActionDeclineFriend, friendName, helpers.NoError()) 213 | u.mux.Lock() 214 | for _, conn := range u.conns { 215 | (*conn).socket.WriteJSON(clientResp) 216 | } 217 | u.mux.Unlock() 218 | 219 | // 220 | return nil 221 | } 222 | 223 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 224 | // Remove a friend /////////////////////////////////////////////////////////////////////////////// 225 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 226 | 227 | // RemoveFriend removes a friend from this this User and this User from the friend's Friend list. 228 | func (u *User) RemoveFriend(friendName string) error { 229 | if _, ok := u.friends[friendName]; !ok { 230 | return errors.New("The user '" + friendName + "' is not your friend") 231 | } else if u.friends[friendName].RequestStatus() != database.FriendStatusAccepted { 232 | return errors.New("The user '" + friendName + "' cannot be removed as a friend") 233 | } 234 | //CHECK IF FRIEND IS ONLINE & GET DATABASE ID 235 | friend, friendErr := GetUser(friendName) 236 | var friendOnline bool = false 237 | var friendID int 238 | if friendErr != nil { 239 | //GET FRIEND'S DATABASE ID FROM database PACKAGE 240 | friendID, friendErr = database.GetUserDatabaseIndex(friendName) 241 | if friendErr != nil { 242 | return errors.New("The user '" + friendName + "' does not exist") 243 | } 244 | } else { 245 | friendID = friend.databaseID 246 | friendOnline = true 247 | } 248 | 249 | //DELETE THE Users' Friends 250 | u.mux.Lock() 251 | delete(u.friends, friendName) 252 | u.mux.Unlock() 253 | //ACCEPT FRIEND FOR FRIEND 254 | if friendOnline { 255 | friend.mux.Lock() 256 | delete(friend.friends, u.name) 257 | friend.mux.Unlock() 258 | } 259 | 260 | //UPDATE FRIENDS ON DATABASE 261 | removeErr := database.RemoveFriend(u.databaseID, friendID) 262 | if removeErr != nil { 263 | return errors.New("Unexpected friend error") 264 | } 265 | 266 | //SEND A FRIEND REQUEST TO THE USER IF THEY ARE ONLINE 267 | if friendOnline { 268 | message := map[string]map[string]interface{}{ 269 | helpers.ServerActionFriendRemove: { 270 | "n": u.name, 271 | }, 272 | } 273 | friend.mux.Lock() 274 | for _, conn := range friend.conns { 275 | (*conn).socket.WriteJSON(message) 276 | } 277 | friend.mux.Unlock() 278 | } 279 | 280 | //SEND RESPONSE TO CLIENT 281 | clientResp := helpers.MakeClientResponse(helpers.ClientActionRemoveFriend, friendName, helpers.NoError()) 282 | u.mux.Lock() 283 | for _, conn := range u.conns { 284 | (*conn).socket.WriteJSON(clientResp) 285 | } 286 | u.mux.Unlock() 287 | 288 | // 289 | return nil 290 | } 291 | 292 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 293 | // Send message to all friends /////////////////////////////////////////////////////////////////// 294 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 295 | 296 | func (u *User) sendToFriends(message interface{}) { 297 | for key, val := range u.friends { 298 | if val.RequestStatus() == database.FriendStatusAccepted { 299 | friend, friendErr := GetUser(key) 300 | if friendErr == nil { 301 | friend.mux.Lock() 302 | for _, friendConn := range friend.conns { 303 | (*friendConn).socket.WriteJSON(message) 304 | } 305 | friend.mux.Unlock() 306 | } 307 | } 308 | } 309 | } -------------------------------------------------------------------------------- /callbacks.go: -------------------------------------------------------------------------------- 1 | package gopher 2 | 3 | import ( 4 | "errors" 5 | "github.com/hewiefreeman/GopherGameServer/core" 6 | "github.com/hewiefreeman/GopherGameServer/database" 7 | "net/http" 8 | ) 9 | 10 | const ( 11 | // ErrorIncorrectFunction is thrown when function input or return parameters don't match with the callback 12 | ErrorIncorrectFunction = "Incorrect function parameters or return parameters" 13 | // ErrorServerRunning is thrown when an action cannot be taken because the server is running. Pausing the server 14 | // will enable you to run the command. 15 | ErrorServerRunning = "Cannot call when the server is running." 16 | ) 17 | 18 | // SetStartCallback sets the callback that triggers when the server first starts up. The 19 | // function passed must have the same parameter types as the following example: 20 | // 21 | // func serverStarted(){ 22 | // //code... 23 | // } 24 | func SetStartCallback(cb interface{}) error { 25 | if serverStarted { 26 | return errors.New(ErrorServerRunning) 27 | } else if callback, ok := cb.(func()); ok { 28 | startCallback = callback 29 | } else { 30 | return errors.New(ErrorIncorrectFunction) 31 | } 32 | return nil 33 | } 34 | 35 | // SetPauseCallback sets the callback that triggers when the server is paused. The 36 | // function passed must have the same parameter types as the following example: 37 | // 38 | // func serverPaused(){ 39 | // //code... 40 | // } 41 | func SetPauseCallback(cb interface{}) error { 42 | if serverStarted { 43 | return errors.New(ErrorServerRunning) 44 | } else if callback, ok := cb.(func()); ok { 45 | pauseCallback = callback 46 | } else { 47 | return errors.New(ErrorIncorrectFunction) 48 | } 49 | return nil 50 | } 51 | 52 | // SetResumeCallback sets the callback that triggers when the server is resumed after being paused. The 53 | // function passed must have the same parameter types as the following example: 54 | // 55 | // func serverResumed(){ 56 | // //code... 57 | // } 58 | func SetResumeCallback(cb interface{}) error { 59 | if serverStarted { 60 | return errors.New(ErrorServerRunning) 61 | } else if callback, ok := cb.(func()); ok { 62 | resumeCallback = callback 63 | } else { 64 | return errors.New(ErrorIncorrectFunction) 65 | } 66 | return nil 67 | } 68 | 69 | // SetShutDownCallback sets the callback that triggers when the server is shut down. The 70 | // function passed must have the same parameter types as the following example: 71 | // 72 | // func serverStopped(){ 73 | // //code... 74 | // } 75 | func SetShutDownCallback(cb interface{}) error { 76 | if serverStarted { 77 | return errors.New(ErrorServerRunning) 78 | } else if callback, ok := cb.(func()); ok { 79 | stopCallback = callback 80 | } else { 81 | return errors.New(ErrorIncorrectFunction) 82 | } 83 | return nil 84 | } 85 | 86 | // SetClientConnectCallback sets the callback that triggers when a client connects to the server. The 87 | // function passed must have the same parameter types as the following example: 88 | // 89 | // func clientConnected(writer *http.ResponseWriter, request *http.Request) bool { 90 | // //code... 91 | // } 92 | // 93 | // The function returns a boolean. If false is returned, the client will receive an HTTP error `http.StatusForbidden` and 94 | // will be rejected from the server. This can be used to, for instance, make a black/white list or implement client sessions. 95 | func SetClientConnectCallback(cb interface{}) error { 96 | if serverStarted { 97 | return errors.New(ErrorServerRunning) 98 | } else if callback, ok := cb.(func(*http.ResponseWriter, *http.Request) bool); ok { 99 | clientConnectCallback = callback 100 | return nil 101 | } 102 | return errors.New(ErrorIncorrectFunction) 103 | } 104 | 105 | // SetLoginCallback sets the callback that triggers when a client logs in as a User. The 106 | // function passed must have the same parameter types as the following example: 107 | // 108 | // func clientLoggedIn(userName string, databaseID int, receivedColumns map[string]interface{}, clientColumns map[string]interface{}) bool { 109 | // //code... 110 | // } 111 | // 112 | // `userName` is the name of the User logging in, `databaseID` is the index of the User on the database, `receivedColumns` are the custom `AccountInfoColumn` (keys) and their values 113 | // received from the database, and `clientColumns` have the same keys as the `receivedColumns`, but are the input from the client. 114 | // 115 | // The function returns a boolean. If false is returned, the client will receive a `helpers.ErrorActionDenied` (1052) error and will be 116 | // denied from logging in. This can be used to, for instance, suspend or ban a User. 117 | // 118 | // Note: the `clientColumns` decides which `AccountInfoColumn`s were fetched from the database, so the keys will always be the same as `receivedColumns`. 119 | // You can compare the `receivedColumns` and `clientColumns` to, for instance, compare the key 'email' to make sure the 120 | // client also provided the right email address for that account on the database. 121 | func SetLoginCallback(cb interface{}) error { 122 | if serverStarted { 123 | return errors.New(ErrorServerRunning) 124 | } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { 125 | if (*settings).EnableSqlFeatures { 126 | database.LoginCallback = callback 127 | } else { 128 | core.LoginCallback = callback 129 | } 130 | return nil 131 | } 132 | return errors.New(ErrorIncorrectFunction) 133 | } 134 | 135 | // SetLogoutCallback sets the callback that triggers when a client logs out from a User. The 136 | // function passed must have the same parameter types as the following example: 137 | // 138 | // func clientLoggedOut(userName string, databaseID int) { 139 | // //code... 140 | // } 141 | // 142 | // `userName` is the name of the User logging in, `databaseID` is the index of the User on the database. 143 | func SetLogoutCallback(cb interface{}) error { 144 | if serverStarted { 145 | return errors.New(ErrorServerRunning) 146 | } else if callback, ok := cb.(func(string, int)); ok { 147 | core.LogoutCallback = callback 148 | return nil 149 | } 150 | return errors.New(ErrorIncorrectFunction) 151 | } 152 | 153 | // SetSignupCallback sets the callback that triggers when a client makes an account. The 154 | // function passed must have the same parameter types as the following example: 155 | // 156 | // func clientSignedUp(userName string, clientColumns map[string]interface{}) bool { 157 | // //code... 158 | // } 159 | // 160 | // `userName` is the name of the User logging in, `clientColumns` is the input from the client for setting 161 | // custom `AccountInfoColumn`s on the database. 162 | // 163 | // The function returns a boolean. If false is returned, the client will receive a `helpers.ErrorActionDenied` (1052) error and will be 164 | // denied from signing up. This can be used to, for instance, deny user names or `AccountInfoColumn`s with profanity. 165 | func SetSignupCallback(cb interface{}) error { 166 | if serverStarted { 167 | return errors.New(ErrorServerRunning) 168 | } else if callback, ok := cb.(func(string, map[string]interface{}) bool); ok { 169 | database.SignUpCallback = callback 170 | return nil 171 | } 172 | return errors.New(ErrorIncorrectFunction) 173 | } 174 | 175 | // SetDeleteAccountCallback sets the callback that triggers when a client deletes their account. The 176 | // function passed must have the same parameter types as the following example: 177 | // 178 | // func clientDeletedAccount(userName string, databaseID int, receivedColumns map[string]interface{}, clientColumns map[string]interface{}) bool { 179 | // //code... 180 | // } 181 | // 182 | // `userName` is the name of the User deleting their account, `databaseID` is the index of the User on the database, `receivedColumns` are the custom `AccountInfoColumn` (keys) and their values 183 | // received from the database, and `clientColumns` have the same keys as the `receivedColumns`, but are the input from the client. 184 | // 185 | // The function returns a boolean. If false is returned, the client will receive a `helpers.ErrorActionDenied` (1052) error and will be 186 | // denied from deleting the account. This can be used to, for instance, make extra input requirements for this action. 187 | // 188 | // Note: the `clientColumns` decides which `AccountInfoColumn`s were fetched from the database, so the keys will always be the same as `receivedColumns`. 189 | // You can compare the `receivedColumns` and `clientColumns` to, for instance, compare the keys named 'email' to make sure the 190 | // client also provided the right email address for that account on the database. 191 | func SetDeleteAccountCallback(cb interface{}) error { 192 | if serverStarted { 193 | return errors.New(ErrorServerRunning) 194 | } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { 195 | database.DeleteAccountCallback = callback 196 | return nil 197 | } 198 | return errors.New(ErrorIncorrectFunction) 199 | } 200 | 201 | // SetAccountInfoChangeCallback sets the callback that triggers when a client changes an `AccountInfoColumn`. The 202 | // function passed must have the same parameter types as the following example: 203 | // 204 | // func clientChangedAccountInfo(userName string, databaseID int, receivedColumns map[string]interface{}, clientColumns map[string]interface{}) bool { 205 | // //code... 206 | // } 207 | // 208 | // `userName` is the name of the User changing info, `databaseID` is the index of the User on the database, `receivedColumns` are the custom `AccountInfoColumn` (keys) and their values 209 | // received from the database, and `clientColumns` have the same keys as the `receivedColumns`, but are the input from the client. 210 | // 211 | // The function returns a boolean. If false is returned, the client will receive a `helpers.ErrorActionDenied` (1052) error and will be 212 | // denied from changing the info. This can be used to, for instance, make extra input requirements for this action. 213 | // 214 | // Note: the `clientColumns` decides which `AccountInfoColumn`s were fetched from the database, so the keys will always be the same as `receivedColumns`. 215 | // You can compare the `receivedColumns` and `clientColumns` to, for instance, compare the keys named 'email' to make sure the 216 | // client also provided the right email address for that account on the database. 217 | func SetAccountInfoChangeCallback(cb interface{}) error { 218 | if serverStarted { 219 | return errors.New(ErrorServerRunning) 220 | } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { 221 | database.AccountInfoChangeCallback = callback 222 | return nil 223 | } 224 | return errors.New(ErrorIncorrectFunction) 225 | } 226 | 227 | // SetPasswordChangeCallback sets the callback that triggers when a client changes their password. The 228 | // function passed must have the same parameter types as the following example: 229 | // 230 | // func clientChangedPassword(userName string, databaseID int, receivedColumns map[string]interface{}, clientColumns map[string]interface{}) bool { 231 | // //code... 232 | // } 233 | // 234 | // `userName` is the name of the User changing their password, `databaseID` is the index of the User on the database, `receivedColumns` are the custom `AccountInfoColumn` (keys) and their values 235 | // received from the database, and `clientColumns` have the same keys as the `receivedColumns`, but are the input from the client. 236 | // 237 | // The function returns a boolean. If false is returned, the client will receive a `helpers.ErrorActionDenied` (1052) error and will be 238 | // denied from changing the password. This can be used to, for instance, make extra input requirements for this action. 239 | // 240 | // Note: the `clientColumns` decides which `AccountInfoColumn`s were fetched from the database, so the keys will always be the same as `receivedColumns`. 241 | // You can compare the `receivedColumns` and `clientColumns` to, for instance, compare the keys named 'email' to make sure the 242 | // client also provided the right email address for that account on the database. 243 | func SetPasswordChangeCallback(cb interface{}) error { 244 | if serverStarted { 245 | return errors.New(ErrorServerRunning) 246 | } else if callback, ok := cb.(func(string, int, map[string]interface{}, map[string]interface{}) bool); ok { 247 | database.PasswordChangeCallback = callback 248 | return nil 249 | } 250 | return errors.New(ErrorIncorrectFunction) 251 | } 252 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Package gopher is used to start and change the core settings for the Gopher Game Server. The 2 | // type ServerSettings contains all the parameters for changing the core settings. You can either 3 | // pass a ServerSettings when calling Server.Start() or nil if you want to use the default server 4 | // settings. 5 | package gopher 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "github.com/hewiefreeman/GopherGameServer/actions" 12 | "github.com/hewiefreeman/GopherGameServer/core" 13 | "github.com/hewiefreeman/GopherGameServer/database" 14 | "io/ioutil" 15 | "net/http" 16 | "os" 17 | "strconv" 18 | "time" 19 | ) 20 | 21 | /////////// TO DOs: 22 | /////////// - Make authentication for GopherDB 23 | /////////// - Admin tools 24 | /////////// - More useful command-line macros 25 | 26 | // ServerSettings are the core settings for the Gopher Game Server. You must fill one of these out to customize 27 | // the server's functionality to your liking. 28 | type ServerSettings struct { 29 | ServerName string // The server's name. Used for the server's ownership of private Rooms. (Required) 30 | MaxConnections int // The maximum amount of concurrent connections the server will accept. Setting this to 0 means infinite. 31 | 32 | HostName string // Server's host name. Use 'https://' for TLS connections. (ex: 'https://example.com') (Required) 33 | HostAlias string // Server's host alias name. Use 'https://' for TLS connections. (ex: 'https://www.example.com') 34 | IP string // Server's IP address. (Required) 35 | Port int // Server's port. (Required) 36 | 37 | TLS bool // Enables TLS/SSL connections. 38 | CertFile string // SSL/TLS certificate file location (starting from system's root folder). (Required for TLS) 39 | PrivKeyFile string // SSL/TLS private key file location (starting from system's root folder). (Required for TLS) 40 | 41 | 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. 42 | 43 | MultiConnect bool // Enables multiple connections under the same User. When enabled, will override KickDupOnLogin's functionality. 44 | MaxUserConns uint8 // Overrides the default (255) of maximum simultaneous connections on a single User 45 | KickDupOnLogin bool // When enabled, a logged in User will be disconnected from service when another User logs in with the same name. 46 | 47 | UserRoomControl bool // Enables Users to create Rooms, invite/uninvite(AKA revoke) other Users to their owned private rooms, and destroy their owned rooms. 48 | 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! 49 | 50 | EnableSqlFeatures bool // Enables the built-in SQL User authentication and friending. NOTE: It is HIGHLY recommended to use TLS over an SSL/HTTPS connection when using the SQL features. Otherwise, sensitive User information can be compromised with network "snooping" (AKA "sniffing"). 51 | SqlIP string // SQL Database IP address. (Required for SQL features) 52 | SqlPort int // SQL Database port. (Required for SQL features) 53 | SqlProtocol string // The protocol to use while comminicating with the MySQL database. Most use either 'udp' or 'tcp'. (Required for SQL features) 54 | SqlUser string // SQL user name (Required for SQL features) 55 | SqlPassword string // SQL user password (Required for SQL features) 56 | SqlDatabase string // SQL database name (Required for SQL features) 57 | 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. 58 | CustomLoginColumn string // The custom AccountInfoColumn you wish to use for logging in instead of the default name column. 59 | RememberMe bool // Enables the "Remember Me" login feature. You can read more about this in project's wiki. 60 | 61 | EnableRecovery bool // Enables the recovery of all Rooms, their settings, and their variables on start-up after terminating the server. 62 | RecoveryLocation string // The folder location (starting from system's root folder) where you would like to store the recovery data. (Required for recovery) 63 | 64 | AdminLogin string // The login name for the Admin Tools (Required for Admin Tools) 65 | AdminPassword string // The password for the Admin Tools (Required for Admin Tools) 66 | } 67 | 68 | type serverRestore struct { 69 | R map[string]core.RoomRecoveryState 70 | } 71 | 72 | var ( 73 | httpServer *http.Server 74 | 75 | settings *ServerSettings 76 | 77 | serverStarted bool = false 78 | serverPaused bool = false 79 | serverStopping bool = false 80 | serverEndChan chan error = make(chan error) 81 | 82 | startCallback func() 83 | pauseCallback func() 84 | stopCallback func() 85 | resumeCallback func() 86 | clientConnectCallback func(*http.ResponseWriter, *http.Request) bool 87 | 88 | //SERVER VERSION NUMBER 89 | version string = "1.0-BETA.2" 90 | ) 91 | 92 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 93 | // Server start-up /////////////////////////////////////////////////////////////////////////////// 94 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 95 | 96 | // Start will start the server. Call with a pointer to your `ServerSettings` (or nil for defaults) to start the server. The default 97 | // settings are for local testing ONLY. There are security-related options in `ServerSettings` 98 | // for SSL/TLS, connection origin testing, administrator tools, and more. It's highly recommended to look into 99 | // all `ServerSettings` options to tune the server for your desired functionality and security needs. 100 | // 101 | // This function will block the thread that it is ran on until the server either errors, or is manually shut-down. To run code after the 102 | // server starts/stops/pauses/etc, use the provided server callback setter functions. 103 | func Start(s *ServerSettings) { 104 | if serverStarted || serverPaused { 105 | return 106 | } 107 | serverStarted = true 108 | fmt.Println(" _______ __\n | _ |.-----..-----.| |--..-----..----.\n |. |___||. _ ||. _ ||. ||. -__||. _|\n |. | ||:. . ||:. __||: |: ||: ||: |\n |: | |'-----'|: | '--'--''-----''--'\n |::.. . | '--' - Game Server -\n '-------'\n\n ") 109 | fmt.Println("Starting server...") 110 | // Set server settings 111 | if s != nil { 112 | if !s.verify() { 113 | return 114 | } 115 | settings = s 116 | } else { 117 | // Default localhost settings 118 | fmt.Println("Using default settings...") 119 | settings = &ServerSettings{ 120 | ServerName: "!server!", 121 | MaxConnections: 0, 122 | 123 | HostName: "localhost", 124 | HostAlias: "localhost", 125 | IP: "localhost", 126 | Port: 8080, 127 | 128 | TLS: false, 129 | CertFile: "", 130 | PrivKeyFile: "", 131 | 132 | OriginOnly: false, 133 | 134 | MultiConnect: false, 135 | KickDupOnLogin: false, 136 | 137 | UserRoomControl: true, 138 | RoomDeleteOnLeave: true, 139 | 140 | EnableSqlFeatures: false, 141 | SqlIP: "localhost", 142 | SqlPort: 3306, 143 | SqlProtocol: "tcp", 144 | SqlUser: "user", 145 | SqlPassword: "password", 146 | SqlDatabase: "database", 147 | EncryptionCost: 4, 148 | CustomLoginColumn: "", 149 | RememberMe: false, 150 | 151 | EnableRecovery: false, 152 | RecoveryLocation: "C:/", 153 | 154 | AdminLogin: "admin", 155 | AdminPassword: "password"} 156 | } 157 | 158 | // Update package settings 159 | core.SettingsSet((*settings).KickDupOnLogin, (*settings).ServerName, (*settings).RoomDeleteOnLeave, (*settings).EnableSqlFeatures, 160 | (*settings).RememberMe, (*settings).MultiConnect, (*settings).MaxUserConns) 161 | 162 | // Notify packages of server start 163 | core.SetServerStarted(true) 164 | actions.SetServerStarted(true) 165 | database.SetServerStarted(true) 166 | 167 | // Start database 168 | if (*settings).EnableSqlFeatures { 169 | fmt.Println("Initializing database...") 170 | dbErr := database.Init((*settings).SqlUser, (*settings).SqlPassword, (*settings).SqlDatabase, 171 | (*settings).SqlProtocol, (*settings).SqlIP, (*settings).SqlPort, (*settings).EncryptionCost, 172 | (*settings).RememberMe, (*settings).CustomLoginColumn) 173 | if dbErr != nil { 174 | fmt.Println("Database error:", dbErr.Error()) 175 | fmt.Println("Shutting down...") 176 | return 177 | } 178 | fmt.Println("Database initialized") 179 | } 180 | 181 | // Recover state 182 | if settings.EnableRecovery { 183 | recoverState() 184 | } 185 | 186 | // Start socket listener 187 | if settings.TLS { 188 | httpServer = makeServer("/wss", settings.TLS) 189 | } else { 190 | httpServer = makeServer("/ws", settings.TLS) 191 | } 192 | 193 | // Run callback 194 | if startCallback != nil { 195 | startCallback() 196 | } 197 | 198 | // Start macro listener 199 | go macroListener() 200 | 201 | fmt.Println("Startup complete") 202 | 203 | // Wait for server shutdown 204 | doneErr := <-serverEndChan 205 | 206 | if doneErr != http.ErrServerClosed { 207 | fmt.Println("Fatal server error:", doneErr.Error()) 208 | 209 | if !serverStopping { 210 | fmt.Println("Disconnecting users...") 211 | 212 | // Pause server 213 | core.Pause() 214 | actions.Pause() 215 | database.Pause() 216 | 217 | // Save state 218 | if settings.EnableRecovery { 219 | saveState() 220 | } 221 | } 222 | } 223 | 224 | fmt.Println("Server shut-down completed") 225 | 226 | if stopCallback != nil { 227 | stopCallback() 228 | } 229 | } 230 | 231 | func (settings *ServerSettings) verify() bool { 232 | if settings.ServerName == "" { 233 | fmt.Println("ServerName in ServerSettings is required. Shutting down...") 234 | return false 235 | 236 | } else if settings.HostName == "" || settings.IP == "" || settings.Port < 1 { 237 | fmt.Println("HostName, IP, and Port in ServerSettings are required. Shutting down...") 238 | return false 239 | 240 | } else if settings.TLS == true && (settings.CertFile == "" || settings.PrivKeyFile == "") { 241 | fmt.Println("CertFile and PrivKeyFile in ServerSettings are required for a TLS connection. Shutting down...") 242 | return false 243 | 244 | } else if settings.EnableSqlFeatures == true && (settings.SqlIP == "" || settings.SqlPort < 1 || settings.SqlProtocol == "" || 245 | settings.SqlUser == "" || settings.SqlPassword == "" || settings.SqlDatabase == "") { 246 | fmt.Println("SqlIP, SqlPort, SqlProtocol, SqlUser, SqlPassword, and SqlDatabase in ServerSettings are required for the SQL features. Shutting down...") 247 | return false 248 | 249 | } else if settings.EnableRecovery == true && settings.RecoveryLocation == "" { 250 | fmt.Println("RecoveryLocation in ServerSettings is required for server recovery. Shutting down...") 251 | return false 252 | 253 | } else if settings.EnableRecovery { 254 | // Check if invalid file location 255 | if _, err := os.Stat(settings.RecoveryLocation); err != nil { 256 | fmt.Println("RecoveryLocation error:", err) 257 | fmt.Println("Shutting down...") 258 | return false 259 | } 260 | var d []byte 261 | if err := ioutil.WriteFile(settings.RecoveryLocation+"/test.txt", d, 0644); err != nil { 262 | fmt.Println("RecoveryLocation error:", err) 263 | fmt.Println("Shutting down...") 264 | return false 265 | } 266 | os.Remove(settings.RecoveryLocation + "/test.txt") 267 | 268 | } else if settings.AdminLogin == "" || settings.AdminPassword == "" { 269 | fmt.Println("AdminLogin and AdminPassword in ServerSettings are required. Shutting down...") 270 | return false 271 | } 272 | 273 | return true 274 | } 275 | 276 | func makeServer(handleDir string, tls bool) *http.Server { 277 | server := &http.Server{Addr: settings.IP + ":" + strconv.Itoa(settings.Port)} 278 | http.HandleFunc(handleDir, socketInitializer) 279 | if tls { 280 | go func() { 281 | err := server.ListenAndServeTLS(settings.CertFile, settings.PrivKeyFile) 282 | serverEndChan <- err 283 | }() 284 | } else { 285 | go func() { 286 | err := server.ListenAndServe() 287 | serverEndChan <- err 288 | }() 289 | } 290 | 291 | // 292 | return server 293 | } 294 | 295 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 296 | // Server actions //////////////////////////////////////////////////////////////////////////////// 297 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 298 | 299 | // 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. 300 | // Same goes for rooms created by Users unless RoomDeleteOnLeave in ServerSettings is set to true. 301 | func Pause() { 302 | if !serverPaused { 303 | serverPaused = true 304 | 305 | fmt.Println("Pausing server...") 306 | 307 | core.Pause() 308 | actions.Pause() 309 | database.Pause() 310 | 311 | // Run callback 312 | if pauseCallback != nil { 313 | pauseCallback() 314 | } 315 | 316 | fmt.Println("Server paused") 317 | 318 | serverStarted = false 319 | } 320 | 321 | } 322 | 323 | // Resume will allow Users to login again after pausing the server. 324 | func Resume() { 325 | if serverPaused { 326 | serverStarted = true 327 | 328 | fmt.Println("Resuming server...") 329 | core.Resume() 330 | actions.Resume() 331 | database.Resume() 332 | 333 | // Run callback 334 | if resumeCallback != nil { 335 | resumeCallback() 336 | } 337 | 338 | fmt.Println("Server resumed") 339 | 340 | serverPaused = false 341 | } 342 | } 343 | 344 | // 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. 345 | func ShutDown() error { 346 | if !serverStopping { 347 | serverStopping = true 348 | fmt.Println("Disconnecting users...") 349 | 350 | // Pause server 351 | core.Pause() 352 | actions.Pause() 353 | database.Pause() 354 | 355 | // Save state 356 | if settings.EnableRecovery { 357 | saveState() 358 | } 359 | 360 | // Shut server down 361 | fmt.Println("Shutting server down...") 362 | shutdownErr := httpServer.Shutdown(context.Background()) 363 | if shutdownErr != http.ErrServerClosed { 364 | return shutdownErr 365 | } 366 | } 367 | // 368 | return nil 369 | } 370 | 371 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 372 | // Saving and recovery /////////////////////////////////////////////////////////////////////////// 373 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 374 | 375 | func saveState() { 376 | fmt.Println("Saving server state...") 377 | saveErr := writeState(getState(), settings.RecoveryLocation) 378 | if saveErr != nil { 379 | fmt.Println("Error saving state:", saveErr) 380 | return 381 | } 382 | fmt.Println("Save state successful") 383 | } 384 | 385 | func writeState(stateObj serverRestore, saveFolder string) error { 386 | state, err := json.Marshal(stateObj) 387 | if err != nil { 388 | return err 389 | } 390 | err = ioutil.WriteFile(saveFolder+"/Gopher Recovery - "+time.Now().Format("2006-01-02 15-04-05")+".grf", state, 0644) 391 | if err != nil { 392 | return err 393 | } 394 | 395 | return nil 396 | } 397 | 398 | func getState() serverRestore { 399 | return serverRestore{ 400 | R: core.GetRoomsState(), 401 | } 402 | } 403 | 404 | func recoverState() { 405 | fmt.Println("Recovering previous state...") 406 | 407 | // Get last recovery file 408 | files, fileErr := ioutil.ReadDir(settings.RecoveryLocation) 409 | if fileErr != nil { 410 | fmt.Println("Error recovering state:", fileErr) 411 | return 412 | } 413 | var newestFile string 414 | var newestTime int64 415 | for _, f := range files { 416 | if len(f.Name()) < 19 || f.Name()[0:15] != "Gopher Recovery" { 417 | continue 418 | } 419 | fi, err := os.Stat(settings.RecoveryLocation + "/" + f.Name()) 420 | if err != nil { 421 | fmt.Println("Error recovering state:", err) 422 | return 423 | } 424 | currTime := fi.ModTime().Unix() 425 | if currTime > newestTime { 426 | newestTime = currTime 427 | newestFile = f.Name() 428 | } 429 | } 430 | 431 | // Read file 432 | r, err := ioutil.ReadFile(settings.RecoveryLocation + "/" + newestFile) 433 | if err != nil { 434 | fmt.Println("Error recovering state:", err) 435 | return 436 | } 437 | 438 | // Convert JSON 439 | var recovery serverRestore 440 | if err = json.Unmarshal(r, &recovery); err != nil { 441 | fmt.Println("Error recovering state:", err) 442 | return 443 | } 444 | 445 | if recovery.R == nil || len(recovery.R) == 0 { 446 | fmt.Println("No rooms to restore!") 447 | return 448 | } 449 | 450 | // Recover rooms 451 | for name, val := range recovery.R { 452 | room, roomErr := core.NewRoom(name, val.T, val.P, val.M, val.O) 453 | if roomErr != nil { 454 | fmt.Println("Error recovering room '"+name+"':", roomErr) 455 | continue 456 | } 457 | for _, userName := range val.I { 458 | invErr := room.AddInvite(userName) 459 | if invErr != nil { 460 | fmt.Println("Error inviting '"+userName+"' to the room '"+name+"':", invErr) 461 | } 462 | } 463 | room.SetVariables(val.V) 464 | } 465 | 466 | // 467 | fmt.Println("State recovery successful") 468 | } 469 | -------------------------------------------------------------------------------- /core/rooms.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/hewiefreeman/GopherGameServer/helpers" 6 | "sync" 7 | ) 8 | 9 | // Room represents a room on the server that Users can join and leave. Use core.NewRoom() to make a new Room. 10 | // 11 | // WARNING: When you use a *Room object in your code, DO NOT dereference it. Instead, there are 12 | // many methods for *Room for maniupulating and retrieving any information about them you could possibly need. 13 | // Dereferencing them could cause data races in the Room fields that get locked by mutexes. 14 | type Room struct { 15 | name string 16 | rType string 17 | private bool 18 | owner string 19 | maxUsers int 20 | 21 | //mux LOCKS ALL FIELDS BELOW 22 | mux sync.Mutex 23 | inviteList []string 24 | usersMap map[string]*RoomUser 25 | vars map[string]interface{} 26 | } 27 | 28 | // RoomUser represents a User inside of a Room. Use the *RoomUser.User() function to get a *User from a *RoomUser 29 | type RoomUser struct { 30 | user *User 31 | 32 | mux sync.Mutex 33 | conns map[string]*userConn 34 | } 35 | 36 | var ( 37 | rooms map[string]*Room = make(map[string]*Room) 38 | roomsMux sync.Mutex 39 | ) 40 | 41 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | // MAKE A NEW ROOM //////////////////////////////////////////////////////////////////////////////////////////// 43 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | 45 | // NewRoom adds a new room to the server. This can be called before or after starting the server. 46 | // Parameters: 47 | // 48 | // - name (string): Name of the Room 49 | // 50 | // - rType (string): Room type name (Note: must be a valid RoomType's name) 51 | // 52 | // - isPrivate (bool): Indicates if the room is private or not 53 | // 54 | // - maxUsers (int): Maximum User capacity (Note: 0 means no limit) 55 | // 56 | // - owner (string): The owner of the room. If provided a blank string, will set the owner to the ServerName from ServerSettings 57 | func NewRoom(name string, rType string, isPrivate bool, maxUsers int, owner string) (*Room, error) { 58 | //REJECT INCORRECT INPUT 59 | if len(name) == 0 { 60 | return &Room{}, errors.New("core.NewRoom() requires a name") 61 | } else if maxUsers < 0 { 62 | maxUsers = 0 63 | } else if owner == "" { 64 | owner = serverName 65 | } 66 | 67 | var roomType *RoomType 68 | var ok bool 69 | if roomType, ok = roomTypes[rType]; !ok { 70 | return &Room{}, errors.New("Invalid room type") 71 | } 72 | 73 | //ADD THE ROOM 74 | roomsMux.Lock() 75 | if _, ok := rooms[name]; ok { 76 | roomsMux.Unlock() 77 | return &Room{}, errors.New("A Room with the name '" + name + "' already exists") 78 | } 79 | theRoom := Room{name: name, private: isPrivate, inviteList: []string{}, usersMap: make(map[string]*RoomUser), maxUsers: maxUsers, 80 | vars: make(map[string]interface{}), owner: owner, rType: rType} 81 | rooms[name] = &theRoom 82 | roomsMux.Unlock() 83 | 84 | //CALLBACK 85 | if roomType.HasCreateCallback() { 86 | roomType.CreateCallback()(&theRoom) 87 | } 88 | 89 | return &theRoom, nil 90 | } 91 | 92 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 93 | // DELETE A ROOM ////////////////////////////////////////////////////////////////////////////////////////////// 94 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 95 | 96 | // Delete deletes the Room from the server. Will also send a room leave message to all the Users in the Room that you can 97 | // capture with the client APIs. 98 | func (r *Room) Delete() error { 99 | r.mux.Lock() 100 | if r.usersMap == nil { 101 | r.mux.Unlock() 102 | return errors.New("The room '" + r.name + "' does not exist") 103 | } 104 | 105 | // MAKE LEAVE MESSAGE 106 | leaveMessage := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, nil, helpers.NoError()) 107 | 108 | // GO THROUGH ALL Users IN ROOM 109 | for _, u := range r.usersMap { 110 | //CHANGE User's room POINTER TO nil & SEND MESSAGES 111 | u.mux.Lock() 112 | for key := range u.conns { 113 | (*u.conns[key]).socket.WriteJSON(leaveMessage) 114 | u.user.mux.Lock() 115 | (*u.conns[key]).room = nil 116 | u.user.mux.Unlock() 117 | } 118 | u.mux.Unlock() 119 | } 120 | 121 | r.usersMap = nil 122 | r.mux.Unlock() 123 | 124 | // DELETE THE ROOM 125 | roomsMux.Lock() 126 | delete(rooms, r.name) 127 | roomsMux.Unlock() 128 | 129 | // CALLBACK 130 | rType := roomTypes[r.rType] 131 | if rType.HasDeleteCallback() { 132 | rType.DeleteCallback()(r) 133 | } 134 | 135 | // 136 | return nil 137 | } 138 | 139 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 140 | // GET A ROOM ///////////////////////////////////////////////////////////////////////////////////////////////// 141 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 142 | 143 | // GetRoom finds a Room on the server. If the room does not exit, an error will be returned. 144 | func GetRoom(roomName string) (*Room, error) { 145 | //REJECT INCORRECT INPUT 146 | if len(roomName) == 0 { 147 | return &Room{}, errors.New("core.GetRoom() requires a room name") 148 | } 149 | 150 | var room *Room 151 | var ok bool 152 | 153 | roomsMux.Lock() 154 | if room, ok = rooms[roomName]; !ok { 155 | roomsMux.Unlock() 156 | return &Room{}, errors.New("The room '" + roomName + "' does not exist") 157 | } 158 | roomsMux.Unlock() 159 | 160 | // 161 | return room, nil 162 | } 163 | 164 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 165 | // ADD A USER ///////////////////////////////////////////////////////////////////////////////////////////////// 166 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 167 | 168 | // AddUser adds a User to the Room. If you are using MultiConnect in ServerSettings, the connID 169 | // parameter is the connection ID associated with one of the connections attached to that User. This must 170 | // be provided when adding a User to a Room with MultiConnect enabled. Otherwise, an empty string can be used. 171 | func (r *Room) AddUser(user *User, connID string) error { 172 | userName := user.Name() 173 | // REJECT INCORRECT INPUT 174 | if user == nil { 175 | return errors.New("*Room.AddUser() requires a valid User") 176 | } else if multiConnect && len(connID) == 0 { 177 | return errors.New("Must provide a connID when MultiConnect is enabled") 178 | } else if !multiConnect { 179 | connID = "1" 180 | } 181 | r.mux.Lock() 182 | if r.usersMap == nil { 183 | r.mux.Unlock() 184 | return errors.New("The room '" + r.name + "' does not exist") 185 | } else if r.maxUsers != 0 && len(r.usersMap) == r.maxUsers { 186 | r.mux.Unlock() 187 | return errors.New("The room '" + r.name + "' is full") 188 | } 189 | // CHECK IF THE ROOM IS PRIVATE, OWNER JOINS FREELY 190 | if r.private && userName != r.owner { 191 | // IF PRIVATE AND NOT OWNER, CHECK IF THIS USER IS ON THE INVITE LIST 192 | if len(r.inviteList) > 0 { 193 | for i := 0; i < len(r.inviteList); i++ { 194 | if (r.inviteList)[i] == userName { 195 | // INVITED User 196 | break 197 | } 198 | if i == len(r.inviteList)-1 { 199 | r.mux.Unlock() 200 | return errors.New("User '" + userName + "' is not on the invite list") 201 | } 202 | } 203 | } else { 204 | r.mux.Unlock() 205 | return errors.New("User '" + userName + "' is not on the invite list") 206 | } 207 | } 208 | 209 | // CHECK IF USER IS ALREADY IN THE ROOM 210 | var ru *RoomUser 211 | var ok bool 212 | if ru, ok = r.usersMap[userName]; ok { 213 | if !multiConnect { 214 | r.mux.Unlock() 215 | return errors.New("User '" + userName + "' is already in room '" + r.name + "'") 216 | } 217 | ru.mux.Lock() 218 | if _, ok := ru.conns[connID]; ok { 219 | r.mux.Unlock() 220 | ru.mux.Unlock() 221 | return errors.New("User '" + userName + "' is already in room '" + r.name + "'") 222 | } 223 | ru.mux.Unlock() 224 | } 225 | // ADD User TO ROOM 226 | user.mux.Lock() 227 | c := user.conns[connID] 228 | if c == nil { 229 | r.mux.Unlock() 230 | return errors.New("Invalid connection ID") 231 | } 232 | if ru != nil { 233 | (*r.usersMap[userName]).mux.Lock() 234 | (*r.usersMap[userName]).conns[connID] = c 235 | (*r.usersMap[userName]).mux.Unlock() 236 | } else { 237 | conns := make(map[string]*userConn) 238 | conns[connID] = c 239 | newUser := RoomUser{user: user, conns: conns} 240 | r.usersMap[userName] = &newUser 241 | ru = r.usersMap[userName] 242 | } 243 | // CHANGE USER'S ROOM 244 | c.room = r 245 | 246 | user.mux.Unlock() 247 | r.mux.Unlock() 248 | 249 | // 250 | roomType := roomTypes[r.rType] 251 | if roomType.BroadcastUserEnter() { 252 | //BROADCAST ENTER TO USERS IN ROOM 253 | message := map[string]map[string]interface{}{ 254 | helpers.ServerActionUserEnter: { 255 | "u": userName, 256 | "g": user.isGuest, 257 | }, 258 | } 259 | for _, u := range r.usersMap { 260 | u.mux.Lock() 261 | if u.user.Name() != userName { 262 | for _, conn := range u.conns { 263 | (*conn).socket.WriteJSON(message) 264 | } 265 | } 266 | u.mux.Unlock() 267 | } 268 | } 269 | // CALLBACK 270 | if roomType.HasUserEnterCallback() { 271 | roomType.UserEnterCallback()(r, ru) 272 | } 273 | 274 | // SEND RESPONSE TO CLIENT 275 | clientResp := helpers.MakeClientResponse(helpers.ClientActionJoinRoom, r.Name(), helpers.NoError()) 276 | c.socket.WriteJSON(clientResp) 277 | 278 | // 279 | return nil 280 | } 281 | 282 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 283 | // REMOVE A USER ////////////////////////////////////////////////////////////////////////////////////////////// 284 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 285 | 286 | // RemoveUser removes a User from the room. If you are using MultiConnect in ServerSettings, the connID 287 | // parameter is the connection ID associated with one of the connections attached to that User. This must 288 | // be provided when removing a User from a Room with MultiConnect enabled. Otherwise, an empty string can be used. 289 | func (r *Room) RemoveUser(user *User, connID string) error { 290 | //REJECT INCORRECT INPUT 291 | if user == nil || len(user.name) == 0 { 292 | return errors.New("*Room.RemoveUser() requires a valid *User") 293 | } else if multiConnect && len(connID) == 0 { 294 | return errors.New("Must provide a connID when MultiConnect is enabled") 295 | } else if !multiConnect { 296 | connID = "1" 297 | } 298 | // 299 | r.mux.Lock() 300 | if r.usersMap == nil { 301 | r.mux.Unlock() 302 | return errors.New("The room '" + r.name + "' does not exist") 303 | } 304 | var ok bool 305 | var ru *RoomUser 306 | if ru, ok = r.usersMap[user.name]; !ok { 307 | r.mux.Unlock() 308 | return errors.New("User '" + user.name + "' is not in room '" + r.name + "'") 309 | } 310 | ru.mux.Lock() 311 | var uConn *userConn 312 | if uConn, ok = ru.conns[connID]; !ok { 313 | r.mux.Unlock() 314 | ru.mux.Unlock() 315 | return errors.New("Invalid connID") 316 | } 317 | delete(ru.conns, connID) 318 | // Remove user when no conns are left in room 319 | if len(ru.conns) == 0 { 320 | delete(r.usersMap, user.name) 321 | } 322 | ru.mux.Unlock() 323 | userList := r.usersMap 324 | r.mux.Unlock() 325 | // 326 | roomType := roomTypes[r.rType] 327 | 328 | //DELETE THE ROOM IF THE OWNER LEFT AND UserRoomControl IS ENABLED 329 | if deleteRoomOnLeave && user.name == r.owner { 330 | deleteErr := r.Delete() 331 | if deleteErr != nil { 332 | return deleteErr 333 | } 334 | } else if roomType.BroadcastUserLeave() { 335 | //CONSTRUCT LEAVE MESSAGE 336 | message := map[string]map[string]interface{}{ 337 | helpers.ServerActionUserLeave: { 338 | "u": user.name, 339 | }, 340 | } 341 | 342 | //SEND MESSAGE TO USERS 343 | for _, u := range userList { 344 | u.mux.Lock() 345 | for _, conn := range u.conns { 346 | conn.socket.WriteJSON(message) 347 | } 348 | u.mux.Unlock() 349 | } 350 | } 351 | 352 | // CHANGE USER'S ROOM 353 | user.mux.Lock() 354 | uConn.room = nil 355 | user.mux.Unlock() 356 | 357 | //CALLBACK 358 | if roomType.HasUserLeaveCallback() { 359 | roomType.UserLeaveCallback()(r, ru) 360 | } 361 | 362 | //SEND RESPONSE TO CLIENT 363 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLeaveRoom, r.Name(), helpers.NoError()) 364 | uConn.socket.WriteJSON(clientResp) 365 | 366 | // 367 | return nil 368 | } 369 | 370 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 371 | // ADD TO inviteList ////////////////////////////////////////////////////////////////////////////////////////// 372 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 373 | 374 | // AddInvite adds a User to a private Room's invite list. This is only meant for internal Gopher Game Server mechanics. 375 | // If you want a User to invite someone to a private room, use the *User.Invite() function instead. 376 | // 377 | // NOTE: Remember that private rooms are designed to have an "owner", 378 | // and only the owner should be able to send an invite and revoke an invitation for their Rooms. Also, *User.Invite() 379 | // will send an invite notification message to the invited User that the client API can easily receive. Though if you wish to make 380 | // your own implementations for sending and receiving these notifications, this function is safe to use. 381 | func (r *Room) AddInvite(userName string) error { 382 | if !r.private { 383 | return errors.New("Room is not private") 384 | } else if len(userName) == 0 { 385 | return errors.New("*Room.AddInvite() requires a userName") 386 | } 387 | 388 | r.mux.Lock() 389 | if r.usersMap == nil { 390 | r.mux.Unlock() 391 | return errors.New("The room '" + r.name + "' does not exist") 392 | } 393 | for i := 0; i < len(r.inviteList); i++ { 394 | if r.inviteList[i] == userName { 395 | r.mux.Unlock() 396 | return errors.New("User '" + userName + "' is already on the invite list") 397 | } 398 | } 399 | r.inviteList = append(r.inviteList, userName) 400 | r.mux.Unlock() 401 | 402 | // 403 | return nil 404 | } 405 | 406 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 407 | // REMOVE FROM inviteList ///////////////////////////////////////////////////////////////////////////////////// 408 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 409 | 410 | // RemoveInvite removes a User from a private Room's invite list. To make a User remove someone from their room themselves, 411 | // use the *User.RevokeInvite() function. 412 | // 413 | // NOTE: You can use this function safely, but remember that private rooms are designed to have an "owner", 414 | // and only the owner should be able to send an invite and revoke an invitation for their Rooms. But if you find the 415 | // need to break the rules here, by all means do so! 416 | func (r *Room) RemoveInvite(userName string) error { 417 | if !r.private { 418 | return errors.New("Room is not private") 419 | } else if len(userName) == 0 { 420 | return errors.New("*Room.RemoveInvite() requires a userName") 421 | } 422 | 423 | r.mux.Lock() 424 | 425 | if r.usersMap == nil { 426 | r.mux.Unlock() 427 | return errors.New("The room '" + r.name + "' does not exist") 428 | } 429 | for i := 0; i < len(r.inviteList); i++ { 430 | if r.inviteList[i] == userName { 431 | r.inviteList[i] = r.inviteList[len(r.inviteList)-1] 432 | r.inviteList = r.inviteList[:len(r.inviteList)-1] 433 | break 434 | } 435 | if i == len(r.inviteList)-1 { 436 | r.mux.Unlock() 437 | return errors.New("User '" + userName + "' is not on the invite list") 438 | } 439 | } 440 | r.mux.Unlock() 441 | 442 | // 443 | return nil 444 | } 445 | 446 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 447 | // Room ATTRIBUTE READERS ///////////////////////////////////////////////////////////////////////////////////// 448 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 449 | 450 | // Name gets the name of the Room. 451 | func (r *Room) Name() string { 452 | return r.name 453 | } 454 | 455 | // Type gets the type of the Room. 456 | func (r *Room) Type() string { 457 | return r.rType 458 | } 459 | 460 | // IsPrivate returns true of the Room is private. 461 | func (r *Room) IsPrivate() bool { 462 | return r.private 463 | } 464 | 465 | // Owner gets the name of the owner of the room 466 | func (r *Room) Owner() string { 467 | return r.owner 468 | } 469 | 470 | // MaxUsers gets the maximum User capacity of the Room. 471 | func (r *Room) MaxUsers() int { 472 | return r.maxUsers 473 | } 474 | 475 | // NumUsers gets the number of Users in the Room. 476 | func (r *Room) NumUsers() int { 477 | m, e := r.GetUserMap() 478 | if e != nil { 479 | return 0 480 | } 481 | return len(m) 482 | } 483 | 484 | // InviteList gets a private Room's invite list. 485 | func (r *Room) InviteList() ([]string, error) { 486 | r.mux.Lock() 487 | if r.usersMap == nil { 488 | r.mux.Unlock() 489 | return []string{}, errors.New("The room '" + r.name + "' does not exist") 490 | } 491 | list := r.inviteList 492 | r.mux.Unlock() 493 | // 494 | return list, nil 495 | } 496 | 497 | // GetUserMap retrieves all the RoomUsers as a map[string]*RoomUser. 498 | func (r *Room) GetUserMap() (map[string]*RoomUser, error) { 499 | var err error 500 | var userMap map[string]*RoomUser 501 | 502 | r.mux.Lock() 503 | if r.usersMap == nil { 504 | err = errors.New("The room '" + r.name + "' does not exist") 505 | } else { 506 | userMap = r.usersMap 507 | } 508 | r.mux.Unlock() 509 | 510 | return userMap, err 511 | } 512 | 513 | // RoomCount returns the number of Rooms created on the server. 514 | func RoomCount() int { 515 | roomsMux.Lock() 516 | length := len(rooms) 517 | roomsMux.Unlock() 518 | return length 519 | } 520 | 521 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 522 | // RoomUser ATTRIBUTE READERS ///////////////////////////////////////////////////////////////////////////////// 523 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 524 | 525 | // User gets the *User object of a *RoomUser. 526 | func (u *RoomUser) User() *User { 527 | return u.user 528 | } 529 | 530 | // ConnectionIDs returns a []string of all the RoomUser's connection IDs. With MultiConnect in ServerSettings enabled, 531 | // this will give you all the connections for this User that are currently in the Room. Otherwise, if you want 532 | // all the User's connection IDs (not just the connections in the specified Room), use *User.ConnectionIDs() after getting 533 | // the *User object with the *RoomUser.User() function. 534 | func (u *RoomUser) ConnectionIDs() []string { 535 | u.mux.Lock() 536 | ids := make([]string, 0, len(u.conns)) 537 | for id := range u.conns { 538 | ids = append(ids, id) 539 | } 540 | u.mux.Unlock() 541 | return ids 542 | } 543 | -------------------------------------------------------------------------------- /core/users.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/gorilla/websocket" 6 | "github.com/hewiefreeman/GopherGameServer/database" 7 | "github.com/hewiefreeman/GopherGameServer/helpers" 8 | "sync" 9 | ) 10 | 11 | // User represents a client who has logged into the service. A User can 12 | // be a guest, join/leave/create rooms, and call any client action, including your 13 | // custom client actions. If you are not using the built-in authentication, be aware 14 | // that you will need to make sure any client who has not been authenticated by the server 15 | // can't simply log themselves in through the client API. A User has a lot of useful information, 16 | // so it's highly recommended you look through all the *User methods to get a good understanding 17 | // about everything you can do with them. 18 | // 19 | // WARNING: When you use a *User object in your code, DO NOT dereference it. Instead, there are 20 | // many methods for *User for retrieving any information about them you could possibly need. 21 | // Dereferencing them could cause data races (which will panic and stop the server) in the User 22 | // fields that get locked for synchronizing access. 23 | type User struct { 24 | name string 25 | databaseID int 26 | isGuest bool 27 | 28 | //mux lock all items below 29 | mux sync.Mutex 30 | status int 31 | friends map[string]*database.Friend 32 | conns map[string]*userConn 33 | } 34 | 35 | type userConn struct { 36 | // Must lock *clientMux when using *user 37 | clientMux *sync.Mutex 38 | user **User 39 | 40 | socket *websocket.Conn 41 | 42 | //Must lock user's mux to use below items 43 | room *Room 44 | vars map[string]interface{} 45 | } 46 | 47 | var ( 48 | users map[string]*User = make(map[string]*User) 49 | usersMux sync.Mutex 50 | 51 | // LoginCallback is only for internal Gopher Game Server mechanics. 52 | LoginCallback func(string, int, map[string]interface{}, map[string]interface{}) bool 53 | // LogoutCallback is only for internal Gopher Game Server mechanics. 54 | LogoutCallback func(string, int) 55 | ) 56 | 57 | // These represent the four statuses a User could be. 58 | const ( 59 | StatusAvailable = iota // User is available 60 | StatusInGame // User is in a game 61 | StatusIdle // User is idle 62 | StatusOffline // User is offline 63 | ) 64 | 65 | // Error messages 66 | const ( 67 | errorDenied = "Action was denied" 68 | errorRequiredName = "A user name is required" 69 | errorRequiredID = "An ID is required" 70 | errorRequiredSocket = "A socket is required" 71 | errorNameUnavail = "Username is unavailable" 72 | errorUnexpected = "Unexpected error" 73 | errorAlreadyLogged = "User is already logged in" 74 | errorServerPaused = "Server is paused" 75 | ) 76 | 77 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 78 | // LOG A USER IN ///////////////////////////////////////////////////////////////////////////////// 79 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 80 | 81 | // Login logs a User in to the service. 82 | func Login(userName string, dbID int, autologPass string, isGuest bool, remMe bool, socket *websocket.Conn, 83 | connUser **User, clientMux *sync.Mutex) (string, helpers.GopherError) { 84 | // Verify input 85 | if serverPaused { 86 | return "", helpers.NewError(errorServerPaused, helpers.ErrorServerPaused) 87 | } else if len(userName) == 0 { 88 | return "", helpers.NewError(errorRequiredName, helpers.ErrorAuthRequiredName) 89 | } else if userName == serverName { 90 | return "", helpers.NewError(errorNameUnavail, helpers.ErrorAuthNameUnavail) 91 | } else if dbID < -1 { 92 | return "", helpers.NewError(errorRequiredID, helpers.ErrorAuthRequiredID) 93 | } else if socket == nil { 94 | return "", helpers.NewError(errorRequiredSocket, helpers.ErrorAuthRequiredSocket) 95 | } 96 | 97 | // Guests always have -1 databaseID 98 | databaseID := dbID 99 | if isGuest { 100 | databaseID = -1 101 | } 102 | 103 | // Callback 104 | if LoginCallback != nil && !LoginCallback(userName, dbID, nil, nil) { 105 | return "", helpers.NewError(errorDenied, helpers.ErrorActionDenied) 106 | } 107 | 108 | // Make *User in users & make connID 109 | var connID string 110 | var connErr error 111 | var userExists bool = false 112 | // 113 | usersMux.Lock() 114 | // 115 | if userOnline, ok := users[userName]; ok { 116 | userExists = true 117 | if kickOnLogin { 118 | // Kick user & remove from room 119 | userOnline.mux.Lock() 120 | for connKey, conn := range userOnline.conns { 121 | userRoom := (*conn).room 122 | if userRoom != nil && userRoom.Name() != "" { 123 | userOnline.mux.Unlock() 124 | userRoom.RemoveUser(userOnline, connKey) 125 | userOnline.mux.Lock() 126 | } 127 | (*(*conn).clientMux).Lock() 128 | *((*conn).user) = nil 129 | (*(*conn).clientMux).Unlock() 130 | // Send logout message to client 131 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) 132 | (*conn).socket.WriteJSON(clientResp) 133 | } 134 | userOnline.mux.Unlock() 135 | 136 | // Remove user from users map 137 | delete(users, userName) 138 | 139 | // Make connID 140 | connID = "1" 141 | userExists = false 142 | } else if multiConnect { 143 | // Make a unique connID 144 | for { 145 | connID, connErr = helpers.GenerateSecureString(5) 146 | if connErr != nil { 147 | usersMux.Unlock() 148 | return "", helpers.NewError(errorUnexpected, helpers.ErrorAuthUnexpected) 149 | } 150 | userOnline.mux.Lock() 151 | if _, found := (*userOnline).conns[connID]; !found { 152 | userOnline.mux.Unlock() 153 | break 154 | } 155 | userOnline.mux.Unlock() 156 | } 157 | } else { 158 | usersMux.Unlock() 159 | return "", helpers.NewError(errorAlreadyLogged, helpers.ErrorAuthAlreadyLogged) 160 | } 161 | } else if multiConnect { 162 | // Make connID 163 | connID, connErr = helpers.GenerateSecureString(5) 164 | if connErr != nil { 165 | usersMux.Unlock() 166 | return "", helpers.NewError(errorUnexpected, helpers.ErrorAuthUnexpected) 167 | } 168 | } else { 169 | // Make connID 170 | connID = "1" 171 | } 172 | // Make the userConn 173 | vars := make(map[string]interface{}) 174 | conn := userConn{socket: socket, room: nil, vars: vars, user: connUser, clientMux: clientMux} 175 | // Make friends objects 176 | var u *User 177 | var friends []map[string]interface{} 178 | var friendsMap map[string]*database.Friend 179 | // Add the userConn to the User or make new User 180 | if userExists { 181 | (*users[userName]).mux.Lock() 182 | (*users[userName]).conns[connID] = &conn 183 | friendsMap = (*users[userName]).friends 184 | (*users[userName]).mux.Unlock() 185 | // Make friends list for response 186 | friends = makeFriendsResponse(friendsMap) 187 | } else { 188 | // Get friend list from database 189 | if dbID != -1 && sqlFeatures { 190 | var friendsErr error 191 | if friendsMap, friendsErr = database.GetFriends(dbID); friendsErr == nil { 192 | // Make friends list for response 193 | friends = makeFriendsResponse(friendsMap) 194 | } 195 | } 196 | conns := map[string]*userConn{ 197 | connID: &conn, 198 | } 199 | newUser := User{name: userName, databaseID: databaseID, isGuest: isGuest, status: 0, 200 | friends: friendsMap, conns: conns} 201 | u = &newUser 202 | users[userName] = u 203 | } 204 | (*conn.clientMux).Lock() 205 | *(conn.user) = users[userName] 206 | (*conn.clientMux).Unlock() 207 | // 208 | usersMux.Unlock() 209 | 210 | // Send online message to friends 211 | statusMessage := map[string]map[string]interface{}{ 212 | helpers.ServerActionFriendStatusChange: { 213 | "n": userName, 214 | "s": 0, 215 | }, 216 | } 217 | u.sendToFriends(statusMessage) 218 | 219 | // Login success, send response to client 220 | var responseVal map[string]interface{} 221 | if rememberMe && len(autologPass) > 0 && remMe { 222 | responseVal = map[string]interface{}{ 223 | "n": userName, 224 | "f": friends, 225 | "ai": dbID, 226 | "ap": autologPass, 227 | } 228 | } else { 229 | responseVal = map[string]interface{}{ 230 | "n": userName, 231 | "f": friends, 232 | } 233 | } 234 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLogin, responseVal, helpers.NoError()) 235 | socket.WriteJSON(clientResp) 236 | 237 | // 238 | return connID, helpers.NoError() 239 | } 240 | 241 | func makeFriendsResponse(friendsMap map[string]*database.Friend) []map[string]interface{} { 242 | friends := make([]map[string]interface{}, len(friendsMap), len(friendsMap)) 243 | i := 0; 244 | for _, val := range friendsMap { 245 | frs := val.RequestStatus() 246 | friendEntry := map[string]interface{}{ 247 | "n": val.Name(), 248 | "rs": frs, 249 | } 250 | if frs == database.FriendStatusAccepted { 251 | // Get the friend's status 252 | if friend, ok := users[val.Name()]; ok { 253 | friendEntry["s"] = friend.Status() 254 | } else { 255 | friendEntry["s"] = StatusOffline 256 | } 257 | } 258 | friends[i] = friendEntry 259 | i++; 260 | } 261 | return friends 262 | } 263 | 264 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 265 | // AUTOLOG A USER IN ///////////////////////////////////////////////////////////////////////////// 266 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 267 | 268 | // AutoLogIn logs a user in automatically with RememberMe and SqlFeatures enabled in ServerSettings. 269 | // 270 | // WARNING: This is only meant for internal Gopher Game Server mechanics. If you want the "Remember Me" 271 | // (AKA auto login) feature, enable it in ServerSettings along with the SqlFeatures and corresponding 272 | // options. You can read more about the "Remember Me" login in the project's usage section. 273 | func AutoLogIn(tag string, pass string, newPass string, dbID int, conn *websocket.Conn, connUser **User, clientMux *sync.Mutex) (string, helpers.GopherError) { 274 | if serverPaused { 275 | return "", helpers.NewError(errorServerPaused, helpers.ErrorServerPaused) 276 | } 277 | 278 | // Verify and get user name from database 279 | userName, autoLogErr := database.AutoLoginClient(tag, pass, newPass, dbID) 280 | if autoLogErr.ID != 0 { 281 | return "", autoLogErr 282 | } 283 | // Log user in 284 | connID, userErr := Login(userName, dbID, newPass, false, true, conn, connUser, clientMux) 285 | if userErr.ID != 0 { 286 | return "", userErr 287 | } 288 | 289 | return connID, helpers.NoError() 290 | } 291 | 292 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 293 | // LOG/KICK A USER OUT /////////////////////////////////////////////////////////////////////////// 294 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 295 | 296 | // Logout logs a User out from the service. If you are using MultiConnect in ServerSettings, the connID 297 | // parameter is the connection ID associated with one of the connections attached to that User. This must 298 | // be provided when logging a User out with MultiConnect enabled. Otherwise, an empty string can be used. 299 | func (u *User) Logout(connID string) { 300 | if multiConnect && len(connID) == 0 { 301 | return 302 | } else if !multiConnect { 303 | connID = "1" 304 | } 305 | 306 | // Remove user from their room 307 | u.mux.Lock() 308 | if _, ok := u.conns[connID]; !ok { 309 | u.mux.Unlock() 310 | return 311 | } 312 | currRoom := (*u.conns[connID]).room 313 | if currRoom != nil && currRoom.Name() != "" { 314 | u.mux.Unlock() 315 | currRoom.RemoveUser(u, connID) 316 | u.mux.Lock() 317 | } 318 | 319 | if len(u.conns) == 1 { 320 | // Send status change to friends 321 | statusMessage := map[string]map[string]interface{}{ 322 | helpers.ServerActionFriendStatusChange: { 323 | "n": u.name, 324 | "s": StatusOffline, 325 | }, 326 | } 327 | u.sendToFriends(statusMessage) 328 | } 329 | // Log user out 330 | (*u.conns[connID]).clientMux.Lock() 331 | if *((*u.conns[connID]).user) != nil { 332 | *((*u.conns[connID]).user) = nil 333 | } 334 | (*u.conns[connID]).clientMux.Unlock() 335 | socket := (*u.conns[connID]).socket 336 | delete(u.conns, connID) 337 | if len(u.conns) == 0 { 338 | // Delete user if there are no more conns 339 | u.mux.Unlock() 340 | usersMux.Lock() 341 | delete(users, u.name) 342 | usersMux.Unlock() 343 | } else { 344 | u.mux.Unlock() 345 | } 346 | 347 | // Send response 348 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) 349 | socket.WriteJSON(clientResp) 350 | 351 | // Run callback 352 | if LogoutCallback != nil { 353 | LogoutCallback(u.Name(), u.DatabaseID()) 354 | } 355 | } 356 | 357 | // Kick will log off all connections on this User. 358 | func (u *User) Kick() { 359 | u.mux.Lock() 360 | 361 | // Send status change message to friends 362 | statusMessage := map[string]map[string]interface{}{ 363 | helpers.ServerActionFriendStatusChange: { 364 | "n": u.name, 365 | "s": StatusOffline, 366 | }, 367 | } 368 | u.sendToFriends(statusMessage) 369 | 370 | // Make response 371 | clientResp := helpers.MakeClientResponse(helpers.ClientActionLogout, nil, helpers.NoError()) 372 | 373 | // Go through all connections 374 | for connID, conn := range u.conns { 375 | // Remove from room 376 | currRoom := (*conn).room 377 | if currRoom != nil && currRoom.Name() != "" { 378 | u.mux.Unlock() 379 | currRoom.RemoveUser(u, connID) 380 | u.mux.Lock() 381 | } 382 | 383 | // Log connection out 384 | (*conn).clientMux.Lock() 385 | if *((*conn).user) != nil { 386 | *((*conn).user) = nil 387 | } 388 | (*conn).clientMux.Unlock() 389 | 390 | // Send response 391 | (*conn).socket.WriteJSON(clientResp) 392 | } 393 | 394 | u.mux.Unlock() 395 | 396 | // Remove from users 397 | usersMux.Lock() 398 | delete(users, u.name) 399 | usersMux.Unlock() 400 | 401 | // Run callback 402 | if LogoutCallback != nil { 403 | LogoutCallback(u.Name(), u.DatabaseID()) 404 | } 405 | } 406 | 407 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 408 | // GET A USER //////////////////////////////////////////////////////////////////////////////////// 409 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 410 | 411 | // GetUser finds a logged in User by their name. Returns an error if the User is not online. 412 | func GetUser(userName string) (*User, error) { 413 | // Verify input 414 | if len(userName) == 0 { 415 | return &User{}, errors.New("users.Get() requires a user name") 416 | } else if serverPaused { 417 | return &User{}, errors.New(errorServerPaused) 418 | } 419 | 420 | var user *User 421 | var ok bool 422 | 423 | usersMux.Lock() 424 | if user, ok = users[userName]; !ok { 425 | usersMux.Unlock() 426 | return &User{}, errors.New("User '" + userName + "' is not logged in") 427 | } 428 | usersMux.Unlock() 429 | 430 | // 431 | return user, nil 432 | } 433 | 434 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 435 | // MAKE A USER JOIN/LEAVE A ROOM ///////////////////////////////////////////////////////////////// 436 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 437 | 438 | // Join makes a User join a Room. If you are using MultiConnect in ServerSettings, the connID 439 | // parameter is the connection ID associated with one of the connections attached to that User. This must 440 | // be provided when making a User join a Room with MultiConnect enabled. Otherwise, an empty string can be used. 441 | func (u *User) Join(r *Room, connID string) error { 442 | if multiConnect && len(connID) == 0 { 443 | return errors.New("Must provide a connID when MultiConnect is enabled") 444 | } else if !multiConnect { 445 | connID = "1" 446 | } 447 | u.mux.Lock() 448 | if _, ok := u.conns[connID]; !ok { 449 | u.mux.Unlock() 450 | return errors.New("Invalid connID") 451 | } 452 | currRoom := (*u.conns[connID]).room 453 | if currRoom != nil && currRoom.Name() == r.Name() { 454 | u.mux.Unlock() 455 | return errors.New("User '" + u.name + "' is already in room '" + r.Name() + "'") 456 | } else if currRoom != nil && currRoom.Name() != "" { 457 | // Leave current room 458 | u.mux.Unlock() 459 | u.Leave(connID) 460 | u.mux.Lock() 461 | } 462 | u.mux.Unlock() 463 | 464 | // Add user to room 465 | addErr := r.AddUser(u, connID) 466 | if addErr != nil { 467 | return addErr 468 | } 469 | 470 | // 471 | return nil 472 | } 473 | 474 | // Leave makes a User leave their current room. If you are using MultiConnect in ServerSettings, the connID 475 | // parameter is the connection ID associated with one of the connections attached to that User. This must 476 | // be provided when making a User leave a Room with MultiConnect enabled. Otherwise, an empty string can be used. 477 | func (u *User) Leave(connID string) error { 478 | if multiConnect && len(connID) == 0 { 479 | return errors.New("Must provide a connID when MultiConnect is enabled") 480 | } else if !multiConnect { 481 | connID = "1" 482 | } 483 | 484 | u.mux.Lock() 485 | if _, ok := u.conns[connID]; !ok { 486 | u.mux.Unlock() 487 | return errors.New("Invalid connID") 488 | } 489 | currRoom := (*u.conns[connID]).room 490 | u.mux.Unlock() 491 | if currRoom != nil && currRoom.Name() != "" { 492 | removeErr := currRoom.RemoveUser(u, connID) 493 | if removeErr != nil { 494 | return removeErr 495 | } 496 | } else { 497 | return errors.New("User '" + u.name + "' is not in a room.") 498 | } 499 | 500 | return nil 501 | } 502 | 503 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 504 | // SET THE STATUS OF A USER ////////////////////////////////////////////////////////////////////// 505 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 506 | 507 | // SetStatus sets the status of a User. Also sends a notification to all the User's Friends (with the request 508 | // status "accepted") that they changed their status. 509 | func (u *User) SetStatus(status int) { 510 | u.mux.Lock() 511 | u.status = status 512 | u.mux.Unlock() 513 | 514 | // Send status to friends 515 | message := map[string]map[string]interface{}{ 516 | helpers.ServerActionFriendStatusChange: { 517 | "n": u.name, 518 | "s": status, 519 | }, 520 | } 521 | u.sendToFriends(message) 522 | } 523 | 524 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 525 | // INVITE TO User's PRIVATE ROOM ///////////////////////////////////////////////////////////////// 526 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 527 | 528 | // Invite allows Users to invite other Users to their private Rooms. The inviting User must be in the Room, 529 | // and the Room must be private and owned by the inviting User. If you are using MultiConnect in ServerSettings, the connID 530 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 531 | // be provided when making a User invite another with MultiConnect enabled. Otherwise, an empty string can be used. 532 | func (u *User) Invite(invUser *User, connID string) error { 533 | if multiConnect && len(connID) == 0 { 534 | return errors.New("Must provide a connID when MultiConnect is enabled") 535 | } else if !multiConnect { 536 | connID = "1" 537 | } 538 | 539 | u.mux.Lock() 540 | if _, ok := u.conns[connID]; !ok { 541 | u.mux.Unlock() 542 | return errors.New("Invalid connID") 543 | } 544 | currRoom := (*u.conns[connID]).room 545 | u.mux.Unlock() 546 | rType := GetRoomTypes()[currRoom.Type()] 547 | if currRoom == nil || currRoom.Name() == "" { 548 | return errors.New("The user '" + u.name + "' is not in a room") 549 | } else if !currRoom.IsPrivate() { 550 | return errors.New("The room '" + currRoom.Name() + "' is not private") 551 | } else if currRoom.Owner() != u.name { 552 | return errors.New("The user '" + u.name + "' is not the owner of the room '" + currRoom.Name() + "'") 553 | } else if rType.ServerOnly() { 554 | return errors.New("Only the server can manipulate that type of room") 555 | } 556 | 557 | // Add to invite list 558 | addErr := currRoom.AddInvite(invUser.name) 559 | if addErr != nil { 560 | return addErr 561 | } 562 | 563 | // Make response message 564 | invMessage := map[string]map[string]interface{}{ 565 | helpers.ServerActionRoomInvite: { 566 | "u": u.name, 567 | "r": currRoom.Name(), 568 | }, 569 | } 570 | 571 | // Send response to all connections 572 | invUser.mux.Lock() 573 | for _, conn := range invUser.conns { 574 | (*conn).socket.WriteJSON(invMessage) 575 | } 576 | invUser.mux.Unlock() 577 | 578 | // 579 | return nil 580 | } 581 | 582 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 583 | // REVOKE INVITE TO User's PRIVATE ROOM ////////////////////////////////////////////////////////// 584 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 585 | 586 | // RevokeInvite revokes the invite to the specified user to their current Room, provided they are online, the Room is private, and this User 587 | // is the owner of the Room. If you are using MultiConnect in ServerSettings, the connID 588 | // parameter is the connection ID associated with one of the connections attached to the inviting User. This must 589 | // be provided when making a User revoke an invite with MultiConnect enabled. Otherwise, an empty string can be used. 590 | func (u *User) RevokeInvite(revokeUser string, connID string) error { 591 | if multiConnect && len(connID) == 0 { 592 | return errors.New("Must provide a connID when MultiConnect is enabled") 593 | } else if !multiConnect { 594 | connID = "1" 595 | } 596 | 597 | u.mux.Lock() 598 | if _, ok := u.conns[connID]; !ok { 599 | u.mux.Unlock() 600 | return errors.New("Invalid connID") 601 | } 602 | currRoom := (*u.conns[connID]).room 603 | u.mux.Unlock() 604 | rType := GetRoomTypes()[currRoom.Type()] 605 | if currRoom == nil || currRoom.Name() == "" { 606 | return errors.New("The user '" + u.name + "' is not in a room") 607 | } else if !currRoom.IsPrivate() { 608 | return errors.New("The room '" + currRoom.Name() + "' is not private") 609 | } else if currRoom.Owner() != u.name { 610 | return errors.New("The user '" + u.name + "' is not the owner of the room '" + currRoom.Name() + "'") 611 | } else if rType.ServerOnly() { 612 | return errors.New("Only the server can manipulate that type of room") 613 | } 614 | 615 | // Remove from invite list 616 | removeErr := currRoom.RemoveInvite(revokeUser) 617 | if removeErr != nil { 618 | return removeErr 619 | } 620 | 621 | // 622 | return nil 623 | } 624 | 625 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 626 | // User ATTRIBUTE READERS //////////////////////////////////////////////////////////////////////// 627 | ////////////////////////////////////////////////////////////////////////////////////////////////////// 628 | 629 | // UserCount returns the number of Users logged into the server. 630 | func UserCount() int { 631 | usersMux.Lock() 632 | length := len(users) 633 | usersMux.Unlock() 634 | return length 635 | } 636 | 637 | // Name gets the name of the User. 638 | func (u *User) Name() string { 639 | return u.name 640 | } 641 | 642 | // DatabaseID gets the database table index of the User. 643 | func (u *User) DatabaseID() int { 644 | return u.databaseID 645 | } 646 | 647 | // Friends gets the Friend list of the User as a map[string]database.Friend where the key string is the friend's 648 | // User name. 649 | func (u *User) Friends() map[string]database.Friend { 650 | u.mux.Lock() 651 | friends := make(map[string]database.Friend) 652 | for key, val := range u.friends { 653 | friends[key] = *val 654 | } 655 | u.mux.Unlock() 656 | return friends 657 | } 658 | 659 | // 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 660 | // parameter is the connection ID associated with one of the connections attached to that User. This must 661 | // be provided when getting a User's Room with MultiConnect enabled. Otherwise, an empty string can be used. 662 | func (u *User) RoomIn(connID string) *Room { 663 | if multiConnect && len(connID) == 0 { 664 | return nil 665 | } else if !multiConnect { 666 | connID = "1" 667 | } 668 | u.mux.Lock() 669 | room := (*u.conns[connID]).room 670 | u.mux.Unlock() 671 | // 672 | return room 673 | } 674 | 675 | // Status gets the status of the User. 676 | func (u *User) Status() int { 677 | u.mux.Lock() 678 | status := u.status 679 | u.mux.Unlock() 680 | return status 681 | } 682 | 683 | // Socket gets the WebSocket connection of a User. If you are using MultiConnect in ServerSettings, the connID 684 | // parameter is the connection ID associated with one of the connections attached to that User. This must 685 | // be provided when getting a User's socket connection with MultiConnect enabled. Otherwise, an empty string can be used. 686 | func (u *User) Socket(connID string) *websocket.Conn { 687 | if multiConnect && len(connID) == 0 { 688 | return nil 689 | } else if !multiConnect { 690 | connID = "1" 691 | } 692 | u.mux.Lock() 693 | socket := (*u.conns[connID]).socket 694 | u.mux.Unlock() 695 | // 696 | return socket 697 | } 698 | 699 | // IsGuest returns true if the User is a guest. 700 | func (u *User) IsGuest() bool { 701 | return u.isGuest 702 | } 703 | 704 | // ConnectionIDs returns a []string of all the User's connection IDs 705 | func (u *User) ConnectionIDs() []string { 706 | u.mux.Lock() 707 | ids := make([]string, 0, len(u.conns)) 708 | for id := range u.conns { 709 | ids = append(ids, id) 710 | } 711 | u.mux.Unlock() 712 | return ids 713 | } --------------------------------------------------------------------------------