package calls import ( "amadan.net/rastrillo/rastrillo/sessions" "errors" "html/template" "net/http" "net/url" "github.com/go-chi/chi/v5" ) // followLink is GET /j/{token}: one 404 for unknown, revoked, ended or // expired; otherwise one of three outcomes (spec §3.2). It is mounted // outside the session group because the person is often not signed in. func (a *app) followLink(w http.ResponseWriter, r *http.Request) { link, m, live, err := a.store.liveLink(r.Context(), chi.URLParam(r, "token")) if err != nil { a.logger.Error("calls: follow link", "err", err) a.ErrorPage(w, r, http.StatusInternalServerError, "") return } if !live { a.ErrorPage(w, r, http.StatusNotFound, "") return } id, signedIn := a.auth.SessionFrom(r) if !signedIn { a.setJoinCookie(w, link.TokenHash) if !m.RequireSignin { a.guestPage(w, r, m, chi.URLParam(r, "token"), "", "") return } http.Redirect(w, r, signinPath, http.StatusSeeOther) return } p, err := a.personForSession(r.Context(), sessions.Session{Subject: id.Address, Method: string(id.Method), AuthTime: id.AuthTime, At: id.At}) if errors.Is(err, errHomeIdentity) { a.ErrorPage(w, r, http.StatusNotFound, "") return } if err != nil { a.logger.Error("calls: ensure person", "err", err) a.ErrorPage(w, r, http.StatusInternalServerError, "") return } if p.Anonymous && m.RequireSignin { a.setJoinCookie(w, link.TokenHash) http.Redirect(w, r, signinPath, 303) return } if !a.isMember(p) { // Via the link, not the meeting: this handler read the link // before it resolved the person, and a member may have replaced // it in between. The re-read happens inside the grant's own // transaction, and a link that is no longer live gets the same // 404 an unknown token gets (review C1-3). _, live, err := a.store.mintGrantViaLink(r.Context(), link.ID, p.ID) if err != nil { a.logger.Error("calls: mint grant", "err", err) a.ErrorPage(w, r, http.StatusInternalServerError, "") return } if !live { a.ErrorPage(w, r, http.StatusNotFound, "") return } } http.Redirect(w, r, "/m/"+m.Sqid+"/join", http.StatusSeeOther) } // holding is GET /m/{sqid}/join, authorized by standing or a live // participation for this session (spec §2.5), the same row the room // page uses. func (a *app) holding(w http.ResponseWriter, r *http.Request) { m, p, st, ok := a.roomAccess(w, r) if !ok { return } // This session's own peer, if it already has one here — a second // tab open on the same meeting — points the person at the takeover // confirm page rather than a plain join that would collide with it. var openElsewhere bool if session, ok := a.sessionKey(r); ok { _, openElsewhere = a.hub.LiveForSession(m.Sqid, session) } title := a.store.title(m) render(w, "join", a.page(r, onMeeting("", title), map[string]any{ "Sqid": m.Sqid, "Title": title, "Starts": a.fmtStart(m.StartsAt), "Name": p.Name, "Standing": st.String(), "IsMember": st == StandingMember, "Floor": !a.relayConfigured(), "OpenElsewhere": openElsewhere, // Not oficina-calls://join?url=…/m/{sqid} (until 2026-10-02): the // app has a session of its own, or none, and the meeting's // address told it nothing about who this is, so a guest — and // anyone whose default browser was not this one — could not get // in. The offer hands this browser's person over (appOffer). "AppHref": "/m/" + m.Sqid + "/app", })) } // Only app-constructed meeting URLs are promoted to the custom URL scheme. func nativeJoinURL(meeting string) template.URL { return template.URL("oficina-calls://join?url=" + url.QueryEscape(meeting)) }