Commit Graph

271 Commits

Author SHA1 Message Date
Stanko Krtalić e0e31846c9 Merge pull request #299 from rubys/fix-boost-soft-keyboard-action
Wire the inline boost link to the soft keyboard
2026-10-05 16:16:25 +02:00
Stanko Krtalić f2bdfde610 Merge pull request #298 from namespaceMarcello/search-operator-words
Search for operator words instead of parsing them
2026-10-05 16:15:23 +02:00
Stanko Krtalić acef0c71cf Merge pull request #295 from namespaceMarcello/messages-room-created-at-index
Index messages by room and creation time
2026-10-05 15:53:19 +02:00
David Heinemeier Hansson 345e63718f Update README with latest Campfire benchmark comparison (#315) 2026-10-05 07:52:23 -04:00
Marcello Costagliola e12651240c Drop the query plan test
The index is a schema change; a test that pins SQLite's plan for each
pagination scope locks in more than the change needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142qgjggdJ2KDdGk7RF9Xm9
2026-10-05 13:51:56 +02:00
Sam Ruby c057484dfb Wire the inline boost link to the soft keyboard
link_to passed `action: "soft-keyboard#open"` as a plain option, so it
rendered as an `action` attribute, which Stimulus never reads. The link
in messages/_actions.html.erb already passes it under `data:`; this one
now does too, so tapping "Add a boost" on a touch device opens the
keyboard for the boost form as the menu's link does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:06:53 -04:00
Marcello Costagliola 74bdbb0764 Keep the room_id index and date the migration in UTC
The composite index doesn't serve everything the room_id one did. In
SQLite that index is (room_id, rowid), so it also reads a room's messages
in id order, and it is the narrower one to count a room with. Keeping both,
as memberships already does, leaves those lookups as they were.

The migration was named after local time, two hours ahead of the UTC
timestamp Rails generates; it now carries a UTC one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
2026-10-05 01:39:36 +02:00
Marcello Costagliola 6cdcb459b9 Search for operator words instead of parsing them
The search box keeps only word characters, but FTS5 still reads AND, OR,
NOT and NEAR in the query as operators, so searching for "AND" or for
"salt AND" raised "fts5: syntax error" and answered with a 500. Quoting
each word makes FTS5 look for it as text, which is how it already treats
the same words in lower case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
2026-10-05 01:08:53 +02:00
Marcello Costagliola 5cb4316133 Index messages by room and creation time
Every page of a room's messages (opening the room, scrolling back, the
refresh after reconnecting, the bot API) selects WHERE room_id = ? ORDER BY
created_at LIMIT 40. With only the room_id index, SQLite reads every
message in the room and sorts them in a temporary B-tree to return 40, so
these requests grow with the room's history.

