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

NK-546 Add user_edge metadata #1305

Merged
merged 8 commits into from
Jan 24, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
## [Unreleased]
### Added
- Allow account filtering by email in the Console.
- Add friend metadata support.

### Fixed
- Ensure persisted chat messages listing returns correct order.
Expand Down
2 changes: 1 addition & 1 deletion apigrpc/apigrpc.pb.go

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

11 changes: 11 additions & 0 deletions apigrpc/apigrpc.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,13 @@
"type": "string"
},
"collectionFormat": "multi"
},
{
"name": "metadata",
"description": "Optional metadata to add to friends.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
Expand Down Expand Up @@ -4169,6 +4176,10 @@
"type": "string",
"format": "date-time",
"description": "Time of the latest relationship update."
},
"metadata": {
"type": "string",
"description": "Metadata."
}
},
"description": "A friend of a user."
Expand Down
2 changes: 1 addition & 1 deletion apigrpc/apigrpc_grpc.pb.go

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

2 changes: 1 addition & 1 deletion console/console.pb.go

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

4 changes: 4 additions & 0 deletions console/console.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2971,6 +2971,10 @@
"type": "string",
"format": "date-time",
"description": "Time of the latest relationship update."
},
"metadata": {
"type": "string",
"description": "Metadata."
}
},
"description": "A friend of a user."
Expand Down
2 changes: 1 addition & 1 deletion console/console_grpc.pb.go

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

