A thread reply reached everyone in the channel. Now the server names the
people taking part (starter and repliers, minus muted forum topics); only
they hear a reply, plus mentions and replies to you. The thread chip shows
a dot and N new, the Threads button counts threads with news, and the
sidebar dots a channel with news in a thread you follow. Opening clears it.
A new forum_blog switch on channels (off by default), turned on and off
through the same channel permission toggle as Forum mode and NSFW, so only
someone who can change the channel's settings can flip it. The channel list
carries it.
With it on, a topic belongs to its author. A reply in the topic's thread is
part of the post when the topic author's own account wrote it (and it is not
a bot or relayed message); everything else is a comment. Nothing is stored
per message, so turning it on or off needs no migration and changes nothing
on disk. The rule lives in src/forumBlog.js.
The server works the answer out and sends it: thread history marks each reply
post_part and says the topic is a blog post, a new reply carries post_part,
and topic cards and live thread updates carry a comments count alongside the
reply count. user_id comes from the signed-in socket, so nobody can make
their reply count as the author's, and bot replies are always comments.
The push subscribe routes (socket and /api/push/subscribe) refuse endpoints
that resolve to a private address, but unlike bot callbacks they ignored the
HAVEN_ALLOW_PRIVATE_CALLBACKS opt-in. A self-hosted ntfy for UnifiedPush on
the same Docker network as Haven resolves to a private address from inside
the container, so every new registration was dropped. Existing subscriptions
kept working because the check only runs on subscribe, so it surfaced only
once a phone got a new endpoint.
The socket route dropped refused registrations silently, leaving the device
without notifications and nothing in the logs. Both routes now log why a
subscription was refused. .env.example notes that the setting covers push
endpoints too.
Co-authored-by: bo0sted <mc526+github@proton.me>
Admins download the template under Settings > Admin > Server Template, and import
one with a preview of what it will create or update. Add missing leaves existing
channels, roles and settings alone; Replace layout updates them to match. Neither
mode deletes anything. Members, messages, DMs, keys, invite and channel codes,
webhook tokens and secret settings are never exported. Settings go through an
allowlist, and imports are validated strictly: sizes, names, references, image
types and checksums.
The channel-name rule, built-in theme list, automod word and escalation checks
and image sniffing now each live in one place, since the template code shares
them.
Co-authored-by: ancsemi <ancsemi@gmail.com>
When a self-destructing message expires, its files have always been moved
to deleted-attachments for the retention window, so a moderator can
recover something abusive that was posted on a short timer. That is the
right default for community servers, but small servers where everyone
trusts each other may want those files gone for good when the timer
runs out.
A new "Keep self-destructed attachments" toggle sits under "Keep deleted
files for (days)" in Admin settings, Cleanup. It is on by default, so
nothing changes for existing servers. When it is on, the files are held
for the same retention period as other deleted attachments. When it is
off, the sweep deletes them directly instead of holding them.
Only an explicit off deletes for good. If the setting can't be read (for
example, a busy database), the files are still held, so a read failure
never destroys evidence. The server only accepts true or false for the
setting.
This commit also fixes a loophole that let anyone stop a self-destructing
message's attachments from being removed, just by quoting the message or
linking its attachments before the timer ran out. The message went away,
but its attachments stayed in uploads for good.
Self-destructing messages can no longer be replied to or quoted by
anyone, mods included, so the sender can't accidentally undo their own
self-destruct either. The server also drops a reply to one. Mods can
still keep a message by pinning or protecting it, which removes the
timer as before and allows replies and quotes again. When the timer
runs out, a link to the attachments posted in any other message no
longer keeps them. The link text stays, but it stops working.
Co-authored-by: bo0sted <mc526+github@proton.me>
Expired attachments were unlinked at once, so something abusive posted on
a 30 second timer left moderators nothing to recover. They now go the way
a deleted message's files do: only the sender's own, kept while a quote or
a profile still uses them, and into deleted-attachments for the retention
window. The send dialog says so.
* Self-destructing messages
A flame toggle next to the mic in channel composers. Sending with it on
asks how long the message lives (1 minute to 24 hours, in minutes or
hours), then the message and the attachments sent with it are deleted
when the time runs out. The sender's own files are removed for good,
skipping deleted-attachments. Each message shows "Self-destructing in
..." under its content until then.
On the server one timer waits for the soonest deadline instead of
polling: it catches up at startup, sleeps when nothing is left, wakes
when a new self-destructing message is sent, and moves on when the
message it waits for is deleted early.
* Self-destruct modal: show how long until it deletes as you type
Updates on every keystroke and when switching between minutes and hours. Spelled out exactly ("1 hour, 30 minutes") rather than rounded, and in the reader's language.
* Self-destruct: allow timers as short as 30 seconds
* Self-destruct: voice messages too
With the flame on, clicking the mic asks how long first and only starts
recording once a time is confirmed; backing out records nothing and keeps
the flame on. The timer counts from when the recording is sent, and
turning the flame off while recording sends it as a normal message.
* Self-destruct: GIFs follow the flame too
In a channel a GIF used to go straight to the server, so it ignored the
self-destruct toggle. With the flame on it now goes through the
composer's send, the way DM GIFs already do, so it asks how long first.
---------
Co-authored-by: bo0sted <mc526+github@proton.me>
A pure move: the two admin routes leave server.js for
src/routes/extensionUpdates.js (io is read through late, as backup.js does,
since it does not exist yet at registration), and the Settings UI leaves
app-admin-controls.js for a new app-extension-updates.js mixin. Proved with
an identical client method set and identical route handlers.
For servers that can only reach the internet through a proxy: set
https_proxy, http_proxy and no_proxy in .env and Haven's outgoing requests
(link previews, the media proxy, bot callbacks, push notifications, update and
DDNS checks) go through it. Only http:// proxy addresses are supported, and
voice is unaffected.
With no proxy set, nothing changes. With one set, Haven warns at startup that
the proxy becomes part of its security: requests through it cannot be pinned
to the address Haven checked, so the proxy should be limited to the
destinations Haven needs. .env.example and GUIDE.md ("Outgoing Proxy") explain
it. Proxy passwords are hidden in the log, HTTPS certificates are checked
against the real target, and proxied requests time out and can be cancelled.
- Discord replies relayed into a Haven thread are shown as "[BOT] name",
live and in thread history, the way channel history already shows them,
so a Discord nickname cannot pass for a Haven member's name.
- A Discord post update is passed on only for what actually changed on
Discord (title, tags, lock). Discord archiving or unarchiving a quiet post
no longer reopens, retitles or retags a topic a Haven moderator changed.
- A topic marked NSFW is not sent to a Discord forum that is not
age-restricted; the author is told and the pairing shows why.
- The test hooks that point Ferry at a stand-in Discord only work when
NODE_ENV is test.
server.js goes from 6,600 lines to 3,700. Profile pictures and personas,
image and file uploads, sounds/emoji/stickers, backup and restore, link
previews and the media proxy, the Discord import, and the background jobs
(auto-cleanup, auto-backup, dynamic DNS, updates) each live in their own file
under src/routes/, registered from the same spot in server.js they used to
occupy, so the order of every route and middleware is unchanged.
The code was moved, not rewritten. A scope analysis of server.js decided what
each file needs and gives back; the only edits are mechanical ones it placed:
- values server.js creates later or reassigns while running (the socket
server, bot audio) are read through a small `late` object when used, so a
route never holds a stale copy
- relative require() paths and __dirname are adjusted for the new folder
Checked by: rebuilding the old server.js from the new files (every line the
same apart from indentation, every template literal byte-identical); the
route table (all 126 layers, same paths, methods, middleware and order); each
of the 107 route handlers' source matching the original once the rewrites
above are undone; the full test suite; and a live run hitting endpoints from
every moved file.
Went through all of them (about 220 across server.js and src/). The ones
that could leave data, settings or files wrong without anyone knowing now
log a short warning: failed file moves and deletes after a delete or
restore, backup entries that could not be added, startup steps like the
sticker folder or the Referrer-Policy, auto-role grants on join, audit log
writes, attachment tags dropped on send, webhook delivery status, the AFK,
temp-channel and disk-space timers, DM call and push lookups, Discord import
files that could not be parsed, Ferry's loop guard and automod checks, the
media cache folder and the one-time data folder migration. Ones that repeat
on busy paths log at most once every few minutes.
The rest are expected failures (a temp file already gone, a socket already
closed, untrusted input with a fallback) and each now carries a comment
saying so instead of a bare catch {}. Two guards that could never fire were
removed. No bare catch {} is left in the server code.
Found while going through every silent catch in the server:
- a personal "deny" on a permission was skipped if reading it failed, so a
role could still grant it (server routes, socket permission checks, and the
list that decides what a non-admin may hand out to others)
- deleting your own messages, singly or in bulk, went ahead when a deny on
that could not be read
- a banned account was given voice relay (TURN) credentials if the ban
check errored
- link previews went ahead for hosts the admin had not allowed if the
automod gate errored, which is the IP leak that gate exists to stop
- the FCM privacy switch stayed on if it could not be read at startup
- a login could be issued with no expiry if the session length setting
could not be read; it now falls back to 7 days
- someone who turned activity sharing off, or went invisible, could have
their activity shown if their preference could not be read
- a failed read of the IP ban list cleared every IP ban for 30 seconds; the
last known list is kept instead
- a backup restore overwrote the live database even when saving the safety
copy failed, and that copy could miss writes still in the WAL; it now
checkpoints first and stops if the copy cannot be made
- a corrupt android-beta-signups file was read as empty and the next signup
wiped the rest
- failing to remove ADMIN_RESET_PASSWORD from .env (which resets the admin
password on every start) is now reported with what to do