A composite index on (room_id, created_at) lets SQLite read the 40 rows in
order straight from the index. It covers every lookup the room_id index
served, so that one goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
2026-10-05 00:55:24 +02:00
David Heinemeier Hansson 254dd1d46f Add Django and Laravel benchmarks and label the baseline Rails (#294) 2026-10-04 18:18:31 -04:00
David Heinemeier Hansson 6c7f8fa15f Document other Campfire implementations and benchmarks (#293)
* Document other Campfire implementations and benchmarks

* Remeasure README benchmarks with the latest Ruby optimizations

* Shorten the README benchmark context
2026-10-04 14:10:38 -04:00
David Heinemeier Hansson 659f95748a Preload only uncached messages and reduce rendering overhead (#292)
* Preload only uncached messages and reduce rendering overhead

* Keep benchmark summaries without raw JSON results

* Use Ruby benchmark drivers and keep generated results out of the repo
2026-10-04 12:36:36 -04:00
Stanko Krtalić 90b330024d Merge pull request #284 from basecamp/mention-menu-spacing
Fix mention menu spacing and check the registry login before releasing
v1.5.1
2026-09-26 13:03:34 +02:00
Stanko K.R. e5cfffda67 Check the registry login before releasing
The GitHub release was created before the image push failed on a missing
docker login, leaving the release half done.
2026-09-26 11:16:03 +02:00
Stanko K.R. 87ba7aa3cc Keep the message body list styles out of the mention menu
The composer editor carries the lexxy-content class, so the !important
list rules for message bodies hit the menu's ul and li too, leaving the
items cramped and pushed in from the left.
2026-09-26 11:16:03 +02:00
Stanko Krtalić 12249b9b3a Merge pull request #283 from basecamp/composer-drafts
Keep an unsent message as a draft while switching rooms
v1.5.0
2026-09-26 10:43:55 +02:00
Stanko K.R. 5c5821a395 Keep an unsent message as a draft while switching rooms
The composer stores what's being written in localStorage per room and
restores it when the room is opened again. Sending clears it.
2026-09-26 10:38:13 +02:00
Stanko Krtalić 8fa6138fb9 Merge pull request #254 from basecamp/dependabot/bundler/web-push-3.1.0
build(deps): bump web-push from 3.0.2 to 3.1.0
2026-09-26 10:28:20 +02:00
Stanko Krtalić beaf467a38 Merge pull request #257 from basecamp/dependabot/bundler/mocha-3.1.0
build(deps-dev): bump mocha from 2.7.1 to 3.1.0
2026-09-26 10:27:58 +02:00
Stanko Krtalić 9a258bd1a0 Merge pull request #224 from basecamp/replace-trix-with-lexxy
Replace Trix with Lexxy
2026-09-26 10:27:29 +02:00
Stanko Krtalić f8e36e9761 Merge pull request #282 from basecamp/dependabot/bundler/rubyzip-3.4.0
build(deps-dev): bump rubyzip from 3.0.2 to 3.4.0
2026-09-26 10:23:25 +02:00
Stanko Krtalić 83e7a7f4c9 Merge pull request #281 from basecamp/dependabot/github_actions/github-actions-f89073ee97
build(deps): bump the github-actions group with 4 updates
2026-09-26 10:23:12 +02:00
Stanko Krtalić eadbaab7d2 Merge pull request #262 from excid3/redis-url-env
Support Redis configuration with REDIS_URL env var
2026-09-26 10:22:53 +02:00
Stanko K.R. b5d3543e64 Test that Trix-formatted messages render and survive editing 2026-09-26 10:21:40 +02:00
Stanko K.R. 1694accbf8 Let tables through the message sanitizers
Lexxy imports pasted and Markdown tables, and the tag sanitizer dropped
them with everything in them on render.
2026-09-26 10:04:38 +02:00
Stanko K.R. 8bdff5ddc6 Run the link preview hardening tests against both stored forms
Every case only exercised the Trix attribute form, so dropping the URL
validation from the Lexxy content parser went unnoticed.
2026-09-26 09:59:13 +02:00
Stanko K.R. 6acad0549d Rebuild every attachment before editing a message
The editor keeps an attachment's content as it finds it, so a hand-written
embed with a url but no href reached the editor with its markup unvalidated,
and a mention saved under Trix's editor carried the generic octet-stream
content type the editor doesn't permit and was dropped on save. Rebuilding
each attachment from its attachable renders the hardened preview partial
and restores the mention content type.
2026-09-26 09:51:05 +02:00
dependabot[bot] 6e7dd2fa0a build(deps-dev): bump rubyzip from 3.0.2 to 3.4.0
Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 3.0.2 to 3.4.0.
- [Release notes](https://github.com/rubyzip/rubyzip/releases)
- [Changelog](https://github.com/rubyzip/rubyzip/blob/main/Changelog.md)
- [Commits](https://github.com/rubyzip/rubyzip/compare/v3.0.2...v3.4.0)

---
updated-dependencies:
- dependency-name: rubyzip
  dependency-version: 3.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-26 07:33:43 +00:00
Stanko K.R. eab554aa4b Adapt the Lexxy composer to main's link preview hardening
Main strips a javascript: href from a preview node before it's parsed, so
tell legacy Trix attachments apart by their filename instead. escapeHTML
moved to the string helpers, and the unfurling system test now drives
the Lexxy editor.
2026-09-26 09:26:53 +02:00
Stanko K.R. 1bb7ef7c17 Fix Codex's code review comments 2026-09-26 09:13:44 +02:00
Stanko K.R. c38e77a897 Ensure backwards compatibility with Trix 2026-09-26 09:13:35 +02:00
Stanko K.R. 9b1602d088 Polish 2026-09-26 09:13:27 +02:00
Stanko K.R. 47bc5f5425 Replace Trix with Lexxy 2026-09-26 09:13:27 +02:00
dependabot[bot] 07f2482970 build(deps): bump the github-actions group with 4 updates
Bumps the github-actions group with 4 updates: [ruby/setup-ruby](https://github.com/ruby/setup-ruby), [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `ruby/setup-ruby` from 1.321.0 to 1.324.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/95ef2b042f9d7a56d8268cba8559e2842e2ad01b...a0102e0972be65f351c307e2d64b9314a57c8073)

Updates `zizmorcore/zizmor-action` from 0.6.3 to 0.6.4
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/70fb788f84895a7701f5643d103d587e460b5c99...cc914d7f3750a2d13d75c7f184a1060aa0e9d482)

Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-version: 1.324.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-25 19:35:29 +00:00
Sam Ruby 91d294f4a0 Mint the tampered attachable sgid without extending a Room
The companion to #279: the same per-instance `extend` appeared in
test/lib/rails_ext/action_text_attachables_test.rb. `attachable_sgid` is
`to_sgid(expires_in: nil, for: ActionText::Attachable::LOCATOR_NAME).to_s`,
so mint that directly; the minted bytes and the assertion are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 14:29:07 -05:00
Jeremy Daer 9ff6b4a381 Keep .claude out of the Docker build context (#266)
Agent worktrees under .claude/worktrees are whole extra checkouts of
this repo, and the build context picks them up. .claude is local
session state and never belongs in an image.
2026-09-22 14:09:33 -05:00
Jeremy Daer e3b4cbc674 Bump surfguard to the published 0.2.0 (#271)
Moves the pin from the pre-release 910be91 (0.1.0) to 59e278c, the v0.2.0
tag Fizzy already runs. The default policy now admits IPv6 only inside
IANA-allocated unicast prefixes instead of default-allowing reserved and
unallocated space. No call-site changes: both shim entry points take the
new policy keyword's default.
2026-09-22 14:09:24 -05:00
dependabot[bot] b7da9c63f7 build(deps): bump zizmorcore/zizmor-action in the github-actions group (#277)
Bumps the github-actions group with 1 update: [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action).


Updates `zizmorcore/zizmor-action` from 0.6.2 to 0.6.3
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/3dc1ecc9bcb9e94e9b2c709687979e1298497054...70fb788f84895a7701f5643d103d587e460b5c99)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 14:09:10 -05:00
Rosa Gutierrez ed0f9a5dba Merge pull request #279 from rubys/attachment-test-without-extend
Mint the tampered sgid without extending a Room
2026-09-20 21:08:13 +02:00
Sam Ruby 0ed0af44ef Mint the tampered sgid without extending a Room
The invalid-sgid test made one live Room attachable — `rooms(:pets).tap
{ |r| r.extend ActionText::Attachable }` — only to call
`attachable_sgid` on it. That method is `to_sgid(expires_in: nil,
for: ActionText::Attachable::LOCATOR_NAME).to_s`, so the test can mint
the same sgid directly and drop the per-instance extend; the assertion
(a signature that does not verify resolves to MissingAttachable) is
unchanged, and so are the bytes it tampers with.
2026-09-20 11:19:14 -04:00
Rosa Gutierrez 977cbcd135 Merge pull request #276 from basecamp/card-7348960853-room-render-injection
Render link previews only from web URLs
2026-09-11 21:29:14 +02:00
Rosa Gutierrez 5742dfaf73 Cover markup-only OpenGraph title and description in link previews
Link previews fetch a page's OpenGraph title and description, and
Opengraph::Metadata strips tags from both before the values reach the
browser. When a field consists entirely of a markup tag, stripping
leaves it blank, the metadata fails its presence validation, and the
unfurl endpoint returns no content, so no preview is produced.

Add regression tests at the model and controller layers that pin this:
a title or description made only of a markup tag is stripped to blank
and rejected, and the endpoint answers 204. The existing sanitize tests
only cover fields that keep non-blank text after stripping, so this
blank-and-rejected path was previously untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 21:11:13 +02:00
Rosa Gutierrez 8722057545 Keep one escapeHTML, and make it safe in an attribute
There were two: the one in dom_helpers escaped through a text node, which
leaves double quotes alone, so it could not have closed the hole in the
link preview's img src.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 21:10:57 +02:00
Rosa Gutierrez e6022a52c3 Assert only that the preview image gained no extra attributes
Pinning the exact attribute set also pinned which of them Trix's own
sanitizer keeps, which is not what this test is about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 21:10:57 +02:00
Rosa Gutierrez c1ad057db8 Escape the OpenGraph image URL in link previews
Pasting a link builds the preview by interpolating the unfurled metadata
into an HTML string. The image URL went into src="..." unescaped, so a
page whose og:image carries a double quote closes the attribute early and
everything after it becomes attributes on the preview's img element.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 21:10:57 +02:00
Rosa Gutierrez 436ea06457 Read a preview's host as a name by its last label
A domain name ends in a word, which is what keeps it from reading as an
address. "0x7f.0.0.1" carries a dot and a letter, so the previous shape
check let it through while a browser fetched 127.0.0.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 20:25:35 +02:00
Rosa Gutierrez 9e19658ee0 Take a link preview's host as a domain name, not an address
A browser rewrites the many spellings of an address into one before it
fetches, so "http://2130706433/rooms/1" arrives at 127.0.0.1 while a
comparison here still reads the digits. A preview names a page on the
public internet, so require its host to look like a domain name and leave
the rewriting race alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 20:21:01 +02:00
Rosa Gutierrez 32e3e5aea8 Bust the cached message presentation
The room caches each message's rendered presentation, and its key can't
see the link preview partial, which ActionText renders by name rather than
through a render call the digestor can follow. Without a new version, a
message already in the cache would keep its old preview.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 19:20:46 +02:00
Rosa Gutierrez ef4b88d748 Compare a preview's host to ours with the escapes resolved
Ruby leaves a percent-escape in URI#host, so "https://%77ww.example.com"
read as a different host than the one Campfire answers on while a browser
unescaped it straight back to us. A host that carries an escape, or a
trailing dot, is now measured the way the browser will read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 19:14:59 +02:00
Rosa Gutierrez eceec2898b Keep a link preview's link and image off this Campfire's own host
A preview belongs to the page it previews, so both URLs point somewhere
else. An absolute URL on our own host passed the scheme and host checks,
and every reader's browser fetched it with their session attached, which
turns a message into a GET request made on the reader's behalf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186eyzivcTn6wqjEE4Wnxdt
2026-09-11 19:08:54 +02:00