In case you wonder how it’d work with a custom RPC, this is how I’m doing it:
- Send a request via RPC to the friend (only invitations by friends allowed in my case, not visible in this example)
- Friend sends an accept RPC.
- Server creates a match and sends a notification to both users with the match ID.
- Client accepts.
Sample Code
To invite a user using notifications:
func ChallengeUser(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, challengerID, opponentID string) error {
userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
if !ok {
return errNoUserIdFound
}
if opponentID == "" || opponentID == challengerID {
return errors.New("invalid opponent ID")
}
notificationContent := map[string]interface{}{
"challenger_id": challengerID,
}
if err := nk.NotificationSend(ctx, opponentID, "match_invitation", notificationContent, NotificationCodeInvitation, userId, false); err != nil {
logger.Error("Failed to send challenge notification")
return err
}
return nil
}
To respond to the inviting one on success/failure:
func ChallengeUserRpc() nakamaRpcFunc {
return func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
var data map[string]string
if err := json.Unmarshal([]byte(payload), &data); err != nil {
return "", errors.New("invalid payload")
}
challengerID := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
opponentID := data["opponent_id"]
if err := ChallengeUser(ctx, logger, nk, challengerID, opponentID); err != nil {
return "", err
}
response := map[string]interface{}{
"success": true,
"message": "Challenge sent successfully",
"challenger_id": challengerID,
"opponent_id": opponentID,
}
respBytes, _ := json.Marshal(response)
return string(respBytes), nil
}
}
To finally accept & send the notification to join a match to both users (in case of a 1v1):
func SendAcceptedNotifications(context context.Context, nk runtime.NakamaModule, logger runtime.Logger, challengerId string, userId string, matchId string) error {
notificationContent := map[string]interface{}{
"matchId": matchId,
}
if err := nk.NotificationSend(context, userId, "match_invitation", notificationContent, NotificationCodeInvitiationAccepted, challengerId, false); err != nil {
logger.Error("Failed to send challenge notification")
return err
}
if err := nk.NotificationSend(context, challengerId, "match_invitation", notificationContent, NotificationCodeInvitiationAccepted, userId, false); err != nil {
logger.Error("Failed to send challenge notification")
return err
}
return nil
}
func AcceptChallengeRpc() nakamaRpcFunc {
return func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
var data map[string]string
if err := json.Unmarshal([]byte(payload), &data); err != nil {
return "", errors.New("invalid payload")
}
challengedId := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
challengerId := data["opponent_id"]
matchId := "" // Gather match id by creating a new match
// Check if opponentID is valid, befriended and whatever you like to validate...
SendAcceptedNotifications(ctx, nk, logger, challengedId, challengerId, matchId)
return "", nil
}
}
Don’t pay attention to the usage of JSON instead of protobuf or missing validations, it’s just for demonstration.