Skip to content

Latest commit

Β 

History

4,280 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Nextcloud Social

Your Nextcloud is a Fediverse server.

Post, follow and reply across Mastodon, Pixelfed, PeerTube, GoToSocial and Akkoma β€” from the same place your files, calendar and chat already live. No new account. No new app. No algorithm. No advertising.

The home timeline

Nextcloud Social gives every user on your server a real Fediverse identity β€” @you@your.cloud β€” that anyone on Mastodon can follow, mention and reply to. What you write leaves your server signed, and lands in their timeline. What they write comes back into yours. Nobody else holds it, nobody sells it, and nobody reorders it.

It is a partial implementation of ActivityPub and of the Mastodon client API: enough to post, follow, read, converse, moderate and run an instance β€” and honest about what it is not. Read Not implemented yet before you make it somebody's only Fediverse client.


✍️ Write

The composer

One box, and everything a post can carry.

  • Say who sees it. Public, unlisted, followers-only or direct, chosen per post from the default you set once. A reply inherits the audience of the post it answers, and starts addressed to everyone in the conversation rather than to one person.
  • Pictures, video and audio. JPEG, PNG, GIF, WebP, AVIF and HEIC/HEIF straight off an iPhone (transcoded on the way in), MP4, WebM and QuickTime, MP3, AAC, Opus, WAV and FLAC. Video is never re-encoded and is streamed to storage a chunk at a time, so it gets a 2 GB ceiling rather than the 10 MB a picture is held to.
  • And the files people actually have. PDF, text, Markdown, CSV, ZIP, EPUB, ODF and the Office formats ride on a post too (DOCUMENT_MIME_TYPES in lib/Service/CacheDocumentService.php), stored as they are and drawn as a card you press to download. On the wire they are an ActivityPub Document carrying its mime type, which Mastodon shows as a link and another Nextcloud shows as a file.
  • Every picture is stripped of its metadata before it is stored or sent β€” Exif, XMP, IPTC, and the GPS coordinates a phone quietly attaches. Done on the container rather than by re-encoding, so nothing loses a generation of quality, and the colour profile is deliberately kept.
  • Alt text that actually gets written. Every attachment has a description field, and a post carrying an undescribed picture says so before it goes. A nudge, never a refusal.
  • A focal point. Click or drag a crosshair over a picture to say where the subject is, so a square crop never cuts somebody's head off. It federates, so other servers crop it correctly too.
  • Seven adjustments, previewed live and baked into the copy that is posted.
  • Polls. Up to four options, single or multiple choice. Votes federate both ways and the new totals come back as an Update.
  • Content warnings that fold the post away β€” and keep it out of the page entirely until a reader asks for it.
  • Custom emoji. Type :shortcode: and the post travels with a matching Emoji tag, so it renders on servers that have never heard of it.
  • A GIF picker backed by the instance's own library β€” no Giphy, no Tenor, nobody's tracker.
  • Quote a post, with the original author's permission carried on the wire (FEP-044f).
  • Say what language it is in, starting from your Nextcloud language.
  • Send it later. Pick a time at least five minutes out and Post becomes Schedule.
  • Mentions and hashtags typed by hand become real recipients and real tags.

πŸ“– Read

Photos

My Feed, Local and Global are a switcher above the posts rather than three places to navigate to β€” one pill slides between them, and the arrow keys move through them. Photos and Videos carry the same switcher, so the pictures of the people you follow, of this instance and of the whole Fediverse are one click apart.

Hovering a post opens its actions

  • Photos and Videos are timelines of their own, drawn as grids and asked of the server rather than filtered out of a page you already have. Videos, including federated PeerTube channels

  • PeerTube, properly. A PeerTube video arrives with its title, its channel, its thumbnail and something to play. The video streams from the instance that holds it β€” a two-hour talk is not something to mirror onto somebody's Nextcloud β€” and is proxied, so nobody's IP address reaches a server they never chose to talk to. Posts that are one video go out as an ActivityPub Video, the only shape PeerTube ingests.

  • Blurhash placeholders, so a timeline never jumps as pictures load, and blurred previews for anything marked sensitive.

  • An ALT badge on every described picture, wherever it is drawn, that shows the description when pressed.

  • Lists, made and filled in Settings or from anybody's profile β€” and every Nextcloud group you are in is already a list, built and maintained by nobody.

  • Follow a hashtag and it reads exactly like following a person.

  • Filter out words you would rather not read β€” Settings β†’ Filtered words. A filter is a handful of words, the timelines it applies in, and whether a matching post is folded away behind the filter's name, with a Show anyway, or taken out of the timeline altogether; it can be set to expire on its own. A folded post is not in the page at all until you ask for it, so nothing is read by accident. Nobody is told, nothing is deleted, and what is currently being taken away is said at the top of the page rather than left to be noticed as gaps in a conversation.

  • Bookmarks and favourites, each with a page of their own.

  • Announcements from your administrators at the top of the timeline, above the composer. An unread one interrupts; Got it marks it read for your account on every device, and it does not come back. What you have already read is kept out of the way behind one line, and an emoji reaction is the one thing you can say back.

  • Keyboard throughout: j k l f b r o n g, and ? for the list.

  • Live when notify_push is installed; polling every 30 seconds when it is not.

