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 | }
--------------------------------------------------------------------------------