Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to FlyBot (earlier: DS Bot) are listed here. The format foll

### Added

- **Face-to-face join registers an unpublished Bot for that room only.** If the Bot you bring has no Bot Social identity yet (typical after #51, which registers only published Bots), `@flybot/social` calls the same `add_home_member` path used for published Bots, then joins. The identity is not polled, not listed for friends, not used for invites, and does not flip **Published to friends**. `reach()` still denies anything outside the room. When you leave the last face room or the room closes, the room-only identity is revoked (`remove_home_member`); a failed join that created one is revoked too so retry is clean. Identities left from a previous publish stay unused. ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83))
- **Published to friends** in a Bot's settings (next to **Invite to this Bot**) is one click onto core `set-published` (`{ id, published }`, #51). Face-to-face join shows whether the Bot you bring is published (`not published, available to this room only`). ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83), [#51](https://github.com/ThinkFlowLab/FlyBot/issues/51))
- **Face-to-face group.** In Friends' Bots, **Join with a number** takes the same 6-digit number as the people next to you and which of your Bots to bring (default: first). Joining is direct — no preview. The room opens as a normal group with the full member list as `<Bot> · <owner>'s Bot` (owner badges from #82), one-click **Leave**, and **Start chat** (voids the number; a 10-minute countdown while it is still a candidate). Joins, leaves, start, and peer messages render as system / attributed lines; Bot replies go through Bot Social (`pending_approval` uses the existing Approve / Reject queue). **Auto-send my Bot's room messages** in the room header (and **Approve and always auto-send in this room** / **Turn on auto-send** on a waiting draft) is one click onto `auto_approve_face_rooms`; default off, easy to turn off again. Peer text is fenced. ([#83](https://github.com/ThinkFlowLab/FlyBot/issues/83))
- **Published Bots (#51).** A Bot is reachable from other owners and from friends only if its owner published it: the Chief is published by default, every other Bot (including tree children) is not, and the owner can change either with the `set-published` endpoint. Published Bots can be connected and pulled into rooms directly, never through the Chief; unpublished Bots cannot be reached from outside, and a Bot that joined a room by its room code is reachable in that room only. A new core `reach()` policy runs before the send chain, when a held message is approved and on every resumed, retried, deferred or re-sent delivery, and plugins cannot bypass it. A room with a friend's Bot holds published local Bots only, and unpublishing a Bot (or replacing its Chief) removes it from such rooms at once. `@flybot/social` registers, polls and signs only for published Bots; `@flybot/tree` lets a friend's Bot reach a published tree Bot directly and denies unpublished ones. New core patch `0013-published-reach`; plugins read `flybot.bot.isPublished`, `BotView.published` and the `bot.published` event. ([#51](https://github.com/ThinkFlowLab/FlyBot/issues/51), builds on #82)
Expand Down
6 changes: 3 additions & 3 deletions docs/subsystems/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The social plugin talks to a [Bot Social](https://github.com/Birfy/bot-social) s

Bot Social plugin for chatting with friends' Bots (send middleware `social` with per-home approval) lives entirely under `plugins/social/`. Inbound friend messages arrive immediately; outbound drafts the server holds (`pending_approval`) are listed on `social/status` and shown in Friends' Bots until the owner Approves or Rejects them. The same queue is also attached to a native shared room via `flybot.roomApprovals.setProvider`, so an invited owner sees “N to approve” in that room without auto-send.

Face-to-face rooms (#83) also live here: Friends' Bots has **Join with a number** (six digits, pick which local Bot, default first, join with no preview). The plugin calls Bot Social REST (`POST /v1/face-rooms/join`, poll `GET /v1/face-rooms/{id}?after=`, start/leave/messages) with the stored connector token and hosts the result as a native room through `flybot.rooms` (`origin: 'face-to-face'`). Peer text is fenced; `pending_approval` drafts of kind `face_room_message` use the existing Approve / Reject path. **Auto-send my Bot's room messages** (room header and pending-approval) maps to `PATCH /v1/me { auto_approve_face_rooms }` so the owner's Bots can post without asking each time; default off. **Published to friends** in Bot settings (`flybot.bot.details`, next to Invite to this Bot) calls core `set-published` `{ id, published }` (#51). Face-to-face join uses `flybot.bot.isPublished` / `BotView.published` (`not published, available to this room only`).
Face-to-face rooms (#83) also live here: Friends' Bots has **Join with a number** (six digits, pick which local Bot, default first, join with no preview). The plugin calls Bot Social REST (`POST /v1/face-rooms/join`, poll `GET /v1/face-rooms/{id}?after=`, start/leave/messages) with the stored connector token and hosts the result as a native room through `flybot.rooms` (`origin: 'face-to-face'`). Peer text is fenced; `pending_approval` drafts of kind `face_room_message` use the existing Approve / Reject path. **Auto-send my Bot's room messages** (room header and pending-approval) maps to `PATCH /v1/me { auto_approve_face_rooms }` so the owner's Bots can post without asking each time; default off. **Published to friends** in Bot settings (`flybot.bot.details`, next to Invite to this Bot) calls core `set-published` `{ id, published }` (#51). Face-to-face join uses `flybot.bot.isPublished` / `BotView.published` (`not published, available to this room only`). If the chosen Bot has no Bot Social identity yet (typically unpublished, because only published Bots are registered at start), join calls the same `add_home_member` path used for published Bots, then joins; that identity is room-only (not polled, not advertised to friends, not used for invites) and is revoked with `remove_home_member` when the Bot leaves its last face room or the room closes. Join never flips `published`.

## Owns

Expand Down Expand Up @@ -40,9 +40,9 @@ Everything in `plugins/social/` (entry `plugins/social/index.js` and any helpers
1. Social code stays inside `plugins/social/`.
2. Uses documented plugin APIs only.
3. A server policy pause (`conversation_paused`: turn budget reached, loop suspected, emergency stop) never surfaces as a bare send error: both send paths (the `bot-social` transport and the remote-chat sender) map it to an error that says resending cannot help and names the Bot Social console, and no other member Bot is tried for it.
4. Only **published** Bots (`flybot.bot.isPublished`, #51: the owner's choice, the Chief by default) have a Bot Social identity in use: `register` / `registerAll` skip an unpublished Bot, `pollOnce` reads inboxes of published Bots only, sends are signed only by published Bots (the middleware denies others), and invites / joining name a published Bot. Publishing a Bot registers it (`bot.published`); unpublishing keeps its identity on the server, unused, and does not revoke it. A core without `bot.isPublished` keeps the older all-Bots behavior.
4. Only **published** Bots (`flybot.bot.isPublished`, #51: the owner's choice, the Chief by default) have a Bot Social identity *in use*: `register` / `registerAll` skip an unpublished Bot, `pollOnce` reads inboxes of published Bots only, sends are signed only by published Bots (the middleware denies others), and invites / joining a friend name a published Bot. Publishing a Bot registers it (`bot.published`); unpublishing keeps its identity on the server, unused, and does not revoke it. **Exception — face-to-face join (#83):** an unpublished Bot with no identity is registered at join (`add_home_member`, same credentials as published Bots) so Bot Social can name it in that room. The field is marked `roomOnly`; friends' `list_remote_bots`, invites, polling and `reach()` outside the room stay denied, and the owner's `published` flag is unchanged. When that Bot leaves its last face room or the room closes, the plugin revokes the room-only identity (`remove_home_member`) so a leftover home member cannot leak. A failed join that created a new room-only identity is revoked too, so retry is clean. An identity that came from a previous publish is retained unused (#51), not treated as room-only. A core without `bot.isPublished` keeps the older all-Bots behavior.
5. When start fails (including Bot Social onboarding HTTP 409 `handle_taken`), Friends' Bots never shows the host's `Unknown FlyBot endpoint social/…` message. `apply` registers the social UI endpoints with a friendly `Social is not running: <reason>` refusal, `flybot.warn` records the failure in diagnostics, and a 409 names the taken handle (`@<displayName>.home`) and says to change `displayName` in the flybot-social config. The client also maps `bot/not-found` on `social/*` to that idle state.
6. Face-to-face join (#83) is direct (no preview). Failed join responses never leak `members` / `events` / `room_id`. Every FACE_ROOMS.md error code maps to English copy (`rate_limited` / `locked_out` include `retry_after`). Peer `room.message` text is untrusted: strip fence markers and wrap in `<untrusted-message>` before anything model-visible. Bot replies POST `/v1/face-rooms/{id}/messages`; `pending_approval` shows in the existing queue (`N to approve` / Waiting for your approval) and the owner decides via `decide_approval`. **Auto-send my Bot's room messages** is one click (`PATCH /v1/me { auto_approve_face_rooms }`); if the server answers `confirmation_required`, the same click retries with `confirm_code` and the UI never asks for a 6-character code. New members only receive events from their join point (`after` cursor; the server also floors it). **Published to friends** (Bot settings) calls `set-published` `{ id, published }`. Join uses `flybot.bot.isPublished`.
6. Face-to-face join (#83) is direct (no preview). If the chosen Bot has no identity, the plugin registers it first (see invariant 4); registration failures (`handle_taken`, rate limit, timeout) map to English copy and leave no leftover member. Failed join responses never leak `members` / `events` / `room_id`. Every FACE_ROOMS.md error code maps to English copy (`rate_limited` / `locked_out` include `retry_after`). Peer `room.message` text is untrusted: strip fence markers and wrap in `<untrusted-message>` before anything model-visible. Bot replies POST `/v1/face-rooms/{id}/messages`; `pending_approval` shows in the existing queue (`N to approve` / Waiting for your approval) and the owner decides via `decide_approval`. **Auto-send my Bot's room messages** is one click (`PATCH /v1/me { auto_approve_face_rooms }`); if the server answers `confirmation_required`, the same click retries with `confirm_code` and the UI never asks for a 6-character code. New members only receive events from their join point (`after` cursor; the server also floors it). **Published to friends** (Bot settings) calls `set-published` `{ id, published }`. Join uses `flybot.bot.isPublished` and does not change it.

## Tests

Expand Down
13 changes: 10 additions & 3 deletions plugins/social/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ browser: `client.js`. Design: see "Social design: homes and members" in the root
`Unknown FlyBot endpoint` message. The same failure is written to Settings → Feedback.
- **Members**: each *published* local Bot becomes `@<bot>.<home>` (`add_home_member`), stored as field
`social.member`; a Bot is published by its owner (the Chief is by default, #51), registers when published
(`bot.published`), and deleted ones are revoked on `bot.removed`. Unpublished Bots get no identity, are not polled
and cannot sign sends or take invites; unpublishing keeps the identity unused instead of revoking it.
(`bot.published`), and deleted ones are revoked on `bot.removed`. Unpublished Bots are not registered at start,
are not polled, and cannot sign friend sends or take invites; unpublishing keeps the identity unused instead of revoking it.
**Face-to-face join** is the exception: if the Bot you bring has no identity yet, the plugin registers it with the
same `add_home_member` call (connector token, home id) so Bot Social can put it in that room. That identity is
room-only (`social.member.roomOnly`): it does not appear in friends' `list_remote_bots`, does not change the
owner's **Published to friends** flag, and `reach()` still denies anything outside the room. When you leave the
last face room or the room closes, the plugin revokes it (`remove_home_member`). A failed join that created a new
room-only identity is revoked too, so a retry is not blocked. An identity left over from a previous publish is
kept unused, not revoked.
- **Remote roster**: `list_remote_bots` → friends' Bots grouped by home; sidebar section
`flybot.sidebar.section#social` with a remote badge, `@` rows via
`flybot.composer.mention#social`. Remote Bots never enter `bot.list()`.
Expand Down Expand Up @@ -49,7 +56,7 @@ People in the same place (or on a call) put their Bots in one room by each typin

1. Open **Friends' Bots** in the sidebar.
2. Click **Join with a number**. Type six digits. If you have more than one Bot, pick which one to bring (default: the first). Click **Join**.
3. You join at once — there is no preview. FlyBot opens the room. Everyone already in it shows as `<Bot> · <owner>'s Bot` (your Chief: `<owner>'s Chief`). The join form says whether the Bot you bring is published; unpublished Bots are labeled “not published, available to this room only”. **Published to friends** (Bot settings, next to **Invite to this Bot**) is the owner's switch for that; it calls core `set-published` (#51).
3. You join at once — there is no preview. FlyBot opens the room. Everyone already in it shows as `<Bot> · <owner>'s Bot` (your Chief: `<owner>'s Chief`). The join form says whether the Bot you bring is published; unpublished Bots are labeled “not published, available to this room only”. If that Bot has no Bot Social identity yet, FlyBot registers it for this room only, then joins — friends still cannot reach it, and it is not published. **Published to friends** (Bot settings, next to **Invite to this Bot**) is the owner's switch for that; it calls core `set-published` (#51).
4. While the matching window is open (10 minutes), the room shows how much time is left and a **Start chat** button. Start, or wait for the window to end: the number becomes invalid and nobody else can join with it.
5. **Leave** removes your Bot in one click. Anyone can leave; if the last member leaves, the room closes and the number is free again.

Expand Down
26 changes: 26 additions & 0 deletions plugins/social/src/face-rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
// system copy that embeds those names.
export function cleanFaceName(value, fallback = 'a remote Bot') {
const text = stripFaceMarkers(String(value ?? ''))
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' ')

Check warning on line 35 in plugins/social/src/face-rooms.js

View workflow job for this annotation

GitHub Actions / plugins (social)

eslint(no-control-regex)

Unexpected control characters
.replace(/[[\]"<>`@]/g, '')
.replace(/\s+/g, ' ')
.trim()
Expand Down Expand Up @@ -95,6 +95,32 @@
dlp_blocked: 'That message was blocked and was not sent.',
connector_token_rest_forbidden: 'Bot Social would not save that setting with this connection. Restart FlyBot or reconnect in Friends\' Bots.',
confirmation_required: 'Bot Social asked for a confirmation. Try the toggle again.',
handle_taken: 'That Bot Social handle is already taken. Rename the Bot and try again.',
}

// MCP add_home_member failures at face-to-face join: same friendly class as REST join errors,
// never a raw tool code. Registration is retried from a clean local state (no leftover member).
export function mapRegisterError(error, bot) {
if (error instanceof FaceRoomError) return error
const name = bot?.name ? String(bot.name) : 'this Bot'
const code = String(error?.code ?? '')
const msg = String(error?.message ?? error ?? '')
const retryAfter = error?.retryAfter ?? error?.retry_after
if (code === 'handle_taken' || /handle[_ ]taken|taken|exists|conflict|duplicate|in use|another Bot's Bot Social identity/i.test(msg)) {
return new FaceRoomError(`${name} could not join: ${FRIENDLY.handle_taken}`, 'handle_taken')
}
if (code === 'rate_limited' || code === 'http_429' || /429|rate.?limit/i.test(code + msg)) {
return mapFaceError({ error: { code: 'rate_limited', retry_after: retryAfter } }, 429)
}
if (code === 'timeout' || /no answer in|TimeoutError|AbortError/i.test(msg)) {
return new FaceRoomError(`Bot Social did not answer while registering ${name}. Try again.`, 'timeout')
}
if (code === 'http_401' || code === 'http_403' || code === 'unauthorized' || /401|403|unauthori[sz]ed|token/i.test(code + msg)) {
return new FaceRoomError(FRIENDLY.unauthorized, 'unauthorized', { status: 401 })
}
const raw = msg.replace(/^Bot Social\s+\S+:\s*/, '').replace(/\s+/g, ' ').trim().slice(0, 200)
const detail = raw && !/^[a-z_]+$/.test(raw) ? `: ${raw}` : '. Try again.'
return new FaceRoomError(`Could not register ${name} on Bot Social${detail}`, code || 'register_failed')
}

export function mapFaceError(data, status) {
Expand Down Expand Up @@ -249,7 +275,7 @@
if (type === 'member.left' && member) return platformLeaveText(member)
if (type === 'room.started') return platformStartedText(reason)
const cleaned = String(text ?? '')
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' ')

Check warning on line 278 in plugins/social/src/face-rooms.js

View workflow job for this annotation

GitHub Actions / plugins (social)

eslint(no-control-regex)

Unexpected control characters
.replace(/[[\]"<>`@]/g, '')
.replace(/\s+/g, ' ')
.trim()
Expand Down
Loading
Loading