diff --git a/mobile/unibus.go b/mobile/unibus.go new file mode 100644 index 0000000..d2a6a9c --- /dev/null +++ b/mobile/unibus.go @@ -0,0 +1,108 @@ +// Package mobile exposes a flat, gomobile-friendly API over the unibus client +// so an Android app can join rooms, publish, and receive messages with the same +// end-to-end encryption as any native Go peer. +// +// gomobile only supports a limited set of types across the binding boundary +// (string, []byte, int, bool, error, named structs, and interfaces). This layer +// translates the richer client API into those primitives and delivers incoming +// frames through a Java/Kotlin-implemented FrameListener callback. No protocol +// or cryptography is reimplemented here: every call delegates to pkg/client, +// which is the single source of truth shared with every other peer on the bus. +package mobile + +import ( + "time" + + "github.com/enmanuel/unibus/pkg/client" + "github.com/enmanuel/unibus/pkg/frame" + "github.com/enmanuel/unibus/pkg/room" +) + +// FrameListener receives decrypted messages for a subscribed room. The Android +// side implements this interface. Its methods are invoked from a NATS delivery +// goroutine, so implementations must hop back to the UI thread (for example via +// a coroutine on the main dispatcher) before touching Android views. +type FrameListener interface { + OnFrame(roomID string, sender string, msgID string, text string) +} + +// Session is a connected unibus peer. Create it with NewSession and close it +// with Close when the app stops. +type Session struct { + c *client.Client +} + +// GenerateIdentity creates (or loads) the long-term keypair stored at path. +// Call it once on first launch. The resulting file holds the peer's private +// Ed25519 and X25519 keys and must be kept private to the app sandbox. +func GenerateIdentity(path string) error { + _, err := client.LoadOrCreateIdentity(path) + return err +} + +// NewSession loads the identity at idPath and connects to the bus. natsURL is +// the data plane (for example nats://host:4250) and ctrlURL is the control +// plane HTTP endpoint (for example http://host:8470). +func NewSession(idPath, natsURL, ctrlURL string) (*Session, error) { + id, err := client.LoadOrCreateIdentity(idPath) + if err != nil { + return nil, err + } + c, err := client.New(natsURL, ctrlURL, id) + if err != nil { + return nil, err + } + return &Session{c: c}, nil +} + +// EndpointID returns this peer's stable endpoint identifier, derived from its +// signing public key. It is the value that appears as the sender of frames. +func (s *Session) EndpointID() string { + return s.c.Endpoint().ID +} + +// CreateRoom opens a room on the given subject. mode is "matrix" for the +// encrypted, persisted and signed policy, or "nats" for plain cleartext. It +// returns the room id used by Join, Publish and Subscribe. +func (s *Session) CreateRoom(subject, mode string) (string, error) { + p := room.ModeNATS + if mode == "matrix" { + p = room.ModeMatrix + } + return s.c.CreateRoom(subject, p) +} + +// Join fetches the room key when the room is encrypted and prepares the session +// to publish to and receive from the room. +func (s *Session) Join(roomID string) error { + return s.c.Join(roomID) +} + +// Publish sends a UTF-8 text message to the room. +func (s *Session) Publish(roomID, text string) error { + return s.c.Publish(roomID, []byte(text)) +} + +// Subscribe streams decrypted messages of the room to the listener until the +// session is closed. +func (s *Session) Subscribe(roomID string, l FrameListener) error { + _, err := s.c.Subscribe(roomID, func(f frame.Frame, plaintext []byte) { + l.OnFrame(roomID, f.Sender, f.MsgID, string(plaintext)) + }) + return err +} + +// Request performs an RPC request/reply against subject and returns the reply +// payload as text. timeoutMs bounds the wait in milliseconds. +func (s *Session) Request(subject, text string, timeoutMs int) (string, error) { + out, err := s.c.Request(subject, []byte(text), time.Duration(timeoutMs)*time.Millisecond) + if err != nil { + return "", err + } + return string(out), nil +} + +// Close disconnects the peer from the bus. +func (s *Session) Close() error { + return s.c.Close() +}