Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new runtime function to get a list of user's friend status #1286

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
98 changes: 98 additions & 0 deletions server/core_friend.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/gofrs/uuid/v5"
Expand Down Expand Up @@ -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: &timestamppb.Timestamp{Seconds: createTime.Time.Unix()},
UpdateTime: &timestamppb.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: &timestamppb.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 != "" {
Expand Down
25 changes: 25 additions & 0 deletions server/runtime_go_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions server/runtime_javascript_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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.
Expand Down
66 changes: 66 additions & 0 deletions server/runtime_lua_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading