diff --git a/CHANGELOG.md b/CHANGELOG.md index 0067f12644..64fc36f5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ 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 +- Add new runtime function to get a list of user's friend status. + ### Changed - Increase limit of runtime friend listing operations to 1,000. - Leaving a group is now treated as a deletion when done by the last member. diff --git a/go.mod b/go.mod index 4263197bb3..b2ff77ea26 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 - github.com/heroiclabs/nakama-common v1.34.0 + github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056 github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.6.0 diff --git a/go.sum b/go.sum index 9e96678ee8..87a8272e2a 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/heroiclabs/nakama-common v1.34.0 h1:7/F5v5yoCFBMTn5Aih/cqR/GW7hbEbup8blq5OmhzjM= github.com/heroiclabs/nakama-common v1.34.0/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0= +github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056 h1:a3N9IG9w1K9m1EEZu9bFVcfb3PDdb7vGTUInkkkIsfA= +github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0= github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a h1:tuL2ZPaeCbNw8rXmV9ywd00nXRv95V4/FmbIGKLQJAE= github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a/go.mod h1:hzCTPoEi/oml2BllVydJcNP63S7b56e5DzeQeLGvw1U= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= diff --git a/server/core_friend.go b/server/core_friend.go index 822112b31e..7b989dc53c 100644 --- a/server/core_friend.go +++ b/server/core_friend.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/gofrs/uuid/v5" @@ -85,6 +86,103 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1` return &api.FriendList{Friends: friends}, nil } +func GetFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegistry StatusRegistry, userID uuid.UUID, userIDs []uuid.UUID) ([]*api.Friend, error) { + if len(userIDs) == 0 { + return []*api.Friend{}, nil + } + + placeholders := make([]string, len(userIDs)) + uids := make([]any, len(userIDs)) + idx := 2 + for i, uid := range userIDs { + placeholders[i] = fmt.Sprintf("$%d", idx) + uids[i] = uid + idx++ + } + + query := fmt.Sprintf(` +SELECT id, username, display_name, avatar_url, + lang_tag, location, timezone, metadata, + create_time, users.update_time, user_edge.update_time, state, position, + facebook_id, google_id, gamecenter_id, steam_id, facebook_instant_game_id, apple_id +FROM users, user_edge WHERE id = destination_id AND source_id = $1 AND destination_id IN (%s)`, strings.Join(placeholders, ",")) + params := append([]any{userID}, uids...) + rows, err := db.QueryContext(ctx, query, params...) + if err != nil { + logger.Error("Error retrieving friends.", zap.Error(err)) + return nil, err + } + defer rows.Close() + + friends := make([]*api.Friend, 0, len(userIDs)) + for rows.Next() { + var id string + var username sql.NullString + var displayName sql.NullString + var avatarURL sql.NullString + var lang sql.NullString + var location sql.NullString + var timezone sql.NullString + var metadata []byte + var createTime pgtype.Timestamptz + var updateTime pgtype.Timestamptz + var edgeUpdateTime pgtype.Timestamptz + var state sql.NullInt64 + var position sql.NullInt64 + var facebookID sql.NullString + var googleID sql.NullString + var gamecenterID sql.NullString + var steamID sql.NullString + var facebookInstantGameID sql.NullString + var appleID sql.NullString + + if err = rows.Scan(&id, &username, &displayName, &avatarURL, &lang, &location, &timezone, &metadata, + &createTime, &updateTime, &edgeUpdateTime, &state, &position, + &facebookID, &googleID, &gamecenterID, &steamID, &facebookInstantGameID, &appleID); err != nil { + logger.Error("Error retrieving friends.", zap.Error(err)) + return nil, err + } + + user := &api.User{ + Id: id, + Username: username.String, + DisplayName: displayName.String, + AvatarUrl: avatarURL.String, + LangTag: lang.String, + Location: location.String, + Timezone: timezone.String, + Metadata: string(metadata), + CreateTime: ×tamppb.Timestamp{Seconds: createTime.Time.Unix()}, + UpdateTime: ×tamppb.Timestamp{Seconds: updateTime.Time.Unix()}, + // Online filled below. + FacebookId: facebookID.String, + GoogleId: googleID.String, + GamecenterId: gamecenterID.String, + SteamId: steamID.String, + FacebookInstantGameId: facebookInstantGameID.String, + AppleId: appleID.String, + } + + friends = append(friends, &api.Friend{ + User: user, + State: &wrapperspb.Int32Value{ + Value: int32(state.Int64), + }, + UpdateTime: ×tamppb.Timestamp{Seconds: edgeUpdateTime.Time.Unix()}, + }) + } + if err = rows.Err(); err != nil { + logger.Error("Error retrieving friends.", zap.Error(err)) + return nil, err + } + + if statusRegistry != nil { + statusRegistry.FillOnlineFriends(friends) + } + + return friends, nil +} + func ListFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegistry StatusRegistry, userID uuid.UUID, limit int, state *wrapperspb.Int32Value, cursor string) (*api.FriendList, error) { var incomingCursor *edgeListCursor if cursor != "" { diff --git a/server/runtime_go_nakama.go b/server/runtime_go_nakama.go index 15cc08e177..7826cd5071 100644 --- a/server/runtime_go_nakama.go +++ b/server/runtime_go_nakama.go @@ -628,6 +628,31 @@ func (n *RuntimeGoNakamaModule) UsersGetUsername(ctx context.Context, usernames return users.Users, nil } +// @group users +// @summary Get user's friend status information for a list of target users. +// @param ctx(type=context.Context) The context object represents information about the server and requester. +// @param userID (type=string) The current user ID. +// @param userIDs(type=[]string) An array of target user IDs. +// @return friends([]*api.Friend) A list of user friends objects. +// @return error(error) An optional error value if an error occurred. +func (n *RuntimeGoNakamaModule) UsersGetFriendStatus(ctx context.Context, userID string, userIDs []string) ([]*api.Friend, error) { + uid, err := uuid.FromString(userID) + if err != nil { + return nil, errors.New("expects user ID to be a valid identifier") + } + + fids := make([]uuid.UUID, 0, len(userIDs)) + for _, id := range userIDs { + fid, err := uuid.FromString(id) + if err != nil { + return nil, errors.New("expects user ID to be a valid identifier") + } + fids = append(fids, fid) + } + + return GetFriends(ctx, n.logger, n.db, n.statusRegistry, uid, fids) +} + // @group users // @summary Fetch one or more users randomly. // @param ctx(type=context.Context) The context object represents information about the server and requester. diff --git a/server/runtime_javascript_nakama.go b/server/runtime_javascript_nakama.go index 4617204a9f..8a0270da6d 100644 --- a/server/runtime_javascript_nakama.go +++ b/server/runtime_javascript_nakama.go @@ -194,6 +194,7 @@ func (n *runtimeJavascriptNakamaModule) mappings(r *goja.Runtime) map[string]fun "accountExportId": n.accountExportId(r), "usersGetId": n.usersGetId(r), "usersGetUsername": n.usersGetUsername(r), + "usersGetFriendStatus": n.usersGetFriendStatus(r), "usersGetRandom": n.usersGetRandom(r), "usersBanId": n.usersBanId(r), "usersUnbanId": n.usersUnbanId(r), @@ -2175,6 +2176,61 @@ func (n *runtimeJavascriptNakamaModule) usersGetUsername(r *goja.Runtime) func(g } } +// @group users +// @summary Get user's friend status information for a list of target users. +// @param userID (type=string) The current user ID. +// @param userIDs(type=string[]) An array of target user IDs. +// @return friends(nkruntime.Friend[]) A list of user friends objects. +// @return error(error) An optional error value if an error occurred. +func (n *runtimeJavascriptNakamaModule) usersGetFriendStatus(r *goja.Runtime) func(goja.FunctionCall) goja.Value { + return func(f goja.FunctionCall) goja.Value { + id := getJsString(r, f.Argument(0)) + + uid, err := uuid.FromString(id) + if err != nil { + panic(r.NewTypeError("invalid user id")) + } + + ids := f.Argument(1) + + uids, err := exportToSlice[[]string](ids) + if err != nil { + panic(r.NewTypeError("expects an array of strings")) + } + + fids := make([]uuid.UUID, 0, len(uids)) + for _, id := range uids { + fid, err := uuid.FromString(id) + if err != nil { + panic(r.NewTypeError("invalid user id")) + } + fids = append(fids, fid) + } + + friends, err := GetFriends(n.ctx, n.logger, n.db, n.statusRegistry, uid, fids) + if err != nil { + panic(r.NewGoError(fmt.Errorf("failed to get user friends status: %s", err.Error()))) + } + + userFriends := make([]interface{}, 0, len(friends)) + for _, f := range friends { + fum, err := userToJsObject(f.User) + if err != nil { + panic(r.NewGoError(err)) + } + + fm := make(map[string]interface{}, 3) + fm["state"] = f.State.Value + fm["updateTime"] = f.UpdateTime.Seconds + fm["user"] = fum + + userFriends = append(userFriends, fm) + } + + return r.ToValue(userFriends) + } +} + // @group users // @summary Fetch one or more users randomly. // @param count(type=number) The number of users to fetch. diff --git a/server/runtime_lua_nakama.go b/server/runtime_lua_nakama.go index 79015e95c7..d74499c155 100644 --- a/server/runtime_lua_nakama.go +++ b/server/runtime_lua_nakama.go @@ -210,6 +210,7 @@ func (n *RuntimeLuaNakamaModule) Loader(l *lua.LState) int { "account_export_id": n.accountExportId, "users_get_id": n.usersGetId, "users_get_username": n.usersGetUsername, + "users_get_friend_status": n.usersGetFriendStatus, "users_get_random": n.usersGetRandom, "users_ban_id": n.usersBanId, "users_unban_id": n.usersUnbanId, @@ -2924,6 +2925,71 @@ func (n *RuntimeLuaNakamaModule) usersGetUsername(l *lua.LState) int { return 1 } +// @group users +// @summary Get user's friend status information for a list of target users. +// @param userID (type=string) The current user ID. +// @param userIDs(type=table) An array of target user IDs. +// @return friends(table) A list of user friends objects. +// @return error(error) An optional error value if an error occurred. +func (n *RuntimeLuaNakamaModule) usersGetFriendStatus(l *lua.LState) int { + id := l.CheckString(1) + + uid, err := uuid.FromString(id) + if err != nil { + l.ArgError(1, "invalid user id") + } + + ids := l.CheckTable(2) + + uidsTable, ok := RuntimeLuaConvertLuaValue(ids).([]interface{}) + if !ok { + l.ArgError(2, "invalid user ids list") + return 0 + } + + fids := make([]uuid.UUID, 0, len(uidsTable)) + for _, id := range uidsTable { + ids, ok := id.(string) + if !ok || ids == "" { + l.ArgError(2, "each user id must be a string") + return 0 + } + fid, err := uuid.FromString(ids) + if err != nil { + l.ArgError(2, "invalid user id") + return 0 + } + fids = append(fids, fid) + } + + friends, err := GetFriends(l.Context(), n.logger, n.db, n.statusRegistry, uid, fids) + if err != nil { + l.RaiseError("failed to get users friend status: %s", err.Error()) + return 0 + } + + userFriends := l.CreateTable(len(friends), 0) + for i, f := range friends { + u := f.User + + fut, err := userToLuaTable(l, u) + if err != nil { + l.RaiseError("failed to convert user data to lua table: %s", err.Error()) + return 0 + } + + ft := l.CreateTable(0, 3) + ft.RawSetString("state", lua.LNumber(f.State.Value)) + ft.RawSetString("update_time", lua.LNumber(f.UpdateTime.Seconds)) + ft.RawSetString("user", fut) + + userFriends.RawSetInt(i+1, ft) + } + + l.Push(userFriends) + return 1 +} + // @group users // @summary Fetch one or more users randomly. // @param count(type=int) The number of users to fetch. diff --git a/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go b/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go index 90a269f268..c2d66726aa 100644 --- a/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go +++ b/vendor/github.com/heroiclabs/nakama-common/runtime/runtime.go @@ -1058,6 +1058,7 @@ type NakamaModule interface { UsersGetId(ctx context.Context, userIDs []string, facebookIDs []string) ([]*api.User, error) UsersGetUsername(ctx context.Context, usernames []string) ([]*api.User, error) + UsersGetFriendStatus(ctx context.Context, userID string, userIDs []string) ([]*api.Friend, error) UsersGetRandom(ctx context.Context, count int) ([]*api.User, error) UsersBanId(ctx context.Context, userIDs []string) error UsersUnbanId(ctx context.Context, userIDs []string) error diff --git a/vendor/modules.txt b/vendor/modules.txt index a6256d1a82..1b39ced658 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -139,7 +139,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopena github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options github.com/grpc-ecosystem/grpc-gateway/v2/runtime github.com/grpc-ecosystem/grpc-gateway/v2/utilities -# github.com/heroiclabs/nakama-common v1.34.0 +# github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056 ## explicit; go 1.19 github.com/heroiclabs/nakama-common/api github.com/heroiclabs/nakama-common/rtapi