Manage T5DE rooms from the client; the AP+ badge on avatar cards
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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 08:19:33 -07:00
1 parent 6aac769246
commit 8daf91ede3
7 files changed
+608 -2

No files matched your search

+46 -1
View File
@@ -8,13 +8,16 @@
package hub
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
@@ -491,10 +494,52 @@ func (c *Client) Call(ctx context.Context, method, path string, body, out any) e
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
return answer(resp, data, out)
}
// Upload sends a file to the hub as multipart form data (one field), as Call does JSON.
func (c *Client) Upload(ctx context.Context, method, path, field, filename, contentType string, file []byte, out any) error {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
h := textproto.MIMEHeader{}
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name=%q; filename=%q`, field, filename))
h.Set("Content-Type", contentType)
part, err := w.CreatePart(h)
if err != nil {
return err
}
part.Write(file)
w.Close()
req, err := http.NewRequestWithContext(ctx, method, c.saved.URL+path, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.saved.Token)
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
return answer(resp, data, out)
}
// Error is the hub refusing a call: its status, error code and message (shown to people as is).
type Error struct {
Status int
Code string
Message string
}
func (e *Error) Error() string { return fmt.Sprintf("the hub answered %d %s", e.Status, e.Code) }
func answer(resp *http.Response, data []byte, out any) error {
if resp.StatusCode/100 != 2 {
var e struct{ Error, Message string }
json.Unmarshal(data, &e)
return fmt.Errorf("the hub answered %d %s", resp.StatusCode, e.Error)
return &Error{Status: resp.StatusCode, Code: e.Error, Message: e.Message}
}
if out == nil {
return nil