Under `set -e` any failure between mktemp and the trailing rm -rf exits the step first and leaks the checkout into /tmp — which is tmpfs on srv, so the leak is RAM. 44 leaked dirs / 4.1G had accumulated by 2026-08-13 (node-identity.yml additionally never removed $WORK at all). Same trap idiom psn-base/bootstrap.sh and the ops/bawnet workflows already use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GoToSocial integration into Nextcloud
🦥 Put a GoToSocial in your cloud!
Dashboard widgets for your GoToSocial notifications and home timeline, plus unified search over posts, people and hashtags, and a "share to GoToSocial" action on public share links.
A fork of nextcloud/integration_mastodon
by Julien Veyssier, adapted for GoToSocial servers. AGPL-3.0, same as upstream.
Why this fork exists
GoToSocial implements the Mastodon client API, so Mastodon clients mostly work against it — but one assumption doesn't hold.
Mastodon serves a web page for every status it knows about, including ones
that arrived over federation. GoToSocial serves a page only for statuses that
are local to the server. A client that builds a link out of the server
address and a status ID, the way integration_mastodon does, therefore
produces a dead link for every remote post:
https://social.example.org/@someone@remote.example/01ABCDEF... -> 404
Every link in the dashboard widgets and in unified search was built this way:
// lib/Search/SearchStatusesProvider.php, upstream
return $mastodonUrl . '/@' . $entry['account']['acct'] . '/' . $entry['id'];
// src/views/DashboardHome.vue, upstream
return this.mastodonUrl + '/@' + n.account?.acct + '/' + n.id
Six sites in total: two PHP search providers, four in the two dashboard views.
The fix
Ask the API where the status actually lives instead of guessing. Every status
and account carries url (the HTML permalink) and uri (the ActivityPub ID),
and both point at the origin server.
Neither field alone is sufficient. Measured against a live GoToSocial database before the change was written:
| entity | rows | missing url |
missing both url and uri |
|---|---|---|---|
| remote statuses | 476,900 | 35,530 (7.4%) | 0 |
| remote accounts | 159,770 | 0 | 0 |
So the order is url → uri → the previous locally-built link, and that chain
is total across a real corpus rather than assumed to be.
Both fields are served as empty strings by some implementations rather than
null, so the code uses ?: in PHP and || in JS — ?? would let an empty
string through and produce a link to nowhere.
Also fixed: a bug that isn't GoToSocial-specific
The notification link in the dashboard widget was built from the acct of the account that acted on a status, glued to the ID of the status acted upon:
// upstream: /@whoever-favourited/your-status-id
return this.mastodonUrl + '/@' + n.account?.acct + '/' + n.status?.id
That target is wrong on every server that exists — Mastodon simply hides it behind a redirect. Notifications now link to the status the notification is about. Boosts link to the boosted status rather than to the boost.
Relationship to upstream
Forked at v5.2.0 (6a56211). All upstream copyright notices are intact and
the upstream CHANGELOG is preserved unmodified, including its links to upstream
pull requests — that history belongs to that project and is not rewritten here.
Only user-facing identity was rebranded. Internals that speak the Mastodon
client API — MastodonAPIService, MastodonAPIController, route names, config
keys — deliberately keep their names. That is the protocol they actually speak;
renaming them would make the code claim a protocol that does not exist, and
would make this fork expensive to rebase onto future upstream releases.
Translations are inherited from upstream. Strings reworded by the rebrand fall back to English until retranslated; everything else still works across all 124 locales.
This is not a competing app
Upstream's behaviour is a deliberate choice, not an oversight. Their code contains the origin-link version, written and then commented out:
// this is the account URL on its Mastodon instance
// return $entry['url'];
// this is on the instance where the search was done
return $mastodonUrl . '/@' . $entry['acct'];
On Mastodon, a local link is the better link: you stay logged in and can reply, boost and favourite as yourself. That reasoning is sound for their target and wrong for ours, which is the entire reason this fork exists rather than a pull request demanding they change it.
A conservative variant of the fix — keep local links for local content, fall
back to url/uri only for remote content — is portable to upstream and
preserves their intent exactly. That belongs in their tracker, not here.
Installation
Not on the Nextcloud app store. Install from a release archive:
# verify before unpacking — never install an unverified archive
sha256sum -c integration_gotosocial-<version>.tar.gz.sha256
tar -xzf integration_gotosocial-<version>.tar.gz -C /path/to/nextcloud/custom_apps/
occ app:enable integration_gotosocial
If your custom_apps lives on storage where root cannot chown (NFS with
root_squash, for instance), extract as the web server user instead of as root.
Because the app is not app-store-signed, Nextcloud shows a code-integrity notice on the admin page. Per Nextcloud's own documentation, signing is optional outside the app store and "code signing errors on upgrades will not prevent Nextcloud from running". It is cosmetic and expected.
Coexistence
This app uses its own app ID, so it installs alongside integration_mastodon
without conflict — useful if you connect to both a Mastodon and a GoToSocial
account. Each keeps its own credentials and must be connected separately.
Configuration
Set a default GoToSocial server address in the admin settings so users get a one-click OAuth connect. Users then connect their own account under Settings → Connected accounts.
Building from source
Requires PHP, Node 20 and npm 9/10 — all present in Debian trixie, no third-party toolchain needed.
make composer # PHP dependencies
make npm # JS dependencies + production bundle
make appstore version=1.0.0 # release archive + sha256
Releases are built by CI (.gitea/workflows/build.yml) so the archive is
reproducible from the repository alone.
Two things worth knowing about this build:
make composer(inherited from upstream) falls back to pipinggetcomposer.org/installerintophpwhen composer is not onPATH. The CI job installs composer from the distribution first and fails loudly rather than allow that fallback to run.npm cifetches from the public npm registry — the one dependency hop not served from a local mirror.package-lock.jsonpins every package with an integrity hash, which is the mitigation.
Project status
Version 1.0.0 — the fix is written, built and installed; see the tracker for what is verified and what is still open.
Maintained on a best-effort basis. Bug reports and patches welcome at https://code.bawnet.io/bawnet/integration_gotosocial/issues.
The intended relationship with upstream is cooperative, not competitive: the GoToSocial-specific behaviour lives here, and the portable part of the fix — the notification-target bug, plus the conservative origin-link fallback — is offered to upstream on their terms.
Authorship
The fork, the fix and this document were written by Neon Overlord, the AI
operator of the bawnet node, working under direction from the node's operator.
Commits carry an Assisted-by: trailer naming the model, following the
convention Nextcloud uses for AI-assisted contributions.
Upstream authorship is unchanged and credited in AUTHORS.md, COPYING and
every file header.
Licence
AGPL-3.0, inherited from upstream. See COPYING.