Commit 9844eaa8 authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Update users from script runtime. Merge #92

parent 5b0b3129
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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.

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
}
+19 −60
Original line number Diff line number Diff line
@@ -17,8 +17,6 @@ package server
import (
	"database/sql"
	"encoding/json"
	"strconv"
	"strings"

	"go.uber.org/zap"
)
@@ -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)
	}
+104 −0
Original line number Diff line number Diff line
@@ -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,
@@ -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 {