Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr

## [Unreleased]

### Added
- Add runtime Satori client feature to delete identities.
- Add device identifiers as lookup options for runtime account get operations.
- Add Go runtime function to import an account export snapshot.
- Add TypeScript/JavaScript runtime function to import an account export snapshot.
- Add Lua runtime function to import an account export snapshot.

### Fixed
- Correct field usage in voided Google In-App Purchase subscription notifications.
- Fix an issue where the Storage Index would keep entries which should have been filtered.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4
github.com/heroiclabs/nakama-common v1.44.3-0.20260302104831-c0b688e207fb
github.com/heroiclabs/nakama-common v1.44.3-0.20260304155720-c380ba5165c6
github.com/heroiclabs/sql-migrate v0.0.0-20241125131053-95a7949783b0
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.44.3-0.20260302104831-c0b688e207fb h1:WJ8xmBD4d7FD211R+WgI2Bu4tfMxYLcF0aBwt48Th04=
github.com/heroiclabs/nakama-common v1.44.3-0.20260302104831-c0b688e207fb/go.mod h1:/KLxEy4+JdasHJLPt+tAXJJ8eXywWr/J+Q+KY+/vg7E=
github.com/heroiclabs/nakama-common v1.44.3-0.20260304155720-c380ba5165c6 h1:CPXLS3BGl/AawIbsUU6JrNyZ5aimxgqJKYdOpiPte3I=
github.com/heroiclabs/nakama-common v1.44.3-0.20260304155720-c380ba5165c6/go.mod h1:/KLxEy4+JdasHJLPt+tAXJJ8eXywWr/J+Q+KY+/vg7E=
github.com/heroiclabs/sql-migrate v0.0.0-20241125131053-95a7949783b0 h1:hHJcYOP6L2/wZIEnYjjkJM+rOk/bK0uaYkDAejYpLhI=
github.com/heroiclabs/sql-migrate v0.0.0-20241125131053-95a7949783b0/go.mod h1:uwcmopkVQIfb/JQqul5zmGI9ounclRC08j9S9lLcpRQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
23 changes: 19 additions & 4 deletions server/core_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,29 @@ WHERE u.id = $1`
}, nil
}

func GetAccounts(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegistry StatusRegistry, userIDs []string) ([]*api.Account, error) {
func GetAccounts(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegistry StatusRegistry, userIDs, deviceIDs []string) ([]*api.Account, error) {
if len(userIDs) == 0 && len(deviceIDs) == 0 {
return []*api.Account{}, nil
}

query := `
SELECT u.id, u.username, u.display_name, u.avatar_url, u.lang_tag, u.location, u.timezone, u.metadata, u.wallet,
u.email, u.apple_id, u.facebook_id, u.facebook_instant_game_id, u.google_id, u.gamecenter_id, u.steam_id, u.custom_id, u.edge_count,
u.create_time, u.update_time, u.verify_time, u.disable_time, array(select ud.id from user_device ud where u.id = ud.user_id)
FROM users u
WHERE u.id = ANY($1)`
rows, err := db.QueryContext(ctx, query, userIDs)
FROM users u`
params := make([]interface{}, 0, 2)
switch {
case len(userIDs) > 0 && len(deviceIDs) > 0:
query += " WHERE u.id = ANY($1) OR u.id IN (SELECT ud.user_id FROM user_device ud WHERE ud.id = ANY($2))"
params = append(params, userIDs, deviceIDs)
case len(userIDs) > 0:
query += " WHERE u.id = ANY($1)"
params = append(params, userIDs)
case len(deviceIDs) > 0:
query += " WHERE u.id IN (SELECT ud.user_id FROM user_device ud WHERE ud.id = ANY($1))"
params = append(params, deviceIDs)
}
rows, err := db.QueryContext(ctx, query, params...)
if err != nil {
logger.Error("Error retrieving user accounts.", zap.Error(err))
return nil, err
Expand Down
49 changes: 44 additions & 5 deletions server/runtime_go_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama-common/rtapi"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/heroiclabs/nakama/v3/console"
"github.com/heroiclabs/nakama/v3/internal/cronexpr"
"github.com/heroiclabs/nakama/v3/social"
"go.uber.org/zap"
Expand Down Expand Up @@ -455,11 +456,12 @@ func (n *RuntimeGoNakamaModule) AccountGetId(ctx context.Context, userID string)
// @group accounts
// @summary Fetch information for multiple accounts by user IDs.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userIDs(type=[]string) Array of user IDs to fetch information for. Must be valid UUID.
// @param userIDs(type=[]string, optional=true) Array of user IDs to fetch information for. Must be valid UUID when supplied.
// @param deviceIDs(type=[]string, optional=true) Array of device IDs to fetch information for.
// @return account([]*api.Account) An array of accounts.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) AccountsGetId(ctx context.Context, userIDs []string) ([]*api.Account, error) {
if len(userIDs) == 0 {
func (n *RuntimeGoNakamaModule) AccountsGetId(ctx context.Context, userIDs, deviceIDs []string) ([]*api.Account, error) {
if len(userIDs) == 0 && len(deviceIDs) == 0 {
return make([]*api.Account, 0), nil
}

Expand All @@ -469,7 +471,7 @@ func (n *RuntimeGoNakamaModule) AccountsGetId(ctx context.Context, userIDs []str
}
}

return GetAccounts(ctx, n.logger, n.db, n.statusRegistry, userIDs)
return GetAccounts(ctx, n.logger, n.db, n.statusRegistry, userIDs, deviceIDs)
}

// @group accounts
Expand Down Expand Up @@ -572,14 +574,51 @@ func (n *RuntimeGoNakamaModule) AccountExportId(ctx context.Context, userID stri
return string(exportBytes), nil
}

// @group accounts
// @summary Import account information, optionally overwriting data for a specified user ID.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param data(type=string) Data to import
// @param userID(type=string, optional=true) User ID to import data into. Must be valid UUID.
// @return account(*api.Account) All account information including wallet, device IDs and more.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) AccountImportId(ctx context.Context, data, userID string) (*api.Account, error) {
if data == "" {
return nil, errors.New("expects data to be present")
}
d := &console.AccountExport{}
if err := json.Unmarshal([]byte(data), d); err != nil {
return nil, errors.New("expects data to be a valid account export format")
}

u := uuid.Nil
if userID != "" {
var err error
u, err = uuid.FromString(userID)
if err != nil {
return nil, errors.New("expects user ID to be a valid identifier")
}
}

account, err := ImportAccount(ctx, n.logger, n.db, n.statusRegistry, u, d)
if err != nil {
return nil, fmt.Errorf("error importing account: %v", err.Error())
}

if account == nil {
return nil, errors.New("account import returned no data")
}

return account.Account, nil
}

