package transcoder import ( "errors" "fmt" "time" usermodel "galaxy/model/user" userfbs "galaxy/schema/fbs/user" flatbuffers "github.com/google/flatbuffers/go" ) // GetMyAccountRequestToPayload converts usermodel.GetMyAccountRequest to // FlatBuffers bytes suitable for the authenticated gateway transport. func GetMyAccountRequestToPayload(request *usermodel.GetMyAccountRequest) ([]byte, error) { if request == nil { return nil, errors.New("encode get my account request payload: request is nil") } builder := flatbuffers.NewBuilder(32) userfbs.GetMyAccountRequestStart(builder) offset := userfbs.GetMyAccountRequestEnd(builder) userfbs.FinishGetMyAccountRequestBuffer(builder, offset) return builder.FinishedBytes(), nil } // PayloadToGetMyAccountRequest converts FlatBuffers payload bytes into // usermodel.GetMyAccountRequest. func PayloadToGetMyAccountRequest(data []byte) (result *usermodel.GetMyAccountRequest, err error) { if len(data) == 0 { return nil, errors.New("decode get my account request payload: data is empty") } defer recoverUserDecodePanic("decode get my account request payload", &result, &err) _ = userfbs.GetRootAsGetMyAccountRequest(data, 0) return &usermodel.GetMyAccountRequest{}, nil } // UpdateMyProfileRequestToPayload converts usermodel.UpdateMyProfileRequest to // FlatBuffers bytes suitable for the authenticated gateway transport. func UpdateMyProfileRequestToPayload(request *usermodel.UpdateMyProfileRequest) ([]byte, error) { if request == nil { return nil, errors.New("encode update my profile request payload: request is nil") } builder := flatbuffers.NewBuilder(128) displayName := builder.CreateString(request.DisplayName) userfbs.UpdateMyProfileRequestStart(builder) userfbs.UpdateMyProfileRequestAddDisplayName(builder, displayName) offset := userfbs.UpdateMyProfileRequestEnd(builder) userfbs.FinishUpdateMyProfileRequestBuffer(builder, offset) return builder.FinishedBytes(), nil } // PayloadToUpdateMyProfileRequest converts FlatBuffers payload bytes into // usermodel.UpdateMyProfileRequest. func PayloadToUpdateMyProfileRequest(data []byte) (result *usermodel.UpdateMyProfileRequest, err error) { if len(data) == 0 { return nil, errors.New("decode update my profile request payload: data is empty") } defer recoverUserDecodePanic("decode update my profile request payload", &result, &err) request := userfbs.GetRootAsUpdateMyProfileRequest(data, 0) return &usermodel.UpdateMyProfileRequest{ DisplayName: string(request.DisplayName()), }, nil } // UpdateMySettingsRequestToPayload converts // usermodel.UpdateMySettingsRequest to FlatBuffers bytes suitable for the // authenticated gateway transport. func UpdateMySettingsRequestToPayload(request *usermodel.UpdateMySettingsRequest) ([]byte, error) { if request == nil { return nil, errors.New("encode update my settings request payload: request is nil") } builder := flatbuffers.NewBuilder(128) preferredLanguage := builder.CreateString(request.PreferredLanguage) timeZone := builder.CreateString(request.TimeZone) userfbs.UpdateMySettingsRequestStart(builder) userfbs.UpdateMySettingsRequestAddPreferredLanguage(builder, preferredLanguage) userfbs.UpdateMySettingsRequestAddTimeZone(builder, timeZone) offset := userfbs.UpdateMySettingsRequestEnd(builder) userfbs.FinishUpdateMySettingsRequestBuffer(builder, offset) return builder.FinishedBytes(), nil } // PayloadToUpdateMySettingsRequest converts FlatBuffers payload bytes into // usermodel.UpdateMySettingsRequest. func PayloadToUpdateMySettingsRequest(data []byte) (result *usermodel.UpdateMySettingsRequest, err error) { if len(data) == 0 { return nil, errors.New("decode update my settings request payload: data is empty") } defer recoverUserDecodePanic("decode update my settings request payload", &result, &err) request := userfbs.GetRootAsUpdateMySettingsRequest(data, 0) return &usermodel.UpdateMySettingsRequest{ PreferredLanguage: string(request.PreferredLanguage()), TimeZone: string(request.TimeZone()), }, nil } // AccountResponseToPayload converts usermodel.AccountResponse to FlatBuffers // bytes suitable for the authenticated gateway transport. func AccountResponseToPayload(response *usermodel.AccountResponse) ([]byte, error) { if response == nil { return nil, errors.New("encode account response payload: response is nil") } builder := flatbuffers.NewBuilder(512) accountOffset, err := encodeAccount(builder, response.Account) if err != nil { return nil, fmt.Errorf("encode account response payload: %w", err) } userfbs.AccountResponseStart(builder) userfbs.AccountResponseAddAccount(builder, accountOffset) offset := userfbs.AccountResponseEnd(builder) userfbs.FinishAccountResponseBuffer(builder, offset) return builder.FinishedBytes(), nil } // PayloadToAccountResponse converts FlatBuffers payload bytes into // usermodel.AccountResponse. func PayloadToAccountResponse(data []byte) (result *usermodel.AccountResponse, err error) { if len(data) == 0 { return nil, errors.New("decode account response payload: data is empty") } defer recoverUserDecodePanic("decode account response payload", &result, &err) response := userfbs.GetRootAsAccountResponse(data, 0) account := response.Account(nil) if account == nil { return nil, errors.New("decode account response payload: account is missing") } decodedAccount, err := decodeAccount(account) if err != nil { return nil, fmt.Errorf("decode account response payload: %w", err) } return &usermodel.AccountResponse{Account: decodedAccount}, nil } // ErrorResponseToPayload converts usermodel.ErrorResponse to FlatBuffers bytes // suitable for the authenticated gateway transport. func ErrorResponseToPayload(response *usermodel.ErrorResponse) ([]byte, error) { if response == nil { return nil, errors.New("encode error response payload: response is nil") } builder := flatbuffers.NewBuilder(128) errorOffset := encodeErrorBody(builder, response.Error) userfbs.ErrorResponseStart(builder) userfbs.ErrorResponseAddError(builder, errorOffset) offset := userfbs.ErrorResponseEnd(builder) userfbs.FinishErrorResponseBuffer(builder, offset) return builder.FinishedBytes(), nil } // PayloadToErrorResponse converts FlatBuffers payload bytes into // usermodel.ErrorResponse. func PayloadToErrorResponse(data []byte) (result *usermodel.ErrorResponse, err error) { if len(data) == 0 { return nil, errors.New("decode error response payload: data is empty") } defer recoverUserDecodePanic("decode error response payload", &result, &err) response := userfbs.GetRootAsErrorResponse(data, 0) errorBody := response.Error(nil) if errorBody == nil { return nil, errors.New("decode error response payload: error is missing") } return &usermodel.ErrorResponse{ Error: usermodel.ErrorBody{ Code: string(errorBody.Code()), Message: string(errorBody.Message()), }, }, nil } func encodeAccount(builder *flatbuffers.Builder, account usermodel.Account) (flatbuffers.UOffsetT, error) { entitlementOffset, err := encodeEntitlementSnapshot(builder, account.Entitlement) if err != nil { return 0, fmt.Errorf("encode account: %w", err) } activeSanctionOffsets := make([]flatbuffers.UOffsetT, len(account.ActiveSanctions)) for index := range account.ActiveSanctions { activeSanctionOffsets[index], err = encodeActiveSanction(builder, account.ActiveSanctions[index]) if err != nil { return 0, fmt.Errorf("encode account active sanction %d: %w", index, err) } } var activeSanctionsVector flatbuffers.UOffsetT if len(activeSanctionOffsets) > 0 { userfbs.AccountViewStartActiveSanctionsVector(builder, len(activeSanctionOffsets)) for index := len(activeSanctionOffsets) - 1; index >= 0; index-- { builder.PrependUOffsetT(activeSanctionOffsets[index]) } activeSanctionsVector = builder.EndVector(len(activeSanctionOffsets)) } activeLimitOffsets := make([]flatbuffers.UOffsetT, len(account.ActiveLimits)) for index := range account.ActiveLimits { activeLimitOffsets[index], err = encodeActiveLimit(builder, account.ActiveLimits[index]) if err != nil { return 0, fmt.Errorf("encode account active limit %d: %w", index, err) } } var activeLimitsVector flatbuffers.UOffsetT if len(activeLimitOffsets) > 0 { userfbs.AccountViewStartActiveLimitsVector(builder, len(activeLimitOffsets)) for index := len(activeLimitOffsets) - 1; index >= 0; index-- { builder.PrependUOffsetT(activeLimitOffsets[index]) } activeLimitsVector = builder.EndVector(len(activeLimitOffsets)) } userID := builder.CreateString(account.UserID) email := builder.CreateString(account.Email) userName := builder.CreateString(account.UserName) var displayName flatbuffers.UOffsetT if account.DisplayName != "" { displayName = builder.CreateString(account.DisplayName) } preferredLanguage := builder.CreateString(account.PreferredLanguage) timeZone := builder.CreateString(account.TimeZone) var declaredCountry flatbuffers.UOffsetT if account.DeclaredCountry != "" { declaredCountry = builder.CreateString(account.DeclaredCountry) } userfbs.AccountViewStart(builder) userfbs.AccountViewAddUserId(builder, userID) userfbs.AccountViewAddEmail(builder, email) userfbs.AccountViewAddUserName(builder, userName) if displayName != 0 { userfbs.AccountViewAddDisplayName(builder, displayName) } userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage) userfbs.AccountViewAddTimeZone(builder, timeZone) if declaredCountry != 0 { userfbs.AccountViewAddDeclaredCountry(builder, declaredCountry) } userfbs.AccountViewAddEntitlement(builder, entitlementOffset) if activeSanctionsVector != 0 { userfbs.AccountViewAddActiveSanctions(builder, activeSanctionsVector) } if activeLimitsVector != 0 { userfbs.AccountViewAddActiveLimits(builder, activeLimitsVector) } userfbs.AccountViewAddCreatedAtMs(builder, account.CreatedAt.UTC().UnixMilli()) userfbs.AccountViewAddUpdatedAtMs(builder, account.UpdatedAt.UTC().UnixMilli()) return userfbs.AccountViewEnd(builder), nil } func decodeAccount(account *userfbs.AccountView) (usermodel.Account, error) { entitlement := account.Entitlement(nil) if entitlement == nil { return usermodel.Account{}, errors.New("account entitlement is missing") } decodedEntitlement, err := decodeEntitlementSnapshot(entitlement) if err != nil { return usermodel.Account{}, fmt.Errorf("decode account entitlement: %w", err) } createdAt := time.UnixMilli(account.CreatedAtMs()).UTC() updatedAt := time.UnixMilli(account.UpdatedAtMs()).UTC() result := usermodel.Account{ UserID: string(account.UserId()), Email: string(account.Email()), UserName: string(account.UserName()), DisplayName: string(account.DisplayName()), PreferredLanguage: string(account.PreferredLanguage()), TimeZone: string(account.TimeZone()), DeclaredCountry: string(account.DeclaredCountry()), Entitlement: decodedEntitlement, ActiveSanctions: make([]usermodel.ActiveSanction, 0, account.ActiveSanctionsLength()), ActiveLimits: make([]usermodel.ActiveLimit, 0, account.ActiveLimitsLength()), CreatedAt: createdAt, UpdatedAt: updatedAt, } activeSanction := new(userfbs.ActiveSanction) for index := 0; index < account.ActiveSanctionsLength(); index++ { if !account.ActiveSanctions(activeSanction, index) { return usermodel.Account{}, fmt.Errorf("account active sanction %d is missing", index) } decodedSanction, err := decodeActiveSanction(activeSanction) if err != nil { return usermodel.Account{}, fmt.Errorf("decode account active sanction %d: %w", index, err) } result.ActiveSanctions = append(result.ActiveSanctions, decodedSanction) } activeLimit := new(userfbs.ActiveLimit) for index := 0; index < account.ActiveLimitsLength(); index++ { if !account.ActiveLimits(activeLimit, index) { return usermodel.Account{}, fmt.Errorf("account active limit %d is missing", index) } decodedLimit, err := decodeActiveLimit(activeLimit) if err != nil { return usermodel.Account{}, fmt.Errorf("decode account active limit %d: %w", index, err) } result.ActiveLimits = append(result.ActiveLimits, decodedLimit) } return result, nil } func encodeEntitlementSnapshot(builder *flatbuffers.Builder, snapshot usermodel.EntitlementSnapshot) (flatbuffers.UOffsetT, error) { actorOffset := encodeActorRef(builder, snapshot.Actor) planCode := builder.CreateString(snapshot.PlanCode) source := builder.CreateString(snapshot.Source) reasonCode := builder.CreateString(snapshot.ReasonCode) userfbs.EntitlementSnapshotStart(builder) userfbs.EntitlementSnapshotAddPlanCode(builder, planCode) userfbs.EntitlementSnapshotAddIsPaid(builder, snapshot.IsPaid) userfbs.EntitlementSnapshotAddSource(builder, source) userfbs.EntitlementSnapshotAddActor(builder, actorOffset) userfbs.EntitlementSnapshotAddReasonCode(builder, reasonCode) userfbs.EntitlementSnapshotAddStartsAtMs(builder, snapshot.StartsAt.UTC().UnixMilli()) if snapshot.EndsAt != nil { userfbs.EntitlementSnapshotAddEndsAtMs(builder, snapshot.EndsAt.UTC().UnixMilli()) } userfbs.EntitlementSnapshotAddUpdatedAtMs(builder, snapshot.UpdatedAt.UTC().UnixMilli()) return userfbs.EntitlementSnapshotEnd(builder), nil } func decodeEntitlementSnapshot(snapshot *userfbs.EntitlementSnapshot) (usermodel.EntitlementSnapshot, error) { actor := snapshot.Actor(nil) if actor == nil { return usermodel.EntitlementSnapshot{}, errors.New("entitlement actor is missing") } decodedActor, err := decodeActorRef(actor) if err != nil { return usermodel.EntitlementSnapshot{}, fmt.Errorf("decode entitlement actor: %w", err) } return usermodel.EntitlementSnapshot{ PlanCode: string(snapshot.PlanCode()), IsPaid: snapshot.IsPaid(), Source: string(snapshot.Source()), Actor: decodedActor, ReasonCode: string(snapshot.ReasonCode()), StartsAt: time.UnixMilli(snapshot.StartsAtMs()).UTC(), EndsAt: optionalUnixMilli(snapshot.EndsAtMs()), UpdatedAt: time.UnixMilli(snapshot.UpdatedAtMs()).UTC(), }, nil } func encodeActiveSanction(builder *flatbuffers.Builder, sanction usermodel.ActiveSanction) (flatbuffers.UOffsetT, error) { actorOffset := encodeActorRef(builder, sanction.Actor) sanctionCode := builder.CreateString(sanction.SanctionCode) scope := builder.CreateString(sanction.Scope) reasonCode := builder.CreateString(sanction.ReasonCode) userfbs.ActiveSanctionStart(builder) userfbs.ActiveSanctionAddSanctionCode(builder, sanctionCode) userfbs.ActiveSanctionAddScope(builder, scope) userfbs.ActiveSanctionAddReasonCode(builder, reasonCode) userfbs.ActiveSanctionAddActor(builder, actorOffset) userfbs.ActiveSanctionAddAppliedAtMs(builder, sanction.AppliedAt.UTC().UnixMilli()) if sanction.ExpiresAt != nil { userfbs.ActiveSanctionAddExpiresAtMs(builder, sanction.ExpiresAt.UTC().UnixMilli()) } return userfbs.ActiveSanctionEnd(builder), nil } func decodeActiveSanction(sanction *userfbs.ActiveSanction) (usermodel.ActiveSanction, error) { actor := sanction.Actor(nil) if actor == nil { return usermodel.ActiveSanction{}, errors.New("sanction actor is missing") } decodedActor, err := decodeActorRef(actor) if err != nil { return usermodel.ActiveSanction{}, fmt.Errorf("decode sanction actor: %w", err) } return usermodel.ActiveSanction{ SanctionCode: string(sanction.SanctionCode()), Scope: string(sanction.Scope()), ReasonCode: string(sanction.ReasonCode()), Actor: decodedActor, AppliedAt: time.UnixMilli(sanction.AppliedAtMs()).UTC(), ExpiresAt: optionalUnixMilli(sanction.ExpiresAtMs()), }, nil } func encodeActiveLimit(builder *flatbuffers.Builder, limit usermodel.ActiveLimit) (flatbuffers.UOffsetT, error) { actorOffset := encodeActorRef(builder, limit.Actor) limitCode := builder.CreateString(limit.LimitCode) reasonCode := builder.CreateString(limit.ReasonCode) userfbs.ActiveLimitStart(builder) userfbs.ActiveLimitAddLimitCode(builder, limitCode) userfbs.ActiveLimitAddValue(builder, int64(limit.Value)) userfbs.ActiveLimitAddReasonCode(builder, reasonCode) userfbs.ActiveLimitAddActor(builder, actorOffset) userfbs.ActiveLimitAddAppliedAtMs(builder, limit.AppliedAt.UTC().UnixMilli()) if limit.ExpiresAt != nil { userfbs.ActiveLimitAddExpiresAtMs(builder, limit.ExpiresAt.UTC().UnixMilli()) } return userfbs.ActiveLimitEnd(builder), nil } func decodeActiveLimit(limit *userfbs.ActiveLimit) (usermodel.ActiveLimit, error) { actor := limit.Actor(nil) if actor == nil { return usermodel.ActiveLimit{}, errors.New("limit actor is missing") } decodedActor, err := decodeActorRef(actor) if err != nil { return usermodel.ActiveLimit{}, fmt.Errorf("decode limit actor: %w", err) } value, err := int64ToInt(limit.Value(), "value") if err != nil { return usermodel.ActiveLimit{}, err } return usermodel.ActiveLimit{ LimitCode: string(limit.LimitCode()), Value: value, ReasonCode: string(limit.ReasonCode()), Actor: decodedActor, AppliedAt: time.UnixMilli(limit.AppliedAtMs()).UTC(), ExpiresAt: optionalUnixMilli(limit.ExpiresAtMs()), }, nil } func encodeActorRef(builder *flatbuffers.Builder, actor usermodel.ActorRef) flatbuffers.UOffsetT { actorType := builder.CreateString(actor.Type) var actorID flatbuffers.UOffsetT if actor.ID != "" { actorID = builder.CreateString(actor.ID) } userfbs.ActorRefStart(builder) userfbs.ActorRefAddType(builder, actorType) if actorID != 0 { userfbs.ActorRefAddId(builder, actorID) } return userfbs.ActorRefEnd(builder) } func decodeActorRef(actor *userfbs.ActorRef) (usermodel.ActorRef, error) { return usermodel.ActorRef{ Type: string(actor.Type()), ID: string(actor.Id()), }, nil } func encodeErrorBody(builder *flatbuffers.Builder, errorBody usermodel.ErrorBody) flatbuffers.UOffsetT { code := builder.CreateString(errorBody.Code) message := builder.CreateString(errorBody.Message) userfbs.ErrorBodyStart(builder) userfbs.ErrorBodyAddCode(builder, code) userfbs.ErrorBodyAddMessage(builder, message) return userfbs.ErrorBodyEnd(builder) } func optionalUnixMilli(value int64) *time.Time { if value == 0 { return nil } decoded := time.UnixMilli(value).UTC() return &decoded } func recoverUserDecodePanic[T any](message string, result **T, err *error) { if recovered := recover(); recovered != nil { *result = nil *err = fmt.Errorf("%s: panic recovered: %v", message, recovered) } }