2 changes: 2 additions & 0 deletions console/ui/src/app/console.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ export interface ApiChannelMessageList {

/** A friend of a user. */
export interface ApiFriend {
// Metadata.
metadata?:string
// The friend status. / / one of "Friend.State".
state?:number
// Time of the latest relationship update.
Expand Down
4 changes: 3 additions & 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.3
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1
github.com/heroiclabs/nakama-common v1.35.0
github.com/heroiclabs/nakama-common v1.35.1-0.20250124113809-897f4ac9b74e
github.com/heroiclabs/sql-migrate v0.0.0-20241125131053-95a7949783b0
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.7.1
Expand Down Expand Up @@ -75,3 +75,5 @@ require (
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
)

replace github.com/heroiclabs/nakama-common => ../nakama-common
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.35.0 h1:gO3J2v2E12sZ2uL258lt5YF6yNO1tiPtvL7ZwV8t/n0=
github.com/heroiclabs/nakama-common v1.35.0/go.mod h1:E4kw2QpsINoXXJS7aOjen1dycPkoo9bD9pYPAjmA8rc=
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: 23 additions & 0 deletions migrate/sql/20250113112512_add_user_edge_metadata.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2024 The Nakama Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

-- +migrate Up
ALTER TABLE user_edge
ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';

-- +migrate Down
ALTER TABLE user_edge
DROP COLUMN metadata;
2 changes: 1 addition & 1 deletion server/api_friend.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (s *ApiServer) AddFriends(ctx context.Context, in *api.AddFriendsRequest) (
allIDs = append(allIDs, in.GetIds()...)
allIDs = append(allIDs, userIDs...)

if err := AddFriends(ctx, s.logger, s.db, s.tracker, s.router, userID, username, allIDs); err != nil {
if err := AddFriends(ctx, s.logger, s.db, s.tracker, s.router, userID, username, allIDs, in.Metadata); err != nil {
return nil, status.Error(codes.Internal, "Error while trying to add friends.")
}

Expand Down
16 changes: 8 additions & 8 deletions server/core_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1044,10 +1044,10 @@ func importFriendsByUUID(ctx context.Context, logger *zap.Logger, tx *sql.Tx, us

// Attempt to mark as accepted any previous invite between these users, in any direction.
res, err := tx.ExecContext(ctx, `
UPDATE user_edge SET state = 0, update_time = now()
UPDATE user_edge SET state = 0, update_time = now(), metadata = metadata || jsonb_build_object('provider', $3::TEXT)
WHERE (source_id = $1 AND destination_id = $2 AND (state = 1 OR state = 2))
OR (source_id = $2 AND destination_id = $1 AND (state = 1 OR state = 2))
`, friendID, userID)
`, friendID, userID, strings.ToLower(provider))
if err != nil {
logger.Error("Error accepting invite in friend import.", zap.Error(err))
continue
Expand All @@ -1059,19 +1059,19 @@ OR (source_id = $2 AND destination_id = $1 AND (state = 1 OR state = 2))
}

_, err = tx.ExecContext(ctx, `
INSERT INTO user_edge (source_id, destination_id, state, position, update_time)
SELECT source_id, destination_id, state, position, update_time
INSERT INTO user_edge (source_id, destination_id, state, position, update_time, metadata)
SELECT source_id, destination_id, state, position, update_time, metadata
FROM (VALUES
($1::UUID, $2::UUID, 0, $3::BIGINT, now()),
($2::UUID, $1::UUID, 0, $3::BIGINT, now())
) AS ue(source_id, destination_id, state, position, update_time)
($1::UUID, $2::UUID, 0, $3::BIGINT, now(), jsonb_build_object('provider', $4::TEXT)),
($2::UUID, $1::UUID, 0, $3::BIGINT, now(), jsonb_build_object('provider', $4::TEXT))
) AS ue(source_id, destination_id, state, position, update_time, metadata)
WHERE EXISTS (SELECT id FROM users WHERE id = $2::UUID)
AND NOT EXISTS
(SELECT state
FROM user_edge
WHERE source_id = $2::UUID AND destination_id = $1::UUID AND state = 3)
ON CONFLICT (source_id, destination_id) DO NOTHING
`, userID, friendID, position)
`, userID, friendID, position, strings.ToLower(provider))
if err != nil {
logger.Error("Error adding new edges in friend import.", zap.Error(err))
continue
Expand Down
85 changes: 58 additions & 27 deletions server/core_friend.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ func GetFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegis

query := fmt.Sprintf(`
SELECT id, username, display_name, avatar_url,
lang_tag, location, timezone, metadata,
lang_tag, location, timezone, users.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
facebook_id, google_id, gamecenter_id, steam_id, facebook_instant_game_id, apple_id, user_edge.metadata
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...)
Expand All @@ -123,7 +123,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1 AND destinati
var lang sql.NullString
var location sql.NullString
var timezone sql.NullString
var metadata []byte
var userMetadata []byte
var createTime pgtype.Timestamptz
var updateTime pgtype.Timestamptz
var edgeUpdateTime pgtype.Timestamptz
Expand All @@ -135,10 +135,11 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1 AND destinati
var steamID sql.NullString
var facebookInstantGameID sql.NullString
var appleID sql.NullString
var friendMetadata []byte

if err = rows.Scan(&id, &username, &displayName, &avatarURL, &lang, &location, &timezone, &metadata,
if err = rows.Scan(&id, &username, &displayName, &avatarURL, &lang, &location, &timezone, &userMetadata,
&createTime, &updateTime, &edgeUpdateTime, &state, &position,
&facebookID, &googleID, &gamecenterID, &steamID, &facebookInstantGameID, &appleID); err != nil {
&facebookID, &googleID, &gamecenterID, &steamID, &facebookInstantGameID, &appleID, &friendMetadata); err != nil {
logger.Error("Error retrieving friends.", zap.Error(err))
return nil, err
}
Expand All @@ -151,7 +152,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1 AND destinati
LangTag: lang.String,
Location: location.String,
Timezone: timezone.String,
Metadata: string(metadata),
Metadata: string(userMetadata),
CreateTime: &timestamppb.Timestamp{Seconds: createTime.Time.Unix()},
UpdateTime: &timestamppb.Timestamp{Seconds: updateTime.Time.Unix()},
// Online filled below.
Expand All @@ -169,6 +170,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1 AND destinati
Value: int32(state.Int64),
},
UpdateTime: &timestamppb.Timestamp{Seconds: edgeUpdateTime.Time.Unix()},
Metadata: string(friendMetadata),
})
}
if err = rows.Err(); err != nil {
Expand Down Expand Up @@ -204,9 +206,9 @@ func ListFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, statusRegi
params := make([]interface{}, 0, 4)
query := `
SELECT id, username, display_name, avatar_url,
lang_tag, location, timezone, metadata,
lang_tag, location, timezone, users.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
facebook_id, google_id, gamecenter_id, steam_id, facebook_instant_game_id, apple_id, user_edge.metadata
FROM users, user_edge WHERE id = destination_id AND source_id = $1`
params = append(params, userID)
if state != nil {
Expand Down Expand Up @@ -246,7 +248,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1`
var lang sql.NullString
var location sql.NullString
var timezone sql.NullString
var metadata []byte
var userMetadata []byte
var createTime pgtype.Timestamptz
var updateTime pgtype.Timestamptz
var edgeUpdateTime pgtype.Timestamptz
Expand All @@ -258,10 +260,11 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1`
var steamID sql.NullString
var facebookInstantGameID sql.NullString
var appleID sql.NullString
var friendMetadata []byte

if err = rows.Scan(&id, &username, &displayName, &avatarURL, &lang, &location, &timezone, &metadata,
if err = rows.Scan(&id, &username, &displayName, &avatarURL, &lang, &location, &timezone, &userMetadata,
&createTime, &updateTime, &edgeUpdateTime, &state, &position,
&facebookID, &googleID, &gamecenterID, &steamID, &facebookInstantGameID, &appleID); err != nil {
&facebookID, &googleID, &gamecenterID, &steamID, &facebookInstantGameID, &appleID, &friendMetadata); err != nil {
logger.Error("Error retrieving friends.", zap.Error(err))
return nil, err
}
Expand All @@ -284,7 +287,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1`
LangTag: lang.String,
Location: location.String,
Timezone: timezone.String,
Metadata: string(metadata),
Metadata: string(userMetadata),
CreateTime: &timestamppb.Timestamp{Seconds: createTime.Time.Unix()},
UpdateTime: &timestamppb.Timestamp{Seconds: updateTime.Time.Unix()},
// Online filled below.
Expand All @@ -302,6 +305,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1`
Value: int32(state.Int64),
},
UpdateTime: &timestamppb.Timestamp{Seconds: edgeUpdateTime.Time.Unix()},
Metadata: string(friendMetadata),
})
}
if err = rows.Err(); err != nil {
Expand Down Expand Up @@ -465,7 +469,7 @@ AND state = 0
return &api.FriendsOfFriendsList{FriendsOfFriends: fof, Cursor: outgoingCursor}, nil
}

func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, tracker Tracker, messageRouter MessageRouter, userID uuid.UUID, username string, friendIDs []string) error {
func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, tracker Tracker, messageRouter MessageRouter, userID uuid.UUID, username string, friendIDs []string, metadata string) error {
uniqueFriendIDs := make(map[string]struct{})
for _, fid := range friendIDs {
uniqueFriendIDs[fid] = struct{}{}
Expand All @@ -492,7 +496,7 @@ func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, tracker Tra
continue
}

isFriendAccept, addFriendErr := addFriend(ctx, logger, tx, userID, id)
isFriendAccept, addFriendErr := addFriend(ctx, logger, tx, userID, id, metadata)
if addFriendErr == nil {
notificationToSend[id] = isFriendAccept
} else if addFriendErr != sql.ErrNoRows { // Check to see if friend had blocked user.
Expand Down Expand Up @@ -532,35 +536,62 @@ func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, tracker Tra
return nil
}

// Returns "true" if accepting an invite, otherwise false
func addFriend(ctx context.Context, logger *zap.Logger, tx *sql.Tx, userID uuid.UUID, friendID string) (bool, error) {
func UpdateFriendMetadata(ctx context.Context, logger *zap.Logger, db *sql.DB, userID, friendUserID uuid.UUID, metadata map[string]any) error {
metadataStr := "{}"
if metadata != nil {
metadataBytes, err := json.Marshal(metadata)
if err != nil {
logger.Error("Failed to marshal friend metadata", zap.Error(err))
return err
}
metadataStr = string(metadataBytes)
}

_, err := db.ExecContext(ctx, "UPDATE user_edge SET metadata = $3::JSONB WHERE source_id = $1 AND destination_id = $2", userID, friendUserID, metadataStr)
if err != nil {
logger.Error("Failed to update friend metadata", zap.Error(err))
return err
}

return nil
}

// Returns "true" if accepting an invite, otherwise false.
func addFriend(ctx context.Context, logger *zap.Logger, tx *sql.Tx, userID uuid.UUID, friendID, metadata string) (bool, error) {
if metadata == "" {
metadata = "{}"
}

// Mark an invite as accepted, if one was in place.
res, err := tx.ExecContext(ctx, `
UPDATE user_edge SET state = 0, update_time = now()
UPDATE user_edge SET state = 0, update_time = now(),
metadata = CASE
WHEN source_id = $2 AND destination_id = $1 THEN metadata || $3::JSONB
ELSE metadata
END
WHERE (source_id = $1 AND destination_id = $2 AND state = 1)
OR (source_id = $2 AND destination_id = $1 AND state = 2)
`, friendID, userID)
`, friendID, userID, metadata)
if err != nil {
logger.Debug("Failed to update user state.", zap.Error(err), zap.String("user", userID.String()), zap.String("friend", friendID))
return false, err
}

// If both edges were updated, it was accepting an invite was successful.
// If both edges were updated, it accepted an invite successfully.
if rowsAffected, _ := res.RowsAffected(); rowsAffected == 2 {
logger.Debug("Accepting friend invitation.", zap.String("user", userID.String()), zap.String("friend", friendID))
return true, nil
}

position := fmt.Sprintf("%v", time.Now().UTC().UnixNano())

// If no edge updates took place, it's either a new invite being set up, or user was blocked off by friend.
_, err = tx.ExecContext(ctx, `
INSERT INTO user_edge (source_id, destination_id, state, position, update_time)
SELECT source_id, destination_id, state, position, update_time
INSERT INTO user_edge (source_id, destination_id, state, position, update_time, metadata)
SELECT source_id, destination_id, state, position, update_time, metadata
FROM (VALUES
($1::UUID, $2::UUID, 1, $3::BIGINT, now()),
($2::UUID, $1::UUID, 2, $3::BIGINT, now())
) AS ue(source_id, destination_id, state, position, update_time)
($1::UUID, $2::UUID, 1, $3::BIGINT, now(), $4::JSONB),
($2::UUID, $1::UUID, 2, $3::BIGINT, now(), '{}'::JSONB)
) AS ue(source_id, destination_id, state, position, update_time, metadata)
WHERE
EXISTS (SELECT id FROM users WHERE id = $2::UUID)
AND
Expand All @@ -570,7 +601,7 @@ WHERE
WHERE source_id = $2::UUID AND destination_id = $1::UUID AND state = 3
)
ON CONFLICT (source_id, destination_id) DO NOTHING
`, userID, friendID, position)
`, userID, friendID, position, metadata)
if err != nil {
logger.Debug("Failed to insert new user edge link.", zap.Error(err), zap.String("user", userID.String()), zap.String("friend", friendID))
return false, err
Expand All @@ -582,7 +613,7 @@ ON CONFLICT (source_id, destination_id) DO NOTHING
// This is caused by an existing bug in CockroachDB: https://github.com/cockroachdb/cockroach/issues/10264
if res, err = tx.ExecContext(ctx, `
UPDATE users
SET edge_count = edge_count +1, update_time = now()
SET edge_count = edge_count + 1, update_time = now()
WHERE
(id = $1::UUID OR id = $2::UUID)
AND EXISTS
Expand Down
Loading
Loading