Skip to content

Group join by QR code: render the invite token as a scannable code, with expiry, revocation and a confirm #773

Closed

Description

@Birfy

Goal

A group chat can be joined by scanning a QR code instead of typing a join token by hand. The scan carries the same thing the existing join path carries — this is a new way to hand over the token, not a second membership model.

Related: #83 (face-to-face group, join by a 6-digit number), #50 (room membership), #51 (cross-owner routing).

What the QR carries

The QR must encode the invite URL, not a bare code: a phone camera opens URLs, and an HTTPS URL is the only shape that survives being screenshotted, sent to someone, and opened on a device that does not have the app focused. The join token inside it is the existing one; the QR is a rendering of it.

Flow

  1. In a group's settings, Invite by QR shows a code rendered locally (no third-party image service, no upload) plus a copyable link and a Regenerate button.
  2. Someone who scans it lands on the join path, which asks them to confirm before their Bot enters the group. Scanning is not consent.
  3. Expiry follows the room rule already in place for join tokens: the code is valid while the room is joinable, and the panel shows the remaining time. The person who minted it can revoke it immediately.
  4. A revoked or expired code answers with one plain sentence and a way to ask for a fresh one — never a silent failure.

Why it is not just "draw a QR"

A QR is a bearer credential: whoever has the picture can join. That makes four things part of the feature, not follow-ups:

  • Revocation must be immediate and must not depend on the scanner re-reading anything.
  • Expiry must be visible on the panel, because a screenshot outlives the screen.
  • The scanner confirms, and the confirmation names the group and how many Bots are already in it.
  • Cap: the room's member limit applies to QR joins too; a full room refuses with a reason.

Acceptance

  • A group settings panel renders an invite QR locally, with the remaining time and a Regenerate button.
  • Scanning it opens the existing join path with the token filled in; the joiner confirms before any Bot enters.
  • An expired code, a revoked code and a full room each answer with a distinct, plain sentence and a way forward.
  • Revoking takes effect on the next scan; a scan that was already under way is refused rather than half-applied.
  • No network request carries the token to a third party; the QR is drawn from local data only.
  • A test pins each of the four refusals and one happy path; the happy path goes red when the token check is loosened.

