oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package hub
2
3import (
4 "crypto/rand"
5 "encoding/base32"
6 "strings"
7 "time"
8)
9
10// Quality and problem reports (spec §15.26, 2026-10-03).
11//
12// A report is written to the app log under two random tags and nothing
13// that names the call or the people in it: a call tag, minted when a room
14// that was empty gains a participation, so every line from one call shares
15// it; and a peer tag, minted per participation. Neither is derived from
16// the sqid, the peer id, the person or the session, and neither is ever
17// logged beside any of them, so a log line cannot be walked back to a
18// meeting or a person. They live here, in memory, and die with the
19// process like everything else in the hub.
20//
21// A client is handed a report token in its hello. It is the client's
22// authority to report: for the life of the participation, and for
23// ReportAfterLeave after it ends, so "Report a problem" still works on the
24// screen that says the call is over.
25
26const (
27 // ReportAfterLeave is how long a report token outlives its
28 // participation.
29 ReportAfterLeave = 10 * time.Minute
30 // maxReportGrants bounds the token table: every live participation
31 // (at most sixteen to a room) plus those ended in the last ten
32 // minutes. A real deployment holds tens; past this, the oldest ended
33 // grant is dropped first.
34 maxReportGrants = 4096
35 // maxCallPeers bounds how many participations one call instance
36 // remembers a tag for. A remote peer past it is reported as unknown.
37 maxCallPeers = 64
38)
39
40// callInstance is one call: a room from the moment it stopped being empty
41// to the moment it is empty again.
42type callInstance struct {
43 tag string
44 peers map[PeerID]string // peer id -> peer tag, for every participation this call has held
45}
46
47type reportGrant struct {
48 room string
49 session string
50 call *callInstance
51 peerTag string
52 endedAt time.Time // zero while the participation lives
53}
54
55// Reporter is what a report token authorizes: the tags to log under,
56// and the tags of everyone this call has held, by peer id, so a report
57// that names a remote peer by its id is logged under that peer's tag.
58type Reporter struct {
59 CallTag string
60 PeerTag string
61 Live bool
62 Peers map[PeerID]string
63}
64
65// newTag is a random tag: ten bytes in lowercase base32, sixteen
66// characters, from crypto/rand. Not a hash of anything.
67func newTag() string {
68 b := make([]byte, 10)
69 if _, err := rand.Read(b); err != nil {
70 panic(err)
71 }
72 return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b))
73}
74
75// enrolReportLocked gives a fresh participation its peer tag and report
76// token, starting a new call instance when the room held nobody.
77func (h *Hub) enrolReportLocked(r *room, p *Participation) {
78 if r.call == nil || len(r.peers) == 0 {
79 r.call = &callInstance{tag: newTag(), peers: map[PeerID]string{}}
80 }
81 p.peerTag = newTag()
82 p.reportToken = randomID()
83 if len(r.call.peers) < maxCallPeers {
84 r.call.peers[p.Peer] = p.peerTag
85 }
86 if len(h.reports) >= maxReportGrants {
87 h.evictReportLocked()
88 }
89 h.reports[p.reportToken] = &reportGrant{room: r.sqid, session: p.Session, call: r.call, peerTag: p.peerTag}
90}
91
92// endReportLocked starts a participation's after-leave window.
93func (h *Hub) endReportLocked(p *Participation) {
94 if g, ok := h.reports[p.reportToken]; ok && g.endedAt.IsZero() {
95 g.endedAt = h.cfg.Now()
96 }
97}
98
99// sweepReportsLocked drops grants whose after-leave window has passed.
100func (h *Hub) sweepReportsLocked(now time.Time) {
101 for token, g := range h.reports {
102 if !g.endedAt.IsZero() && now.Sub(g.endedAt) > ReportAfterLeave {
103 delete(h.reports, token)
104 }
105 }
106}
107
108// evictReportLocked makes room: the grant that ended longest ago, or, if
109// none has ended, nothing — a live participation's token is never taken
110// from it, and the table is then bounded by the participations
111// themselves.
112func (h *Hub) evictReportLocked() {
113 var oldest string
114 var at time.Time
115 for token, g := range h.reports {
116 if g.endedAt.IsZero() {
117 continue
118 }
119 if oldest == "" || g.endedAt.Before(at) {
120 oldest, at = token, g.endedAt
121 }
122 }
123 if oldest != "" {
124 delete(h.reports, oldest)
125 }
126}
127
128// ReportTokenFor is the token of the participation this session holds in
129// the room under peer, for a resumed stream's hello.
130func (h *Hub) ReportTokenFor(sqid, session string, peer PeerID) string {
131 h.mu.Lock()
132 defer h.mu.Unlock()
133 p, ok := h.findLocked(sqid, session, peer)
134 if !ok {
135 return ""
136 }
137 return p.reportToken
138}
139
140// Reporter answers a report token, presented by this session for this
141// room: the participation's while it lives, and for ReportAfterLeave
142// after it ends. Anything else is not found.
143func (h *Hub) Reporter(sqid, session, token string) (Reporter, bool) {
144 h.mu.Lock()
145 defer h.mu.Unlock()
146 g, ok := h.reports[token]
147 if !ok || g.room != sqid || g.session != session {
148 return Reporter{}, false
149 }
150 if !g.endedAt.IsZero() && h.cfg.Now().Sub(g.endedAt) > ReportAfterLeave {
151 delete(h.reports, token)
152 return Reporter{}, false
153 }
154 peers := make(map[PeerID]string, len(g.call.peers))
155 for id, tag := range g.call.peers {
156 peers[id] = tag
157 }
158 return Reporter{CallTag: g.call.tag, PeerTag: g.peerTag, Live: g.endedAt.IsZero(), Peers: peers}, true
159}
160