Skip to content

Latest commit

 

History

History
706 lines (587 loc) · 48.8 KB

File metadata and controls

706 lines (587 loc) · 48.8 KB

Administering Nextcloud Social

What an administrator has to set up, what they can change, and what tells them something is wrong. For the routes behind the settings page see API.md, for the commands OCC-Commands.md, and for how the pieces fit together Architecture.md.


Before it federates

Social is a fediverse server that happens to live inside Nextcloud, so the things it needs are the things any fediverse server needs: a stable public address, a .well-known answer, and a cron that runs.

A stable address. Social copies overwrite.cli.url into its own cloud_url the first time somebody opens the app, and builds every account id, post id and WebFinger answer from that copy. It never reads the system value again. Set overwrite.cli.url — and get it right — before anyone opens Social; changing the server's address afterwards is the single most expensive mistake available here, because the old address is inside every id already federated. See the address Social is set up for.

.well-known redirects. Another server looking for @alice@example.com asks https://example.com/.well-known/webfinger. Nextcloud serves that from /index.php/.well-known/webfinger, and the redirect from the domain root is something the web server has to do; Nextcloud's documented redirects are exactly the ones needed. Without them nobody outside can find an account here, however well everything inside works.

Pretty URLs are not required, but an instance reachable only at https://example.com/index.php/apps/social/... federates under ids with index.php in them, which some peers handle poorly. If .htaccess-based URL rewriting is available, turn it on before the first account is created rather than after.

Behind a reverse proxy, set trusted_proxies and overwritehost, overwriteprotocol and overwrite.cli.url as the Nextcloud documentation describes. Social signs its outbound requests over the Host and Date headers and verifies the signatures on what arrives, so a proxy that rewrites the host without Nextcloud knowing produces signature failures on both sides that look like nothing else.

Cron. Background jobs must be set to Cron (system cron), not AJAX and not Webcron. Everything this instance sends leaves through Cron\Queue, which runs every 12 minutes; with AJAX jobs, delivery happens only when somebody happens to load a page, and on a quiet instance that can be hours. See the delivery job has not run.

Outbound HTTP. The server must be able to reach the rest of the fediverse over HTTPS. Requests to local and private addresses are refused unless Nextcloud's allow_local_remote_servers is on, which is a development setting and not one to carry into production.


The setup checks

Social registers seven checks in Administration → Overview, beside Nextcloud's own. They are the things that break federation, or stop clients connecting, without anything else saying so, and each links back to this page.

occ social:check:install runs the same classes (lib/SetupChecks/), prints each with its severity and exits 1 if any of them reports an error, so a deployment script can run it. --offline leaves out the WebFinger probe, the only one that goes out on the network.

Social: upload size. Whether PHP will accept the uploads this app promises to. Social's max_size is in /api/v1/instance and in the composer's refusal message; PHP's upload_max_filesize and post_max_size are enforced before a byte reaches this app's code. When the app's number is the larger one, an upload between the two is refused with nothing in the log — the request never reaches PHP — and the person is told nothing useful. Raise both PHP values, or lower the app's own in Administration → Social → Server.

Social: reachable by other servers. Whether the strict peers will talk to this instance at all. Federation does not fail all at once: a plain-HTTP instance, or one on a private address, federates happily with a permissive server and is refused at the first gate by a strict one. The strictest in common use is Pixelfed, which is also the network a photo server most wants to reach — it refuses any peer that is not HTTPS on a publicly resolvable name, before it checks a signature, from its inbox, its delivery and its actor fetch, with nothing to configure. This is expected on a development or intranet instance and is a warning rather than an error.

Social: client API. Whether a Mastodon app can reach this instance at all. See below — this is the one check that is about apps rather than about federation, and it is a warning rather than an error, because everything else works without it.

Mastodon apps cannot connect

A Mastodon app is given a domain and builds https://<domain>/api/v1/... from it. Not one of them accepts a path, and Social's routes live under /index.php/apps/social, so out of the box adding this instance to an app fails at its first request with nothing in the log to show for it. The web interface and federation with other servers are unaffected.

An administrator opening Social sees this at the top of the app, with the Apache rules to paste, until the web server answers; it is the same check as in Administration → Overview, and it goes quiet within five minutes of the rules being in place.

Nextcloud only lets a short list of apps claim URLs at the root of the domain, and Social is not on it, so this has to be done in the web server. Ready-made rules are in contrib/webserver/: include apache-social-root.conf from the <VirtualHost> that serves Nextcloud, or nginx-social-root.conf from its server block, and reload. For Apache that is:

ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"