Activity

  1. added
    area:securityAuth, grants, approvals, secrets
    area:socialFriends, invites, grants, Bot Social
    enhancementNew feature or request
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 22, 2026
  2. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Triage (Triage, 2026-10-22): enhancement + area:social + area:security + needs-human, not agent-ready. Two premises in the body do not hold against today's code, and both need the owner before an agent can start.

    1. There is no URL-shaped room invite to "render"

    The join path today is a 6-digit number entered on Bot Social, not a token in a link:

    So "this is a new way to hand over the token, not a second membership model" is only true if a room invite URL gets built — and that URL needs a host. FlyBot runs on the owner's machine (dsh web); the only public service in the picture is Bot Social. Either the link points at a Bot Social route that does not exist, or a new public page is hosted somewhere. That decision decides whether this feature can live in this repository at all.

    2. "Scanning is not consent" contradicts a recorded owner decision

    #83 was closed with this explicit decision (2026-10-15, Danyang): whoever enters the same 6-digit number within the 10 minutes joins directly, with no preview and no confirmation — "having the code is enough". The body here requires the scanner to confirm, and the acceptance says a scan that is under way must be refused when revoked. A confirmation step is a behaviour change to the live join path, not a rendering detail. Which one wins is the owner's call, and it changes what gets tested.

    3. Revocation has no endpoint

    The client contract lists join, list, get, start, leave, lead, roles, messages — no revoke, no void. code_void is only "the room already started", not "this invite was cancelled". So "the person who minted it can revoke it immediately" requires a Bot Social server endpoint plus a client method, outside this repo, before any test in core/ can pin it.

    The cap half is already real: FACE_MAX_MEMBERS = 8 and the server answers room_full, mapped to a plain sentence at :118.

    What is buildable here today

    If the owner wants progress without waiting for the two decisions, this half is self-contained and I will label it agent-ready on the word:

    If instead the URL flow is the goal, the two decisions above plus the server endpoint come first; the acceptance in the body is then correct as written, and the extra requirement I would add is that the link is single-use per join and invalidated server-side, because a screenshot outlives both the screen and the countdown.

    Security review requested by Chief is blocked on the same facts: the cap is already enforced, revocation does not exist yet, so there is nothing to review until one of the two paths is chosen.

  3. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:uiWeb/desktop UI and copy
    and removed
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 22, 2026
  4. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    A path chosen by the owner (2026-10-22): local QR, payload is the existing 6-digit number, no confirm step (that is #83's decision and it stands), and FlyBot's own client does the scanning. agent-ready set on that; needs-human removed. B path (public link) is not part of this issue.

    Where it lands, so nobody has to rediscover it:

    • The waiting panel already draws the number: FaceWaitCode at plugins/social/client.js:565-571, rendered at :828 and :1571; the countdown next to it is countdown() (:1443) at :830/:1573, and it already ticks.
    • The only join path is call('social/face-join', { code }) at :732 (the typed 6 digits). The scan must land on exactly this call.
    • There are no runtime dependencies in this repository: core/package.json and plugins/social/package.json both have deps: {}. So the QR encoder has to be self-contained in the repo (a small vendored file with its licence, or written here), not npm install qrcode. That is the bulk of the work and it belongs in the PR, not a later cleanup.
    • The client has no camera code and no BarcodeDetector today (getUserMedia exists only in speech.js/voice.js, for the microphone). So the scanner is new surface: camera permission, and a decoder. Do not hand-write a decoder in this ticket — use the platform BarcodeDetector where it exists, and where it does not, say so plainly and keep the 6-digit entry. A second ticket if a real decoder is ever needed.

    Acceptance, extended on top of the four from triage:

    1. QR payload is the digits only — 6 digits (optionally with a short FlyBot prefix so a foreign code is recognisable). Do not put the countdown in the payload: the image would re-encode every second and a screenshot's countdown would lie. Group title and countdown stay as text beside the code, where :830 already ticks.
    2. Zero network on the render path: no fetch/XMLHttpRequest/Image source, and no new dependency. Assert it in a test rather than trusting review.
    3. One join path: the scanned digits call the same social/face-join (:732) as typed entry. A test proves there is no second endpoint or second code path.
    4. Refusals are sentences: a foreign/unknown QR ("that code is not a FlyBot room number"), camera denied, and BarcodeDetector unsupported each answer plainly and leave the 6-digit entry usable. No silent failure, no auto-join from a background scan.
    5. The code goes away when it stops working: at expiry the panel says "Joining has ended" and the QR is not drawn. A QR still on screen after the room started is an invitation to a dead number.
    6. Encoder is tested, not eyeballed: decode the produced matrix back for a few numbers (test-side decoder or a fixed vector), plus one golden matrix so a silent encoder regression is a red test.
    7. The panel side of the scan needs the person to open the scanner; the scan itself then proceeds to the same join as the typed path — that is "no confirm step", not "joins without the person tapping".

    area:security stays on because this adds a camera permission and consumes untrusted input (an arbitrary QR image); the refusal sentences in (4) are exactly the boundary that matters.

  5. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Owner asked for the second half explicitly: an already-established group should be joinable by an invite link (and by the QR that carries it). That is not what the 6-digit path does today — it forms a room, it does not invite into an existing one.

    The server half is ours (Bot Social is self-hosted, Birfy/bot-social), so I opened it: Birfy/bot-social#39 — mint/revoke/preview/join-invite tickets, 10-minute default expiry, maxMembers applies, and five distinct refusals (expired, revoked, room_full, ended, already_member).

    Scope change for this issue: the client work splits in two.

    1. Now, no dependency: render the invite link and the QR for it once the ticket exists… which means it waits on docs: Magpie-style architecture and subsystem docs #39. What can land before docs: Magpie-style architecture and subsystem docs #39 is the part that does not need a ticket: paste/parse a room invite URL and answer a clear "this server does not support room invites yet" instead of failing silently.
    2. After docs: Magpie-style architecture and subsystem docs #39: mint a ticket from the room panel, show link + QR with the countdown, revoke, and join through the same path as the 6-digit join.

    Triage: keep the existing A-path acceptance for the "form a room together" QR (that part is independent), and treat the invite-link half as blocked on Birfy/bot-social#39. Do not mark the invite-label half agent-ready until #39 lands, or we get a client that calls an endpoint that answers 404.

  6. Birfy commented on Oct 22, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev2

    Taking this one on branch feat/773-room-qr. Scope is the A path in the comments above: the waiting dialog and the room panel draw the 6-digit number as a QR, and FlyBot scans one instead of typing the number.

    Two things from this comment are respected and pinned by tests: the payload is FLYBOT- + the six digits (no deadline, no name — the countdown line beside it keeps ticking), and a scan ends in the existing social/face-join, never in a second join path. The encoder is written in-repo (byte mode, error correction M, version 1; the repo has no runtime dependencies), the camera uses the platform BarcodeDetector, and a browser without it is told so before any permission prompt.

  7. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 22, 2026
  8. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 22, 2026
  9. added a commit that references this issue on Oct 23, 2026
    61a6af6
  10. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:securityAuth, grants, approvals, secretsarea:socialFriends, invites, grants, Bot Socialarea:uiWeb/desktop UI and copyenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions