{ config, lib, pkgs, ... }: let sp = config.selfprivacy; cfg = sp.modules.mastodon; oauthClientID = "mastodon"; auth-passthru = config.selfprivacy.passthru.auth; oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; issuer = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL; oauthRedirectURL = "https://${cfg.subdomain}.${sp.domain}/auth/auth/openid_connect/callback"; usersGroup = "sp.mastodon.users"; adminsGroup = "sp.mastodon.admins"; oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID; serviceAccountFP = auth-passthru.mkServiceAccountTokenFP "mastodon"; secrets = rec { dir = "/run/keys/mastodon"; hashedPasswordFile = "${dir}/hashed_email_password"; passwordFile = "${dir}/email_password"; }; in { options.selfprivacy.modules.mastodon = { enable = (lib.mkOption { default = false; type = lib.types.bool; description = "Enable Mastodon"; }) // { meta = { type = "enable"; }; }; location = (lib.mkOption { type = lib.types.str; description = "Mastodon location"; }) // { meta = { type = "location"; }; }; subdomain = (lib.mkOption { default = "mastodon"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; description = "Subdomain (changing subdomain after setting up will cause breakage of the server!)"; }) // { meta = { widget = "subdomain"; type = "string"; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; weight = 0; }; }; dissallowUnauthenticatedAPI = (lib.mkOption { default = true; type = lib.types.bool; description = "Allow unauthenticated API access"; }) // { meta = { type = "bool"; weight = 1; }; }; }; config = lib.mkIf cfg.enable { fileSystems = lib.mkIf sp.useBinds { "/var/lib/mastodon" = { device = "/volumes/${cfg.location}/mastodon"; options = [ "bind" ]; }; }; services.mastodon = { enable = true; localDomain = "${cfg.subdomain}.${sp.domain}"; enableUnixSocket = false; configureNginx = true; database.createLocally = true; streamingProcesses = 3; smtp = { createLocally = false; fromAddress = "noreply.mastodon@${sp.domain}"; user = "noreply.mastodon"; passwordFile = secrets.passwordFile; authenticate = true; host = "hollowness.top"; port = 465; }; extraConfig = { # "SMTP_ENABLE_STARTTLS" = "never"; "SMTP_ENABLE_STARTTLS_AUTO" = "true"; "SMTP_ENABLE_STARTTLS" = "always"; "SMTP_TLS" = "true"; "SMTP_SSL" = "true"; "DISALLOW_UNAUTHENTICATED_API_ACCESS" = lib.boolToString cfg.dissallowUnauthenticatedAPI; }; }; users.users.mastodon.isSystemUser = lib.mkForce false; users.users.mastodon.isNormalUser = lib.mkForce true; users.users.noreply.mastodon.group = "email-users"; users.groups."email-users" = {}; users.users."noreply.mastodon".isSystemUser = true; selfprivacy.emails."noreply.mastodon" = { hashedPasswordFile = secrets.hashedPasswordFile; systemdTargets = [ "mastodon-email-password-setup.service" ]; sendOnly = true; }; systemd = { services.mastodon-email-password-setup = { enable = true; wantedBy = [ "multi-user.target" "mastodon-web.service" "postfix.service" ]; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "gen-mastodon-email-password" '' export password=$(head -c 32 /dev/urandom | base64 | sed 's/[+=\\/A-Z]//g') mkdir ${secrets.dir} || true # Create ${secrets.dir} if it doesn't exist rm -f ${secrets.passwordFile} || true echo "$password" > ${secrets.passwordFile} chmod 400 ${secrets.passwordFile} chown ${config.services.mastodon.user}:${config.services.mastodon.group} ${secrets.passwordFile} export hashedPassword=$(${lib.getExe pkgs.mkpasswd} -sm bcrypt "$password") rm -f ${secrets.hashedPasswordFile} || true echo "$hashedPassword" > ${secrets.hashedPasswordFile} chmod 440 ${secrets.hashedPasswordFile} chown ${config.services.postfix.user}:${config.services.postfix.group} ${secrets.hashedPasswordFile} ''; }; }; services.mastodon-kanidm-sync = { after = [ # "mastodon.service" # TODO: ?? "postgresql.service" "kanidm.service" ]; requires = [ "kanidm.service" "postgresql.service" ]; wantedBy = [ "multi-user.target" ]; environment = let db = config.services.mastodon.database; in { KANIDM_URL = config.services.kanidm.serverSettings.origin; KANIDM_TOKEN_PATH = serviceAccountFP; POSTGRES_DBNAME = db.name; POSTGRES_USER = db.user; POSTGRES_HOST = db.host; USERDATA_FILE_PATH = "/var/lib/mastodon/.userdata.json"; OWNER_USERNAME = sp.username; }; serviceConfig = { Slice = "mastodon.slice"; User = "mastodon"; Group = "mastodon"; LoadCredential = [ "kanidm-token:${serviceAccountFP}" ]; ExecStart = pkgs.writers.writePython3 "mas-kanidm-sync" { doCheck = false; libraries = with pkgs.python3Packages; [ requests psycopg2 ]; } (builtins.readFile ./mastodon-kanidm-sync.py); }; }; services.mastodon-web = { unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/mastodon"; serviceConfig = { Slice = "hedgedoc.slice"; LoadCredential = ["client-secret:${oauthClientSecretFP}"]; ExecStart = lib.mkForce (pkgs.writeShellScript "run-mastodon-with-client-secret" '' export OIDC_CLIENT_SECRET=$(cat $CREDENTIALS_DIRECTORY/client-secret) ${config.services.mastodon.package}/bin/puma -C config/puma.rb ''); }; environment = { OIDC_ENABLED = "true"; OIDC_DISPLAY_NAME= "Kanidm"; OIDC_ISSUER = issuer; OIDC_DISCOVERY = "true"; OIDC_SCOPE = "openid,profile,email"; OIDC_UID_FIELD = "preferred_username"; OIDC_CLIENT_ID = oauthClientID; OIDC_REDIRECT_URI = oauthRedirectURL; OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED = "false"; }; }; slices.mastodon = { description = "Mastodon service slice"; }; }; selfprivacy.auth.clients.${oauthClientID} = { inherit usersGroup; inherit adminsGroup; isTokenNeeded = true; subdomain = cfg.subdomain; originLanding = "https://${cfg.subdomain}.${sp.domain}/"; originUrl = oauthRedirectURL; clientSystemdUnits = [ "mastodon.service" ]; enablePkce = false; useShortPreferredUsername = true; linuxUserOfClient = "mastodon"; linuxGroupOfClient = "mastodon"; }; }; }