🧡 Converse

A post's own page

A post opened from a timeline gets a page of its own: the post, a reply box already pointed at it, the faces behind the boost and favourite counts, and the details a card leaves out β€” when exactly, to whom, in what language, whether it has been edited, and a link to the original for a post from another server.

Replies read as a conversation. Each reply is followed by the replies to it, oldest first, stepped in by how deep it sits with a line down its side. A reply whose parent this server does not hold keeps its place in time rather than disappearing. When the thread shown is shorter than the reply count, the page says so instead of presenting what it has as the whole conversation.

Four things you can do to a post: reply, boost, favourite, and react with an emoji β€” which federates as EmojiReact and shows who reacted with what.

🧭 Discover

Discover

The hardest part of a new Fediverse account is the first ten follows. This answers it four ways:

  • The people you already share a Nextcloud with. Every Nextcloud profile has a fediverse field; this app fills in yours and reads everybody else's, so your colleagues are suggested before any algorithm has a thing to say.
  • Starter packs β€” a named handful of accounts with one button that follows all of them. Administrators curate their own.
  • Trending hashtags, ranked over a window you choose from one hour to ten days. The busiest hour and the busiest ten days are genuinely different lists, not one list relabelled.
  • Trending pictures and videos.

Search

Search covers people, hashtags and the full text of every post you are allowed to see β€” your own, public content and anything addressed to you β€” through Nextcloud's own unified search. No external search engine to run.

πŸ‘€ Your profile

A profile

  • Posts, Photos and Videos as three tabs, each one a question asked of the server.
  • A banner, a bio and four metadata fields, federated the way Mastodon does it and edited in Edit profile on your own profile.
  • Verified links. A profile field naming a web page gets the tick when that page links back with rel="me" β€” from an <a> anywhere on it or a <link> in its head. The editor says which of your fields are verified and when each was last proved, and hands you the line to paste on the far end. The page is fetched in the background, once a day per account.
  • Featured hashtags. Up to ten tags pinned under the bio, saying what the account is about in its own words; a visitor clicks one and reads what was posted under it, with the count taken from the posts rather than stored. You set your own in Settings β†’ Featured hashtags, which starts from the tags you already post with most instead of an empty box, and your profile links straight there.
  • Highlights β€” a twelve-week posting chart and the tags somebody keeps coming back to.
  • Pinned posts, up to five, published in the actor's featured collection. Remote accounts' pins arrive too.
  • A grid or a timeline, whichever you last chose, cropped to each picture's focal point.
  • A private note about somebody, for you alone. It never leaves this server and the person it is about is never told.

πŸ”” Notifications

Notifications

A page, not a list of everything. Filters across the top (All, Mentions, Favourites, Boosts, Follows, Polls, Edits), a New line where you left off, and a run of likes of the same post drawn as one card with the faces stacked rather than the same post quoted twelve times.

It marks itself read after it has been in front of you for two seconds, not the instant it renders β€” so it stops clearing the badge on your phone for things nobody saw.

Nextcloud's own bell rings too. Mentions, favourites, boosts, new followers, follow requests, edits of posts you boosted and polls you voted in all reach Nextcloud notifications and its mail digest, each linking into this app rather than out to a remote server. A follow request carries Accept and Decline on the bell entry itself.

βš™οΈ Your account, your data

Settings

Everything about your account in one page: the name you publish under, whether people must ask before they follow you, whether other servers may suggest you and index your public posts, whether this is an automated account, and the audience every new post starts with. Only what you changed is sent, so a display name your Nextcloud gets from elsewhere is never written back.

