CI / build (push) Successful in 28s
Chat Rooms > Manage (the t5de-room-manage patch) lists the person's T5DE rooms (room_management_info.php?t5de=1) and saves them from IMVU's room dialog (room_management.php: create with t5de=1, edit and delete naming a hosted room) as hub calls: the room, its moderators by name, and a picture from the person's IMVU gallery, uploaded to the hub. The hub's refusals show in the dialog (hub.Error keeps their message). Avatar cards get show_ap_plus again, which IMVU stopped sending, from the person's public IMVU profile (is_ap_plus). Co-Authored-By: Claude Opus 5.5 <[email protected]>
228 lines
7.6 KiB
Go
228 lines
7.6 KiB
Go
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}}})
|
|
}
|