Room channel: other T5DE users' community products on their avatars
CI / build (push) Successful in 28s
CI / build (push) Successful in 28s
The proxy follows the client's IMQ chat subscriptions and tells the hub which chats it's in (and the room, from chat.getOrMakeChat). For the other T5DE users the hub says are in a chat, it gives the client the community products it has packs for, as *use and *remove lines from them, and adds them to the whole outfits they send. It also takes community products out of the outfit the client posts to IMVU's chat participant endpoint, which had been sending them to IMVU. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e394f1dc24
commit
93fa2f5d90
11 files changed
+858
-12
No files matched your search
+105
-6
@@ -1,6 +1,7 @@
|
||||
// Package hub connects the proxy to T5DE's hub (git.t5de.dev/t5de/t5de-hub): signing in with a code the person
|
||||
// enters on the hub's website (OAuth 2.0 device authorization, by the hub's Better Auth), and a live connection
|
||||
// reporting which IMVU account the client is using and the community products it wears.
|
||||
// reporting which IMVU account the client is using, the community products it wears, and the IMVU chats it's in;
|
||||
// the hub answers with the other T5DE users in those chats and what they wear (the room channel).
|
||||
//
|
||||
// Signing in gives a session token, kept encrypted for the Windows user and sent as a bearer token. The hub extends
|
||||
// the session while it's used, and ends it when the person signs their computers out or is banned.
|
||||
@@ -25,8 +26,15 @@ import (
|
||||
|
||||
// Item is a community product worn.
|
||||
type Item struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Adult bool `json:"adult"`
|
||||
}
|
||||
|
||||
// Avatar is another T5DE user in a chat, and what they wear.
|
||||
type Avatar struct {
|
||||
IMVUID int64 `json:"imvu_id"`
|
||||
Items []Item `json:"items"`
|
||||
}
|
||||
|
||||
// Saved is the proxy's sign-in, in <data dir>/hub.json. The session token is encrypted for the Windows user.
|
||||
@@ -162,16 +170,46 @@ type Client struct {
|
||||
path string
|
||||
log *log.Logger
|
||||
|
||||
// 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)
|
||||
|
||||
mu sync.Mutex
|
||||
saved *Saved
|
||||
imvuID int64
|
||||
outfit []Item
|
||||
chats map[string]string // chat ID -> room instance ("" for other chats)
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
// New makes a client for the sign-in saved at path.
|
||||
func New(path string, saved *Saved, logger *log.Logger) *Client {
|
||||
return &Client{path: path, saved: saved, log: logger, wake: make(chan struct{}, 1)}
|
||||
return &Client{path: path, saved: saved, log: logger, chats: map[string]string{}, wake: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
// Join reports the client is in an IMVU chat (room: the room instance, for a room's chat).
|
||||
func (c *Client) Join(chat, room string) {
|
||||
c.mu.Lock()
|
||||
old, had := c.chats[chat]
|
||||
c.chats[chat] = room
|
||||
c.mu.Unlock()
|
||||
if !had || old != room {
|
||||
c.poke()
|
||||
}
|
||||
}
|
||||
|
||||
// Leave reports the client left an IMVU chat.
|
||||
func (c *Client) Leave(chat string) {
|
||||
c.mu.Lock()
|
||||
_, had := c.chats[chat]
|
||||
delete(c.chats, chat)
|
||||
c.mu.Unlock()
|
||||
if had {
|
||||
c.poke()
|
||||
}
|
||||
if c.OnChat != nil {
|
||||
c.OnChat(chat, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Session reports the IMVU account the client signed in as.
|
||||
@@ -258,9 +296,23 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
var m struct{ Type, Message string }
|
||||
if json.Unmarshal(data, &m) == nil && m.Type == "error" {
|
||||
var m struct {
|
||||
Type, Message, Chat string
|
||||
Avatars []Avatar
|
||||
}
|
||||
if json.Unmarshal(data, &m) != nil {
|
||||
continue
|
||||
}
|
||||
switch m.Type {
|
||||
case "error":
|
||||
c.log.Printf("hub: %s", m.Message)
|
||||
case "chat":
|
||||
c.mu.Lock()
|
||||
_, in := c.chats[m.Chat]
|
||||
c.mu.Unlock()
|
||||
if in && c.OnChat != nil {
|
||||
c.OnChat(m.Chat, m.Avatars)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -270,10 +322,30 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
defer cancel()
|
||||
return conn.Write(wctx, websocket.MessageText, b)
|
||||
}
|
||||
// Without the hub, nobody else's community products are known.
|
||||
defer func() {
|
||||
if c.OnChat == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
var chats []string
|
||||
for chat := range c.chats {
|
||||
chats = append(chats, chat)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
for _, chat := range chats {
|
||||
c.OnChat(chat, nil)
|
||||
}
|
||||
}()
|
||||
sentID, sentOutfit := int64(-1), ""
|
||||
sentChats := map[string]string{}
|
||||
for {
|
||||
c.mu.Lock()
|
||||
id, outfit := c.imvuID, append([]Item{}, c.outfit...) // a list, never null
|
||||
chats := make(map[string]string, len(c.chats))
|
||||
for k, v := range c.chats {
|
||||
chats[k] = v
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if id != sentID && id != 0 {
|
||||
if err := send(map[string]any{"type": "session", "imvu_id": id}); err != nil {
|
||||
@@ -287,6 +359,33 @@ func (c *Client) connect(ctx context.Context) error {
|
||||
}
|
||||
sentOutfit = string(b)
|
||||
}
|
||||
for chat, room := range chats {
|
||||
if r, ok := sentChats[chat]; ok && r == room {
|
||||
continue
|
||||
}
|
||||
m := map[string]any{"type": "join", "chat": chat}
|
||||
if room != "" {
|
||||
m["room"] = room
|
||||
}
|
||||
if err := send(m); err != nil {
|
||||
return err
|
||||
}
|
||||
if room != "" {
|
||||
c.log.Printf("hub: in chat %s (room %s)", chat, room)
|
||||
} else {
|
||||
c.log.Printf("hub: in chat %s", chat)
|
||||
}
|
||||
sentChats[chat] = room
|
||||
}
|
||||
for chat := range sentChats {
|
||||
if _, ok := chats[chat]; !ok {
|
||||
if err := send(map[string]any{"type": "leave", "chat": chat}); err != nil {
|
||||
return err
|
||||
}
|
||||
c.log.Printf("hub: left chat %s", chat)
|
||||
delete(sentChats, chat)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
conn.Close(websocket.StatusNormalClosure, "")
|
||||
|
||||
Reference in new issue
Block a user