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:
1 parent
455dae1013
commit
d8332b21a8
12 files changed
+1492
-12
No files matched your search
@@ -359,6 +359,13 @@ func (p *Products) authorizeProducts2(x *proxy.Exchange, call *xmlrpc.Call) erro
|
||||
type auth struct {
|
||||
user, product int64
|
||||
}
|
||||
// In a room hosted by T5DE's hub, the client asks about the room's products as user 2100000000 (the first part
|
||||
// of the room's ID): IMVU is asked about the room owner's instead, and answers go back as 2100000000.
|
||||
owner := int64(0)
|
||||
if p.HostedOwner != nil {
|
||||
owner = p.HostedOwner()
|
||||
}
|
||||
hostedAsked := map[int64]bool{}
|
||||
var community []auth
|
||||
forward := []xmlrpc.Value{table[0]}
|
||||
asked := map[auth]bool{}
|
||||
@@ -372,6 +379,10 @@ func (p *Products) authorizeProducts2(x *proxy.Exchange, call *xmlrpc.Call) erro
|
||||
if !ok1 || !ok2 {
|
||||
return nil
|
||||
}
|
||||
if user == HostedOwnerID && owner > 0 {
|
||||
user, hostedAsked[product] = owner, true
|
||||
row = []xmlrpc.Value{user, product}
|
||||
}
|
||||
if !IsCommunity(product) {
|
||||
if !asked[auth{user, product}] {
|
||||
forward, asked[auth{user, product}] = append(forward, row), true
|
||||
@@ -386,9 +397,33 @@ func (p *Products) authorizeProducts2(x *proxy.Exchange, call *xmlrpc.Call) erro
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(community) == 0 {
|
||||
if len(community) == 0 && len(hostedAsked) == 0 {
|
||||
return nil
|
||||
}
|
||||
// hostedBack gives rows asked about as 2100000000 that user ID back.
|
||||
hostedBack := func(columns []xmlrpc.Value, rows []xmlrpc.Value) {
|
||||
userCol, productCol := -1, -1
|
||||
for i, c := range columns {
|
||||
switch c {
|
||||
case "userId":
|
||||
userCol = i
|
||||
case "productId":
|
||||
productCol = i
|
||||
}
|
||||
}
|
||||
if userCol < 0 || productCol < 0 {
|
||||
return
|
||||
}
|
||||
for _, row := range rows {
|
||||
r, ok := row.([]xmlrpc.Value)
|
||||
if !ok || len(r) <= userCol || len(r) <= productCol {
|
||||
continue
|
||||
}
|
||||
if product, _ := r[productCol].(int64); r[userCol] == owner && hostedAsked[product] {
|
||||
r[userCol] = int64(HostedOwnerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
ours := func(columns []xmlrpc.Value) []xmlrpc.Value {
|
||||
var rows []xmlrpc.Value
|
||||
for _, a := range community {
|
||||
@@ -421,6 +456,7 @@ func (p *Products) authorizeProducts2(x *proxy.Exchange, call *xmlrpc.Call) erro
|
||||
columns[i] = c
|
||||
}
|
||||
out := append([]xmlrpc.Value{columns}, ours(columns)...)
|
||||
hostedBack(columns, out[1:])
|
||||
x.Response = localResponse(200, "text/xml", (&xmlrpc.Response{Params: []xmlrpc.Value{out}}).Marshal())
|
||||
return nil
|
||||
}
|
||||
@@ -447,7 +483,9 @@ func (p *Products) authorizeProducts2(x *proxy.Exchange, call *xmlrpc.Call) erro
|
||||
if !ok {
|
||||
return body, errors.New("products: unexpected authorizeProducts2 columns")
|
||||
}
|
||||
r.Params[0] = append(rows, ours(columns)...)
|
||||
all := append(rows, ours(columns)...)
|
||||
hostedBack(columns, all[1:])
|
||||
r.Params[0] = all
|
||||
return r.Marshal(), nil
|
||||
}
|
||||
return nil
|
||||
@@ -523,6 +561,15 @@ func (p *Products) interceptBaseLook(x *proxy.Exchange) error {
|
||||
// Outgoing implements proxy.IMQFilter: community products are taken out of outfit chat commands (IMVU and other
|
||||
// people see the defaults), and whole outfits are remembered.
|
||||
func (p *Products) Outgoing(queue, mount string, message []byte) ([]byte, bool) {
|
||||
out, drop := p.outgoing(queue, mount, message)
|
||||
// A hosted room's message goes to the hub (without community products, as to IMVU), never to IMVU.
|
||||
if p.Hosted != nil && !drop && p.Hosted.Send(queue, mount, out) {
|
||||
return nil, true
|
||||
}
|
||||
return out, drop
|
||||
}
|
||||
|
||||
func (p *Products) outgoing(queue, mount string, message []byte) ([]byte, bool) {
|
||||
if !strings.HasPrefix(queue, "/chat/") || mount != "messages" {
|
||||
return message, false
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
"git.t5de.dev/t5de/t5de-api/cfl"
|
||||
"git.t5de.dev/t5de/t5de-api/pack"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/imq"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
|
||||
)
|
||||
|
||||
// Products is the proxy's community products: an Interceptor for HTTP and an IMQFilter for chat.
|
||||
@@ -67,6 +69,11 @@ type Products struct {
|
||||
// Library, if set, is the person's hub library as last read (for the inventory's Recently Added).
|
||||
Library func() []hub.LibraryEntry
|
||||
|
||||
// Hosted, if set, serves T5DE-hosted rooms' queues (the hosted package); HostedOwner is the IMVU account owning
|
||||
// the hosted room the client is in (0 if none), whose products the room's are.
|
||||
Hosted HostedRooms
|
||||
HostedOwner func() int64
|
||||
|
||||
roomMu sync.Mutex
|
||||
room room
|
||||
|
||||
@@ -99,6 +106,26 @@ func (p *Products) Ready(id string) {
|
||||
}
|
||||
}
|
||||
|
||||
// HostedOwnerID is the user ID the client gives hosted rooms' owner: the first part of their instance IDs
|
||||
// (hosted.Base).
|
||||
const HostedOwnerID = 2100000000
|
||||
|
||||
// HostedRooms serves T5DE-hosted rooms' IMQ queues: Route answers subscriptions to them, Send takes the client's
|
||||
// messages to them (reporting whether the queue was one), Closed forgets a connection.
|
||||
type HostedRooms interface {
|
||||
Route(c *proxy.IMQConn, unsubscribe bool, s imq.Subscription) imq.Subscription
|
||||
Send(queue, mount string, message []byte) bool
|
||||
Closed(c *proxy.IMQConn)
|
||||
}
|
||||
|
||||
// Route serves hosted rooms' queues (proxy.IMQRouter).
|
||||
func (p *Products) Route(c *proxy.IMQConn, unsubscribe bool, s imq.Subscription) imq.Subscription {
|
||||
if p.Hosted == nil {
|
||||
return s
|
||||
}
|
||||
return p.Hosted.Route(c, unsubscribe, s)
|
||||
}
|
||||
|
||||
// Observer hears about the client's IMVU account, its community outfit, and the chats it's in.
|
||||
type Observer interface {
|
||||
Session(imvuID int64)
|
||||
@@ -371,3 +398,22 @@ func (p *Products) reportOutfit(user string, locals []int64) {
|
||||
}
|
||||
p.Observer.Outfit(id, items)
|
||||
}
|
||||
|
||||
// LocalID is the client's local ID for a product (a hub product ID or a local pack's), if it's here.
|
||||
func (p *Products) LocalID(id string) (int64, bool) {
|
||||
if s := p.byProductID(id); s != nil {
|
||||
return s.local, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ProductID is the product a local ID was given to.
|
||||
func (p *Products) ProductID(local int64) (string, bool) { return p.state.productID(local) }
|
||||
|
||||
// User is the IMVU account the client signed in as (0 before it has).
|
||||
func (p *Products) User() int64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
id, _ := strconv.ParseInt(p.user, 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -417,3 +417,24 @@ func TestSavedOutfits(t *testing.T) {
|
||||
t.Fatalf("outfits %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostedRoomProducts(t *testing.T) {
|
||||
h, _ := newHarness(t)
|
||||
h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false)
|
||||
h.p.HostedOwner = func() int64 { return 7 } // the fake IMVU answers as user 7
|
||||
auths := xmlrpc.Struct{{Name: "auths", Value: []xmlrpc.Value{
|
||||
[]xmlrpc.Value{"userId", "productId"}, []xmlrpc.Value{int64(HostedOwnerID), int64(500)}}}}
|
||||
_, body := h.do("POST", "http://client-dynamic.imvu.com/api/xmlrpc/client.php", call("test.authorizeProducts2", auths), true, false)
|
||||
asked := h.imvu.asked["test.authorizeProducts2"]
|
||||
if len(asked) != 1 || strings.Contains(asked[0], "2100000000") {
|
||||
t.Fatalf("IMVU was asked %q", asked)
|
||||
}
|
||||
r, err := xmlrpc.ParseResponse(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows := r.Params[0].([]xmlrpc.Value)
|
||||
if first := rows[1].([]xmlrpc.Value); first[0] != int64(HostedOwnerID) || first[2] != int64(500) {
|
||||
t.Fatalf("answer %v", rows)
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,9 @@ func (p *Products) Unsubscribed(c *proxy.IMQConn, queues []string) {
|
||||
|
||||
// Closed implements proxy.IMQWatcher: a connection's subscriptions end with it.
|
||||
func (p *Products) Closed(c *proxy.IMQConn) {
|
||||
if p.Hosted != nil {
|
||||
p.Hosted.Closed(c)
|
||||
}
|
||||
p.roomMu.Lock()
|
||||
p.room.init()
|
||||
var queues []string
|
||||
|
||||
Reference in new issue
Block a user