Hub: accounts, IMVU linking by tagline, proxy sign-in, presence, console
CI / test (push) Failing after 42s

People sign up on the website and link IMVU accounts by putting a code in the account's tagline, which the
hub reads from IMVU's public API (the most recent proof wins; link caps per person). IMVU's age verification
or Access Pass make an account adult. T5DE's proxy signs in with a device code and keeps a WebSocket open,
reporting the IMVU account in use and the community products it wears; /online shows those who opted in.
Moderators get a console: who's online, people, link caps, bans, roles, and the audit log. Tests run the
website and API against an embedded Postgres and a fake IMVU API.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 03:45:52 -07:00
1 parent 54e569bea3
commit b9dc3f0ec1
36 files changed
+3548 -5

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
package web
import (
"crypto/rand"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"git.t5de.dev/t5de/t5de-hub/internal/imvu"
"git.t5de.dev/t5de/t5de-hub/internal/realtime"
"git.t5de.dev/t5de/t5de-hub/internal/store"
"git.t5de.dev/t5de/t5de-hub/internal/token"
)
func (s *Server) home(q *request) error {
data := map[string]any{"OnlineCount": len(s.visibleOnline(q))}
if q.person != nil {
links, err := s.Store.Links(s.ctx(q), q.person.ID)
if err != nil {
return err
}
data["Links"] = links
}
s.render(q, "home", data)
return nil
}
// linkView is a linked account on the account page.
type linkView struct {
store.Link
Online bool
Outfit []realtime.Item
}
func (s *Server) account(q *request) error {
ctx := s.ctx(q)
links, err := s.Store.Links(ctx, q.person.ID)
if err != nil {
return err
}
challenges, err := s.Store.Challenges(ctx, q.person.ID)
if err != nil {
return err
}
signins, err := s.Store.Signins(ctx, q.person.ID)
if err != nil {
return err
}
online := map[int64]realtime.Online{}
for _, o := range s.Realtime.Online() {
online[o.IMVUID] = o
}
views := make([]linkView, len(links))
for i, l := range links {
o, ok := online[l.IMVUID]
views[i] = linkView{Link: l, Online: ok, Outfit: o.Outfit}
}
s.render(q, "account", map[string]any{
"Links": views, "Challenges": challenges, "Signins": signins,
"CanLink": len(links) < q.person.LinkCap, "Error": q.r.URL.Query().Get("error"),
})
return nil
}
// linkCode is what goes in the tagline: "t5de-" and eight letters and digits (no look-alikes).
func linkCode() string {
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
b := make([]byte, 8)
rand.Read(b)
for i := range b {
b[i] = alphabet[int(b[i])%len(alphabet)]
}
return "t5de-" + string(b[:4]) + "-" + string(b[4:])
}
func (s *Server) startLink(q *request) error {
username := strings.TrimSpace(q.r.PostFormValue("username"))
if !s.limiter.allow("imvu:"+strconv.FormatInt(q.person.ID, 10), 20, time.Hour) {
return q.redirect("/account?error="+queryEscape("Too many lookups. Try again in a while."), "")
}
u, err := s.IMVU.ByUsername(s.ctx(q), username)
if errors.Is(err, imvu.ErrNoSuchUser) {
return q.redirect("/account?error="+queryEscape(fmt.Sprintf("IMVU has no account named %q.", username)), "")
}
if err != nil {
s.Log.Printf("looking up %q on IMVU: %v", username, err)
return q.redirect("/account?error="+queryEscape("IMVU didn't answer. Try again in a moment."), "")
}
if l, err := s.Store.LinkByIMVUID(s.ctx(q), u.ID); err == nil && l.PersonID == q.person.ID {
return q.redirect("/account", u.Username+" is already linked.")
}
if err := s.Store.SetChallenge(s.ctx(q), q.person.ID, u.ID, u.Username, linkCode()); err != nil {
return err
}
return q.redirect("/account#link-"+strconv.FormatInt(u.ID, 10), "")
}
func imvuIDParam(q *request) (int64, error) {
id, err := strconv.ParseInt(q.r.PathValue("imvu"), 10, 64)
if err != nil || id <= 0 {
return 0, fail(http.StatusNotFound, "No such account.")
}
return id, nil
}
// checkLink reads the account's tagline from IMVU and links it if the code is there.
func (s *Server) checkLink(q *request) error {
ctx := s.ctx(q)
id, err := imvuIDParam(q)
if err != nil {
return err
}
c, err := s.Store.Challenge(ctx, q.person.ID, id)
if errors.Is(err, store.ErrNotFound) {
return q.redirect("/account?error="+queryEscape("That code expired. Start again."), "")
}
if err != nil {
return err
}
if !s.limiter.allow("imvu:"+strconv.FormatInt(q.person.ID, 10), 20, time.Hour) {
return q.redirect("/account?error="+queryEscape("Too many checks. Try again in a while."), "")
}
u, err := s.IMVU.ByID(ctx, id)
if err != nil {
s.Log.Printf("checking IMVU account %d: %v", id, err)
return q.redirect("/account?error="+queryEscape("IMVU didn't answer. Try again in a moment."), "")
}
if !strings.Contains(strings.ToLower(u.Tagline), strings.ToLower(c.Code)) {
return q.redirect("/account?error="+queryEscape(fmt.Sprintf("%s's tagline doesn't have the code yet. IMVU can take a minute to show a new tagline.", u.Username))+"#link-"+strconv.FormatInt(id, 10), "")
}
moved, err := s.Store.LinkAccount(ctx, q.person.ID, u.ID, u.Username, token.Random(16), u.AgeVerified, u.AccessPass)
if errors.Is(err, store.ErrLinkCap) {
return q.redirect("/account?error="+queryEscape(fmt.Sprintf("You can link up to %d IMVU accounts. Ask a moderator if you need more.", q.person.LinkCap)), "")
}
if err != nil {
return err
}
detail := map[string]any{"imvu": u.Username}
if moved != 0 {
detail["moved_from"] = moved
}
s.Store.Audit(ctx, q.person.ID, "link.add", strconv.FormatInt(u.ID, 10), detail)
return q.redirect("/account", fmt.Sprintf("Linked %s. You can take the code out of your tagline now.", u.Username))
}
func (s *Server) cancelLink(q *request) error {
id, err := imvuIDParam(q)
if err != nil {
return err
}
if err := s.Store.DeleteChallenge(s.ctx(q), q.person.ID, id); err != nil {
return err
}
return q.redirect("/account", "")
}
func (s *Server) ownLink(q *request) (*store.Link, error) {
id, err := imvuIDParam(q)
if err != nil {
return nil, err
}
l, err := s.Store.LinkByIMVUID(s.ctx(q), id)
if errors.Is(err, store.ErrNotFound) || err == nil && l.PersonID != q.person.ID {
return nil, fail(http.StatusNotFound, "That account isn't linked to you.")
}
return l, err
}
func (s *Server) linkSettings(q *request) error {
l, err := s.ownLink(q)
if err != nil {
return err
}
badge, presence := q.r.PostFormValue("badge") == "on", q.r.PostFormValue("presence") == "on"
if err := s.Store.SetLinkSettings(s.ctx(q), q.person.ID, l.IMVUID, badge, presence); err != nil {
return err
}
return q.redirect("/account", "Saved settings for "+l.Username+".")
}
// recheckLink reads the account's age status from IMVU again.
func (s *Server) recheckLink(q *request) error {
l, err := s.ownLink(q)
if err != nil {
return err
}
if !s.limiter.allow("imvu:"+strconv.FormatInt(q.person.ID, 10), 20, time.Hour) {
return q.redirect("/account?error="+queryEscape("Too many checks. Try again in a while."), "")
}
u, err := s.IMVU.ByID(s.ctx(q), l.IMVUID)
if err != nil {
return q.redirect("/account?error="+queryEscape("IMVU didn't answer. Try again in a moment."), "")
}
if err := s.Store.UpdateAge(s.ctx(q), l.IMVUID, u.Username, u.AgeVerified, u.AccessPass); err != nil {
return err
}
return q.redirect("/account", "Checked "+u.Username+" with IMVU.")
}
func (s *Server) unlink(q *request) error {
l, err := s.ownLink(q)
if err != nil {
return err
}
if err := s.Store.Unlink(s.ctx(q), q.person.ID, l.IMVUID); err != nil {
return err
}
s.Store.Audit(s.ctx(q), q.person.ID, "link.remove", strconv.FormatInt(l.IMVUID, 10), map[string]any{"imvu": l.Username})
return q.redirect("/account", "Unlinked "+l.Username+".")
}
func (s *Server) signOutProxies(q *request) error {
if err := s.Store.SignOutProxies(s.ctx(q), q.person.ID); err != nil {
return err
}
s.Realtime.Disconnect(q.person.ID)
return q.redirect("/account", "Signed out of T5DE on all your computers.")
}
func (s *Server) deviceForm(q *request) error {
s.render(q, "device", map[string]any{"Code": strings.ToUpper(q.r.URL.Query().Get("code"))})
return nil
}
func (s *Server) approveDevice(q *request) error {
code := normalizeUserCode(q.r.PostFormValue("code"))
err := s.Store.ApproveDeviceCode(s.ctx(q), code, q.person.ID)
if errors.Is(err, store.ErrNotFound) {
q.w.WriteHeader(http.StatusBadRequest)
s.render(q, "device", map[string]any{"Code": code, "Error": "That code isn't right, or it expired. Check the code T5DE shows."})
return nil
}
if err != nil {
return err
}
s.Store.Audit(s.ctx(q), q.person.ID, "proxy.signin", "", nil)
s.render(q, "device", map[string]any{"Approved": true})
return nil
}
// visibleOnline is who the viewer may see online in T5DE: accounts that opted in to presence (moderators see all).
func (s *Server) visibleOnline(q *request) []onlineView {
var out []onlineView
for _, o := range s.Realtime.Online() {
l, err := s.Store.LinkByIMVUID(s.ctx(q), o.IMVUID)
if err != nil {
continue
}
mod := q.person != nil && q.person.IsModerator()
if !l.ShowPresence && !mod {
continue
}
out = append(out, onlineView{Link: *l, Online: o, Hidden: !l.ShowPresence})
}
return out
}
type onlineView struct {
Link store.Link
Online realtime.Online
Hidden bool // shown to a moderator only
}
func (s *Server) online(q *request) error {
s.render(q, "online", map[string]any{"Online": s.visibleOnline(q)})
return nil
}
func (s *Server) onlineList(q *request) error {
s.fragment(q, "online", "online-list", map[string]any{"Online": s.visibleOnline(q)})
return nil
}
func queryEscape(s string) string { return url.QueryEscape(s) }