Scheduled posts

Below it: your lists, your scheduled posts with a way to cancel one, your blocked and muted accounts with unblock and unmute inline, and migration.

Migration takes your account with you. Export writes your profile, follows, followers, blocks, mutes, bookmarks, likes and every post you have written to a zip β€” and the pictures and videos come with it, copied into the archive in the layout Mastodon's own export uses, with each attachment pointing at the copy rather than at the server you are leaving. Import reads one back, including an archive from occ user:export. A third section brings your follows over from Mastodon, Pixelfed, GoToSocial or Akkoma via their following_accounts.csv. Your private key is deliberately not in the archive.

πŸ“± On a phone, and in the dark

The timeline on a phone Β Β  The timeline in dark mode

Under 600px the avatar moves inside the card, the card takes the width of the screen, the composer's toolbar wraps instead of pushing Post off the edge, and a post's page gives up the column it kept for an avatar that is no longer beside it. Dark mode is the same app, not a second design.

🧩 It is a Nextcloud app, so it behaves like one

  • Share to Social, from Files. Select a picture or a video β€” up to ten β€” pick Share to Social from the menu, and the composer opens with them already attached. Nothing is uploaded a second time.
  • Links unfurl. Paste a link to a post or a profile into a Talk message, a Text document or a Deck card and it becomes a card with the author, the text and the first picture. Only what anybody could read is rendered, because the card is cached once for everyone who sees the link.
  • Nine Dashboard widgets and an entry in the contacts menu.
  • The Activity app lists your follows, mentions, boosts and favourites, and puts them in the Activity digest mail. Activity's own notifications stay off: the bell is the bell.
  • Unified search, so Social posts turn up where every other search result does.
  • Deleting a Nextcloud user takes their Fediverse account with it β€” tombstoned, dropped, and a Delete federated so other servers drop their copies too.

πŸ›‘οΈ Safety, and privacy that is the default

  • Block to sever a relationship in both directions and hide somebody everywhere.
  • Mute to hide them from your timelines and optionally your notifications without them ever knowing β€” for an hour, a day, seven days, thirty, or until you lift it. A profile says when a timed mute runs out.
  • Both are offered on the profile and in the menu of any post they wrote, which is usually where you decided.
  • Report an account or a post. With forwarding on, a report about a remote account also reaches the instance that hosts it β€” anonymised, and signed as this server rather than as the person who filed it, because they would otherwise be handing their handle to the very instance they are complaining about.
  • Locked accounts, so follows must be approved, with a Follow requests page.
  • Keyword filters, written and read in Settings β†’ Filtered words: they are yours alone, they apply to every timeline this app draws, and a filter set months ago from a phone is finally visible from here.
  • Per-user domain blocks and conversation mute, through the API.
  • Nothing is sent to a third party. No geocoder β€” a place on a post is one this instance has seen or one you name yourself, because sending somebody's location to a stranger at the moment they are deciding whether to publish it is exactly the failure the Exif stripping exists to prevent. Link previews are read by this server, never federated, and never fetched from a local address.

πŸ›οΈ For administrators

The administration page

Everything in Administration β†’ Social, built out of the same components as the rest of the administration settings:

  • Reports with Silence, Suspend, Lift and take-a-post-down, each recorded with the moderator who did it and written to Nextcloud's audit log. Suspending deletes what the account posted here, and the confirmation says so.
  • An account browser over every account this instance knows, with the standing decision and the strike history against each one.
  • Federation health β€” how many deliveries are waiting, how many keep failing, which instances they are stacked up against, how close each is to being abandoned (16 attempts), and which instances have been given up on in the last seven days. occ social:queue:status prints the same summary and occ social:queue:retry --instance HOST puts one host's deliveries back.
  • Server settings with an interface β€” contact address, instance description, upload ceilings, inbox rate limit, secure mode, whether the block list is published, whether self-signed certificates are accepted. Every one of these used to be an occ config:app:set key that almost nobody set.
  • Announcements β€” a notice to the whole instance, optionally between a start and an end, that everybody reads at the top of their timeline and dismisses once. Retention and the instance access list (an allow-list or a deny-list of remote hosts, enforced both ways) are here too.
  • Domain blocks that clean up. Adding a host to the deny list also removes what it already sent: its accounts, their posts, the follows in both directions and the deliveries still queued towards it.
  • Setup checks in Administration β†’ Overview β€” whether .well-known/webfinger answers, whether the address Social builds its ids from is still the server's, whether the delivery job has run lately, and whether anything is stuck. occ social:check:install runs the same four and exits non-zero, so a deployment script can ask.

Statistics

A statistics page for the instance, and Mastodon's admin API β€” accounts, reports, domain blocks, IP and email-domain blocks, trends, measures and retention β€” for anything you would rather automate.

Retention keeps the database honest: remote statuses older than retention_days that nobody here interacted with are pruned with their attachments, and cached accounts nobody follows are evicted after cache_actor_days. Local content is never touched. occ social:media:usage says what the media is costing and how much of it is somebody else's.

πŸ”‘ For developers

  • The Mastodon client API, core surface plus OAuth 2: every timeline, posting with media and polls, follows, favourites, boosts, bookmarks, search, follow requests, reports, filters, conversations, markers, announcements, edit history and the admin API. See docs/API.md for exactly which routes exist.
  • Pixelfed's own routes β€” the /api/v2/config bootstrap its app reads on launch and the v1.1 discover namespace. Every limit in the config is derived from the one the server actually enforces.
  • Full ActivityPub delivery: signed HTTP for Create, Update, Delete, Like, Announce, Follow, Accept, Undo, Block, Flag and EmojiReact, an outbound queue and a stream queue, both drained by background jobs and by occ social:queue:process.
  • Inbox forwarding, so a reply from a stranger's instance reaches your followers β€” forwarded untouched and only when it carries its author's linked-data signature.
  • 26 occ commands, documented in docs/OCC-Commands.md.

Important

Third-party Mastodon clients cannot reach the API yet. Every route is served under /apps/social/, and the Mastodon client protocol has no way to be told about a non-root API base β€” so a client given your domain looks for /api/v1/... and finds nothing. Serving those paths at the domain root is the one thing standing between this and stock clients. See docs/Mastodon-Compatibility.md.

🚧 Not implemented yet

These are absent from the code today, not merely rough edges:

  • No status translation. The translate action returns the post unchanged (lib/Service/ActionService.php).
  • No streaming API and no push subscriptions. Third-party clients poll. (The web client does get live timelines when notify_push is installed β€” that is a Nextcloud channel, not a Mastodon one.)

πŸ“¦ Quickstart (install & develop)

  1. Clone this repository into your Nextcloud apps/ directory.
  2. Install the dependencies and build the frontend:
cd /var/www/nextcloud/apps/social
composer install     # PHP dependencies
npm ci
npm run build        # production bundle into js/
  1. Enable the app in Nextcloud (occ app:enable social) and check the install with occ social:check:install.
  2. While working on the UI, use npm run dev for a development build or npm run watch to rebuild on change. The Makefile wraps the same scripts (make build-js, make build-js-production, make watch-js, make lint).
  3. To produce a release archive, run make appstore (or ./build-package.sh, which is now a three-line wrapper around it β€” two copies of the exclusion list had already drifted apart, so there is one). It installs from the lock files, builds the frontend, stages the app without its development files and writes build/artifacts/social.tar.gz. It refuses to package when js/.htaccess is missing: that is a committed file rather than webpack output, and the target used to delete js/ wholesale before building. Despite the sign_dir name it only stages and tars, it does not sign anything.

🧭 ".well-known/webfinger isn't properly set up!" β€” Troubleshooting

That banner has two quite different causes, and the app now tells them apart.

The first is the one it names: the server does not answer /.well-known/webfinger. Follow the documented redirects.

The second is that Social is set up for a different address than the server now uses. Social reads overwrite.cli.url once, the first time the app is opened, and builds every account id, post id and WebFinger answer from that stored copy (social.cloud_url). Change the server's URL afterwards and the two drift apart in silence: WebFinger answers for a host nobody asks about, and the app blames .well-known when .well-known is fine.

Social reports the mismatch with both addresses but will not correct it, because the stored address is baked into 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. It asks twice; add --force to run it from a script (without it, --no-interaction refuses rather than quietly doing nothing).

To see the two values:

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

Administration β†’ Overview reports both of these β€” and two more things that break federation quietly β€” without anybody having to open Social. See docs/Admin.md for the whole setup, every configuration key and what to watch.

πŸ–ΌοΈ Banner / Header upload β€” Troubleshooting

Banner/header uploads work: the image is stored in the app's document cache, the local actor's cached header is updated, and the change is federated as an actor Update (lib/Controller/LocalController.php, uploadBanner()). A banner can also be set from a URL. The upload goes through the same filterMimeTypes() list as an attachment (lib/Service/CacheDocumentService.php), so every image type an attachment may have is accepted β€” JPEG, PNG, GIF, WebP, AVIF and HEIC/HEIF (the last converted on the way in); nothing narrows that list to pictures for a banner.

If an uploaded banner does not appear immediately:

  • Reload the profile page (clear the browser cache if necessary).
  • Check the server log for [LocalController] uploadBanner failed and for Failed to federate banner change, which is only a warning and does not undo the local upload.
  • Re-fetch the account data, or run occ social:cache:refresh, to refresh the cached actor.

βœ… Tests

Backend and frontend unit tests run without a Nextcloud server or database.

composer install            # PHPUnit + OCP interface stubs
composer run test:unit      # PHP: vendor/bin/phpunit -c tests/phpunit.xml

npm ci
npm test                    # JS: vitest run (tests/js/** and src/**/*.test.js)
npm run test:coverage       # with a coverage report in coverage/js
npm run typecheck           # tsc over the plain-JS half of src/, against src/types/

PHP tests live in tests/ mirroring lib/ (lib/Service/PostService.php β†’ tests/Service/PostServiceTest.php). Everything a class needs is mocked; the server code that resolves a service statically β€” OCP\Server::get(), which Response itself calls on every render β€” finds a TestContainer instead, which knows a silent logger and a session with nobody in it and raises a named error for anything else (see tests/Helper/TestContainer.php). Migration steps are given a FakeTable that records the schema they ask for rather than building it (tests/Migration/). Frontend tests use Vitest with @vue/test-utils and jsdom; tests/js/setup.js provides the Nextcloud globals (t, n, OC, OCA, localStorage, router webroots).

A second PHP suite in tests/Integration/ runs against a real Nextcloud and database β€” migrations, query SQL and storage boundaries the mocks cannot reach. CI runs it on SQLite, MySQL and PostgreSQL; locally, point it at an installed server:

NEXTCLOUD_ROOT=/path/to/nextcloud composer run test:integration

See tests/Integration/README.md for what it covers.

Browser tests

tests/e2e/ drives a real Nextcloud with the app installed, in Chromium, through Playwright: sign in, open the app, write a post and see it in the feed, walk the Discover page, switch scopes, open a group list. Nothing is mocked, and the bundle under test is the committed one in js/. .github/workflows/e2e.yml sets up a throwaway server for every pull request; against an instance of your own:

npx playwright install chromium
E2E_BASE_URL=https://cloud.example E2E_USER=alice E2E_PASSWORD=… E2E_GROUP="Design" npm run test:e2e

E2E_GROUP is optional β€” the display name of a Nextcloud group the account is in, for the list test; without it that test is skipped.

πŸ› οΈ Contributing

  • Contributions welcome β€” open a pull request and run the build and tests locally first (npm run lint, npm test, composer run test:unit).
  • Reset local Social data for development with:
occ social:reset

This prompts twice and then empties every Social table. occ social:reset --uninstall additionally drops the tables, migrations, background jobs and app config. See docs/OCC-Commands.md for all commands.

  • docs/Admin.md is the administrator's guide: what federation needs before it works, every app configuration key with its meaning and default, the sections of the administration page, how moderation is recorded, and the occ commands by task.
  • docs/Mastodon-Compatibility.md answers how close this is to Mastodon in the three senses that can mean β€” whether its clients work, whether peers can tell the difference, and whether an instance could move onto it. Its last section is the backlog that follows: everything still between this app and a full replacement, in tiers, with what each item actually fixes and whether it is done.
  • docs/User-Guide.md is the guide for the people using the app: getting an account, following, posting, reading, managing the account, keyboard shortcuts.
  • Before picking up refactoring work, read docs/Technical-Debt.md β€” what in the app is old, borrowed or load-bearing, and what changing it would cost β€” and docs/Performance.md, which lists the query and scalability problems that are still open and the ones that have been fixed. Neither is checked by a test, so re-verify a claim before acting on it and update the file in the same change as the code.

License

See the repository's license files in the LICENSES/ directory.


Admin guide Β· User guide Β· API Β· occ commands Β· Architecture Β· Mastodon compatibility

Screenshots are of a development instance with seeded demo accounts.

Releases

Packages

Used by

Contributors

Languages