Serve T5DE-hosted rooms; a second client instance's own data
CI / build (push) Successful in 36s

Hosted rooms (internal/hosted): room calls for them answered here, their IMQ queues served (a new
proxy.IMQRouter hook takes them out of subscriptions to IMVU), chat relayed through the hub's
WebSocket (hub.Client gains Send, OnRoom and OnConnect), the room list extended, room products
checked against the real owner, and anything else naming a hosted room kept from IMVU.

T5DE_INSTANCE gives a second client's proxy its own data folder and log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 03:36:17 -07:00
1 parent 455dae1013
commit d8332b21a8
12 files changed
+1492 -12

No files matched your search

+56 -1
View File
@@ -176,6 +176,10 @@ type Client struct {
OnChat func(chat string, avatars []Avatar)
// OnLibrary, if set, is called when connected and whenever the person's library changes.
OnLibrary func()
// OnConnect, if set, is called each time the connection to the hub is made (to send state again).
OnConnect func()
// OnRoom, if set, gets the hub's messages about hosted rooms ("room.*"), as sent.
OnRoom func(data []byte)
mu sync.Mutex
saved *Saved
@@ -183,11 +187,13 @@ type Client struct {
outfit []Item
chats map[string]string // chat ID -> room instance ("" for other chats)
wake chan struct{}
outbox chan []byte // messages sent as they come (Send), not state
}
// New makes a client for the sign-in saved at path.
func New(path string, saved *Saved, logger *log.Logger) *Client {
return &Client{path: path, saved: saved, log: logger, chats: map[string]string{}, wake: make(chan struct{}, 1)}
return &Client{path: path, saved: saved, log: logger, chats: map[string]string{}, wake: make(chan struct{}, 1),
outbox: make(chan []byte, 256)}
}
// Join reports the client is in an IMVU chat (room: the room instance, for a room's chat).
@@ -241,6 +247,22 @@ func (c *Client) Outfit(imvuID int64, items []Item) {
c.poke()
}
// Send sends a message to the hub once (hosted rooms' messages); false if it can't be queued. Messages queued while
// disconnected are dropped on connecting (OnConnect sends what's still true).
func (c *Client) Send(v any) bool {
b, err := json.Marshal(v)
if err != nil {
return false
}
select {
case c.outbox <- b:
return true
default:
c.log.Printf("hub: too many messages waiting; dropped one")
return false
}
}
func (c *Client) poke() {
select {
case c.wake <- struct{}{}:
@@ -289,6 +311,13 @@ func (c *Client) connect(ctx context.Context) error {
if c.OnLibrary != nil {
go c.OnLibrary()
}
for drained := false; !drained; {
select {
case <-c.outbox:
default:
drained = true
}
}
errs := make(chan error, 1)
go func() {
@@ -302,6 +331,17 @@ func (c *Client) connect(ctx context.Context) error {
errs <- err
return
}
var kind struct{ Type string }
if json.Unmarshal(data, &kind) != nil {
continue
}
if strings.HasPrefix(kind.Type, "room.") {
// Hosted rooms' messages, whose fields differ (chat IDs are numbers).
if c.OnRoom != nil {
c.OnRoom(data)
}
continue
}
var m struct {
Type, Message, Chat string
Avatars []Avatar
@@ -349,6 +389,7 @@ func (c *Client) connect(ctx context.Context) error {
}()
sentID, sentOutfit := int64(-1), ""
sentChats := map[string]string{}
connected := false
for {
c.mu.Lock()
id, outfit := c.imvuID, append([]Item{}, c.outfit...) // a list, never null
@@ -396,12 +437,26 @@ func (c *Client) connect(ctx context.Context) error {
delete(sentChats, chat)
}
}
if !connected {
// After the session (so the hub knows the IMVU account), say what's still true.
connected = true
if c.OnConnect != nil {
go c.OnConnect()
}
}
select {
case <-ctx.Done():
conn.Close(websocket.StatusNormalClosure, "")
return ctx.Err()
case err := <-errs:
return err
case b := <-c.outbox:
wctx, cancel := context.WithTimeout(ctx, 10*time.Second)
err := conn.Write(wctx, websocket.MessageText, b)
cancel()
if err != nil {
return err
}
case <-c.wake:
}
}