The proxy keeps the person's library in a cache folder (hub-products in its data folder), one pack per product, served like any other pack: the hub says which revision to load and which catalogs host it, each file comes from a catalog and is checked against the hub's sha256 (files are kept by hash, so a new revision only downloads what changed), and adult files are fetched with the hub's access token. Products someone wears in a room that aren't here yet are fetched too, and put on them when they arrive. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
93fa2f5d90
commit
c281fb572c
6 files changed
+579
-3
No files matched your search
@@ -173,6 +173,8 @@ type Client struct {
|
||||
// OnChat, if set, hears the other T5DE users in a chat the client is in, whenever that changes (and with none
|
||||
// when the connection to the hub is lost).
|
||||
OnChat func(chat string, avatars []Avatar)
|
||||
// OnLibrary, if set, is called when connected and whenever the person's library changes.
|
||||
OnLibrary func()
|
||||
|
||||
mu sync.Mutex
|
||||
saved *Saved
|
||||
@@ -283,6 +285,9 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
c.log.Printf("hub: connected to %s", c.saved.URL)
|
||||
if c.OnLibrary != nil {
|
||||
go c.OnLibrary()
|
||||
}
|
||||
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
@@ -306,6 +311,10 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
switch m.Type {
|
||||
case "error":
|
||||
c.log.Printf("hub: %s", m.Message)
|
||||
case "library":
|
||||
if c.OnLibrary != nil {
|
||||
go c.OnLibrary()
|
||||
}
|
||||
case "chat":
|
||||
c.mu.Lock()
|
||||
_, in := c.chats[m.Chat]
|
||||
@@ -396,3 +405,89 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call makes a request to the hub's API as the signed-in person: path under the hub's URL, body (if any) sent as
|
||||
// JSON, the answer decoded into out.
|
||||
func (c *Client) Call(ctx context.Context, method, path string, body, out any) error {
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
b, _ := json.Marshal(body)
|
||||
r = strings.NewReader(string(b))
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.saved.URL+path, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.saved.Token)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
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)
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, out)
|
||||
}
|
||||
|
||||
// FileEntry is one of a revision's files.
|
||||
type FileEntry struct {
|
||||
Name string `json:"name"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// Files is the revision of a product the person uses, and where to get it (GET /api/v1/products/:id/files).
|
||||
type Files struct {
|
||||
Product string `json:"product"`
|
||||
Revision int `json:"revision"`
|
||||
Rating string `json:"rating"`
|
||||
Parent int64 `json:"parent"`
|
||||
Name string `json:"name"`
|
||||
Creator string `json:"creator"`
|
||||
Files []FileEntry `json:"files"`
|
||||
Catalogs []string `json:"catalogs"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// LibraryEntry is a product in the person's library.
|
||||
type LibraryEntry struct {
|
||||
Product struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"product"`
|
||||
StayOn *int `json:"stayOn"`
|
||||
}
|
||||
|
||||
// Library is the person's library.
|
||||
func (c *Client) Library(ctx context.Context) ([]LibraryEntry, error) {
|
||||
var out struct{ Library []LibraryEntry }
|
||||
err := c.Call(ctx, "GET", "/api/v1/library", nil, &out)
|
||||
return out.Library, err
|
||||
}
|
||||
|
||||
// ProductFiles is the revision of a product to load.
|
||||
func (c *Client) ProductFiles(ctx context.Context, id string) (*Files, error) {
|
||||
var out Files
|
||||
if err := c.Call(ctx, "GET", "/api/v1/products/"+id+"/files", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AccessToken is a token for catalogs' adult files (an hour long).
|
||||
func (c *Client) AccessToken(ctx context.Context) (string, error) {
|
||||
var out struct{ Token string }
|
||||
err := c.Call(ctx, "POST", "/api/v1/access", nil, &out)
|
||||
return out.Token, err
|
||||
}
|
||||
Reference in new issue
Block a user