Loading CHANGELOG.md +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p ### Added - New storage list feature. - Ban users and create groups from within the Runtime environment. - Update users from within the Runtime environment. - New In-App Purchase validation feature. - New In-App Notification feature. Loading server/core_self.go 0 → 100644 +143 −0 Original line number Diff line number Diff line // Copyright 2017 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "database/sql" "encoding/json" "errors" "go.uber.org/zap" "strconv" "strings" ) type SelfUpdateOp struct { UserId []byte Handle string Fullname string Timezone string Location string Lang string Metadata []byte AvatarUrl string } func SelfUpdate(logger *zap.Logger, db *sql.DB, updates []*SelfUpdateOp) (Error_Code, error) { // Use same timestamp for all updates in this batch. ts := nowMs() // Start a transaction. tx, e := db.Begin() if e != nil { logger.Error("Could not update user profile, transaction error", zap.Error(e)) return RUNTIME_EXCEPTION, errors.New("Could not update user profile") } var code Error_Code var err error defer func() { if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { // don't override value of err logger.Error("Could not update user profile, rollback error", zap.Error(rollbackErr)) } } else { if e := tx.Commit(); e != nil { logger.Error("Could not update user profile, commit error", zap.Error(e)) code = RUNTIME_EXCEPTION err = errors.New("Could not update user profile") } } }() for _, update := range updates { index := 1 statements := make([]string, 0) params := make([]interface{}, 0) if update.Handle != "" { statements = append(statements, "handle = $"+strconv.Itoa(index)) params = append(params, update.Handle) index++ } if update.Fullname != "" { statements = append(statements, "fullname = $"+strconv.Itoa(index)) params = append(params, update.Fullname) index++ } if update.Timezone != "" { statements = append(statements, "timezone = $"+strconv.Itoa(index)) params = append(params, update.Timezone) index++ } if update.Location != "" { statements = append(statements, "location = $"+strconv.Itoa(index)) params = append(params, update.Location) index++ } if update.Lang != "" { statements = append(statements, "lang = $"+strconv.Itoa(index)) params = append(params, update.Lang) index++ } if update.Metadata != nil { // Make this `var js interface{}` if we want to allow top-level JSON arrays. var maybeJSON map[string]interface{} if json.Unmarshal(update.Metadata, &maybeJSON) != nil { code = BAD_INPUT err = errors.New("Metadata must be a valid JSON object") return code, err } statements = append(statements, "metadata = $"+strconv.Itoa(index)) params = append(params, update.Metadata) index++ } if update.AvatarUrl != "" { statements = append(statements, "avatar_url = $"+strconv.Itoa(index)) params = append(params, update.AvatarUrl) index++ } if len(statements) == 0 { code = BAD_INPUT err = errors.New("No fields to update") return code, err } params = append(params, ts, update.UserId) res, err := tx.Exec( "UPDATE users SET updated_at = $"+strconv.Itoa(index)+", "+strings.Join(statements, ", ")+" WHERE id = $"+strconv.Itoa(index+1), params...) if err != nil { if strings.HasSuffix(err.Error(), "violates unique constraint \"users_handle_key\"") { code = USER_HANDLE_INUSE err = errors.New("Handle is in use") } else { logger.Warn("Could not update user profile, update error", zap.Error(err)) code = RUNTIME_EXCEPTION err = errors.New("Could not update user profile") } return code, err } else if count, _ := res.RowsAffected(); count == 0 { logger.Warn("Could not update user profile, rows affected error") code = RUNTIME_EXCEPTION err = errors.New("Failed to update user profile") return code, err } } return code, err } server/pipeline_self.go +19 −60 Original line number Diff line number Diff line Loading @@ -17,8 +17,6 @@ package server import ( "database/sql" "encoding/json" "strconv" "strings" "go.uber.org/zap" ) Loading Loading @@ -109,77 +107,38 @@ WHERE u.id = $1`, func (p *pipeline) selfUpdate(logger *zap.Logger, session *session, envelope *Envelope) { update := envelope.GetSelfUpdate() index := 1 statements := make([]string, 0) params := make([]interface{}, 0) if update.Handle != "" { statements = append(statements, "handle = $"+strconv.Itoa(index)) params = append(params, update.Handle) index++ } if update.Fullname != "" { statements = append(statements, "fullname = $"+strconv.Itoa(index)) params = append(params, update.Fullname) index++ } if update.Timezone != "" { statements = append(statements, "timezone = $"+strconv.Itoa(index)) params = append(params, update.Timezone) index++ } if update.Location != "" { statements = append(statements, "location = $"+strconv.Itoa(index)) params = append(params, update.Location) index++ } if update.Lang != "" { statements = append(statements, "lang = $"+strconv.Itoa(index)) params = append(params, update.Lang) index++ // Validate any input possible before we hit database. if update.Handle == "" && update.Fullname == "" && update.Timezone == "" && update.Location == "" && update.Lang == "" && len(update.Metadata) == 0 && update.AvatarUrl == "" { session.Send(ErrorMessageBadInput(envelope.CollationId, "No fields to update")) return } if update.Metadata != nil { if len(update.Metadata) != 0 { // Make this `var js interface{}` if we want to allow top-level JSON arrays. var maybeJSON map[string]interface{} if json.Unmarshal(update.Metadata, &maybeJSON) != nil { session.Send(ErrorMessageBadInput(envelope.CollationId, "Metadata must be a valid JSON object")) return } statements = append(statements, "metadata = $"+strconv.Itoa(index)) params = append(params, update.Metadata) index++ } if update.AvatarUrl != "" { statements = append(statements, "avatar_url = $"+strconv.Itoa(index)) params = append(params, update.AvatarUrl) index++ } if len(statements) == 0 { session.Send(ErrorMessageBadInput(envelope.CollationId, "No fields to update")) return } params = append(params, nowMs(), session.userID.Bytes()) res, err := p.db.Exec( "UPDATE users SET updated_at = $"+strconv.Itoa(index)+", "+strings.Join(statements, ", ")+" WHERE id = $"+strconv.Itoa(index+1), params...) // Run the update. code, err := SelfUpdate(logger, p.db, []*SelfUpdateOp{&SelfUpdateOp{ UserId: session.userID.Bytes(), Handle: update.Handle, Fullname: update.Fullname, Timezone: update.Timezone, Location: update.Location, Lang: update.Lang, Metadata: update.Metadata, AvatarUrl: update.AvatarUrl, }}) if err != nil { if strings.HasSuffix(err.Error(), "violates unique constraint \"users_handle_key\"") { session.Send(ErrorMessage(envelope.CollationId, USER_HANDLE_INUSE, "Handle is in use")) } else { logger.Warn("Could not update user profile", zap.Error(err)) session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not update user profile")) } return } else if count, _ := res.RowsAffected(); count == 0 { session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Failed to update user profile")) session.Send(ErrorMessage(envelope.CollationId, code, err.Error())) return } // Update handle in session and any presences. // Update handle in session and any presences, if a handle update was processed. if update.Handle != "" { session.handle.Store(update.Handle) } Loading server/runtime_nakama_module.go +104 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "register_http": n.registerHTTP, "users_fetch_id": n.usersFetchId, "users_fetch_handle": n.usersFetchHandle, "users_update": n.usersUpdate, "users_ban": n.usersBan, "storage_list": n.storageList, "storage_fetch": n.storageFetch, Loading Loading @@ -298,6 +299,109 @@ func (n *NakamaModule) usersFetchHandle(l *lua.LState) int { return 1 } func (n *NakamaModule) usersUpdate(l *lua.LState) int { updatesTable := l.CheckTable(1) if updatesTable == nil || updatesTable.Len() == 0 { l.ArgError(1, "expects a valid set of user updates") return 0 } conversionError := "" updates := make([]*SelfUpdateOp, 0) updatesTable.ForEach(func(i lua.LValue, u lua.LValue) { updateTable, ok := u.(*lua.LTable) if !ok { conversionError = "expects a valid set of user updates" return } updateTable.ForEach(func(k lua.LValue, v lua.LValue) { update := &SelfUpdateOp{} switch k.String() { case "UserId": if v.Type() != lua.LTString { conversionError = "expects valid user IDs in each update" return } if uid, err := uuid.FromString(v.String()); err != nil { conversionError = "expects valid user IDs in each update" return } else { update.UserId = uid.Bytes() } case "Handle": if v.Type() != lua.LTString { conversionError = "expects valid handles in each update" return } update.Handle = v.String() case "Fullname": if v.Type() != lua.LTString { conversionError = "expects valid fullnames in each update" return } update.Fullname = v.String() case "Timezone": if v.Type() != lua.LTString { conversionError = "expects valid timezones in each update" return } update.Timezone = v.String() case "Location": if v.Type() != lua.LTString { conversionError = "expects valid locations in each update" return } update.Location = v.String() case "Lang": if v.Type() != lua.LTString { conversionError = "expects valid langs in each update" return } update.Lang = v.String() case "Metadata": if v.Type() != lua.LTString { conversionError = "expects valid metadata in each update" return } update.Metadata = []byte(v.String()) case "AvatarUrl": if v.Type() != lua.LTString { conversionError = "expects valid avatar urls in each update" return } update.AvatarUrl = v.String() default: conversionError = "unrecognised update key, expects a valid set of user updates" return } // Check it's a valid update op. if len(update.UserId) == 0 { conversionError = "expects each update to contain a user ID" return } if update.Handle == "" && update.Fullname == "" && update.Timezone == "" && update.Location == "" && update.Lang == "" && len(update.Metadata) == 0 && update.AvatarUrl == "" { conversionError = "expects each update to contain at least one field to change" return } updates = append(updates, update) }) }) if conversionError != "" { l.ArgError(1, conversionError) return 0 } if _, err := SelfUpdate(n.logger, n.db, updates); err != nil { l.RaiseError(fmt.Sprintf("failed to update users: %s", err.Error())) } return 0 } func (n *NakamaModule) usersBan(l *lua.LState) int { usersTable := l.CheckTable(1) if usersTable == nil || usersTable.Len() == 0 { Loading Loading
CHANGELOG.md +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p ### Added - New storage list feature. - Ban users and create groups from within the Runtime environment. - Update users from within the Runtime environment. - New In-App Purchase validation feature. - New In-App Notification feature. Loading
server/core_self.go 0 → 100644 +143 −0 Original line number Diff line number Diff line // Copyright 2017 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "database/sql" "encoding/json" "errors" "go.uber.org/zap" "strconv" "strings" ) type SelfUpdateOp struct { UserId []byte Handle string Fullname string Timezone string Location string Lang string Metadata []byte AvatarUrl string } func SelfUpdate(logger *zap.Logger, db *sql.DB, updates []*SelfUpdateOp) (Error_Code, error) { // Use same timestamp for all updates in this batch. ts := nowMs() // Start a transaction. tx, e := db.Begin() if e != nil { logger.Error("Could not update user profile, transaction error", zap.Error(e)) return RUNTIME_EXCEPTION, errors.New("Could not update user profile") } var code Error_Code var err error defer func() { if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { // don't override value of err logger.Error("Could not update user profile, rollback error", zap.Error(rollbackErr)) } } else { if e := tx.Commit(); e != nil { logger.Error("Could not update user profile, commit error", zap.Error(e)) code = RUNTIME_EXCEPTION err = errors.New("Could not update user profile") } } }() for _, update := range updates { index := 1 statements := make([]string, 0) params := make([]interface{}, 0) if update.Handle != "" { statements = append(statements, "handle = $"+strconv.Itoa(index)) params = append(params, update.Handle) index++ } if update.Fullname != "" { statements = append(statements, "fullname = $"+strconv.Itoa(index)) params = append(params, update.Fullname) index++ } if update.Timezone != "" { statements = append(statements, "timezone = $"+strconv.Itoa(index)) params = append(params, update.Timezone) index++ } if update.Location != "" { statements = append(statements, "location = $"+strconv.Itoa(index)) params = append(params, update.Location) index++ } if update.Lang != "" { statements = append(statements, "lang = $"+strconv.Itoa(index)) params = append(params, update.Lang) index++ } if update.Metadata != nil { // Make this `var js interface{}` if we want to allow top-level JSON arrays. var maybeJSON map[string]interface{} if json.Unmarshal(update.Metadata, &maybeJSON) != nil { code = BAD_INPUT err = errors.New("Metadata must be a valid JSON object") return code, err } statements = append(statements, "metadata = $"+strconv.Itoa(index)) params = append(params, update.Metadata) index++ } if update.AvatarUrl != "" { statements = append(statements, "avatar_url = $"+strconv.Itoa(index)) params = append(params, update.AvatarUrl) index++ } if len(statements) == 0 { code = BAD_INPUT err = errors.New("No fields to update") return code, err } params = append(params, ts, update.UserId) res, err := tx.Exec( "UPDATE users SET updated_at = $"+strconv.Itoa(index)+", "+strings.Join(statements, ", ")+" WHERE id = $"+strconv.Itoa(index+1), params...) if err != nil { if strings.HasSuffix(err.Error(), "violates unique constraint \"users_handle_key\"") { code = USER_HANDLE_INUSE err = errors.New("Handle is in use") } else { logger.Warn("Could not update user profile, update error", zap.Error(err)) code = RUNTIME_EXCEPTION err = errors.New("Could not update user profile") } return code, err } else if count, _ := res.RowsAffected(); count == 0 { logger.Warn("Could not update user profile, rows affected error") code = RUNTIME_EXCEPTION err = errors.New("Failed to update user profile") return code, err } } return code, err }
server/pipeline_self.go +19 −60 Original line number Diff line number Diff line Loading @@ -17,8 +17,6 @@ package server import ( "database/sql" "encoding/json" "strconv" "strings" "go.uber.org/zap" ) Loading Loading @@ -109,77 +107,38 @@ WHERE u.id = $1`, func (p *pipeline) selfUpdate(logger *zap.Logger, session *session, envelope *Envelope) { update := envelope.GetSelfUpdate() index := 1 statements := make([]string, 0) params := make([]interface{}, 0) if update.Handle != "" { statements = append(statements, "handle = $"+strconv.Itoa(index)) params = append(params, update.Handle) index++ } if update.Fullname != "" { statements = append(statements, "fullname = $"+strconv.Itoa(index)) params = append(params, update.Fullname) index++ } if update.Timezone != "" { statements = append(statements, "timezone = $"+strconv.Itoa(index)) params = append(params, update.Timezone) index++ } if update.Location != "" { statements = append(statements, "location = $"+strconv.Itoa(index)) params = append(params, update.Location) index++ } if update.Lang != "" { statements = append(statements, "lang = $"+strconv.Itoa(index)) params = append(params, update.Lang) index++ // Validate any input possible before we hit database. if update.Handle == "" && update.Fullname == "" && update.Timezone == "" && update.Location == "" && update.Lang == "" && len(update.Metadata) == 0 && update.AvatarUrl == "" { session.Send(ErrorMessageBadInput(envelope.CollationId, "No fields to update")) return } if update.Metadata != nil { if len(update.Metadata) != 0 { // Make this `var js interface{}` if we want to allow top-level JSON arrays. var maybeJSON map[string]interface{} if json.Unmarshal(update.Metadata, &maybeJSON) != nil { session.Send(ErrorMessageBadInput(envelope.CollationId, "Metadata must be a valid JSON object")) return } statements = append(statements, "metadata = $"+strconv.Itoa(index)) params = append(params, update.Metadata) index++ } if update.AvatarUrl != "" { statements = append(statements, "avatar_url = $"+strconv.Itoa(index)) params = append(params, update.AvatarUrl) index++ } if len(statements) == 0 { session.Send(ErrorMessageBadInput(envelope.CollationId, "No fields to update")) return } params = append(params, nowMs(), session.userID.Bytes()) res, err := p.db.Exec( "UPDATE users SET updated_at = $"+strconv.Itoa(index)+", "+strings.Join(statements, ", ")+" WHERE id = $"+strconv.Itoa(index+1), params...) // Run the update. code, err := SelfUpdate(logger, p.db, []*SelfUpdateOp{&SelfUpdateOp{ UserId: session.userID.Bytes(), Handle: update.Handle, Fullname: update.Fullname, Timezone: update.Timezone, Location: update.Location, Lang: update.Lang, Metadata: update.Metadata, AvatarUrl: update.AvatarUrl, }}) if err != nil { if strings.HasSuffix(err.Error(), "violates unique constraint \"users_handle_key\"") { session.Send(ErrorMessage(envelope.CollationId, USER_HANDLE_INUSE, "Handle is in use")) } else { logger.Warn("Could not update user profile", zap.Error(err)) session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not update user profile")) } return } else if count, _ := res.RowsAffected(); count == 0 { session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Failed to update user profile")) session.Send(ErrorMessage(envelope.CollationId, code, err.Error())) return } // Update handle in session and any presences. // Update handle in session and any presences, if a handle update was processed. if update.Handle != "" { session.handle.Store(update.Handle) } Loading
server/runtime_nakama_module.go +104 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "register_http": n.registerHTTP, "users_fetch_id": n.usersFetchId, "users_fetch_handle": n.usersFetchHandle, "users_update": n.usersUpdate, "users_ban": n.usersBan, "storage_list": n.storageList, "storage_fetch": n.storageFetch, Loading Loading @@ -298,6 +299,109 @@ func (n *NakamaModule) usersFetchHandle(l *lua.LState) int { return 1 } func (n *NakamaModule) usersUpdate(l *lua.LState) int { updatesTable := l.CheckTable(1) if updatesTable == nil || updatesTable.Len() == 0 { l.ArgError(1, "expects a valid set of user updates") return 0 } conversionError := "" updates := make([]*SelfUpdateOp, 0) updatesTable.ForEach(func(i lua.LValue, u lua.LValue) { updateTable, ok := u.(*lua.LTable) if !ok { conversionError = "expects a valid set of user updates" return } updateTable.ForEach(func(k lua.LValue, v lua.LValue) { update := &SelfUpdateOp{} switch k.String() { case "UserId": if v.Type() != lua.LTString { conversionError = "expects valid user IDs in each update" return } if uid, err := uuid.FromString(v.String()); err != nil { conversionError = "expects valid user IDs in each update" return } else { update.UserId = uid.Bytes() } case "Handle": if v.Type() != lua.LTString { conversionError = "expects valid handles in each update" return } update.Handle = v.String() case "Fullname": if v.Type() != lua.LTString { conversionError = "expects valid fullnames in each update" return } update.Fullname = v.String() case "Timezone": if v.Type() != lua.LTString { conversionError = "expects valid timezones in each update" return } update.Timezone = v.String() case "Location": if v.Type() != lua.LTString { conversionError = "expects valid locations in each update" return } update.Location = v.String() case "Lang": if v.Type() != lua.LTString { conversionError = "expects valid langs in each update" return } update.Lang = v.String() case "Metadata": if v.Type() != lua.LTString { conversionError = "expects valid metadata in each update" return } update.Metadata = []byte(v.String()) case "AvatarUrl": if v.Type() != lua.LTString { conversionError = "expects valid avatar urls in each update" return } update.AvatarUrl = v.String() default: conversionError = "unrecognised update key, expects a valid set of user updates" return } // Check it's a valid update op. if len(update.UserId) == 0 { conversionError = "expects each update to contain a user ID" return } if update.Handle == "" && update.Fullname == "" && update.Timezone == "" && update.Location == "" && update.Lang == "" && len(update.Metadata) == 0 && update.AvatarUrl == "" { conversionError = "expects each update to contain at least one field to change" return } updates = append(updates, update) }) }) if conversionError != "" { l.ArgError(1, conversionError) return 0 } if _, err := SelfUpdate(n.logger, n.db, updates); err != nil { l.RaiseError(fmt.Sprintf("failed to update users: %s", err.Error())) } return 0 } func (n *NakamaModule) usersBan(l *lua.LState) int { usersTable := l.CheckTable(1) if usersTable == nil || usersTable.Len() == 0 { Loading