You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Group join by QR code: render the invite token as a scannable code, with expiry, revocation and a confirm #773
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
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.
Someone who scans it lands on the join path, which asks them to confirm before their Bot enters the group. Scanning is not consent.
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.
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.
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:
Tests: the render path makes no network call (assert it, do not trust convention), the countdown ends at "Joining has ended", and the QR encodes exactly the rendered join text.
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.
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:
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.
Zero network on the render path: no fetch/XMLHttpRequest/Image source, and no new dependency. Assert it in a test rather than trusting review.
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.
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.
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.
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.
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.
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.
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.
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.
Activity
Birfy commentedon Oct 22, 2026
Triage (Triage, 2026-10-22):
enhancement+area:social+area:security+needs-human, notagent-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:
plugins/social/src/face-rooms.js:16-18:FACE_JOIN_CODE = /^\d{6}$/,FACE_TTL_MS = 10 * 60 * 1000,FACE_MAX_MEMBERS = 8.join({ code, bots })at:637posts{ code }to Bot Social/join. There is no URL, no token, no landing page.core/src/shared/room-share.jsis not one either: it is a pasted text marker (FLYBOT-ROOM-SHARE, opsjoin/leave/line). The/i/links in Add friend: a pasted invite without https:// or /i/ fails with raw 'invite_not_found' #310/Invites are hard to make and accept: unnamed 'Copy invite link' in Add friend, link shown only in Sharing, cut-off paste box, no lasting result after Accept invite #343/Add friend shows 'This invite was already used' for every unknown, mistyped or truncated invite link (regression of #310) #347 are friend/Bot invites, a different flow.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_voidis 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 incore/can pin it.The cap half is already real:
FACE_MAX_MEMBERS = 8and the server answersroom_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-readyon the word:countdownLabel(:93-101) so the panel ticks like the rest of the join UI (Face-to-face join/waiting/leave polish: silent disabled Join, frozen countdown, waiting card twice, Leave next to Auto-send and unconfirmed in waiting, held lines shown twice #306), and it degrades honestly: today's join is immediate, per [Feature] Face-to-face group: Bots join one room by entering the same 6-digit number #83, so there is no confirm page.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.
Birfy commentedon Oct 22, 2026
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-readyset on that;needs-humanremoved. B path (public link) is not part of this issue.Where it lands, so nobody has to rediscover it:
FaceWaitCodeatplugins/social/client.js:565-571, rendered at:828and:1571; the countdown next to it iscountdown()(:1443) at:830/:1573, and it already ticks.call('social/face-join', { code })at:732(the typed 6 digits). The scan must land on exactly this call.core/package.jsonandplugins/social/package.jsonboth havedeps: {}. So the QR encoder has to be self-contained in the repo (a small vendored file with its licence, or written here), notnpm install qrcode. That is the bulk of the work and it belongs in the PR, not a later cleanup.BarcodeDetectortoday (getUserMediaexists only inspeech.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 platformBarcodeDetectorwhere 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:
:830already ticks.fetch/XMLHttpRequest/Imagesource, and no new dependency. Assert it in a test rather than trusting review.social/face-join(:732) as typed entry. A test proves there is no second endpoint or second code path.BarcodeDetectorunsupported each answer plainly and leave the 6-digit entry usable. No silent failure, no auto-join from a background scan.area:securitystays 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.Birfy commentedon Oct 22, 2026
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,maxMembersapplies, and five distinct refusals (expired,revoked,room_full,ended,already_member).Scope change for this issue: the client work splits in two.
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-readyuntil #39 lands, or we get a client that calls an endpoint that answers 404.Birfy commentedon Oct 22, 2026
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 existingsocial/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 platformBarcodeDetector, and a browser without it is told so before any permission prompt.feat(social): a room number as a QR code, and invite links into a room (
feat(social): a room number as a QR code, and invite links into a room (
feat(social): a room number as a QR code, and invite links into a room (
feat(social): a room number as a QR code, and invite links into a room (
feat(social): a room number as a QR code, and invite links into a room (
feat(social): a room number as a QR code, and invite links into a room (
feat(rooms): Invite by link in the Members card, with its QR, and Joi…