RewriteEngine On
RewriteRule ^/?api/(.*)$   http://127.0.0.1/index.php/apps/social/api/$1   [P,QSA,L]
RewriteRule ^/?oauth/(.*)$ http://127.0.0.1/index.php/apps/social/oauth/$1 [P,QSA,L]
RewriteRule ^/?\.well-known/host-meta$ http://127.0.0.1/index.php/.well-known/host-meta [P,QSA,L]
RewriteRule ^/?\.well-known/oauth-authorization-server$ http://127.0.0.1/index.php/apps/social/.well-known/oauth-authorization-server [P,QSA,L]

mod_proxy, mod_proxy_http, mod_rewrite and mod_headers have to be enabled. This cannot go in an .htaccess file, because ProxyPreserveHost is not allowed there and without it the app is handed Host: 127.0.0.1 and refuses the request as an untrusted domain.

Check what 127.0.0.1:80 actually serves. These rules proxy to plain HTTP on the loopback address, which only works if something there serves this Nextcloud. A server whose virtual hosts are all on :443 — what a certbot-managed config looks like once the HTTP ones have been commented out — has no :80 virtual host for the domain at all, so the request falls through to the main server and its DocumentRoot, and every proxied request answers 404 from there.

That 404 is indistinguishable from having no rules at all, which is the trap: the rules are correct, they fire, and the warning stays up. The way to tell them apart is that an Apache error page names the port it came from —

/api/v1/instance  →  "Server at example.com Port 80"    the rules fired, the target is wrong
/login            →  "Server at example.com Port 443"   served by the HTTPS vhost itself

— and apachectl -S will show no *:80 section. The app says which of the two it is as well: the warning carries what the probe got back.

The fix is to give the proxy something to land on. A loopback-only listener keeps plain HTTP off the public interface:

Listen 127.0.0.1:8081

<VirtualHost 127.0.0.1:8081>
    ServerName example.com
    DocumentRoot /path/to/nextcloud
    <Directory /path/to/nextcloud>
        Options FollowSymLinks
        AllowOverride All
        Require ip 127.0.0.1
    </Directory>
</VirtualHost>

then point the three rules at http://127.0.0.1:8081 instead. AllowOverride All is not optional here: Nextcloud's own .htaccess is what hands the Authorization header to PHP, so without it the client API answers the access_token was revoked for every signed-in request — see below.

They map three things onto the app:

Path Why
/api/… every client call, from /api/v1/instance onwards
/oauth/… registering the app, the consent screen, the token
/.well-known/host-meta some clients ask for it before anything else
/.well-known/oauth-authorization-server RFC 8414 discovery; a Mastodon 4.3 client asks for it before it registers anything, and reads a 404 as "not a server I can sign in to"

Use an internal rewrite, not a redirect. Nextcloud routes on the address the request arrived at, so a plain internal rewrite to /index.php/apps/social reaches PHP but not the route, and answers 404. Sending a redirect instead does reach the route, but many HTTP clients drop the Authorization header when they follow one, so every signed-in request then answers 401. The Apache rules therefore proxy internally (mod_proxy, mod_proxy_http and ProxyPreserveHost On), which keeps both the address and the header.

Apache also has to hand the Authorization header to PHP. Nextcloud's own .htaccess does that, but only where AllowOverride lets it be read; with AllowOverride None the rule never runs and every request from a signed-in app answers the access_token was revoked. The shipped rules set it themselves for the same reason.

The discovery document describes the addresses it was reached at. RFC 8414 requires the issuer in /.well-known/oauth-authorization-server to be the URL the document was fetched from, minus the well-known suffix. A client that asks the domain root and is told the issuer is https://cloud.example/index.php/apps/social/ has been handed a mismatch and rejects it — before it opens a browser, so nothing here sees it fail. So while the rewrite is working, the document advertises the root addresses the client used; where it is not, it advertises the app's own, which is the only reachable answer. Which of the two it is comes from the check below, read from its cache — an internal proxy hands PHP the app path either way, so the request itself cannot say.

X-Forwarded-Proto is not optional. mod_proxy sends X-Forwarded-For and X-Forwarded-Host by itself and stops there, and the rules above proxy to plain HTTP — so without that header Nextcloud believes every proxied request arrived over http and builds every absolute URL it answers with accordingly. A client is then handed http:// addresses for avatars, posts and the instance thumbnail, on a site that is https. An iOS client refuses them outright (App Transport Security), and its sign-in stops dead after registering the app, with nothing in any log to show for it. The nginx rules set the equivalent (proxy_set_header X-Forwarded-Proto $scheme) and always did.

The symptom is visible without a client: /api/v2/instance fetched through the root answers a thumbnail.url beginning http://, where the same document fetched at /index.php/apps/social/api/v2/instance begins https://.

Add 127.0.0.1 to trusted_proxies. Client requests reach PHP from the proxy afterwards, so without it every app in the world shares one address for rate limiting, brute-force protection and the log — one client tripping a limit locks out all of them. The real address arrives in X-Forwarded-For, which Apache's mod_proxy sends by itself and the nginx rules set explicitly.

The check probes /api/v1/instance at the address Social is configured for, then at the host the request came in on, then at the server's base URL, and reads the answer rather than only its status, so a login page or a catch-all index at the root does not pass for a client API. A success is remembered for an hour and a failure for five minutes.

WebFinger does not answer

Nothing answers /.well-known/webfinger for an account of this instance, so no other server can find anybody here — searches for a local account fail everywhere else, and follows from outside never arrive.

Two quite different causes, and the check cannot tell them apart from outside:

  1. The redirects are missing. Add the documented ones and reload the web server.
  2. Social is set up for a different address than the one the request arrives on, so it answers for a host nobody asks about. The next check is about that.

The probe is tried at the address Social is configured for, then at the host the request came in on, then at the server's base URL, and a success is remembered for an hour. If the instance uses a certificate the server itself does not trust — a private CA during setup — set social.checkssl to false in config.php to stop the probe verifying it. That switch is for the probe only, not for federation.

The address Social is set up for

Social builds every id from its stored cloud_url and the server now reports something else. Accounts here cannot be found under the address the server advertises, and every new post carries an id that resolves nowhere.

To see the two values:

occ config:app:get social cloud_url
occ config:system:get overwrite.cli.url

Social reports the mismatch and will not correct it, because the stored address is inside every id already written. Either point overwrite.cli.url back at the address Social knows, or accept the rename and run occ social:reset --uri=<new address>, which deletes everything Social holds — every post, follow and cached account, local and remote alike.

The delivery job has not run

Cron\Queue is what sends. It is meant to run every 12 minutes; the check warns once an hour has passed without it, and errors if the job is not registered at all (disabling and re-enabling the app registers it again).

Nothing posted here leaves the server while it is not running, and nobody is told: the author sees their post in their own timeline and nowhere else. Check that background jobs are set to Cron, that the system cron is actually running cron.php, and that no other job is monopolising the runs.

Deliveries are stuck

Two things in the outbound queue do not move on their own: a delivery the drain has given up on after every retry (16 attempts over a widening delay, the last gap fourteen hours), and one it should have come back for a day ago and has not. The first usually means an instance that is gone or refusing us; the second means the drain is not draining.

occ social:queue:status                  # the same summary the settings page shows
occ social:queue:retry --min-tries 16    # give the abandoned ones the full run again
occ social:queue:retry --flush --min-tries 16   # or drop them, for a peer that is gone

The Federation health section of the Social settings names the instances the failures are stacked against, with the highest attempt count so far and when each was last tried.


The administration page

Administration → Social. Seventeen cards, grouped by what they are for -- Overview, Moderation, What people see, What is kept, Federation, Server -- with a list of them beside the page on a wide screen:

  • Reports — what people here and peers elsewhere have complained about. The open ones are the table; the resolved ones are folded away below them and read a page at a time when the fold is opened. Fifty to a page, server-side, with a Show more under each.
  • Activity here — posts written on this server in the last day and the last week, and how many accounts wrote them. Local posts only: a count that included what arrived would be a number about other servers and about this one's retention setting.
  • Posts waiting to be looked at — the review queue: the first post of a new account, and posts that tripped one of the spam rules. Each row carries the text, because a held post is in no timeline and there is nowhere else to go and read it. Publish sends it out; Refuse deletes it and tells its author. Two switches at the top turn first-post review and the spam rules on and off.
  • Sections — what this instance offers the people using it. Four switches, all on by default: Stories, and the Photos, Videos and News timelines. Turning one off takes it out of the sidebar and stops it being offered; nothing already posted is touched, so a video posted while Videos was off is still a video and appears again the moment it is turned back on. The fifth setting is the Nextcloud groups that become Social lists, and it is empty by default: everybody in a chosen group gets a list for it holding the members who have a Social account, nobody is followed by it and nothing federates, but a group list does tell everybody in the group who else is in it -- which is why no group becomes one until an administrator chooses. Deselecting a group takes its lists away on the next cron reconcile. Administrators only, not delegates.
  • Accounts — every account this instance knows, whether or not anybody has complained. Search by username, by handle or by instance, filter by origin and by what stands against them, and act on any of them.
  • Refused pictures — files this instance will not store, named by their sha256. The one thing the account-level tools do not do is stop a file coming back; a refused one is turned away wherever it arrives, an upload here or an attachment fetched from another server.
  • What this server is about — a few named subjects, each a handful of hashtags, shown at the top of Explore above the trending lists. Trending on a small server is four hashtags and a wedding; this is the part of that page chosen rather than counted, and it is what makes Explore look like somewhere to start. Nothing is named by default and the section of the page is absent until something is.
  • What may trend — Explore shows whatever is being talked about, counted and nothing else, so the first ugly hashtag to catch on does so on everybody's Explore page and the only remedy used to be waiting. Keeping one out hides it from every trending list here and from the discover grid; the counters keep counting, so letting it back in restores the number it would have had. Hashtags can be kept out from the live trending list beside them; a link or a post is kept out by pasting its address. What is kept out is one list whatever kind it is, because the question is "what am I keeping out of Explore".
  • Custom emoji — the pictures people here can write into a post as :shortcode:. They travel with the post, so somebody on another server sees them too. The upload goes through the same code occ social:emoji uses, so what is refused here is exactly what the command refuses.
  • The rules of this server — one per line. Every client shows them to somebody deciding whether to join, and /api/v1/instance/rules serves them. Both this and the emoji were occ-only until now, which is why most instances had neither.
  • Retention — how long remote statuses nobody here cares about are kept.
  • Storage — what is on disk, split into what was posted here (somebody's own work, not going anywhere) and what is cached from other servers (what Retention removes). Added up by the background job once a day, because counting it is one file lookup per stored file; the page says when it was measured. occ social:media:usage measures it on demand.
  • Federation health — what the outbound queue is doing, including how long the longest-failing delivery has been failing: the counts say how much and where, and that says whether it started an hour ago or a week ago, which is the difference between a peer rebooting and a delivery that is never going to happen.
  • Background work — when each of this app's cron jobs last ran, and whether that is later than it should be. Almost everything the app does away from a page happens on a schedule: posts go out, stories expire, media is swept, videos are transcoded, the storage figures are taken. When cron stops, the symptom is a post that never arrives and a disk that never shrinks, and nothing in the app said so. A job is called late after three of its own intervals, because Nextcloud's cron is itself periodic and a five-minute job on a fifteen-minute timer is always two intervals behind with nothing wrong with it. A job missing from the list altogether is what an upgrade whose migrations have not run looks like; the page says so rather than showing a zero. Read from Nextcloud's own job list, so it cannot drift from what actually happened.
  • Fediverse access — the block list or the allow list, the same one occ social:fediverse manages.
  • Announcements — a notice every account here is shown once.
  • Server — the instance-wide settings below, which had no interface at all before and could only be set with occ config:app:set.
  • Relays — the relays this server subscribes to. A new server sees only what the people on it follow, so its federated timeline is empty on the first day and thin for months, and nobody out there has heard of it either. A relay breaks that circle: it rebroadcasts the public posts of every server subscribed to it, and sends this server's public posts on to all of them. Paste the relay's address — relays publish it on their own front page, and it usually ends in /actor — and the row will say Waiting for an answer until the relay replies, which for some relays means when a human has looked at the request. Public posts and nothing else are shared: a followers-only post has an audience that was chosen, and a relay is the opposite of a chosen audience. A relayed post is fetched from the server that wrote it rather than believed from the relay, so it arrives as the post it is and not as "relay.example boosted this".

Each section is one settings card, like everywhere else in the administration settings, and the three things that cannot be taken back — suspending an account, taking a post down, removing an announcement — ask in a dialog that says what they will cost before they do it.

The page can be delegated: hand the Social section to a group under Administration privileges and that group can moderate without administering the server. Server and Relays are the exception — neither is rendered for a delegate and their endpoints refuse them, because what they hold is a decision about the server rather than about a report: a relay changes what every federated timeline here holds and where every public post written here is sent.


Moderating

Reports arrive from two directions: a local user reporting somebody through a client, and a peer instance sending POST /api/v1/reports about one of its own users' complaints. Both land in the same table, and the administrators (and the delegated group) are notified.

There are five things a moderator can do to an account, in order of weight:

Action What it costs Reversible
Warn Nothing. The account is told there is a problem and everything else stays as it is. n/a
Mark everything sensitive Every post the account makes from now on is marked sensitive, whatever it said. It stays in the timelines and its followers still see it — behind a click. yes, completely
Silence The account leaves the public and global timelines. Whoever deliberately follows it still sees it. yes, completely
Take down One post is deleted. A local post is deleted everywhere it reached; a remote one only here. no
Suspend Everything the account posted here is deleted, its cached actor is dropped, its follows in both directions go, and everything it sends afterwards is refused. A local account's suspension is federated as a Delete. the refusal stops; nothing deleted comes back

Resolve marks the complaint handled and changes nothing about the account, so a report can be closed with any of the four applied, or with none.

What is recorded. social_moderation holds what stands now — one row an account, replaced by the next decision and gone when it is lifted. social_strikes is the history: a warning, a silence, a suspension, a takedown and a lift each write a row naming the moderator, and nothing removes one except the account itself going. The strike count in the account browser counts what stands against the account, so a lift is in the history and not in the count.

The audit log. Suspending, silencing, lifting, taking a post down and blocking or unblocking an instance each emit OCP\Log\Audit\CriticalActionPerformedEvent, so with core's admin_audit app enabled they land in the audit log beside "user X was added to group Y", naming the moderator who acted. Without that app nothing listens and nothing is written.

Before anybody sees it. Two rules hold a post back instead of publishing it. The spam rules are on by default; first-post review is off until an administrator turns it on:

  • first-post review (review_first_post) holds the first post of an account that has published nothing here yet. Accounts here are Nextcloud users, so a spammer has to be given an account by this server before they can post at all — and on an instance that hands accounts to strangers, the cheapest thing that can be done about it is that the first thing they write is seen by a person. Off by default: on the ordinary instance, whose accounts are colleagues an administrator created, it held every new person's first post and made their first five minutes look broken — the composer closed, the post did not appear, and it existed only in a panel nobody had opened yet. Turn it on where registration is open. An administrator's own posts are never held: somebody who can empty the queue is not somebody to put in it, and the first post on a brand-new instance is the administrator's — held, it made a fresh install look broken, because the post did not appear and the only place it existed was a panel they had not opened yet.
  • autospam (autospam) holds a post that trips one of a very short list of rules: more than five links in a short post, or more than five mentions from an account that nobody follows and that follows nobody. There is no wordlist and no score — "seven links" is something a moderator can agree or disagree with in a second, and "0.82" is not.

A direct message is never held, whatever it says: holding one would put private correspondence in front of a moderator who was not written to, for a machine's reason. Nor is a post by an account a decision already stands against — that account is being dealt with by the decision.

Nothing is published, and nothing is deleted, without somebody pressing something. A held post is stored as the request the client sent, not as a post: it is in no timeline, no profile, no outbox and no search, not even its author's, and their own copy of it is in Settings → Waiting to be looked at, where they can take it back. Approving replays the request down the path an immediate post takes and dates it now. Refusing deletes it, tells the author, and records a takedown strike. One account may have twenty waiting; past that its posts are refused outright, because nobody is going to read the fortieth.

Instances, not accounts. Fediverse access is the instance-wide list. In block-list mode (the default, access_type=all_but) everything on it is refused, including every subdomain, and blocking a domain queues a purge of everything it ever sent. In allow-list mode (none_but) the list is the only instances this server will talk to at all. An instance that is a nuisance rather than a menace can be silenced instead with occ social:fediverse silence: its accounts leave the public timelines and stay readable for the people who follow them.


From a Pixelfed admin app

The administration screens of Pixelfed's official app call Pixelfed's own /api/admin/* routes, and this instance answers them — behind the same gate as everything above, and through the same service, so a takedown from the app is the same takedown as one from this page. Users, open reports and the instances this server federates with are all there; Pixelfed's unlisted on an instance is the silence tier and its banned the deny list. Its autospam screen draws this instance's own review queue, with not spam publishing a held post and delete refusing it. What this instance does not have — per-account "unlisted" or "content warning" flags, switches that flip from the app — is answered as absent or refused with a reason, never as a 200 that changed nothing. The routes and their answers are in API.md.

Configuration

Everything below is app configuration under social, read and written with:

occ config:app:get social <key>
occ config:app:set social <key> --value=<value>

Nothing here is reachable through core's app-config API: the settings page writes through its own validated endpoints, and a delegate can write only what the moderation routes accept.

Set by the app, not by you

Key Default Meaning
cloud_url (empty) The base address every id is built from, copied from overwrite.cli.url the first time the app is opened. Changing it by hand does not rewrite the ids already issued.
social_url (empty) The app's own base URL (…/apps/social/), used for profile links, WebFinger and NodeInfo. Derived at the same moment as cloud_url.
social_address (empty) The hostname accounts are federated under, when it is not the host of cloud_url. Only set this if the fediverse address genuinely differs from the Nextcloud host, and only before the first account exists.
service 1 Unused; a leftover of the original installer.
installed_version Written by the upgrade machinery.
media_usage (written by the job) The last measurement of what is on disk, as JSON with the moment it was taken. Bookkeeping, not a setting: the walk is a stat per stored file and belongs in the cron, so the administration page reads this rather than counting on page load.
polls_swept 0 How far the closed-poll sweep has got, as a timestamp.
story_secret (generated) The secret a story's fetch capability is derived from, made the first time a story is published. Changing it invalidates every outstanding capability at once, which is the only revocation it needs: a story lives a day. Never set this by hand.

The Server card

These eight are what the Server section of the settings page writes. Each can still be set with occ; the page validates the ranges given here.

Key Default Meaning
contact_email (empty) Who to write to about this instance. Mastodon's instance.email: every client reads it on its first request and shows it on the server's about page. Empty until somebody fills it in, which until now most instances never did, because nothing said it existed.
extended_description (empty) The long form of what this instance is, for /api/v1/instance/extended_description. Up to 10000 characters.
max_size 10 The largest picture or file an upload may be, in MB. 1–10240.
max_video_size 2048 The largest video, in MB. 1–102400. A peer will refuse a great deal less than the ceiling.
image_max_edge 0 The longest edge a stored picture may have, in pixels. 0 stores every upload exactly as it arrived — the default, and the only setting that loses nothing: this app strips metadata losslessly and re-encodes only a picture it has to rotate. Set it (480–16384) on an instance where storage costs money or whose people post from a 48-megapixel phone. A picture already inside the ceiling is not re-encoded, because shrinking nothing and losing a generation anyway is the worst of both.
image_quality 85 What a re-encoded picture is stored at, 40–100. Only consulted when image_max_edge is set.
video_transcode 0 Whether stored videos are re-encoded to H.264 in an MP4 by a background job. Off by default, because re-encoding is lossy and it is somebody's file — but there is a concrete reason to turn it on: Pixelfed's default media_types accepts video/mp4 and nothing else, so every video/quicktime posted from here, which is every video straight off an iPhone, is dropped by its inbox without a word to anybody, and Safari will not play WebM. Needs ffmpeg; nothing happens without it. Never runs during an upload: converting a video is minutes rather than the seconds a poster frame takes, so the upload finishes as it always did and the video plays as it is until the job gets to it. One video every quarter of an hour, or occ social:media:transcode to work through a backlog now.
video_max_height 1080 The tallest a converted video is written, 240–2160. Only smaller, never larger: a 480p video is left at 480p. Only consulted when video_transcode is on.
video_ladder 0 Whether each stored MP4 is also written at a ladder of smaller sizes, as HLS, so a player can pick the one that fits the connection. A different question from video_transcode, which is about a video being playable at all elsewhere; this is about it being watchable on a phone on a train. Off by default, because it is several ffmpeg encodes per video on this server. Each rung is one file — -hls_flags single_file writes the rendition as a fragmented MP4 and the playlist addresses its segments as byte ranges — so a forty-minute video is three files rather than a thousand, which is also the shape PeerTube publishes. The original is kept and is what a player without HLS falls back to. Needs ffmpeg and ffprobe. One video every half-hour, or occ social:media:ladder to work through a backlog now. Built from video/mp4 only: a .mov goes through the transcoder first.
video_ladder_heights 360,720,1080 Which heights, comma-separated, 144–2160. Heights at or above a video's own are skipped rather than upscaled, and the video's own height is always a rung, so the best rung is never worse than the file beside it. A list with nothing usable in it is refused by the admin card rather than silently replaced with the default. Only consulted when video_ladder is on.
search_window_days 365 How far back a content search looks. content ILIKE '%term%' cannot use an index — a leading wildcard never can — so an unbounded search reads every post the instance has ever stored, joined to seven other tables, on every keystroke; at ten million rows that is a table scan with the rate limit as the only defence. A year covers what anybody is looking for. 0 searches everything, which an instance small enough can afford to say.
local_actor_cursor '' Bookkeeping, not a setting: where the cron's local-account refresh walk got to. It used to read every local account into memory on every pass; it pages now, and this is what makes the next pass carry on rather than start again.
video_quota 0 How many megabytes of video one account may keep here; 0 is no quota, which is what every instance has in effect today. A different question from max_video_size, which is a ceiling on one file: that is about a single request, this about a year of them. Off by default because an instance that has been running without a quota and acquires one on upgrade would start refusing uploads from exactly the accounts that use it most. Checked once, where an upload is written, against the size recorded on each stored file — so a video uploaded before this app recorded sizes counts as nothing until the daily usage job has been past it, which fills the column in as it walks. The ladders this server builds do not count against it: they are made because an administrator asked for them, are several times the size of the upload, and would turn a quota somebody was told about into one several times smaller. They are counted in what an administrator is shown, because they are real disk. Who is holding what is under Administration → Social → Storage.
nsfw_policy default What happens to media somebody marked sensitive, for readers who have not chosen for themselves. PeerTube's three NSFW policies under Mastodon's names for the same three states: show_all (PeerTube's display), default (its blur — covered, the blurhash showing, one press away, and what this app has always done) and hide_all (its hide — not drawn, and no button to draw it). Anybody can override it for themselves under Settings in the app, and "follow the instance" stays a state of its own, so changing this moves everybody who is following it and nobody who has chosen. A content warning is a different thing and always covers its post, whatever this says.
review_videos 0 Whether every post with a video on it waits for a moderator. The third rule of the review queue, beside a new account's first post and the spam rules, and the one an instance that hosts video wants: a video is minutes of somebody's attention and a great deal of somebody else's disk. Unlike the other two it is not about the account — a trusted account with a thousand posts behind it is held by it too, every time, because what it is about is the video. A moderator's own video is not held, and neither is a direct message. Under Administration → Social → Posts waiting to be looked at.
inbox_throttle 300 Incoming inbox requests allowed per origin host per minute. 0 accepts everything, which is what an instance behind its own rate limiter wants.
rate_limit_user 900 How many client API requests one signed-in account may make per window. The routes that fan out — search, the directories, the follow graph — carry limits of their own and Nextcloud enforces those; this is the budget for everything else, which was some three hundred routes with no limit at all. Generous on purpose: it exists to stop a scraper reading the whole instance at machine speed, not to pace an app. One budget for the whole API, not one per route. 0 switches it off, which is what an instance behind its own limiter wants. Federation, the internal queue and the routes that serve bytes (attachments, avatars, emoji, GIFs) are never counted — one public page is forty requests for pictures, and deliveries arrive in bursts from a handful of addresses.
rate_limit_anon 300 The same budget for a caller with no account, counted per address.
rate_limit_window 300 How long that window is, in seconds.
follow_limit 100 How many follows one account may send in an hour. A compromised account, or one running a script, can fan out follows to thousands of servers from this instance's address in a few minutes — every one a signed request this instance is answerable for. An hour rather than a day because what this catches is a burst, and high enough that importing a follow list from another server still goes through. Counted from the rows, so it holds on an instance with no memcache. 0 is no limit.
domain_media_quota 0 How many megabytes of media one other server may keep here. Every picture on a post somebody here follows is fetched and cached, and nothing bounded that by where it came from: one server posting large images at a high rate fills the disk of every instance that follows anybody on it. Counted from the figure the daily storage walk takes, plus what has been cached from that host since — so it is up to a day coarse and errs towards refusing early. Off by default, because an instance that has been federating for a year and acquires a quota on upgrade would start refusing the pictures of the servers it talks to most. Who is holding what is under Administration → Social → Storage.
secure_mode 0 Refuse ActivityPub fetches that are not signed. Mastodon's secure mode. Turning it on makes this instance invisible to every peer that does not sign what it asks for, and to every anonymous reader; it is a decision about who to federate with, not a hardening step to apply by default.
publish_blocks 0 Publish the deny list on /api/v1/instance/domain_blocks, the way Mastodon does, so somebody choosing a server can see who it will not talk to. Whether this server wants that read by anybody is a disclosure decision.
allow_self_signed 0 Accept peers whose certificates do not check out. Development only: on a server anybody else uses, this hands every federated request to whoever can answer for the address.

Moderation and federation

Key Default Meaning
access_type all_but all_but — federate with everyone except the listed instances (block list). none_but — federate only with the listed ones (allow list).
access_list [] The instances on that list, as a JSON array of hostnames. An entry covers every subdomain of itself. Managed from the settings page and by occ social:fediverse.
silenced_list [] Instances whose accounts are kept out of the public and global timelines but stay readable for whoever follows them. The middle tier a block does not have.
retention_days 0 Remote statuses older than this that no local user interacted with, follows the author of, or replied below are deleted, with their cached attachments. 0 disables it. Local content is never touched.
federate_blocks 1 Whether a user's own blocks are federated to the blocked account's instance. 0 keeps them local.
publish_video_objects 0 Whether a post that is a video is federated as an ActivityPub Video (PeerTube's shape) rather than a Note with an attachment. Off by default: Pixelfed's inbox handles only Notes and silently drops a Video, so with this on no video posted here reaches a Pixelfed follower. Mastodon draws both shapes; PeerTube draws only the Video. Turn it on for an instance whose audience is on PeerTube.
network_stats 1 Whether the statistics page may ask FediDB how big the fediverse is — servers, accounts, accounts that posted in the last month — to show beside the number of servers this one federates with. One request every six hours for the whole instance, carrying no account, no query and nothing about this instance; a survey that does not answer is left alone for half an hour. Set it to 0 and the section is absent rather than zeroed: an instance that makes no outbound request to draw a page is a legitimate thing to want. The same value governs the growth section below it, which asks Fediverse Observer for the last two years month by month — FediDB publishes a snapshot and no history, so the shape over time comes from a second survey, once a day. The two do not agree about the totals, because they crawl different servers and count dormant accounts differently, so the page keeps them apart and names each: a page that averaged them would produce a number neither survey would stand behind.
rules (empty) The instance rules shown by /api/v1/instance/rules, one per line.
review_first_post 0 Hold the first post of an account that has published nothing here yet, for a moderator to see before it goes out. Off by default; turn it on where accounts are handed to strangers. An administrator's own posts are never held — a moderator waiting on themselves is a circle, and on a new instance the first post is theirs.
review_posts 1 How many posts an account must have had published before its posts stop being held. 1 is first-post review as it has always meant. An account graduates by having that many posts approved — a person having looked at it that many times, which is the only measure of trust here that is not a guess. Capped at 20.
autospam 1 Hold a post that trips one of the spam rules — more than five links in a short post, or more than five mentions from an account nobody follows and that follows nobody. Nothing is ever refused by the rules, only shown to a person.

System configuration

Two config.php values, neither documented anywhere else:

Key Default Meaning
social.checkssl true Whether the WebFinger setup check verifies the certificate it is offered. Set it to false on an instance using a certificate the server itself does not trust. It affects the probe only — federation still verifies.
social.tests (unset) Enables GET /apps/social/test/{account}/, a WebFinger self-test. Leave it unset outside development.

And one of Nextcloud's own that matters here: allow_local_remote_servers, which must be on for an instance to federate with anything on a private address. That is a development arrangement.


Commands by task

The full reference is OCC-Commands.md; these are the ones an administrator reaches for.

Is it working?

occ social:check:install            # the four setup checks, plus repairs
occ social:check:install --offline  # the same without the network probe
occ social:queue:status             # what the outbound queue is doing
occ social:details <id>             # who can see one post and where it lands

Delivery is behind

occ social:queue:process            # drain it now instead of waiting for cron
occ social:queue:retry              # give failing deliveries the full run of retries again

Moderation

occ social:fediverse list                  # the access list; bare, it prints the mode
occ social:fediverse add <instance>        # block it (or allow it, in allow-list mode)
occ social:fediverse remove <instance>
occ social:fediverse silence <instance>    # out of the public timelines, still followable

Housekeeping

occ social:stream:prune             # apply retention and purge finished queue rows
occ social:cache:refresh            # re-fetch cached remote accounts
occ social:domain:purge <instance>  # remove what a now-blocked instance sent

Starting over

occ social:reset                    # empties every Social table; asks twice
occ social:reset --uninstall        # and drops the tables, jobs and app config

Running a large instance

Everything below is optional and nothing on a small instance needs any of it. These are the three things that stop working first as an instance grows, and what to do about each.

Install notify_push. Without it every open tab asks the server for the home timeline and the unread count every thirty seconds; with it the server tells the client instead and the poll drops to once every five minutes — a tenfold cut in the request volume, for one app install. The polls also answer 304 now when nothing has changed, so even without it most of them cost an index probe and an empty response rather than a rendered page.

Run delivery workers. Cron\Queue moves at most 200 deliveries every twelve minutes and, when peers are slow, as few as ten — a ceiling of about a thousand an hour, which an instance whose accounts are followed across thousands of servers exceeds with a single popular post. occ social:worker is the same delivery in a loop that does not stop, and several may run at once; see OCC-Commands.md for a systemd unit. The cron job is unchanged, so an instance that will not run a daemon keeps what it has.

Watch the cron actually finish. The steps are budgeted at 300 seconds a pass and resume where they stopped, so a pass that runs out of time is normal. A pass that never reaches the later steps is not: social:check and the Nextcloud log are where that shows.

Two settings exist for size and are listed above: search_window_days bounds what a content search scans, and retention_days bounds what cached remote media costs. Both trade completeness for a bounded cost, and the default of each is the one a medium instance wants.

occ social:benchmark seeds a realistic amount of content and times the timeline queries against it, which is the only honest way to find out what any of this costs on the hardware in front of you.

What to watch

  • Administration → Overview. The four checks above are there precisely so that an administrator who never opens Social still hears about it.
  • The delivery queue. A rising count of failing deliveries against one host is that instance's problem; a rising count against all of them is this one's.
  • social.log / the Nextcloud log. Signature verification failures on inbound requests, and could not federate warnings on outbound ones.
  • Disk. Cached remote attachments are the largest thing this app stores. retention_days is what bounds them; on an instance that federates widely, leaving it at 0 means keeping every picture anybody here ever scrolled past.
  • The audit log, if admin_audit is enabled: every moderation decision, with the moderator who took it.