package hosted // Managing the person's own hosted rooms from the client's Chat Rooms > Manage tab, which shows them in a list of // their own (the t5de-rooms patch): room_management_info.php?t5de=1 lists them as IMVU lists the person's rooms, and // IMVU's room dialog saves them (room_management.php with t5de=1, or naming a hosted room), as calls to the hub. import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "git.t5de.dev/t5de/t5de-proxy/internal/hub" "git.t5de.dev/t5de/t5de-proxy/internal/proxy" ) // maxRoomsPerPerson is the hub's cap on a person's open rooms (t5de-hub's maxRoomsPerPerson). const maxRoomsPerPerson = 10 // uploader is a hub that takes files (hub.Client). type uploader interface { Upload(ctx context.Context, method, path, field, filename, contentType string, file []byte, out any) error } // manageInfo answers room_management_info.php?t5de=1: the person's hosted rooms owned by the account in the client, // and how many more they may make. func (r *Rooms) manageInfo(ctx context.Context) *http.Response { var out struct { Rooms []Room `json:"rooms"` } if err := r.Hub.Call(ctx, "GET", "/api/v1/hosted-rooms?mine=1", nil, &out); err != nil { r.logf("hosted: the person's rooms: %v", err) return manageJSON(map[string]any{"error": "T5DE's hub isn't available."}) } me := r.user() rooms := []any{} for _, rm := range out.Rooms { if rm.OwnerImvuID == me { rooms = append(rooms, manageRow(rm)) } } return manageJSON(map[string]any{"result": map[string]any{"rooms": rooms, "limit": maxRoomsPerPerson - len(out.Rooms) + len(rooms)}}) } // manageRow is a hosted room as room_management_info.php lists the person's rooms (and the room dialog edits them). func manageRow(rm Room) map[string]any { adult, open := 0, 2 // 2: public, as IMVU's "Public" if rm.Rating == "adult" { adult = 1 } if rm.Privacy == "private" { open = 0 } var mods []string for _, id := range rm.Moderators { mods = append(mods, strconv.FormatInt(id, 10)) } return map[string]any{ "name": plain(rm.Name), "description": plain(rm.Description), "customers_id": strconv.FormatInt(rm.OwnerImvuID, 10), "customers_room_id": strconv.FormatInt(Base+rm.N, 10), "room_instance_id": instanceID(rm.OwnerImvuID, rm.N), "image_url": rm.Image, "num_participants": rm.Occupancy, "max_users": rm.MaxUsers, "room_pid": rm.RoomPid, "is_ap": adult, "is_vip": 0, "is_age_verified_only": 0, "is_non_guest_only": 0, "is_friends_only": 0, "is_favorite": boolNumber(rm.Favorite), "language": "", "open": open, "lieutenants": strings.Join(mods, ", "), "t5de_t5": 1, "t5de_hosted": 1, } } // manage answers room_management.php for hosted rooms: action create (with t5de=1), edit or delete (naming one). func (r *Rooms) manage(ctx context.Context, x *proxy.Exchange) bool { form := params(x) n, hosted := RoomNumber(form.Get("room_instance_id")) action := form.Get("action") if !hosted && !(action == "create" && form.Get("t5de") == "1") { return false } if action == "delete" { if err := r.Hub.Call(ctx, "DELETE", fmt.Sprintf("/api/v1/hosted-rooms/%d", n), nil, nil); err != nil { x.Response = manageErrors(err) return true } x.Response = manageJSON(map[string]any{"result": true}) return true } if action != "create" && action != "edit" { x.Response = manageJSON(map[string]any{"errors": []any{map[string]string{"0": "T5DE rooms can't do that."}}}) return true } room := map[string]any{"name": strings.TrimSpace(form.Get("room_name")), "description": form.Get("room_description")} if pid, err := strconv.ParseInt(form.Get("room_pid"), 10, 64); err == nil && pid > 0 && action == "create" { room["roomPid"] = pid // (the dialog doesn't show the room when editing) } if max, err := strconv.Atoi(form.Get("max_users")); err == nil && max > 0 { room["maxUsers"] = max } room["rating"] = "general" if ap := form.Get("ap"); ap != "" && ap != "0" { room["rating"] = "adult" } room["privacy"] = "public" if form.Get("permissions") == "0" { room["privacy"] = "private" } var answer struct { Room Room `json:"room"` } if action == "create" { if room["roomPid"] == nil { x.Response = manageErrors(errors.New("Choose a room.")) return true } room["imvuId"] = r.user() if err := r.Hub.Call(ctx, "POST", "/api/v1/hosted-rooms", room, &answer); err != nil { x.Response = manageErrors(err) return true } n = answer.Room.N } // Moderators by name or ID ("a, b"), set after creating (the hub looks names up). mods := []string{} for _, m := range strings.FieldsFunc(form.Get("lieutenants"), func(c rune) bool { return c == ',' || c == ';' || c == '\n' }) { if m = strings.TrimSpace(m); m != "" { mods = append(mods, m) } } change := room if action == "create" { change = map[string]any{} } if action == "edit" || len(mods) > 0 { change["moderators"] = mods } if len(change) > 0 { if err := r.Hub.Call(ctx, "PATCH", fmt.Sprintf("/api/v1/hosted-rooms/%d", n), change, &answer); err != nil { x.Response = manageErrors(err) return true } } if err := r.setImage(ctx, n, answer.Room.Image, form.Get("image"), &answer.Room); err != nil { x.Response = manageErrors(err) return true } r.mu.Lock() if r.owners == nil { r.owners = map[int64]int64{} } r.owners[n] = answer.Room.OwnerImvuID r.mu.Unlock() x.Response = manageJSON(map[string]any{"result": manageRow(answer.Room)}) return true } // setImage gives a room the image chosen in the dialog (a picture from the person's IMVU gallery, uploaded to the hub), // or takes it away ("" when it had one). The hub's own image URL means unchanged. func (r *Rooms) setImage(ctx context.Context, n int64, current, chosen string, into *Room) error { if chosen == current || (chosen != "" && current != "" && strings.SplitN(chosen, "?", 2)[0] == strings.SplitN(current, "?", 2)[0]) { return nil } var answer struct { Room Room `json:"room"` } path := fmt.Sprintf("/api/v1/hosted-rooms/%d/image", n) if chosen == "" { if current == "" { return nil } if err := r.Hub.Call(ctx, "DELETE", path, nil, &answer); err != nil { return err } *into = answer.Room return nil } up, ok := r.Hub.(uploader) if !ok { return errors.New("T5DE's hub can't take images here.") } u, err := url.Parse(chosen) if err != nil || !(u.Scheme == "http" || u.Scheme == "https") || !strings.HasSuffix(strings.ToLower(u.Hostname()), ".imvu.com") { return errors.New("Choose a picture from your gallery.") } req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return err } resp, err := r.client().Do(req) if err != nil { return errors.New("Couldn't get that picture from IMVU.") } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, 512<<10+1)) if err != nil || resp.StatusCode != http.StatusOK { return errors.New("Couldn't get that picture from IMVU.") } if len(data) > 512<<10 { return errors.New("That picture is too big for a T5DE room (512 KB at most).") } if err := up.Upload(ctx, "PUT", path, "image", "room", http.DetectContentType(data), data, &answer); err != nil { return err } *into = answer.Room return nil } func manageJSON(v any) *http.Response { b, _ := json.Marshal(v) return proxy.LocalResponse(200, "application/json", b) } // manageErrors is a failed save as the room dialog shows it. func manageErrors(err error) *http.Response { msg := err.Error() var he *hub.Error if errors.As(err, &he) { msg = he.Message if msg == "" { msg = "T5DE's hub didn't take that. Check the room's details." } } else if !strings.HasSuffix(msg, ".") { msg = "T5DE's hub isn't available." } return manageJSON(map[string]any{"errors": []any{map[string]string{"0": msg}}}) }