* fix: reference every bracket in zulip link labels
* fix: acknowledge backfill-pull-requests all before listing the pull requests
* fix: fail emote-sync on an unreadable discord server and count the uploads
* fix: skip emotes mattermost already has instead of failing them
* fix: stop the zulip side of emote-sync once zulip refuses the user account
* fix: write pull request actions as words in notifications
* fix: shorten the unknown zulip command echo
* fix: keep the full pull request title in its zulip topic
* fix: fail fourthwall update with a clear error when the order is not returned
zulip has no slash commands, modals, buttons, autocomplete or ephemeral
replies, so the bot is driven by mentioning it at the start of a message in a
team stream and it answers in the topic. help, emote-sync,
backfill-pull-requests, fourthwall update and similar are ported; the community
commands stay on discord.
commands are accepted only in the private team streams, because membership of
those is the only authorization signal zulip offers a bot, and the stream is
checked to actually be private rather than assumed. every reply is public in
the topic, which is a real downgrade from the discord commands that answer
ephemerally.
fixes a pre-existing bug the port uncovered: /backfill-pull-requests has never
done anything. getOpenPullRequests overwrote the graphql node id with the
numeric database id and never set node_id, which is the key both team paths
look a pull request up by, so every lookup missed and the command reported
success regardless. it now backfills only what a pull request is missing,
leaves existing threads and topics alone, and reports what it created, skipped
and failed.
the bot can now hear zulip. a long-poll event queue feeds github reference
expansion, github permalink snippets and the x.com mirror into the team
channels, reusing the same methods discord and mattermost already use.
the loop is the point of this change. zulip garbage-collects idle queues and
drops them on restart, so a dead queue is normal operation, and zulip-js
answered that by re-polling the dead queue once a second forever, silently. this
loop re-registers on a dead queue, backs off exponentially on anything else,
treats a long poll that outlives the server timeout as an empty poll, keeps a
floor between polls so a fast server cannot be hammered, says so when it has
been unhealthy for a while, and on shutdown aborts the poll and deletes the
queue.
expanders run only in an allowlist of private team streams. they pass
isPrivileged, so they would expose private repository titles and code if a
public stream were ever added to that list. the server tells us which streams
are private when the queue is registered, so that is now enforced rather than
assumed, and a stream whose privacy is unknown is treated as public.
mirrors the discord team pull-request forum in zulip stream 112: a topic per
public immich pull request, a notice on close, reopen and draft, and the topic
resolved with a checkmark when the pull request is merged or closed.
zulip topics are mutable strings with no id, so the bot stores the id of its
first message and reads the topic back before every rename. a human who renames
or resolves a topic is respected, instead of the bot silently creating a second
empty topic beside the real conversation.
realm settings can refuse an edit, a rename or a resolve: pull requests outlive
message_content_edit_limit_seconds and move_messages_within_stream_limit_seconds,
and resolving needs can_resolve_topics_group. every one of those degrades — it is
logged, a plain message is posted where the information would otherwise be lost,
and the webhook still succeeds.
the discord and zulip paths no longer depend on each other. discord's failure
used to skip the zulip topic entirely, which would have left topics missing and
permanently stale after any discord outage. discord still runs first and its
error still surfaces unchanged.
emote names are normalised for zulip, which lowercases names and treats
underscores as spaces, so colliding names get a numeric suffix and a second sync
is a no-op. the bot warns at boot when it is not subscribed to a stream it posts
to.
adds a zulip renderer and zulip routes for the eight team destinations, so
github feeds, purchases, reports and release alerts reach zulip alongside
discord and mattermost. webhook.service and schedule.service are untouched,
which is what the phase 1 seam was for.
zulip has no embeds or colour, so the renderer flattens a notification into
markdown and maps each accent to an emoji by meaning. untrusted github titles,
bodies and the customer-written merch order message are contained: bodies go in
a tilde quote fence sized to outrun any fence inside them, mentions are
neutralised, and link labels cannot be broken out of.
delivery is now isolated per platform. previously a platform failure stopped the
ones after it, and because community.* destinations are discord-only and the
handlers post community then team in sequence, a discord outage also silenced
zulip. notify now logs failures and never rejects; a webhook reports success
even when every post failed, so the log is the signal.
fhs pull requests and releases stay on mattermost only, as agreed.