feat: game lobby service
This commit is contained in:
+22
-4
@@ -273,19 +273,29 @@ Accepted intents use the original Redis Stream `stream_entry_id` as
|
||||
| `lobby.application.submitted` | `Game Lobby` (`game_lobby`) | private owner (`audience_kind=user`) or public admins (`audience_kind=admin_email`) | private: `push+email`, public: `email` | `game_id`, `game_name`, `applicant_user_id`, `applicant_name` |
|
||||
| `lobby.membership.approved` | `Game Lobby` (`game_lobby`) | applicant user (`audience_kind=user`) | `push+email` | `game_id`, `game_name` |
|
||||
| `lobby.membership.rejected` | `Game Lobby` (`game_lobby`) | applicant user (`audience_kind=user`) | `push+email` | `game_id`, `game_name` |
|
||||
| `lobby.membership.blocked` | `Game Lobby` (`game_lobby`) | private-game owner (`audience_kind=user`) | `push+email` | `game_id`, `game_name`, `membership_user_id`, `membership_user_name`, `reason` |
|
||||
| `lobby.invite.created` | `Game Lobby` (`game_lobby`) | invited user (`audience_kind=user`) | `push+email` | `game_id`, `game_name`, `inviter_user_id`, `inviter_name` |
|
||||
| `lobby.invite.redeemed` | `Game Lobby` (`game_lobby`) | private-game owner (`audience_kind=user`) | `push+email` | `game_id`, `game_name`, `invitee_user_id`, `invitee_name` |
|
||||
| `lobby.invite.expired` | `Game Lobby` (`game_lobby`) | private-game owner (`audience_kind=user`) | `email` | `game_id`, `game_name`, `invitee_user_id`, `invitee_name` |
|
||||
| `lobby.race_name.registration_eligible` | `Game Lobby` (`game_lobby`) | capable member (`audience_kind=user`) | `push+email` | `game_id`, `game_name`, `race_name`, `eligible_until_ms` |
|
||||
| `lobby.race_name.registered` | `Game Lobby` (`game_lobby`) | registering user (`audience_kind=user`) | `push+email` | `race_name` |
|
||||
| `lobby.race_name.registration_denied` | `Game Lobby` (`game_lobby`) | incapable member (`audience_kind=user`) | `email` | `game_id`, `game_name`, `race_name`, `reason` |
|
||||
|
||||
Rules:
|
||||
|
||||
- v1 supports exactly the eleven `notification_type` values listed above
|
||||
- v1 supports exactly the fifteen `notification_type` values listed above
|
||||
- `lobby.application.submitted` keeps one stable `notification_type` and one
|
||||
stable `payload_json` shape; private games publish `audience_kind=user`
|
||||
while public games publish `audience_kind=admin_email`
|
||||
- `lobby.invite.revoked` deliberately produces no notification in v1 and
|
||||
remains outside the supported catalog
|
||||
- private-game invite notifications remain user-bound by internal `user_id`
|
||||
- `lobby.race_name.registration_eligible` and
|
||||
`lobby.race_name.registration_denied` are emitted by `Game Lobby` at
|
||||
`game_finished` based on capability evaluation; the former always pairs
|
||||
with a 30-day `eligible_until_ms` window
|
||||
- `lobby.race_name.registered` is emitted on successful
|
||||
`lobby.race_name.register` commit
|
||||
|
||||
## Recipient Enrichment And Locale Policy
|
||||
|
||||
@@ -343,14 +353,18 @@ User-facing push payloads use
|
||||
| `lobby.application.submitted` | `notification.LobbyApplicationSubmittedEvent` | `game_id`, `applicant_user_id` |
|
||||
| `lobby.membership.approved` | `notification.LobbyMembershipApprovedEvent` | `game_id` |
|
||||
| `lobby.membership.rejected` | `notification.LobbyMembershipRejectedEvent` | `game_id` |
|
||||
| `lobby.membership.blocked` | `notification.LobbyMembershipBlockedEvent` | `game_id`, `membership_user_id`, `reason` |
|
||||
| `lobby.invite.created` | `notification.LobbyInviteCreatedEvent` | `game_id`, `inviter_user_id` |
|
||||
| `lobby.invite.redeemed` | `notification.LobbyInviteRedeemedEvent` | `game_id`, `invitee_user_id` |
|
||||
| `lobby.race_name.registration_eligible` | `notification.LobbyRaceNameRegistrationEligibleEvent` | `game_id`, `race_name`, `eligible_until_ms` |
|
||||
| `lobby.race_name.registered` | `notification.LobbyRaceNameRegisteredEvent` | `race_name` |
|
||||
|
||||
Only the seven user-facing push notification types above are represented in
|
||||
Only the ten user-facing push notification types above are represented in
|
||||
`notification.fbs`.
|
||||
`geo.review_recommended`, `game.generation_failed`,
|
||||
`lobby.runtime_paused_after_start`, and `lobby.invite.expired` remain outside
|
||||
this schema because they are email-only in v1.
|
||||
`lobby.runtime_paused_after_start`, `lobby.invite.expired`, and
|
||||
`lobby.race_name.registration_denied` remain outside this schema because
|
||||
they are email-only in v1.
|
||||
|
||||
Checked-in generated Go bindings for this schema live under
|
||||
[`../pkg/schema/fbs/notification`](../pkg/schema/fbs/notification).
|
||||
@@ -403,9 +417,13 @@ Initial notification-owned template assets:
|
||||
| `lobby.application.submitted` | `lobby.application.submitted` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.membership.approved` | `lobby.membership.approved` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.membership.rejected` | `lobby.membership.rejected` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.membership.blocked` | `lobby.membership.blocked` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.invite.created` | `lobby.invite.created` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.invite.redeemed` | `lobby.invite.redeemed` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.invite.expired` | `lobby.invite.expired` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.race_name.registration_eligible` | `lobby.race_name.registration_eligible` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.race_name.registered` | `lobby.race_name.registered` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.race_name.registration_denied` | `lobby.race_name.registration_denied` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
|
||||
`auth.login_code` does not belong to the notification-owned template set.
|
||||
|
||||
|
||||
@@ -91,9 +91,13 @@ components:
|
||||
- lobby.application.submitted
|
||||
- lobby.membership.approved
|
||||
- lobby.membership.rejected
|
||||
- lobby.membership.blocked
|
||||
- lobby.invite.created
|
||||
- lobby.invite.redeemed
|
||||
- lobby.invite.expired
|
||||
- lobby.race_name.registration_eligible
|
||||
- lobby.race_name.registered
|
||||
- lobby.race_name.registration_denied
|
||||
description: |
|
||||
Exact v1 notification type catalog. `lobby.invite.revoked`
|
||||
deliberately remains outside the supported catalog because it
|
||||
@@ -310,6 +314,21 @@ components:
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyMembershipRejectedPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: lobby.membership.blocked
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: game_lobby
|
||||
audience_kind:
|
||||
const: user
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyMembershipBlockedPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
@@ -355,6 +374,51 @@ components:
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyInviteExpiredPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: lobby.race_name.registration_eligible
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: game_lobby
|
||||
audience_kind:
|
||||
const: user
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyRaceNameRegistrationEligiblePayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: lobby.race_name.registered
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: game_lobby
|
||||
audience_kind:
|
||||
const: user
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyRaceNameRegisteredPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: lobby.race_name.registration_denied
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: game_lobby
|
||||
audience_kind:
|
||||
const: user
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyRaceNameRegistrationDeniedPayload'
|
||||
GeoReviewRecommendedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@@ -491,6 +555,34 @@ components:
|
||||
game_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
LobbyMembershipBlockedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- game_name
|
||||
- membership_user_id
|
||||
- membership_user_name
|
||||
- reason
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
game_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
membership_user_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
membership_user_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
reason:
|
||||
type: string
|
||||
minLength: 1
|
||||
enum:
|
||||
- permanent_blocked
|
||||
- deleted
|
||||
LobbyInviteCreatedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@@ -554,3 +646,54 @@ components:
|
||||
invitee_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
LobbyRaceNameRegistrationEligiblePayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- game_name
|
||||
- race_name
|
||||
- eligible_until_ms
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
game_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
race_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
eligible_until_ms:
|
||||
type: integer
|
||||
minimum: 1
|
||||
LobbyRaceNameRegisteredPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- race_name
|
||||
properties:
|
||||
race_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
LobbyRaceNameRegistrationDeniedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- game_name
|
||||
- race_name
|
||||
- reason
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
game_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
race_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
reason:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
@@ -28,9 +28,13 @@ var expectedNotificationTypeCatalog = []string{
|
||||
"lobby.application.submitted",
|
||||
"lobby.membership.approved",
|
||||
"lobby.membership.rejected",
|
||||
"lobby.membership.blocked",
|
||||
"lobby.invite.created",
|
||||
"lobby.invite.redeemed",
|
||||
"lobby.invite.expired",
|
||||
"lobby.race_name.registration_eligible",
|
||||
"lobby.race_name.registered",
|
||||
"lobby.race_name.registration_denied",
|
||||
}
|
||||
|
||||
var expectedNotificationCatalog = map[string]notificationCatalogExpectation{
|
||||
@@ -82,6 +86,12 @@ var expectedNotificationCatalog = map[string]notificationCatalogExpectation{
|
||||
payloadSchema: "LobbyMembershipRejectedPayload",
|
||||
requiredFields: []string{"game_id", "game_name"},
|
||||
},
|
||||
"lobby.membership.blocked": {
|
||||
producer: "game_lobby",
|
||||
audienceKind: "user",
|
||||
payloadSchema: "LobbyMembershipBlockedPayload",
|
||||
requiredFields: []string{"game_id", "game_name", "membership_user_id", "membership_user_name", "reason"},
|
||||
},
|
||||
"lobby.invite.created": {
|
||||
producer: "game_lobby",
|
||||
audienceKind: "user",
|
||||
@@ -100,6 +110,24 @@ var expectedNotificationCatalog = map[string]notificationCatalogExpectation{
|
||||
payloadSchema: "LobbyInviteExpiredPayload",
|
||||
requiredFields: []string{"game_id", "game_name", "invitee_user_id", "invitee_name"},
|
||||
},
|
||||
"lobby.race_name.registration_eligible": {
|
||||
producer: "game_lobby",
|
||||
audienceKind: "user",
|
||||
payloadSchema: "LobbyRaceNameRegistrationEligiblePayload",
|
||||
requiredFields: []string{"game_id", "game_name", "race_name", "eligible_until_ms"},
|
||||
},
|
||||
"lobby.race_name.registered": {
|
||||
producer: "game_lobby",
|
||||
audienceKind: "user",
|
||||
payloadSchema: "LobbyRaceNameRegisteredPayload",
|
||||
requiredFields: []string{"race_name"},
|
||||
},
|
||||
"lobby.race_name.registration_denied": {
|
||||
producer: "game_lobby",
|
||||
audienceKind: "user",
|
||||
payloadSchema: "LobbyRaceNameRegistrationDeniedPayload",
|
||||
requiredFields: []string{"game_id", "game_name", "race_name", "reason"},
|
||||
},
|
||||
}
|
||||
|
||||
const expectedNotificationCatalogTable = `| ` + "`notification_type`" + ` | Producer | Audience | Channels | Required ` + "`payload_json`" + ` fields |
|
||||
@@ -112,9 +140,13 @@ const expectedNotificationCatalogTable = `| ` + "`notification_type`" + ` | Prod
|
||||
| ` + "`lobby.application.submitted`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private owner (` + "`audience_kind=user`" + `) or public admins (` + "`audience_kind=admin_email`" + `) | private: ` + "`push+email`" + `, public: ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`applicant_user_id`" + `, ` + "`applicant_name`" + ` |
|
||||
| ` + "`lobby.membership.approved`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | applicant user (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + ` |
|
||||
| ` + "`lobby.membership.rejected`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | applicant user (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + ` |
|
||||
| ` + "`lobby.membership.blocked`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private-game owner (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`membership_user_id`" + `, ` + "`membership_user_name`" + `, ` + "`reason`" + ` |
|
||||
| ` + "`lobby.invite.created`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | invited user (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`inviter_user_id`" + `, ` + "`inviter_name`" + ` |
|
||||
| ` + "`lobby.invite.redeemed`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private-game owner (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`invitee_user_id`" + `, ` + "`invitee_name`" + ` |
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private-game owner (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`invitee_user_id`" + `, ` + "`invitee_name`" + ` |`
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private-game owner (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`invitee_user_id`" + `, ` + "`invitee_name`" + ` |
|
||||
| ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | capable member (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`race_name`" + `, ` + "`eligible_until_ms`" + ` |
|
||||
| ` + "`lobby.race_name.registered`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | registering user (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`race_name`" + ` |
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | incapable member (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`race_name`" + `, ` + "`reason`" + ` |`
|
||||
|
||||
var expectedSharedDocumentationSnippets = []string{
|
||||
"`lobby.application.submitted` keeps one stable `notification_type` and one stable `payload_json` shape",
|
||||
@@ -581,6 +613,8 @@ func TestGatewayREADMEFreezesExactPushVocabulary(t *testing.T) {
|
||||
"- `lobby.membership.rejected`",
|
||||
"- `lobby.invite.created`",
|
||||
"- `lobby.invite.redeemed`",
|
||||
"- `lobby.race_name.registration_eligible`",
|
||||
"- `lobby.race_name.registered`",
|
||||
}, "\n"),
|
||||
)
|
||||
require.Contains(
|
||||
|
||||
@@ -58,6 +58,10 @@ const (
|
||||
// `lobby.membership.rejected` notification.
|
||||
NotificationTypeLobbyMembershipRejected = notificationintent.NotificationTypeLobbyMembershipRejected
|
||||
|
||||
// NotificationTypeLobbyMembershipBlocked identifies the
|
||||
// `lobby.membership.blocked` notification.
|
||||
NotificationTypeLobbyMembershipBlocked = notificationintent.NotificationTypeLobbyMembershipBlocked
|
||||
|
||||
// NotificationTypeLobbyInviteCreated identifies the
|
||||
// `lobby.invite.created` notification.
|
||||
NotificationTypeLobbyInviteCreated = notificationintent.NotificationTypeLobbyInviteCreated
|
||||
@@ -69,6 +73,18 @@ const (
|
||||
// NotificationTypeLobbyInviteExpired identifies the
|
||||
// `lobby.invite.expired` notification.
|
||||
NotificationTypeLobbyInviteExpired = notificationintent.NotificationTypeLobbyInviteExpired
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistrationEligible identifies the
|
||||
// `lobby.race_name.registration_eligible` notification.
|
||||
NotificationTypeLobbyRaceNameRegistrationEligible = notificationintent.NotificationTypeLobbyRaceNameRegistrationEligible
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistered identifies the
|
||||
// `lobby.race_name.registered` notification.
|
||||
NotificationTypeLobbyRaceNameRegistered = notificationintent.NotificationTypeLobbyRaceNameRegistered
|
||||
|
||||
// NotificationTypeLobbyRaceNameRegistrationDenied identifies the
|
||||
// `lobby.race_name.registration_denied` notification.
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied = notificationintent.NotificationTypeLobbyRaceNameRegistrationDenied
|
||||
)
|
||||
|
||||
// Producer identifies one supported upstream producer.
|
||||
@@ -133,7 +149,10 @@ func ClassifyDecodeError(err error) malformedintent.FailureCode {
|
||||
strings.Contains(message, "applicant_name"),
|
||||
strings.Contains(message, "inviter_name"),
|
||||
strings.Contains(message, "invitee_name"),
|
||||
strings.Contains(message, "review_reason"):
|
||||
strings.Contains(message, "review_reason"),
|
||||
strings.Contains(message, "race_name"),
|
||||
strings.Contains(message, "eligible_until_ms"),
|
||||
strings.Contains(message, "reason"):
|
||||
return malformedintent.FailureCodeInvalidPayload
|
||||
default:
|
||||
return malformedintent.FailureCodeInvalidIntent
|
||||
|
||||
@@ -154,6 +154,29 @@ func encodePayload(notificationType intentstream.NotificationType, payloadJSON s
|
||||
return wrapPayloadEncoding(transcoder.LobbyMembershipRejectedEventToPayload(&transcoder.LobbyMembershipRejectedEvent{
|
||||
GameID: payload.GameID,
|
||||
}))
|
||||
case intentstream.NotificationTypeLobbyMembershipBlocked:
|
||||
var payload struct {
|
||||
GameID string `json:"game_id"`
|
||||
MembershipUserID string `json:"membership_user_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := decodePayload(payloadJSON, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload.GameID == "" {
|
||||
return nil, errors.New("payload_encoding_failed: game_id is empty")
|
||||
}
|
||||
if payload.MembershipUserID == "" {
|
||||
return nil, errors.New("payload_encoding_failed: membership_user_id is empty")
|
||||
}
|
||||
if payload.Reason == "" {
|
||||
return nil, errors.New("payload_encoding_failed: reason is empty")
|
||||
}
|
||||
return wrapPayloadEncoding(transcoder.LobbyMembershipBlockedEventToPayload(&transcoder.LobbyMembershipBlockedEvent{
|
||||
GameID: payload.GameID,
|
||||
MembershipUserID: payload.MembershipUserID,
|
||||
Reason: payload.Reason,
|
||||
}))
|
||||
case intentstream.NotificationTypeLobbyInviteCreated:
|
||||
var payload struct {
|
||||
GameID string `json:"game_id"`
|
||||
@@ -190,6 +213,42 @@ func encodePayload(notificationType intentstream.NotificationType, payloadJSON s
|
||||
GameID: payload.GameID,
|
||||
InviteeUserID: payload.InviteeUserID,
|
||||
}))
|
||||
case intentstream.NotificationTypeLobbyRaceNameRegistrationEligible:
|
||||
var payload struct {
|
||||
GameID string `json:"game_id"`
|
||||
RaceName string `json:"race_name"`
|
||||
EligibleUntilMs int64 `json:"eligible_until_ms"`
|
||||
}
|
||||
if err := decodePayload(payloadJSON, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload.GameID == "" {
|
||||
return nil, errors.New("payload_encoding_failed: game_id is empty")
|
||||
}
|
||||
if payload.RaceName == "" {
|
||||
return nil, errors.New("payload_encoding_failed: race_name is empty")
|
||||
}
|
||||
if payload.EligibleUntilMs < 1 {
|
||||
return nil, errors.New("payload_encoding_failed: eligible_until_ms must be at least 1")
|
||||
}
|
||||
return wrapPayloadEncoding(transcoder.LobbyRaceNameRegistrationEligibleEventToPayload(&transcoder.LobbyRaceNameRegistrationEligibleEvent{
|
||||
GameID: payload.GameID,
|
||||
RaceName: payload.RaceName,
|
||||
EligibleUntilMs: payload.EligibleUntilMs,
|
||||
}))
|
||||
case intentstream.NotificationTypeLobbyRaceNameRegistered:
|
||||
var payload struct {
|
||||
RaceName string `json:"race_name"`
|
||||
}
|
||||
if err := decodePayload(payloadJSON, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload.RaceName == "" {
|
||||
return nil, errors.New("payload_encoding_failed: race_name is empty")
|
||||
}
|
||||
return wrapPayloadEncoding(transcoder.LobbyRaceNameRegisteredEventToPayload(&transcoder.LobbyRaceNameRegisteredEvent{
|
||||
RaceName: payload.RaceName,
|
||||
}))
|
||||
default:
|
||||
return nil, fmt.Errorf("payload_encoding_failed: notification type %q does not support push", notificationType)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,30 @@ func TestEncoderEncodesSupportedPushNotificationTypes(t *testing.T) {
|
||||
require.Equal(t, "user-10", event.InviteeUserID)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lobby race name registration eligible",
|
||||
notificationType: intentstream.NotificationTypeLobbyRaceNameRegistrationEligible,
|
||||
payloadJSON: `{"eligible_until_ms":1775208100000,"game_id":"game-8","game_name":"Aurora","race_name":"Skylancer"}`,
|
||||
assertPayload: func(t *testing.T, payload []byte) {
|
||||
t.Helper()
|
||||
event, err := transcoder.PayloadToLobbyRaceNameRegistrationEligibleEvent(payload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "game-8", event.GameID)
|
||||
require.Equal(t, "Skylancer", event.RaceName)
|
||||
require.Equal(t, int64(1775208100000), event.EligibleUntilMs)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lobby race name registered",
|
||||
notificationType: intentstream.NotificationTypeLobbyRaceNameRegistered,
|
||||
payloadJSON: `{"race_name":"Skylancer"}`,
|
||||
assertPayload: func(t *testing.T, payload []byte) {
|
||||
t.Helper()
|
||||
event, err := transcoder.PayloadToLobbyRaceNameRegisteredEvent(payload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Skylancer", event.RaceName)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -20,9 +20,13 @@ const expectedNotificationMailTemplateTable = `| ` + "`notification_type`" + ` |
|
||||
| ` + "`lobby.application.submitted`" + ` | ` + "`lobby.application.submitted`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.membership.approved`" + ` | ` + "`lobby.membership.approved`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.membership.rejected`" + ` | ` + "`lobby.membership.rejected`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.membership.blocked`" + ` | ` + "`lobby.membership.blocked`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.invite.created`" + ` | ` + "`lobby.invite.created`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.invite.redeemed`" + ` | ` + "`lobby.invite.redeemed`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`lobby.invite.expired`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |`
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`lobby.invite.expired`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registered`" + ` | ` + "`lobby.race_name.registered`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`lobby.race_name.registration_denied`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |`
|
||||
|
||||
var expectedNotificationMailReadmeSnippets = []string{
|
||||
"`payload_mode` is always `template`",
|
||||
|
||||
@@ -130,6 +130,15 @@ func compatibleProducerIntents(t *testing.T) []notificationintent.Intent {
|
||||
GameName: "Nebula Clash",
|
||||
})
|
||||
},
|
||||
func() (notificationintent.Intent, error) {
|
||||
return notificationintent.NewLobbyMembershipBlockedIntent(metadata, "owner-1", notificationintent.LobbyMembershipBlockedPayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
MembershipUserID: "user-2",
|
||||
MembershipUserName: "player-aabbccdd",
|
||||
Reason: "permanent_blocked",
|
||||
})
|
||||
},
|
||||
func() (notificationintent.Intent, error) {
|
||||
return notificationintent.NewLobbyInviteCreatedIntent(metadata, "invited-1", notificationintent.LobbyInviteCreatedPayload{
|
||||
GameID: "game-1",
|
||||
@@ -154,6 +163,27 @@ func compatibleProducerIntents(t *testing.T) []notificationintent.Intent {
|
||||
InviteeName: "Nova Pilot",
|
||||
})
|
||||
},
|
||||
func() (notificationintent.Intent, error) {
|
||||
return notificationintent.NewLobbyRaceNameRegistrationEligibleIntent(metadata, "user-7", notificationintent.LobbyRaceNameRegistrationEligiblePayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
RaceName: "Skylancer",
|
||||
EligibleUntilMs: 1775208100000,
|
||||
})
|
||||
},
|
||||
func() (notificationintent.Intent, error) {
|
||||
return notificationintent.NewLobbyRaceNameRegisteredIntent(metadata, "user-8", notificationintent.LobbyRaceNameRegisteredPayload{
|
||||
RaceName: "Skylancer",
|
||||
})
|
||||
},
|
||||
func() (notificationintent.Intent, error) {
|
||||
return notificationintent.NewLobbyRaceNameRegistrationDeniedIntent(metadata, "user-9", notificationintent.LobbyRaceNameRegistrationDeniedPayload{
|
||||
GameID: "game-1",
|
||||
GameName: "Nebula Clash",
|
||||
RaceName: "Skylancer",
|
||||
Reason: "capability_not_met",
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
intents := make([]notificationintent.Intent, 0, len(builders))
|
||||
|
||||
@@ -17,8 +17,11 @@ const expectedPushPayloadMappingTable = `| ` + "`notification_type`" + ` | FlatB
|
||||
| ` + "`lobby.application.submitted`" + ` | ` + "`notification.LobbyApplicationSubmittedEvent`" + ` | ` + "`game_id`" + `, ` + "`applicant_user_id`" + ` |
|
||||
| ` + "`lobby.membership.approved`" + ` | ` + "`notification.LobbyMembershipApprovedEvent`" + ` | ` + "`game_id`" + ` |
|
||||
| ` + "`lobby.membership.rejected`" + ` | ` + "`notification.LobbyMembershipRejectedEvent`" + ` | ` + "`game_id`" + ` |
|
||||
| ` + "`lobby.membership.blocked`" + ` | ` + "`notification.LobbyMembershipBlockedEvent`" + ` | ` + "`game_id`" + `, ` + "`membership_user_id`" + `, ` + "`reason`" + ` |
|
||||
| ` + "`lobby.invite.created`" + ` | ` + "`notification.LobbyInviteCreatedEvent`" + ` | ` + "`game_id`" + `, ` + "`inviter_user_id`" + ` |
|
||||
| ` + "`lobby.invite.redeemed`" + ` | ` + "`notification.LobbyInviteRedeemedEvent`" + ` | ` + "`game_id`" + `, ` + "`invitee_user_id`" + ` |`
|
||||
| ` + "`lobby.invite.redeemed`" + ` | ` + "`notification.LobbyInviteRedeemedEvent`" + ` | ` + "`game_id`" + `, ` + "`invitee_user_id`" + ` |
|
||||
| ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`notification.LobbyRaceNameRegistrationEligibleEvent`" + ` | ` + "`game_id`" + `, ` + "`race_name`" + `, ` + "`eligible_until_ms`" + ` |
|
||||
| ` + "`lobby.race_name.registered`" + ` | ` + "`notification.LobbyRaceNameRegisteredEvent`" + ` | ` + "`race_name`" + ` |`
|
||||
|
||||
var expectedPushPayloadSchemaTableNames = []string{
|
||||
"GameTurnReadyEvent",
|
||||
@@ -26,8 +29,11 @@ var expectedPushPayloadSchemaTableNames = []string{
|
||||
"LobbyApplicationSubmittedEvent",
|
||||
"LobbyMembershipApprovedEvent",
|
||||
"LobbyMembershipRejectedEvent",
|
||||
"LobbyMembershipBlockedEvent",
|
||||
"LobbyInviteCreatedEvent",
|
||||
"LobbyInviteRedeemedEvent",
|
||||
"LobbyRaceNameRegistrationEligibleEvent",
|
||||
"LobbyRaceNameRegisteredEvent",
|
||||
}
|
||||
|
||||
var expectedPushPayloadSchemaFields = map[string][]string{
|
||||
@@ -49,6 +55,11 @@ var expectedPushPayloadSchemaFields = map[string][]string{
|
||||
"LobbyMembershipRejectedEvent": {
|
||||
"game_id:string;",
|
||||
},
|
||||
"LobbyMembershipBlockedEvent": {
|
||||
"game_id:string;",
|
||||
"membership_user_id:string;",
|
||||
"reason:string;",
|
||||
},
|
||||
"LobbyInviteCreatedEvent": {
|
||||
"game_id:string;",
|
||||
"inviter_user_id:string;",
|
||||
@@ -57,6 +68,14 @@ var expectedPushPayloadSchemaFields = map[string][]string{
|
||||
"game_id:string;",
|
||||
"invitee_user_id:string;",
|
||||
},
|
||||
"LobbyRaceNameRegistrationEligibleEvent": {
|
||||
"game_id:string;",
|
||||
"race_name:string;",
|
||||
"eligible_until_ms:int64;",
|
||||
},
|
||||
"LobbyRaceNameRegisteredEvent": {
|
||||
"race_name:string;",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedPushPayloadGeneratedFiles = []string{
|
||||
@@ -66,12 +85,15 @@ var expectedPushPayloadGeneratedFiles = []string{
|
||||
"LobbyInviteCreatedEvent.go",
|
||||
"LobbyInviteRedeemedEvent.go",
|
||||
"LobbyMembershipApprovedEvent.go",
|
||||
"LobbyMembershipBlockedEvent.go",
|
||||
"LobbyMembershipRejectedEvent.go",
|
||||
"LobbyRaceNameRegisteredEvent.go",
|
||||
"LobbyRaceNameRegistrationEligibleEvent.go",
|
||||
}
|
||||
|
||||
var expectedPushPayloadDocumentationSnippets = []string{
|
||||
"Only the seven user-facing push notification types above are represented in `notification.fbs`.",
|
||||
"`geo.review_recommended`, `game.generation_failed`, `lobby.runtime_paused_after_start`, and `lobby.invite.expired` remain outside this schema because they are email-only in v1.",
|
||||
"Only the ten user-facing push notification types above are represented in `notification.fbs`.",
|
||||
"`geo.review_recommended`, `game.generation_failed`, `lobby.runtime_paused_after_start`, `lobby.invite.expired`, and `lobby.race_name.registration_denied` remain outside this schema because they are email-only in v1.",
|
||||
"`notification_type` alone determines the concrete FlatBuffers table.",
|
||||
"No extra envelope or FlatBuffers `union` is added in v1.",
|
||||
"The push payload must stay lightweight and must not attempt to mirror full game, lobby, or profile state.",
|
||||
|
||||
Reference in New Issue
Block a user