248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
package notification
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"galaxy/backend/push"
|
|
"galaxy/transcoder"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// preMarshaledEvent adapts a pre-encoded FlatBuffers payload to the
|
|
// push.Event interface. The factory below pre-encodes the payload at
|
|
// construction time so the kind-specific build error surfaces inside
|
|
// the dispatcher (where it can drive retry / dead-letter logic) rather
|
|
// than inside push.Service.PublishClientEvent.
|
|
type preMarshaledEvent struct {
|
|
kind string
|
|
payload []byte
|
|
}
|
|
|
|
func (e preMarshaledEvent) Kind() string { return e.kind }
|
|
func (e preMarshaledEvent) Marshal() ([]byte, error) { return e.payload, nil }
|
|
|
|
// buildClientPushEvent maps a catalog kind together with the producer
|
|
// payload map onto a typed push.Event. Every catalog kind has a
|
|
// FlatBuffers schema in `pkg/schema/fbs/notification.fbs`; an unknown
|
|
// kind falls back to push.JSONEvent so a misconfigured producer keeps
|
|
// the pipeline flowing while the catalog catches up.
|
|
func buildClientPushEvent(kind string, payload map[string]any) (push.Event, error) {
|
|
switch kind {
|
|
case KindLobbyInviteReceived:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inviter, err := mapUUID(payload, "inviter_user_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyInviteReceivedEventToPayload(&transcoder.LobbyInviteReceivedEvent{
|
|
GameID: gameID,
|
|
InviterUserID: inviter,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyInviteRevoked:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyInviteRevokedEventToPayload(&transcoder.LobbyInviteRevokedEvent{GameID: gameID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyApplicationSubmitted:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appID, err := mapUUID(payload, "application_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyApplicationSubmittedEventToPayload(&transcoder.LobbyApplicationSubmittedEvent{
|
|
GameID: gameID,
|
|
ApplicationID: appID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyApplicationApproved:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyApplicationApprovedEventToPayload(&transcoder.LobbyApplicationApprovedEvent{GameID: gameID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyApplicationRejected:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyApplicationRejectedEventToPayload(&transcoder.LobbyApplicationRejectedEvent{GameID: gameID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyMembershipRemoved:
|
|
bytes, err := transcoder.LobbyMembershipRemovedEventToPayload(&transcoder.LobbyMembershipRemovedEvent{
|
|
Reason: mapStringOpt(payload, "reason"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyMembershipBlocked:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyMembershipBlockedEventToPayload(&transcoder.LobbyMembershipBlockedEvent{
|
|
GameID: gameID,
|
|
Reason: mapStringOpt(payload, "reason"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyRaceNameRegistered:
|
|
raceName, err := mapString(payload, "race_name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyRaceNameRegisteredEventToPayload(&transcoder.LobbyRaceNameRegisteredEvent{RaceName: raceName})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyRaceNamePending:
|
|
raceName, err := mapString(payload, "race_name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyRaceNamePendingEventToPayload(&transcoder.LobbyRaceNamePendingEvent{
|
|
RaceName: raceName,
|
|
ExpiresAt: mapStringOpt(payload, "expires_at"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindLobbyRaceNameExpired:
|
|
raceName, err := mapString(payload, "race_name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.LobbyRaceNameExpiredEventToPayload(&transcoder.LobbyRaceNameExpiredEvent{RaceName: raceName})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindRuntimeImagePullFailed:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.RuntimeImagePullFailedEventToPayload(&transcoder.RuntimeImagePullFailedEvent{
|
|
GameID: gameID,
|
|
ImageRef: mapStringOpt(payload, "image_ref"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindRuntimeContainerStartFailed:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.RuntimeContainerStartFailedEventToPayload(&transcoder.RuntimeContainerStartFailedEvent{GameID: gameID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
|
|
case KindRuntimeStartConfigInvalid:
|
|
gameID, err := mapUUID(payload, "game_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := transcoder.RuntimeStartConfigInvalidEventToPayload(&transcoder.RuntimeStartConfigInvalidEvent{
|
|
GameID: gameID,
|
|
Reason: mapStringOpt(payload, "reason"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
|
}
|
|
|
|
return push.JSONEvent{EventKind: kind, Payload: payload}, nil
|
|
}
|
|
|
|
// mapUUID extracts a required UUID-shaped field from the producer
|
|
// payload. Producers stringify uuid values before assembling Intent
|
|
// payloads, so the JSON-roundtripped form is `string`.
|
|
func mapUUID(payload map[string]any, key string) (uuid.UUID, error) {
|
|
raw, ok := payload[key]
|
|
if !ok {
|
|
return uuid.Nil, fmt.Errorf("notification payload: %s is missing", key)
|
|
}
|
|
str, ok := raw.(string)
|
|
if !ok {
|
|
return uuid.Nil, fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
|
|
}
|
|
parsed, err := uuid.Parse(str)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("notification payload: %s is not a uuid: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
// mapString extracts a required string field from the producer payload.
|
|
func mapString(payload map[string]any, key string) (string, error) {
|
|
raw, ok := payload[key]
|
|
if !ok {
|
|
return "", fmt.Errorf("notification payload: %s is missing", key)
|
|
}
|
|
str, ok := raw.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
|
|
}
|
|
if str == "" {
|
|
return "", fmt.Errorf("notification payload: %s is empty", key)
|
|
}
|
|
return str, nil
|
|
}
|
|
|
|
// mapStringOpt returns the string value for key, or "" when the key is
|
|
// missing or carries a non-string value.
|
|
func mapStringOpt(payload map[string]any, key string) string {
|
|
raw, ok := payload[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
str, _ := raw.(string)
|
|
return str
|
|
}
|