// @group users
// @summary Fetch one or more users by ID.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userIDs(type=[]string) An array of user IDs to fetch.
// @param facebookIDs(type=[]string) An array of Facebook IDs to fetch.
// @return users([]*api.User) A list of user record objects.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) UsersGetId(ctx context.Context, userIDs []string, facebookIDs []string) ([]*api.User, error) {
func (n *RuntimeGoNakamaModule) UsersGetId(ctx context.Context, userIDs, facebookIDs []string) ([]*api.User, error) {
if len(userIDs) == 0 && len(facebookIDs) == 0 {
return make([]*api.User, 0), nil
}
Expand Down
86 changes: 75 additions & 11 deletions server/runtime_javascript_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama-common/rtapi"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/heroiclabs/nakama/v3/console"
"github.com/heroiclabs/nakama/v3/internal/cronexpr"
"github.com/heroiclabs/nakama/v3/social"
"go.uber.org/zap"
Expand Down Expand Up @@ -186,6 +187,7 @@ func (n *RuntimeJavascriptNakamaModule) mappings(r *goja.Runtime) map[string]fun
"accountUpdateId": n.accountUpdateId(r),
"accountDeleteId": n.accountDeleteId(r),
"accountExportId": n.accountExportId(r),
"accountImportId": n.accountImportId(r),
"usersGetId": n.usersGetId(r),
"usersGetUsername": n.usersGetUsername(r),
"usersGetFriendStatus": n.usersGetFriendStatus(r),
Expand Down Expand Up @@ -2018,27 +2020,37 @@ func (n *RuntimeJavascriptNakamaModule) accountGetId(r *goja.Runtime) func(goja.

// @group accounts
// @summary Fetch information for multiple accounts by user IDs.
// @param userIDs(type=[]string) Array of user IDs to fetch information for. Must be valid UUID.
// @param userIDs(type=[]string, optional=true) Array of user IDs to fetch information for. Must be valid UUID when supplied.
// @param deviceIDs(type=[]string, optional=true) Array of device IDs to fetch information for.
// @return account(nkruntime.Account[]) Array of accounts.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeJavascriptNakamaModule) accountsGetId(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
userIDs := f.Argument(0)
if userIDs == goja.Undefined() || userIDs == goja.Null() {
panic(r.NewTypeError("expects an array of user ids"))
var uids []string
var err error
if userIDs != goja.Undefined() && userIDs != goja.Null() {
uids, err = exportToSlice[[]string](userIDs)
if err != nil {
panic(r.NewTypeError("expects an array of strings"))
}
for _, uid := range uids {
if _, err := uuid.FromString(uid); err != nil {
panic(r.NewTypeError(fmt.Sprintf("invalid user id: %s", uid)))
}
}
}

uids, err := exportToSlice[[]string](userIDs)
if err != nil {
panic(r.NewTypeError("expects an array of strings"))
}
for _, uid := range uids {
if _, err := uuid.FromString(uid); err != nil {
panic(r.NewTypeError(fmt.Sprintf("invalid user id: %s", uid)))
deviceIDs := f.Argument(1)
var dids []string
if deviceIDs != goja.Undefined() && deviceIDs != goja.Null() {
dids, err = exportToSlice[[]string](deviceIDs)
if err != nil {
panic(r.NewTypeError("expects an array of strings"))
}
}

accounts, err := GetAccounts(n.ctx, n.logger, n.db, n.statusRegistry, uids)
accounts, err := GetAccounts(n.ctx, n.logger, n.db, n.statusRegistry, uids, dids)
if err != nil {
panic(r.NewGoError(fmt.Errorf("failed to get accounts: %s", err.Error())))
}
Expand Down Expand Up @@ -2185,6 +2197,58 @@ func (n *RuntimeJavascriptNakamaModule) accountExportId(r *goja.Runtime) func(go
}
}

// @group accounts
// @summary Import user account data, optionally overwriting a given user
// @param data(type=string) An account export string to import.
// @param userID(type=string, optional=true) Optional user ID to import into. Must be valid UUID.
// @return account(nkruntime.Account) All account information including wallet, device IDs and more.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeJavascriptNakamaModule) accountImportId(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
data := getJsString(r, f.Argument(0))
if data == "" {
panic(r.NewTypeError("expects data to be present"))
}
d := &console.AccountExport{}
if err := json.Unmarshal([]byte(data), d); err != nil {
panic(r.NewTypeError("expects data to be a valid account export format"))
}

userID := f.Argument(1)
uid := uuid.Nil
if userID != goja.Undefined() && userID != goja.Null() {
u, ok := userID.Export().(string)
if !ok {
panic(r.NewTypeError("expects user id to be a string"))
}
if u == "" {
panic(r.NewTypeError("expects user id"))
}
var err error
uid, err = uuid.FromString(u)
if err != nil {
panic(r.NewTypeError("invalid user id"))
}
}

account, err := ImportAccount(n.ctx, n.logger, n.db, n.statusRegistry, uid, d)
if err != nil {
panic(r.NewGoError(fmt.Errorf("error importing account: %v", err.Error())))
}

if account == nil {
panic(r.NewGoError(errors.New("account import returned no data")))
}

accountData, err := accountToJsObject(account.Account)
if err != nil {
panic(r.NewGoError(err))
}

return r.ToValue(accountData)
}
}

// @group users
// @summary Fetch one or more users by ID.
// @param userIDs(type=[]string) An array of user IDs to fetch.
Expand Down
Loading
Loading