diff --git a/CHANGELOG.md b/CHANGELOG.md index bce926fdf..8a1a41d73 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. diff --git a/server/core_friend.go b/server/core_friend.go index 822112b31..54dc2f294 100644 --- a/server/core_friend.go +++ b/server/core_friend.go @@ -85,6 +85,93 @@ 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 + } + + query := ` +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 $2` + rows, err := db.QueryContext(ctx, query, userID, userIDs) + 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 92da058c1..765c5283b 100644 --- a/server/runtime_go_nakama.go +++ b/server/runtime_go_nakama.go @@ -620,6 +620,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 e56f352b0..5f2fd8e8e 100644 --- a/server/runtime_javascript_nakama.go +++ b/server/runtime_javascript_nakama.go @@ -186,6 +186,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), @@ -2166,6 +2167,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 210a91eb0..33c18c8bd 100644 --- a/server/runtime_lua_nakama.go +++ b/server/runtime_lua_nakama.go @@ -203,6 +203,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, @@ -2916,6 +2917,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.