222 lines
7.3 KiB
Go
222 lines
7.3 KiB
Go
// Package publishpush encodes user-facing notification routes into Gateway
|
|
// client-event payloads.
|
|
package publishpush
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"galaxy/notification/internal/api/intentstream"
|
|
"galaxy/notification/internal/service/acceptintent"
|
|
"galaxy/transcoder"
|
|
)
|
|
|
|
// Event stores one Gateway-compatible client event produced from a
|
|
// user-targeted notification route.
|
|
type Event struct {
|
|
// UserID stores the authenticated user fan-out target.
|
|
UserID string
|
|
|
|
// EventType stores the stable client-facing event type.
|
|
EventType string
|
|
|
|
// EventID stores the stable route-level event identifier.
|
|
EventID string
|
|
|
|
// PayloadBytes stores the encoded FlatBuffers payload bytes.
|
|
PayloadBytes []byte
|
|
|
|
// RequestID stores the optional correlation identifier.
|
|
RequestID string
|
|
|
|
// TraceID stores the optional tracing correlation identifier.
|
|
TraceID string
|
|
}
|
|
|
|
// Encoder maps one supported notification_type to the corresponding checked-in
|
|
// FlatBuffers payload encoder.
|
|
type Encoder struct{}
|
|
|
|
// Encode converts one accepted notification record plus its push route into a
|
|
// Gateway-compatible client event.
|
|
func (Encoder) Encode(notification acceptintent.NotificationRecord, route acceptintent.NotificationRoute) (Event, error) {
|
|
if err := notification.Validate(); err != nil {
|
|
return Event{}, fmt.Errorf("encode push event: %w", err)
|
|
}
|
|
if err := route.Validate(); err != nil {
|
|
return Event{}, fmt.Errorf("encode push event: %w", err)
|
|
}
|
|
if route.Channel != intentstream.ChannelPush {
|
|
return Event{}, fmt.Errorf("encode push event: route channel %q is unsupported", route.Channel)
|
|
}
|
|
|
|
userID, err := userIDFromRecipientRef(route.RecipientRef)
|
|
if err != nil {
|
|
return Event{}, fmt.Errorf("encode push event: %w", err)
|
|
}
|
|
|
|
payloadBytes, err := encodePayload(notification.NotificationType, notification.PayloadJSON)
|
|
if err != nil {
|
|
return Event{}, fmt.Errorf("encode push event: %w", err)
|
|
}
|
|
|
|
return Event{
|
|
UserID: userID,
|
|
EventType: string(notification.NotificationType),
|
|
EventID: notification.NotificationID + "/" + route.RouteID,
|
|
PayloadBytes: payloadBytes,
|
|
RequestID: notification.RequestID,
|
|
TraceID: notification.TraceID,
|
|
}, nil
|
|
}
|
|
|
|
func encodePayload(notificationType intentstream.NotificationType, payloadJSON string) ([]byte, error) {
|
|
switch notificationType {
|
|
case intentstream.NotificationTypeGameTurnReady:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
TurnNumber int64 `json:"turn_number"`
|
|
}
|
|
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.TurnNumber < 1 {
|
|
return nil, errors.New("payload_encoding_failed: turn_number must be at least 1")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.GameTurnReadyEventToPayload(&transcoder.GameTurnReadyEvent{
|
|
GameID: payload.GameID,
|
|
TurnNumber: payload.TurnNumber,
|
|
}))
|
|
case intentstream.NotificationTypeGameFinished:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
FinalTurnNumber int64 `json:"final_turn_number"`
|
|
}
|
|
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.FinalTurnNumber < 1 {
|
|
return nil, errors.New("payload_encoding_failed: final_turn_number must be at least 1")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.GameFinishedEventToPayload(&transcoder.GameFinishedEvent{
|
|
GameID: payload.GameID,
|
|
FinalTurnNumber: payload.FinalTurnNumber,
|
|
}))
|
|
case intentstream.NotificationTypeLobbyApplicationSubmitted:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
ApplicantUserID string `json:"applicant_user_id"`
|
|
}
|
|
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.ApplicantUserID == "" {
|
|
return nil, errors.New("payload_encoding_failed: applicant_user_id is empty")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.LobbyApplicationSubmittedEventToPayload(&transcoder.LobbyApplicationSubmittedEvent{
|
|
GameID: payload.GameID,
|
|
ApplicantUserID: payload.ApplicantUserID,
|
|
}))
|
|
case intentstream.NotificationTypeLobbyMembershipApproved:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
}
|
|
if err := decodePayload(payloadJSON, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
if payload.GameID == "" {
|
|
return nil, errors.New("payload_encoding_failed: game_id is empty")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.LobbyMembershipApprovedEventToPayload(&transcoder.LobbyMembershipApprovedEvent{
|
|
GameID: payload.GameID,
|
|
}))
|
|
case intentstream.NotificationTypeLobbyMembershipRejected:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
}
|
|
if err := decodePayload(payloadJSON, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
if payload.GameID == "" {
|
|
return nil, errors.New("payload_encoding_failed: game_id is empty")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.LobbyMembershipRejectedEventToPayload(&transcoder.LobbyMembershipRejectedEvent{
|
|
GameID: payload.GameID,
|
|
}))
|
|
case intentstream.NotificationTypeLobbyInviteCreated:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
InviterUserID string `json:"inviter_user_id"`
|
|
}
|
|
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.InviterUserID == "" {
|
|
return nil, errors.New("payload_encoding_failed: inviter_user_id is empty")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.LobbyInviteCreatedEventToPayload(&transcoder.LobbyInviteCreatedEvent{
|
|
GameID: payload.GameID,
|
|
InviterUserID: payload.InviterUserID,
|
|
}))
|
|
case intentstream.NotificationTypeLobbyInviteRedeemed:
|
|
var payload struct {
|
|
GameID string `json:"game_id"`
|
|
InviteeUserID string `json:"invitee_user_id"`
|
|
}
|
|
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.InviteeUserID == "" {
|
|
return nil, errors.New("payload_encoding_failed: invitee_user_id is empty")
|
|
}
|
|
return wrapPayloadEncoding(transcoder.LobbyInviteRedeemedEventToPayload(&transcoder.LobbyInviteRedeemedEvent{
|
|
GameID: payload.GameID,
|
|
InviteeUserID: payload.InviteeUserID,
|
|
}))
|
|
default:
|
|
return nil, fmt.Errorf("payload_encoding_failed: notification type %q does not support push", notificationType)
|
|
}
|
|
}
|
|
|
|
func decodePayload(payloadJSON string, target any) error {
|
|
if err := json.Unmarshal([]byte(payloadJSON), target); err != nil {
|
|
return fmt.Errorf("payload_encoding_failed: decode payload_json: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func wrapPayloadEncoding(payload []byte, err error) ([]byte, error) {
|
|
if err != nil {
|
|
return nil, fmt.Errorf("payload_encoding_failed: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
func userIDFromRecipientRef(recipientRef string) (string, error) {
|
|
userID, ok := strings.CutPrefix(recipientRef, "user:")
|
|
if !ok || userID == "" {
|
|
return "", fmt.Errorf("recipient_ref %q is not user-targeted", recipientRef)
|
|
}
|
|
|
|
return userID, nil
|
|
}
|