Commit e48702d3 authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Improve runtime before hook payload formats. Merge #107

parent 5314e519
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -4,6 +4,16 @@ All notable changes to this project are documented below.
The format is based on [keep a changelog](http://keepachangelog.com/) and this project uses [semantic versioning](http://semver.org/).

## [Unreleased]
### Added
- New script runtime functions to convert UUIDs between byte and string representations.

### Changed
- Improve index selection in storage list operations.
- Payloads in `register_before` hooks now use `PascalCase` field names and expose correctly formatted IDs.
- Metadata regions in users, groups, and leaderboard records are now exposed to the script runtime as Lua tables.

### Fixed
- Script runtime batch user update operations now process correctly.

## [1.0.0] - 2017-08-01
### Added
+2 −27
Original line number Diff line number Diff line
@@ -31,16 +31,6 @@ func RuntimeBeforeHook(runtime *Runtime, jsonpbMarshaler *jsonpb.Marshaler, json
		return envelope, nil
	}

	strEnvelope, err := jsonpbMarshaler.MarshalToString(envelope)
	if err != nil {
		return nil, err
	}

	var jsonEnvelope map[string]interface{}
	if err = json.Unmarshal([]byte(strEnvelope), &jsonEnvelope); err != nil {
		return nil, err
	}

	userId := uuid.Nil
	handle := ""
	expiry := int64(0)
@@ -50,22 +40,7 @@ func RuntimeBeforeHook(runtime *Runtime, jsonpbMarshaler *jsonpb.Marshaler, json
		expiry = session.expiry
	}

	result, fnErr := runtime.InvokeFunctionBefore(fn, userId, handle, expiry, jsonEnvelope)
	if fnErr != nil {
		return nil, fnErr
	}

	bytesEnvelope, err := json.Marshal(result)
	if err != nil {
		return nil, err
	}

	resultEnvelope := &Envelope{}
	if err = jsonpbUnmarshaler.Unmarshal(bytes.NewReader(bytesEnvelope), resultEnvelope); err != nil {
		return nil, err
	}

	return resultEnvelope, nil
	return runtime.InvokeFunctionBefore(fn, userId, handle, expiry, jsonpbMarshaler, jsonpbUnmarshaler, envelope)
}

