Commit Graph
9 Commits
Author SHA1 Message Date
ancsemi 86f79ed0b4 Ferry bridges forums: a Discord forum pairs with a Haven forum, new posts on either side become posts on the other with title, tags and author, replies cross both ways, and which topic is which post survives a restart; a forum only pairs with a forum 2026-10-02 00:20:14 -04:00
ancsemi 16d1369225 Discord bridge: @everyone and @here from Discord ping only when Discord says the message really pinged everyone, and a Haven role's name typed on Discord no longer pings that role 2026-09-23 21:26:29 -04:00
ancsemi b7c24b5b1c Ferry: Discord role and channel mentions arrive as names, and with pings on an @Role pings both sides when the Discord role is open to everyone; #channel links cross in both directions 2026-09-23 19:34:32 -04:00
ancsemi 9c5dc4b097 Discord emotes show as pictures on both sides of the bridge
A custom emote relayed from Discord arrived as its bare :name: shortcode, and one typed in Haven so it would show on the Discord side stayed as text here. Haven now draws <:name:id> inline, fetching the picture once through the server so nobody's browser talks to Discord's CDN, and a Haven :name: goes out as the paired server's own emote of that name. Reported by Raidenphantom.
2026-09-09 19:08:16 -04:00
Amnibro 1fd5af3c8c Ferry: relay a stream bot's link and thumbnail, not just its ping line (#5557)
Promotion bots (CouchBot and friends) type the ping line and put the stream link, title, and thumbnail in a rich embed. Ferry only read the embed when the Discord message had no text, so Haven got "X is live" with nothing to click or look at. Rich embeds are now read alongside the typed text, the promoted link is policy-checked on its own so an allowlist server drops the link and keeps the announcement, and Discord's own unfurls of typed links stay ignored.
2026-09-03 21:09:43 -04:00
ancsemi 08e741708c Relay image-bot embeds as pictures rather than links to unfurl (Ferry)
Reported by Raidenphantom against Ferry. SaucyBot and the bots like it post the
picture as an embed image with the source page in embed.url, and Ferry was
relaying only the summary, so what reached Haven was a link. Haven then unfurled
it, which is the fragile path: a channel full of them ends up with dead
previews, and the picture was the point of the message.

The embed image now comes through as a URL of its own, which Haven renders as an
ordinary chat image. Where an image is present the source link is dropped, since
that is the thing that would be unfurled, and the title and description are kept
for context. Embeds with no image relay exactly as before, link included, so
sharing an ordinary link is unchanged. Every embed on the message contributes
its image, because these bots post multi-image sets as several embeds.

The URL goes in the media half rather than the authored half, matching how
attachments are already handled: Ferry builds it out of Discord's response
rather than anyone typing it, and putting it through the link filter would throw
the whole message away on an allowlist server. That loosens nothing. Whether a
viewer's browser actually fetches the image is still decided by the preview
allowlist at render time, which is the control that stops a third-party host
seeing everyone who scrolls past.

No new setting. The concern raised was disk space, and there is none to spend:
Ferry passes the URL through and re-hosts nothing, exactly as it already does
for attachments.
2026-08-30 00:40:05 -04:00
ancsemi 790816fcb1 Make Ferry usable by people other than the admin, and bridge @mentions
Three problems reported from real use:

Ferry appeared to relay only the admin's messages. It was the permission: no
default role holds use_ferry, so an admin sets Ferry up, tests it successfully
as an admin, and it silently does nothing for everyone else. In mirror mode
there is not even a toast, by design, so there was no way to tell. The panel
now has a "Choose who can send" step listing every role with a switch, writing
the same permission the Roles editor does. Nobody should have to go hunting
through another screen to make the feature they just configured work.

The channel dropdowns were flat lists. Sub-channels were indistinguishable from
channels, and Haven allows the same display name under two different parents,
so the list could show two identical entries with no way to pick the right one.
Haven channels now nest under their parent, Discord channels group by their
Discord category, and a sub-channel whose parent is not visible lands under
"Other" instead of disappearing.

@mentions did not cross. Discord only pings when the text contains <@id>, so a
relayed "@someone" was inert no matter what the pings setting said. Outgoing
names are now resolved against the destination server, and only an exact,
unambiguous match is turned into a real ping: two people answering to one name
means it stays plain text, because pinging the wrong stranger is worse than not
pinging. Incoming mentions use the message's own resolved list, so they are
exact rather than guessed. The Server Members intent is now requested when
either DMs or pings are on, not just DMs.
2026-08-27 17:47:49 -04:00
ancsemi e237a8d72b Close four Ferry gaps found in review, and handle Discord edits and emotes
Security:
- The Discord DM recipient came from the client and was never validated or
  authorized. Scoping the autocomplete to paired servers was not a control,
  since the id travels with the send and a client could skip the lookup. The id
  is now shape-checked, and the recipient is confirmed to be a member of a
  server the channel is actually paired with before anything is sent. A channel
  with no pairings can no longer send a DM at all.
- Member lookups had no rate limit. Each one fans out to up to five Discord
  calls, and a burst of 429s is how a bot token gets temporarily banned, so
  they now have their own flood bucket.
- use_ferry was checked without a channel, so a channel-scoped role grant
  silently did nothing and only a server-wide grant worked. Same class of bug
  as #5433.
- The composer remembered a chosen DM recipient's id but not who it belonged
  to, so editing the name by hand sent a private message to whoever was picked
  before. The id is now bound to the exact recipient text and dropped the
  moment it stops matching.

From @Raidenphantom's independent bridge, which surfaced two real gaps:
- A Discord edit is now applied to the Haven copy instead of being ignored.
  Messages we never relayed are not resurrected by an edit, which is what stops
  a third-party bot's delayed image embed from arriving after its original was
  filtered out, and embed-only updates are ignored so a link preview cannot
  replace the author's text.
- Custom emotes arrived as raw <:name:id> mid-sentence and now read as :name:,
  rendering as a real emoji when the Haven server has one by that name.

Inbound Discord messages also now pass through the automod link check. A bridge
is an excellent spam vector, and relayed messages were skipping the controls
every Haven member is held to.
2026-08-27 16:39:31 -04:00
ancsemi 7b7c47f669 Add Ferry, a two-way Haven and Discord message bridge (server side)
Each Haven server brings its own Discord bot, since Discord caps unverified
applications at 100 guilds. Reading uses a gateway WebSocket, because Discord
has no outgoing webhook for new messages. Writing uses a per-pairing Discord
channel webhook rather than the bot user, which is the only way a relayed
message can carry the Haven author's own name and avatar.

Pairings live in ferry_links and act as the allowlist, not just a routing
table: a member can only reach Discord channels an admin paired with the Haven
channel they are standing in. Each pairing picks its own direction (two way, or
either one way) and its own outgoing mode (mirror everything, or only messages
addressed with the "=>" prefix). Sending needs the new use_ferry permission.

The trigger is "=>" and not ">>" because a leading ">" is Haven's blockquote
marker, so an unresolved target would render as a quote instead of visibly
failing. Resolved targets are stripped from the stored message and recorded in
messages.ferry_target so history shows a destination badge instead of raw
routing syntax.

Loop prevention drops the gateway echo of our own webhook ids and, by default,
every other bot. Mentions are blocked by default so one member cannot ping a
whole Discord server through the bridge. DMs are off by default, outbound only,
and stamped with the author's name, since a bot cannot impersonate in a DM.

ws becomes a direct dependency; it was only reaching us through socket.io.
2026-08-27 15:17:17 -04:00