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.d/1085-face-enter-internal-error.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enter chat in a face-to-face room no longer shows "internal error" after the other person has joined. The start request sends an empty JSON body, which Bot Social accepts ([#1085](https://github.com/ThinkFlowLab/FlyBot/issues/1085)).
10 changes: 8 additions & 2 deletions plugins/social/src/face-rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,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 78 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 @@ -448,7 +448,7 @@
|| (typeof member.color === 'string' && !HEX_ONLY.test(member.color) ? member.color.trim().slice(0, 32) : '')
if (color && !/[\n<>]/.test(color)) out.color = color
const src = member.avatar && typeof member.avatar === 'object' ? member.avatar : {}
const avatar = { ...(fromServer.avatar ?? {}) }

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

View workflow job for this annotation

GitHub Actions / plugins (social)

unicorn(no-useless-fallback-in-spread)

Empty fallbacks in spreads are unnecessary
if (typeof src.shape === 'string' && /^[a-z][\w-]{0,31}$/.test(src.shape)) avatar.shape = src.shape
const image = typeof src.image === 'string' ? src.image.trim() : (typeof member.image === 'string' ? member.image.trim() : '')
if (image && image.length <= 65536 && /^(https?:\/\/|data:image\/)/i.test(image) && !/[\s<>]/.test(image)) avatar.image = image
Expand Down Expand Up @@ -478,7 +478,7 @@
export function cleanFaceRole(value) {
if (typeof value !== 'string') return ''
return stripFaceMarkers(value)
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' ')

Check warning on line 481 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 @@ -588,7 +588,7 @@
if (type === 'room.started') return platformStartedText(reason)
if (type === 'room.lead_changed') return platformLeadText(member, reason)
const cleaned = String(text ?? '')
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' ')

Check warning on line 591 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 @@ -706,15 +706,21 @@
let response
let data
const sentAt = Date.now()
// A POST or PUT with no body never reaches the handler on the deployed Bot Social
// (social.botawaken.com, 2026-10-25): it answers 500 "internal error" before auth.
// The same call with `{}` is an empty JSON body and the handler runs. Enter chat
// is POST /face-rooms/:id/start, which otherwise sends nothing (#1085).
const verb = String(method || 'GET').toUpperCase()
const payload = body === undefined && (verb === 'POST' || verb === 'PUT' || verb === 'PATCH') ? {} : body
try {
response = await fetchImpl(url, {
method,
headers: {
authorization: `Bearer ${token}`,
accept: 'application/json',
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
...(payload !== undefined ? { 'content-type': 'application/json' } : {}),
},
body: body !== undefined ? JSON.stringify(body) : undefined,
body: payload !== undefined ? JSON.stringify(payload) : undefined,
signal,
})
const offset = serverClockOffset(response.headers?.get?.('date'), sentAt, Date.now())
Expand Down
24 changes: 24 additions & 0 deletions plugins/social/test/face-rooms.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,30 @@ test('REST join, poll, start, leave, and pending_approval send', async () => {
assert.equal(left.status, 'closed')
})

test('#1085: Enter chat sends a JSON body, so a bodyless POST is not an internal error', async () => {
// social.botawaken.com (2026-10-25): POST /v1/face-rooms/:id/start with no body
// answers 500 { code: "internal_error", message: "internal error" } before auth.
// The same POST with `{}` reaches the handler. Enter chat is that POST.
const fetch = async (url, init) => {
assert.equal(String(url), 'https://s/v1/face-rooms/frm_1/start')
assert.equal(init.method, 'POST')
const raw = init.body
if (raw === undefined || raw === '') {
return new Response(JSON.stringify({ error: { code: 'internal_error', message: 'internal error' } }), { status: 500 })
}
assert.equal(init.headers['content-type'], 'application/json')
assert.equal(raw, '{}')
return new Response(JSON.stringify({
room_id: 'frm_1', status: 'open', members: [], events: [], event_cursor: 1,
started_at: '2026-10-25T00:00:00.000Z',
}), { status: 200 })
}
const api = createFaceRoomApi({ server: 'https://s', token: 'otk_test', fetch })
const started = await api.start('frm_1')
assert.equal(started.status, 'open')
assert.equal(started.roomId, 'frm_1')
})

test('failed join never includes members or events', () => {
const err = mapFaceError({ error: { code: 'room_full', members: [{ handle: '@x' }], events: [1], room_id: 'frm_leak' } }, 409)
assert.equal(err.message.includes('frm_leak'), false)
Expand Down
Loading