feat: game lobby service
This commit is contained in:
@@ -82,6 +82,13 @@ const (
|
||||
// `lobby.membership.rejected` notification.
|
||||
NotificationTypeLobbyMembershipRejected NotificationType = "lobby.membership.rejected"
|
||||
|
||||
// NotificationTypeLobbyMembershipBlocked identifies the
|
||||
// `lobby.membership.blocked` notification published by Game Lobby
|
||||
// to the private-game owner when an active membership is blocked
|
||||
// by the user-lifecycle cascade reacting to a `permanent_block` or
|
||||
// `DeleteUser` event.
|
||||
NotificationTypeLobbyMembershipBlocked NotificationType = "lobby.membership.blocked"
|
||||
|
||||
// NotificationTypeLobbyInviteCreated identifies the
|
||||
// `lobby.invite.created` notification.
|
||||
NotificationTypeLobbyInviteCreated NotificationType = "lobby.invite.created"
|
||||
@@ -93,6 +100,24 @@ const (
|
||||
// NotificationTypeLobbyInviteExpired identifies the
|
||||
// `lobby.invite.expired` notification.
|
||||
NotificationTypeLobbyInviteExpired NotificationType = "lobby.invite.expired"
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistrationEligible identifies the
|
||||
// `lobby.race_name.registration_eligible` notification published by
|
||||
// Game Lobby when capability evaluation at game finish promotes a
|
||||
// reservation to `pending_registration`.
|
||||
NotificationTypeLobbyRaceNameRegistrationEligible NotificationType = "lobby.race_name.registration_eligible"
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistered identifies the
|
||||
// `lobby.race_name.registered` notification published by Game Lobby
|
||||
// when a user converts a `pending_registration` into a permanent
|
||||
// registered race name.
|
||||
NotificationTypeLobbyRaceNameRegistered NotificationType = "lobby.race_name.registered"
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistrationDenied identifies the
|
||||
// `lobby.race_name.registration_denied` notification published by
|
||||
// Game Lobby when capability evaluation at game finish releases a
|
||||
// reservation because the member did not meet the capability rule.
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied NotificationType = "lobby.race_name.registration_denied"
|
||||
)
|
||||
|
||||
// String returns the wire value for notificationType.
|
||||
@@ -111,9 +136,13 @@ func (notificationType NotificationType) IsKnown() bool {
|
||||
NotificationTypeLobbyApplicationSubmitted,
|
||||
NotificationTypeLobbyMembershipApproved,
|
||||
NotificationTypeLobbyMembershipRejected,
|
||||
NotificationTypeLobbyMembershipBlocked,
|
||||
NotificationTypeLobbyInviteCreated,
|
||||
NotificationTypeLobbyInviteRedeemed,
|
||||
NotificationTypeLobbyInviteExpired:
|
||||
NotificationTypeLobbyInviteExpired,
|
||||
NotificationTypeLobbyRaceNameRegistrationEligible,
|
||||
NotificationTypeLobbyRaceNameRegistered,
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -133,9 +162,13 @@ func (notificationType NotificationType) ExpectedProducer() Producer {
|
||||
NotificationTypeLobbyApplicationSubmitted,
|
||||
NotificationTypeLobbyMembershipApproved,
|
||||
NotificationTypeLobbyMembershipRejected,
|
||||
NotificationTypeLobbyMembershipBlocked,
|
||||
NotificationTypeLobbyInviteCreated,
|
||||
NotificationTypeLobbyInviteRedeemed,
|
||||
NotificationTypeLobbyInviteExpired:
|
||||
NotificationTypeLobbyInviteExpired,
|
||||
NotificationTypeLobbyRaceNameRegistrationEligible,
|
||||
NotificationTypeLobbyRaceNameRegistered,
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return ProducerGameLobby
|
||||
default:
|
||||
return ""
|
||||
@@ -169,7 +202,8 @@ func (notificationType NotificationType) SupportsChannel(audienceKind AudienceKi
|
||||
return channel == ChannelEmail
|
||||
}
|
||||
return channel == ChannelPush || channel == ChannelEmail
|
||||
case NotificationTypeLobbyInviteExpired:
|
||||
case NotificationTypeLobbyInviteExpired,
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return audienceKind == AudienceKindUser && channel == ChannelEmail
|
||||
default:
|
||||
return audienceKind == AudienceKindUser && (channel == ChannelPush || channel == ChannelEmail)
|
||||
@@ -752,10 +786,21 @@ func validatePayloadObject(notificationType NotificationType, payload map[string
|
||||
return validateStringFields(payload, "game_id", "game_name", "applicant_user_id", "applicant_name")
|
||||
case NotificationTypeLobbyMembershipApproved, NotificationTypeLobbyMembershipRejected:
|
||||
return validateStringFields(payload, "game_id", "game_name")
|
||||
case NotificationTypeLobbyMembershipBlocked:
|
||||
return validateStringFields(payload, "game_id", "game_name", "membership_user_id", "membership_user_name", "reason")
|
||||
case NotificationTypeLobbyInviteCreated:
|
||||
return validateStringFields(payload, "game_id", "game_name", "inviter_user_id", "inviter_name")
|
||||
case NotificationTypeLobbyInviteRedeemed, NotificationTypeLobbyInviteExpired:
|
||||
return validateStringFields(payload, "game_id", "game_name", "invitee_user_id", "invitee_name")
|
||||
case NotificationTypeLobbyRaceNameRegistrationEligible:
|
||||
if err := validateStringFields(payload, "game_id", "game_name", "race_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
return validatePositiveIntFields(payload, "eligible_until_ms")
|
||||
case NotificationTypeLobbyRaceNameRegistered:
|
||||
return validateStringFields(payload, "race_name")
|
||||
case NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return validateStringFields(payload, "game_id", "game_name", "race_name", "reason")
|
||||
default:
|
||||
return fmt.Errorf("payload_json notification type %q is unsupported", notificationType)
|
||||
}
|
||||
|
||||
@@ -159,6 +159,23 @@ func TestConstructorsBuildExpectedIntentValues(t *testing.T) {
|
||||
recipientUserIDs: []string{"applicant-1"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash"}`,
|
||||
},
|
||||
{
|
||||
name: "lobby membership blocked",
|
||||
build: func() (Intent, error) {
|
||||
return NewLobbyMembershipBlockedIntent(metadata, "owner-1", LobbyMembershipBlockedPayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
MembershipUserID: "user-2",
|
||||
MembershipUserName: "player-aabbccdd",
|
||||
Reason: "permanent_blocked",
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeLobbyMembershipBlocked,
|
||||
producer: ProducerGameLobby,
|
||||
audienceKind: AudienceKindUser,
|
||||
recipientUserIDs: []string{"owner-1"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","membership_user_id":"user-2","membership_user_name":"player-aabbccdd","reason":"permanent_blocked"}`,
|
||||
},
|
||||
{
|
||||
name: "lobby invite created",
|
||||
build: func() (Intent, error) {
|
||||
@@ -207,6 +224,51 @@ func TestConstructorsBuildExpectedIntentValues(t *testing.T) {
|
||||
recipientUserIDs: []string{"owner-1"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","invitee_user_id":"invitee-1","invitee_name":"Nova Pilot"}`,
|
||||
},
|
||||
{
|
||||
name: "lobby race name registration eligible",
|
||||
build: func() (Intent, error) {
|
||||
return NewLobbyRaceNameRegistrationEligibleIntent(metadata, "user-7", LobbyRaceNameRegistrationEligiblePayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
RaceName: "Skylancer",
|
||||
EligibleUntilMs: 1775208100000,
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeLobbyRaceNameRegistrationEligible,
|
||||
producer: ProducerGameLobby,
|
||||
audienceKind: AudienceKindUser,
|
||||
recipientUserIDs: []string{"user-7"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","race_name":"Skylancer","eligible_until_ms":1775208100000}`,
|
||||
},
|
||||
{
|
||||
name: "lobby race name registered",
|
||||
build: func() (Intent, error) {
|
||||
return NewLobbyRaceNameRegisteredIntent(metadata, "user-8", LobbyRaceNameRegisteredPayload{
|
||||
RaceName: "Skylancer",
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeLobbyRaceNameRegistered,
|
||||
producer: ProducerGameLobby,
|
||||
audienceKind: AudienceKindUser,
|
||||
recipientUserIDs: []string{"user-8"},
|
||||
payloadJSON: `{"race_name":"Skylancer"}`,
|
||||
},
|
||||
{
|
||||
name: "lobby race name registration denied",
|
||||
build: func() (Intent, error) {
|
||||
return NewLobbyRaceNameRegistrationDeniedIntent(metadata, "user-9", LobbyRaceNameRegistrationDeniedPayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
RaceName: "Skylancer",
|
||||
Reason: "capability_not_met",
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeLobbyRaceNameRegistrationDenied,
|
||||
producer: ProducerGameLobby,
|
||||
audienceKind: AudienceKindUser,
|
||||
recipientUserIDs: []string{"user-9"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","race_name":"Skylancer","reason":"capability_not_met"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -62,6 +62,20 @@ type LobbyMembershipRejectedPayload struct {
|
||||
GameName string `json:"game_name"`
|
||||
}
|
||||
|
||||
// LobbyMembershipBlockedPayload stores the normalized payload for
|
||||
// `lobby.membership.blocked` published by the user-lifecycle cascade
|
||||
// when an active membership is blocked because the underlying user was
|
||||
// permanently blocked or deleted.
|
||||
type LobbyMembershipBlockedPayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
MembershipUserID string `json:"membership_user_id"`
|
||||
MembershipUserName string `json:"membership_user_name"`
|
||||
// Reason captures the upstream lifecycle event that triggered the
|
||||
// cascade. Frozen vocabulary: `permanent_blocked`, `deleted`.
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// LobbyInviteCreatedPayload stores the normalized payload for
|
||||
// `lobby.invite.created`.
|
||||
type LobbyInviteCreatedPayload struct {
|
||||
@@ -89,6 +103,30 @@ type LobbyInviteExpiredPayload struct {
|
||||
InviteeName string `json:"invitee_name"`
|
||||
}
|
||||
|
||||
// LobbyRaceNameRegistrationEligiblePayload stores the normalized payload
|
||||
// for `lobby.race_name.registration_eligible`.
|
||||
type LobbyRaceNameRegistrationEligiblePayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
RaceName string `json:"race_name"`
|
||||
EligibleUntilMs int64 `json:"eligible_until_ms"`
|
||||
}
|
||||
|
||||
// LobbyRaceNameRegisteredPayload stores the normalized payload for
|
||||
// `lobby.race_name.registered`.
|
||||
type LobbyRaceNameRegisteredPayload struct {
|
||||
RaceName string `json:"race_name"`
|
||||
}
|
||||
|
||||
// LobbyRaceNameRegistrationDeniedPayload stores the normalized payload for
|
||||
// `lobby.race_name.registration_denied`.
|
||||
type LobbyRaceNameRegistrationDeniedPayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
RaceName string `json:"race_name"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// NewGeoReviewRecommendedIntent builds the admin-email intent published by Geo
|
||||
// Profile Service when a user becomes review-worthy.
|
||||
func NewGeoReviewRecommendedIntent(metadata Metadata, payload GeoReviewRecommendedPayload) (Intent, error) {
|
||||
@@ -143,6 +181,14 @@ func NewLobbyMembershipRejectedIntent(metadata Metadata, applicantUserID string,
|
||||
return newIntent(NotificationTypeLobbyMembershipRejected, ProducerGameLobby, AudienceKindUser, []string{applicantUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewLobbyMembershipBlockedIntent builds the private-game owner intent
|
||||
// published by Game Lobby when an active membership is blocked by the
|
||||
// user-lifecycle cascade. ownerUserID is the recipient (private-game
|
||||
// owner whose roster lost the affected member).
|
||||
func NewLobbyMembershipBlockedIntent(metadata Metadata, ownerUserID string, payload LobbyMembershipBlockedPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyMembershipBlocked, ProducerGameLobby, AudienceKindUser, []string{ownerUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewLobbyInviteCreatedIntent builds the invited-user intent published by Game
|
||||
// Lobby when a private-game invite is created.
|
||||
func NewLobbyInviteCreatedIntent(metadata Metadata, invitedUserID string, payload LobbyInviteCreatedPayload) (Intent, error) {
|
||||
@@ -160,3 +206,23 @@ func NewLobbyInviteRedeemedIntent(metadata Metadata, ownerUserID string, payload
|
||||
func NewLobbyInviteExpiredIntent(metadata Metadata, ownerUserID string, payload LobbyInviteExpiredPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyInviteExpired, ProducerGameLobby, AudienceKindUser, []string{ownerUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewLobbyRaceNameRegistrationEligibleIntent builds the capable-member intent
|
||||
// published by Game Lobby at game finish when a reservation is promoted to
|
||||
// `pending_registration`.
|
||||
func NewLobbyRaceNameRegistrationEligibleIntent(metadata Metadata, recipientUserID string, payload LobbyRaceNameRegistrationEligiblePayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyRaceNameRegistrationEligible, ProducerGameLobby, AudienceKindUser, []string{recipientUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewLobbyRaceNameRegisteredIntent builds the registering-user intent
|
||||
// published by Game Lobby on successful `lobby.race_name.register` commit.
|
||||
func NewLobbyRaceNameRegisteredIntent(metadata Metadata, recipientUserID string, payload LobbyRaceNameRegisteredPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyRaceNameRegistered, ProducerGameLobby, AudienceKindUser, []string{recipientUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewLobbyRaceNameRegistrationDeniedIntent builds the incapable-member intent
|
||||
// published by Game Lobby at game finish when a reservation is released
|
||||
// without a pending-registration window.
|
||||
func NewLobbyRaceNameRegistrationDeniedIntent(metadata Metadata, recipientUserID string, payload LobbyRaceNameRegistrationDeniedPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyRaceNameRegistrationDenied, ProducerGameLobby, AudienceKindUser, []string{recipientUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user