From d379d848c1ca9f4ba9c94ec9beb183553e478a0a Mon Sep 17 00:00:00 2001 From: dhkatz Date: Fri, 30 Oct 2026 05:58:12 -0700 Subject: [PATCH] Mark rooms needing T5DE in the room list (t5de_t5), for the client's T5 badge Hosted rooms, and IMVU rooms the hub has decorations for (asked 20 at a time), on every page; hosted rooms are added to recent rooms from the hub's recent list when it has one. Co-Authored-By: Claude Opus 5.5 --- internal/hosted/hosted.go | 114 ++++++++++++++++++++++++++------- internal/hosted/hosted_test.go | 17 +++-- 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/internal/hosted/hosted.go b/internal/hosted/hosted.go index 1f36bbd..2cbca97 100644 --- a/internal/hosted/hosted.go +++ b/internal/hosted/hosted.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "regexp" "sort" "strconv" @@ -763,16 +764,14 @@ func (r *Rooms) others(n int64) []xmlrpc.Value { return out } -// addToRoomList adds hosted rooms to the client's room list (rooms_list_paginated.php: one array per field): all -// of them to the first page without a search, and those the client was in this run to recently visited rooms. +// addToRoomList changes the client's room list (rooms_list_paginated.php: one array per field): hosted rooms are +// added to the first page without a search (all of them; to recently visited rooms, the person's recent ones), and +// every room gets t5de_t5: 1 for rooms needing T5DE to be seen as meant (hosted, or with T5 decorations), for the +// client's T5 badge (its t5de-rooms patch). func (r *Rooms) addToRoomList(x *proxy.Exchange) { q := x.URL.Query() - if p := q.Get("page"); p != "" && p != "1" && p != "0" { - return - } - if q.Get("keywords") != "" || q.Get("search") != "" { - return - } + firstPage := q.Get("page") == "" || q.Get("page") == "1" || q.Get("page") == "0" + addHosted := firstPage && q.Get("keywords") == "" && q.Get("search") == "" recent := q.Get("recent") != "" x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) { var list map[string]json.RawMessage @@ -781,28 +780,97 @@ func (r *Rooms) addToRoomList(x *proxy.Exchange) { } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - var out struct { - Rooms []Room `json:"rooms"` - } - if err := r.Hub.Call(ctx, "GET", "/api/v1/hosted-rooms", nil, &out); err != nil || len(out.Rooms) == 0 { - return body, nil - } - sort.SliceStable(out.Rooms, func(i, j int) bool { return out.Rooms[i].Occupancy > out.Rooms[j].Occupancy }) - r.mu.Lock() - visited := map[int64]bool{} - for n := range r.owners { - visited[n] = true - } - r.mu.Unlock() - for _, rm := range out.Rooms { - if !recent || visited[rm.N] { + if addHosted { + for _, rm := range r.listedRooms(ctx, recent) { appendRoom(list, rm) } } + r.markT5(ctx, list) return json.Marshal(list) } } +// listedRooms is the hosted rooms for the list: the person's recent ones (or, if the hub can't say, those visited +// this run), or all of them, busiest first. +func (r *Rooms) listedRooms(ctx context.Context, recent bool) []Room { + var out struct { + Rooms []Room `json:"rooms"` + } + if recent { + if err := r.Hub.Call(ctx, "GET", "/api/v1/hosted-rooms?recent=1", nil, &out); err == nil { + return out.Rooms + } + } + if err := r.Hub.Call(ctx, "GET", "/api/v1/hosted-rooms", nil, &out); err != nil { + return nil + } + sort.SliceStable(out.Rooms, func(i, j int) bool { return out.Rooms[i].Occupancy > out.Rooms[j].Occupancy }) + if !recent { + return out.Rooms + } + r.mu.Lock() + defer r.mu.Unlock() + var visited []Room + for _, rm := range out.Rooms { + if _, ok := r.owners[rm.N]; ok { + visited = append(visited, rm) + } + } + return visited +} + +// markT5 sets the list's t5de_t5 column: hosted rooms, and IMVU rooms the hub has decorations for. +func (r *Rooms) markT5(ctx context.Context, list map[string]json.RawMessage) { + var owners, numbers []json.RawMessage + if json.Unmarshal(list["customers_id"], &owners) != nil || json.Unmarshal(list["customers_room_id"], &numbers) != nil || len(owners) != len(numbers) { + return + } + marks := make([]int, len(owners)) + var ask []string + index := map[string][]int{} + for i := range owners { + instance := rawText(owners[i]) + "-" + rawText(numbers[i]) + if _, ok := RoomNumber(instance); ok { + marks[i] = 1 + continue + } + if index[instance] == nil { + ask = append(ask, instance) + } + index[instance] = append(index[instance], i) + } + for len(ask) > 0 { + batch := ask[:min(len(ask), 20)] + ask = ask[len(batch):] + var out struct { + Rooms []struct { + Instance string `json:"instance"` + Contents []json.RawMessage `json:"contents"` + } `json:"rooms"` + } + if err := r.Hub.Call(ctx, "GET", "/api/v1/decorations?instances="+url.QueryEscape(strings.Join(batch, ",")), nil, &out); err != nil { + break + } + for _, d := range out.Rooms { + if len(d.Contents) > 0 { + for _, i := range index[d.Instance] { + marks[i] = 1 + } + } + } + } + list["t5de_t5"], _ = json.Marshal(marks) +} + +// rawText is a JSON string's or number's text. +func rawText(raw json.RawMessage) string { + var s string + if json.Unmarshal(raw, &s) == nil { + return s + } + return strings.TrimSpace(string(raw)) +} + // appendRoom adds a room to each of the list's arrays (fields it doesn't know get an empty value of the array's // kind). func appendRoom(list map[string]json.RawMessage, rm Room) { diff --git a/internal/hosted/hosted_test.go b/internal/hosted/hosted_test.go index cb521c8..d42637e 100644 --- a/internal/hosted/hosted_test.go +++ b/internal/hosted/hosted_test.go @@ -50,6 +50,8 @@ func (h *fakeHub) Call(ctx context.Context, method, path string, body, out any) return json.Unmarshal([]byte(`{"room": {"n": 3, "name": "Lounge ", "ownerImvuId": 88, "ownerName": "Erin", "roomPid": 50168116, "rating": "general", "maxUsers": 10, "occupancy": 2}, "scene": {"contents": [[1, 47477426, "37207-46"], [2, "coatcoat22", "37-11"], [3, "notherenn2", "37-12"]], "hash": "h1"}}`), out) + case method == "GET" && strings.HasPrefix(path, "/api/v1/decorations?instances="): + return json.Unmarshal([]byte(`{"rooms": [{"instance": "1-644", "contents": [[1, "coatcoat22", "x"]]}, {"instance": "2-17", "contents": []}]}`), out) case method == "GET" && path == "/api/v1/hosted-rooms": return json.Unmarshal([]byte(`{"rooms": [{"n": 3, "name": "Lounge", "ownerImvuId": 88, "ownerName": "Erin", "maxUsers": 10, "occupancy": 2}]}`), out) case method == "PUT" && path == "/api/v1/hosted-rooms/3/scene": @@ -241,22 +243,25 @@ func TestRoomList(t *testing.T) { req, _ := http.NewRequest("GET", u.String(), nil) x := &proxy.Exchange{URL: u, Request: req} r.Intercept(x) - body := []byte(`{"customers_room_id": ["644"], "customers_id": ["1"], "num_participants": [4], "name": ["A"], "participants": [[]], "is_ap": ["0"], "odd": [{"x": 1}], "number_of_rooms": 1}`) + body := []byte(`{"customers_room_id": ["644", "17"], "customers_id": ["1", "2"], "num_participants": [4, 1], "name": ["A", "B"], "participants": [[], []], "is_ap": ["0", "0"], "odd": [{"x": 1}, {}], "number_of_rooms": 2}`) out, err := x.ModifyResponse(&http.Response{StatusCode: 200}, body) if err != nil { t.Fatal(err) } var l map[string]any json.Unmarshal(out, &l) - if fmt.Sprint(l["customers_room_id"]) != "[644 2100000003]" || fmt.Sprint(l["customers_id"]) != "[1 88]" || - fmt.Sprint(l["num_participants"]) != "[4 2]" || fmt.Sprint(l["odd"]) != "[map[x:1] map[]]" || l["number_of_rooms"] != float64(1) { + if fmt.Sprint(l["customers_room_id"]) != "[644 17 2100000003]" || fmt.Sprint(l["customers_id"]) != "[1 2 88]" || + fmt.Sprint(l["num_participants"]) != "[4 1 2]" || fmt.Sprint(l["odd"]) != "[map[x:1] map[] map[]]" || l["number_of_rooms"] != float64(2) || + fmt.Sprint(l["t5de_t5"]) != "[1 0 1]" { // decorated, plain, hosted t.Fatalf("list %s", out) } - // Other pages and searches are left alone. + // Other pages get no hosted rooms, but their decorated rooms are marked. u, _ = url.Parse("https://client-dynamic.imvu.com/api/rooms/rooms_list_paginated.php?page=2") x = &proxy.Exchange{URL: u, Request: req} r.Intercept(x) - if x.ModifyResponse != nil { - t.Fatal("changed page 2") + out, _ = x.ModifyResponse(&http.Response{StatusCode: 200}, body) + json.Unmarshal(out, &l) + if fmt.Sprint(l["customers_room_id"]) != "[644 17]" || fmt.Sprint(l["t5de_t5"]) != "[1 0]" { + t.Fatalf("page 2: %s", out) } }