feat(scum): add bounded UE4SS notification adapter
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// AuthorizedConfigPort is supplied by a version-bound Companion integration.
|
||||
@@ -19,10 +20,36 @@ type ConfigFieldPatch struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
const (
|
||||
UE4SSReferenceRevision = "bae91527355f14faa63c1df65f742cc48594ba1b"
|
||||
UE4SSReferenceBuild = "3.0.1"
|
||||
fixedNotificationType = 4
|
||||
)
|
||||
|
||||
// UE4SSNotificationPort is implemented only by a Companion-local,
|
||||
// platform-authorized transport for the pinned UE4SS build. It receives a
|
||||
// fixed typed notification, never a raw RCON command, credential, socket, or
|
||||
// host path. Its private audit text is not part of command result payloads.
|
||||
type UE4SSNotificationPort interface {
|
||||
SendPlayerNotification(context.Context, ue4SSPlayerNotification) (UE4SSNotificationReceipt, error)
|
||||
}
|
||||
type UE4SSNotificationReceipt struct{ Accepted bool }
|
||||
type ue4SSPlayerNotification struct {
|
||||
ServerID string
|
||||
RecipientSteamID string
|
||||
Message string
|
||||
chatType int
|
||||
protectedAuditCommand string
|
||||
}
|
||||
|
||||
type VersionedAdapter struct {
|
||||
ServerVersion string
|
||||
Config AuthorizedConfigPort
|
||||
DiagnosticsState map[string]string
|
||||
BoundServerID string
|
||||
ServerVersion string
|
||||
UE4SSBuild string
|
||||
UE4SSReferenceRevision string
|
||||
Config AuthorizedConfigPort
|
||||
Notification UE4SSNotificationPort
|
||||
DiagnosticsState map[string]string
|
||||
}
|
||||
|
||||
func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
|
||||
@@ -75,8 +102,62 @@ func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[str
|
||||
func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("reward adapter is unsupported")
|
||||
}
|
||||
func (VersionedAdapter) NotifyPlayer(context.Context, map[string]any) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("notification adapter is unsupported")
|
||||
func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
||||
if !adapter.supportsUE4SSNotification() || adapter.Notification == nil {
|
||||
return nil, fmt.Errorf("notification adapter is unsupported")
|
||||
}
|
||||
playerID, playerOK := payload["playerId"].(string)
|
||||
message, messageOK := payload["message"].(string)
|
||||
notification, err := newUE4SSPlayerNotification(adapter.BoundServerID, playerID, message)
|
||||
if !playerOK || !messageOK || err != nil {
|
||||
return nil, fmt.Errorf("notification payload is invalid")
|
||||
}
|
||||
receipt, err := adapter.Notification.SendPlayerNotification(ctx, notification)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("notification transport failed")
|
||||
}
|
||||
if !receipt.Accepted {
|
||||
return map[string]any{"accepted": false, "message": "notification was not accepted"}, nil
|
||||
}
|
||||
return map[string]any{"accepted": true, "message": "notification accepted for online recipient"}, nil
|
||||
}
|
||||
|
||||
func (adapter VersionedAdapter) supportsUE4SSNotification() bool {
|
||||
// The pinned source reflects SendChatLineToPlayer at runtime and fails
|
||||
// closed on a schema change, so no unverified SCUM-version mapping is
|
||||
// embedded here. The dispatcher still requires a discovered server version.
|
||||
return adapter.BoundServerID != "" && adapter.UE4SSBuild == UE4SSReferenceBuild && adapter.UE4SSReferenceRevision == UE4SSReferenceRevision
|
||||
}
|
||||
func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayerNotification, error) {
|
||||
if strings.TrimSpace(serverID) == "" || !steamID64(playerID) || !validNotificationMessage(message) {
|
||||
return ue4SSPlayerNotification{}, fmt.Errorf("invalid typed UE4SS notification")
|
||||
}
|
||||
return ue4SSPlayerNotification{ServerID: serverID, RecipientSteamID: playerID, Message: message, chatType: fixedNotificationType, protectedAuditCommand: "SendChat 4 \"" + escapeUE4SSChatMessage(message) + "\" " + playerID}, nil
|
||||
}
|
||||
func steamID64(value string) bool {
|
||||
if len(value) != 17 {
|
||||
return false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < '0' || character > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func validNotificationMessage(value string) bool {
|
||||
if value == "" || len(value) > 200 || !utf8.ValidString(value) {
|
||||
return false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < 0x20 || character == 0x7f {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func escapeUE4SSChatMessage(value string) string {
|
||||
return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value)
|
||||
}
|
||||
|
||||
func supportedAdapterVersion(version string) bool { return version == "0.9.700.90357" }
|
||||
|
||||
Reference in New Issue
Block a user