func RuntimeAfterHook(logger *zap.Logger, runtime *Runtime, jsonpbMarshaler *jsonpb.Marshaler, messageType string, envelope *Envelope, session *session) {
@@ -121,7 +96,7 @@ func RuntimeBeforeHookAuthentication(runtime *Runtime, jsonpbMarshaler *jsonpb.M
	handle := ""
	expiry := int64(0)

	result, fnErr := runtime.InvokeFunctionBefore(fn, userId, handle, expiry, jsonEnvelope)
	result, fnErr := runtime.InvokeFunctionBeforeAuthentication(fn, userId, handle, expiry, jsonEnvelope)
	if fnErr != nil {
		return nil, fnErr
	}
+6 −6
Original line number Diff line number Diff line
@@ -105,6 +105,12 @@ func StorageList(logger *zap.Logger, db *sql.DB, caller uuid.UUID, userID []byte
	// Select the correct index. NOTE: should be removed when DB index selection is smarter.
	index := ""
	if len(userID) == 0 {
		if collection == "" {
			index = "deleted_at_bucket_read_collection_record_user_id_idx"
		} else {
			index = "deleted_at_bucket_collection_read_record_user_id_idx"
		}
	} else {
		if bucket == "" {
			index = "deleted_at_user_id_read_bucket_collection_record_idx"
		} else if collection == "" {
@@ -112,12 +118,6 @@ func StorageList(logger *zap.Logger, db *sql.DB, caller uuid.UUID, userID []byte
		} else {
			index = "deleted_at_user_id_bucket_collection_read_record_idx"
		}
	} else {
		if collection == "" {
			index = "deleted_at_bucket_read_collection_record_user_id_idx"
		} else {
			index = "deleted_at_bucket_collection_read_record_user_id_idx"
		}
	}

	// Set up the query.
+315 −2
Original line number Diff line number Diff line
@@ -24,6 +24,9 @@ import (

	"database/sql"

	"bytes"
	"encoding/json"
	"github.com/gogo/protobuf/jsonpb"
	"github.com/satori/go.uuid"
	"github.com/yuin/gopher-lua"
	"go.uber.org/zap"
@@ -222,7 +225,35 @@ func (r *Runtime) InvokeFunctionRPC(fn *lua.LFunction, uid uuid.UUID, handle str
	return nil, errors.New("Runtime function returned invalid data. Only allowed one return value of type String/Byte")
}

func (r *Runtime) InvokeFunctionBefore(fn *lua.LFunction, uid uuid.UUID, handle string, sessionExpiry int64, payload map[string]interface{}) (map[string]interface{}, error) {
func (r *Runtime) InvokeFunctionBefore(fn *lua.LFunction, uid uuid.UUID, handle string, sessionExpiry int64, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, envelope *Envelope) (*Envelope, error) {
	l, _ := r.NewStateThread()
	defer l.Close()

	ctx := NewLuaContext(l, r.luaEnv, BEFORE, uid, handle, sessionExpiry)
	var lv lua.LValue
	var err error
	if envelope != nil {
		lv, err = ConvertEnvelopeToLTable(l, jsonpbMarshaler, envelope)
		if err != nil {
			return nil, err
		}
	}

	retValue, err := r.invokeFunction(l, fn, ctx, lv)
	if err != nil {
		return nil, err
	}

	if retValue == nil || retValue == lua.LNil {
		return nil, errors.New("Runtime before hook did not return the payload")
	} else if retValue.Type() == lua.LTTable {
		return ConvertLTableToEnvelope(l, jsonpbUnmarshaler, retValue.(*lua.LTable), envelope)
	}

	return nil, errors.New("Runtime function returned invalid data. Only allowed one return value of type Table")
}

func (r *Runtime) InvokeFunctionBeforeAuthentication(fn *lua.LFunction, uid uuid.UUID, handle string, sessionExpiry int64, payload map[string]interface{}) (map[string]interface{}, error) {
	l, _ := r.NewStateThread()
	defer l.Close()

@@ -238,7 +269,7 @@ func (r *Runtime) InvokeFunctionBefore(fn *lua.LFunction, uid uuid.UUID, handle
	}

	if retValue == nil || retValue == lua.LNil {
		return nil, nil
		return nil, errors.New("Runtime before hook did not return the payload")
	} else if retValue.Type() == lua.LTTable {
		return ConvertLuaTable(retValue.(*lua.LTable)), nil
	}
@@ -312,3 +343,285 @@ func (r *Runtime) invokeFunction(l *lua.LState, fn *lua.LFunction, ctx *lua.LTab
func (r *Runtime) Stop() {
	r.vm.Close()
}

func ConvertEnvelopeToLTable(l *lua.LState, jsonpbMarshaler *jsonpb.Marshaler, envelope *Envelope) (lua.LValue, error) {
	lt := l.NewTable()

	switch envelope.Payload.(type) {
	case *Envelope_GroupsRemove:
		ids := l.NewTable()
		for i, l := range envelope.GetGroupsRemove().GroupIds {
			gid, err := uuid.FromBytes(l)
			if err != nil {
				return nil, errors.New("Invalid Group ID in GroupsRemove conversion to script runtime")
			}
			ids.RawSetInt(i+1, lua.LString(gid.String()))
		}
		groupIDs := l.NewTable()
		groupIDs.RawSetString("GroupIds", ids)
		lt.RawSetString("GroupsRemove", groupIDs)
	case *Envelope_GroupsJoin:
		ids := l.NewTable()
		for i, l := range envelope.GetGroupsJoin().GroupIds {
			gid, err := uuid.FromBytes(l)
			if err != nil {
				return nil, errors.New("Invalid Group ID in GroupsJoin conversion to script runtime")
			}
			ids.RawSetInt(i+1, lua.LString(gid.String()))
		}
		groupIDs := l.NewTable()
		groupIDs.RawSetString("GroupIds", ids)
		lt.RawSetString("GroupsJoin", groupIDs)
	case *Envelope_GroupsLeave:
		ids := l.NewTable()
		for i, l := range envelope.GetGroupsLeave().GroupIds {
			gid, err := uuid.FromBytes(l)
			if err != nil {
				return nil, errors.New("Invalid Group ID in GroupsLeave conversion to script runtime")
			}
			ids.RawSetInt(i+1, lua.LString(gid.String()))
		}
		groupIDs := l.NewTable()
		groupIDs.RawSetString("GroupIds", ids)
		lt.RawSetString("GroupsLeave", groupIDs)
	case *Envelope_GroupUsersKick:
		pairs := l.NewTable()
		for i, p := range envelope.GetGroupUsersKick().GroupUsers {
			gid, err := uuid.FromBytes(p.GroupId)
			if err != nil {
				return nil, errors.New("Invalid Group ID in GroupUsersKick conversion to script runtime")
			}
			uid, err := uuid.FromBytes(p.UserId)
			if err != nil {
				return nil, errors.New("Invalid User ID in GroupUsersKick conversion to script runtime")
			}
			pair := l.NewTable()
			pair.RawSetString("GroupId", lua.LString(gid.String()))
			pair.RawSetString("UserId", lua.LString(uid.String()))
			pairs.RawSetInt(i+1, pair)
		}
		kicks := l.NewTable()
		kicks.RawSetString("GroupUsers", pairs)
		lt.RawSetString("GroupUsersKick", kicks)
	case *Envelope_GroupUsersPromote:
		pairs := l.NewTable()
		for i, p := range envelope.GetGroupUsersPromote().GroupUsers {
			gid, err := uuid.FromBytes(p.GroupId)
			if err != nil {
				return nil, errors.New("Invalid Group ID in GroupUsersPromote conversion to script runtime")
			}
			uid, err := uuid.FromBytes(p.UserId)
			if err != nil {
				return nil, errors.New("Invalid User ID in GroupUsersPromote conversion to script runtime")
			}
			pair := l.NewTable()
			pair.RawSetString("GroupId", lua.LString(gid.String()))
			pair.RawSetString("UserId", lua.LString(uid.String()))
			pairs.RawSetInt(i+1, pair)
		}
		kicks := l.NewTable()
		kicks.RawSetString("GroupUsers", pairs)
		lt.RawSetString("GroupUsersPromote", kicks)
	default:
		strEnvelope, err := jsonpbMarshaler.MarshalToString(envelope)
		if err != nil {
			return nil, err
		}

		var jsonEnvelope map[string]interface{}
		if err = json.Unmarshal([]byte(strEnvelope), &jsonEnvelope); err != nil {
			return nil, err
		}

		for k, v := range jsonEnvelope {
			lt.RawSetString(k, convertValue(l, v))
		}
	}

	return lt, nil
}

func ConvertLTableToEnvelope(l *lua.LState, jsonpbUnmarshaler *jsonpb.Unmarshaler, lt *lua.LTable, envelope *Envelope) (*Envelope, error) {
	switch envelope.Payload.(type) {
	case *Envelope_GroupsRemove:
		groupsRemove := lt.RawGetString("GroupsRemove")
		if groupsRemove.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsRemove conversion from script runtime")
		}
		groupIds := groupsRemove.(*lua.LTable).RawGetString("GroupIds")
		if groupIds.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsRemove conversion from script runtime")
		}
		ids := make([][]byte, 0)
		var err error
		groupIds.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
			if v.Type() != lua.LTString {
				err = errors.New("Invalid Group ID in GroupsRemove conversion from script runtime")
				return
			}
			gid, e := uuid.FromString(v.String())
			if e != nil {
				err = errors.New("Invalid Group ID in GroupsRemove conversion from script runtime")
				return
			}
			ids = append(ids, gid.Bytes())
		})
		if err != nil {
			return nil, err
		}
		envelope.GetGroupsRemove().GroupIds = ids
	case *Envelope_GroupsJoin:
		groupsJoin := lt.RawGetString("GroupsJoin")
		if groupsJoin.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsJoin conversion from script runtime")
		}
		groupIds := groupsJoin.(*lua.LTable).RawGetString("GroupIds")
		if groupIds.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsJoin conversion from script runtime")
		}
		ids := make([][]byte, 0)
		var err error
		groupIds.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
			if v.Type() != lua.LTString {
				err = errors.New("Invalid Group ID in GroupsJoin conversion from script runtime")
				return
			}
			gid, e := uuid.FromString(v.String())
			if e != nil {
				err = errors.New("Invalid Group ID in GroupsJoin conversion from script runtime")
				return
			}
			ids = append(ids, gid.Bytes())
		})
		if err != nil {
			return nil, err
		}
		envelope.GetGroupsJoin().GroupIds = ids
	case *Envelope_GroupsLeave:
		groupsLeave := lt.RawGetString("GroupsLeave")
		if groupsLeave.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsLeave conversion from script runtime")
		}
		groupIds := groupsLeave.(*lua.LTable).RawGetString("GroupIds")
		if groupIds.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupsLeave conversion from script runtime")
		}
		ids := make([][]byte, 0)
		var err error
		groupIds.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
			if v.Type() != lua.LTString {
				err = errors.New("Invalid Group ID in GroupsLeave conversion from script runtime")
				return
			}
			gid, e := uuid.FromString(v.String())
			if e != nil {
				err = errors.New("Invalid Group ID in GroupsLeave conversion from script runtime")
				return
			}
			ids = append(ids, gid.Bytes())
		})
		if err != nil {
			return nil, err
		}
		envelope.GetGroupsLeave().GroupIds = ids
	case *Envelope_GroupUsersKick:
		groupUsersKick := lt.RawGetString("GroupUsersKick")
		if groupUsersKick.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupUsersKick conversion from script runtime")
		}
		groupUsers := groupUsersKick.(*lua.LTable).RawGetString("GroupUsers")
		if groupUsers.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupUsersKick conversion from script runtime")
		}
		gu := make([]*TGroupUsersKick_GroupUserKick, 0)
		var err error
		groupUsers.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
			if v.Type() != lua.LTTable {
				err = errors.New("Invalid Group User pair in GroupUsersKick conversion from script runtime")
				return
			}
			vt := v.(*lua.LTable)
			g := vt.RawGetString("GroupId")
			if g.Type() != lua.LTString {
				err = errors.New("Invalid Group ID in GroupUsersKick conversion from script runtime")
				return
			}
			gid, e := uuid.FromString(g.String())
			if e != nil {
				err = errors.New("Invalid Group ID in GroupUsersKick conversion from script runtime")
				return
			}
			u := vt.RawGetString("UserId")
			if u.Type() != lua.LTString {
				err = errors.New("Invalid User ID in GroupUsersKick conversion from script runtime")
				return
			}
			uid, e := uuid.FromString(u.String())
			if e != nil {
				err = errors.New("Invalid User ID in GroupUsersKick conversion from script runtime")
				return
			}
			gu = append(gu, &TGroupUsersKick_GroupUserKick{GroupId: gid.Bytes(), UserId: uid.Bytes()})
		})
		if err != nil {
			return nil, err
		}
		envelope.GetGroupUsersKick().GroupUsers = gu
	case *Envelope_GroupUsersPromote:
		groupUsersPromote := lt.RawGetString("GroupUsersPromote")
		if groupUsersPromote.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupUsersPromote conversion from script runtime")
		}
		groupUsers := groupUsersPromote.(*lua.LTable).RawGetString("GroupUsers")
		if groupUsers.Type() != lua.LTTable {
			return nil, errors.New("Invalid payload in GroupUsersPromote conversion from script runtime")
		}
		gu := make([]*TGroupUsersPromote_GroupUserPromote, 0)
		var err error
		groupUsers.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
			if v.Type() != lua.LTTable {
				err = errors.New("Invalid Group User pair in GroupUsersPromote conversion from script runtime")
				return
			}
			vt := v.(*lua.LTable)
			g := vt.RawGetString("GroupId")
			if g.Type() != lua.LTString {
				err = errors.New("Invalid Group ID in GroupUsersPromote conversion from script runtime")
				return
			}
			gid, e := uuid.FromString(g.String())
			if e != nil {
				err = errors.New("Invalid Group ID in GroupUsersPromote conversion from script runtime")
				return
			}
			u := vt.RawGetString("UserId")
			if u.Type() != lua.LTString {
				err = errors.New("Invalid User ID in GroupUsersPromote conversion from script runtime")
				return
			}
			uid, e := uuid.FromString(u.String())
			if e != nil {
				err = errors.New("Invalid User ID in GroupUsersPromote conversion from script runtime")
				return
			}
			gu = append(gu, &TGroupUsersPromote_GroupUserPromote{GroupId: gid.Bytes(), UserId: uid.Bytes()})
		})
		if err != nil {
			return nil, err
		}
		envelope.GetGroupUsersPromote().GroupUsers = gu
	default:
		result := ConvertLuaTable(lt)

		bytesEnvelope, err := json.Marshal(result)
		if err != nil {
			return nil, err
		}

		if err = jsonpbUnmarshaler.Unmarshal(bytes.NewReader(bytesEnvelope), envelope); err != nil {
			return nil, err
		}
	}

	return envelope, nil
}
+120 −24
Original line number Diff line number Diff line
@@ -77,6 +77,8 @@ func NewNakamaModule(logger *zap.Logger, db *sql.DB, l *lua.LState, notification
func (n *NakamaModule) Loader(l *lua.LState) int {
	mod := l.SetFuncs(l.NewTable(), map[string]lua.LGFunction{
		"uuid_v4":                 n.uuidV4,
		"uuid_bytes_to_string":    n.uuidBytesToString,
		"uuid_string_to_bytes":    n.uuidStringToBytes,
		"http_request":            n.httpRequest,
		"json_encode":             n.jsonEncode,
		"json_decode":             n.jsonDecode,
@@ -122,6 +124,36 @@ func (n *NakamaModule) uuidV4(l *lua.LState) int {
	return 1
}

func (n *NakamaModule) uuidBytesToString(l *lua.LState) int {
	uuidBytes := l.CheckString(1)
	if uuidBytes == "" {
		l.ArgError(1, "Expects a UUID byte string")
		return 0
	}
	u, err := uuid.FromBytes([]byte(uuidBytes))
	if err != nil {
		l.ArgError(1, "Not a valid UUID byte string")
		return 0
	}
	l.Push(lua.LString(u.String()))
	return 1
}

func (n *NakamaModule) uuidStringToBytes(l *lua.LState) int {
	uuidString := l.CheckString(1)
	if uuidString == "" {
		l.ArgError(1, "Expects a UUID string")
		return 0
	}
	u, err := uuid.FromString(uuidString)
	if err != nil {
		l.ArgError(1, "Not a valid UUID string")
		return 0
	}
	l.Push(lua.LString(u.Bytes()))
	return 1
}

func (n *NakamaModule) httpRequest(l *lua.LState) int {
	url := l.CheckString(1)
	method := l.CheckString(2)
@@ -437,13 +469,24 @@ func (n *NakamaModule) usersFetchId(l *lua.LState) int {
		return 0
	}

	//translate uuid to string bytes
	// Convert and push the values.
	lv := l.NewTable()
	for i, u := range users {
		// Convert UUIDs to string representation.
		uid, _ := uuid.FromBytes(u.Id)
		u.Id = []byte(uid.String())
		um := structs.Map(u)
		lv.RawSetInt(i+1, convertValue(l, um))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal(u.Metadata, &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}

		ut := ConvertMap(l, um)
		ut.RawSetString("Metadata", ConvertMap(l, metadataMap))
		lv.RawSetInt(i+1, ut)
	}

	l.Push(lv)
@@ -474,13 +517,24 @@ func (n *NakamaModule) usersFetchHandle(l *lua.LState) int {
		return 0
	}

	//translate uuid to string bytes
	// Convert and push the values.
	lv := l.NewTable()
	for i, u := range users {
		// Convert UUIDs to string representation.
		uid, _ := uuid.FromBytes(u.Id)
		u.Id = []byte(uid.String())
		um := structs.Map(u)
		lv.RawSetInt(i+1, convertValue(l, um))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal(u.Metadata, &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}

		ut := ConvertMap(l, um)
		ut.RawSetString("Metadata", ConvertMap(l, metadataMap))
		lv.RawSetInt(i+1, ut)
	}

	l.Push(lv)
@@ -504,8 +558,8 @@ func (n *NakamaModule) usersUpdate(l *lua.LState) int {
			return
		}

		updateTable.ForEach(func(k lua.LValue, v lua.LValue) {
		update := &SelfUpdateOp{}
		updateTable.ForEach(func(k lua.LValue, v lua.LValue) {
			switch k.String() {
			case "UserId":
				if v.Type() != lua.LTString {
@@ -572,6 +626,7 @@ func (n *NakamaModule) usersUpdate(l *lua.LState) int {
				conversionError = "unrecognised update key, expects a valid set of user updates"
				return
			}
		})

		// Check it's a valid update op.
		if len(update.UserId) == 0 {
@@ -584,7 +639,6 @@ func (n *NakamaModule) usersUpdate(l *lua.LState) int {
		}
		updates = append(updates, update)
	})
	})

	if conversionError != "" {
		l.ArgError(1, conversionError)
@@ -1293,7 +1347,16 @@ func (n *NakamaModule) leaderboardSubmit(l *lua.LState, op string) int {
	oid, _ := uuid.FromBytes(record.OwnerId)
	record.OwnerId = []byte(oid.String())
	rm := structs.Map(record)

	outgoingMetadataMap := make(map[string]interface{})
	err = json.Unmarshal(record.Metadata, &outgoingMetadataMap)
	if err != nil {
		l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
		return 0
	}

	lv := ConvertMap(l, rm)
	lv.RawSetString("Metadata", ConvertMap(l, outgoingMetadataMap))

	l.Push(lv)
	return 1
@@ -1412,13 +1475,24 @@ func (n *NakamaModule) groupsCreate(l *lua.LState) int {
		return 0
	}

	//translate uuid to string bytes
	// Convert and push the values.
	lv := l.NewTable()
	for i, g := range groups {
		uid, _ := uuid.FromBytes(g.Id)
		g.Id = []byte(uid.String())
		// Convert UUIDs to string representation.
		gid, _ := uuid.FromBytes(g.Id)
		g.Id = []byte(gid.String())
		gm := structs.Map(g)
		lv.RawSetInt(i+1, convertValue(l, gm))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal(g.Metadata, &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}

		gt := ConvertMap(l, gm)
		gt.RawSetString("Metadata", ConvertMap(l, metadataMap))
		lv.RawSetInt(i+1, gt)
	}

	l.Push(lv)
@@ -1550,13 +1624,24 @@ func (n *NakamaModule) groupUsersList(l *lua.LState) int {
		return 0
	}

	//translate uuid to string bytes
	// Convert and push the values.
	lv := l.NewTable()
	for i, u := range users {
		// Convert UUIDs to string representation.
		uid, _ := uuid.FromBytes(u.User.Id)
		u.User.Id = []byte(uid.String())
		um := structs.Map(u)
		lv.RawSetInt(i+1, convertValue(l, um))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal(u.User.Metadata, &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}

		ut := ConvertMap(l, um)
		ut.RawGetString("User").(*lua.LTable).RawSetString("Metadata", ConvertMap(l, metadataMap))
		lv.RawSetInt(i+1, ut)
	}

	l.Push(lv)
@@ -1582,13 +1667,24 @@ func (n *NakamaModule) groupsUserList(l *lua.LState) int {
		return 0
	}

	//translate uuid to string bytes
	// Convert and push the values.
	lv := l.NewTable()
	for i, g := range groups {
		// Convert UUIDs to string representation.
		gid, _ := uuid.FromBytes(g.Group.Id)
		g.Group.Id = []byte(gid.String())
		gm := structs.Map(g)
		lv.RawSetInt(i+1, convertValue(l, gm))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal(g.Group.Metadata, &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}

		gt := ConvertMap(l, gm)
		gt.RawGetString("Group").(*lua.LTable).RawSetString("Metadata", ConvertMap(l, metadataMap))
		lv.RawSetInt(i+1, gt)
	}

	l.Push(lv)
Loading