nixos-dotfiles
nixos-dotfiles
https://git.tonybtw.com/nixos-dotfiles.git
git://git.tonybtw.com/nixos-dotfiles.git
updates.
Diff
diff --git a/config/nvim/after/plugin/treesitter.lua b/config/nvim/after/plugin/treesitter.lua
index 4713ce0..fdf3ca3 100644
--- a/config/nvim/after/plugin/treesitter.lua
+++ b/config/nvim/after/plugin/treesitter.lua
@@ -19,7 +19,7 @@ local parsers = {
"json", "python", "ron", "javascript", "haskell", "d", "query",
"typescript", "tsx", "rust", "zig", "php", "yaml", "html", "css",
"markdown", "markdown_inline", "bash", "lua", "vim", "vimdoc", "c",
- "dockerfile", "gitignore", "astro", "nim"
+ "dockerfile", "gitignore", "astro", "nim", "go"
}
vim.api.nvim_create_user_command("TSInstallAll", function()
diff --git a/config/nvim/plugin/lsp.lua b/config/nvim/plugin/lsp.lua
index 8bbc49e..87b20d6 100644
--- a/config/nvim/plugin/lsp.lua
+++ b/config/nvim/plugin/lsp.lua
@@ -245,12 +245,6 @@ vim.lsp.config['gopls'] = {
root_markers = { 'go.mod', 'go.work', '.git' },
capabilities = caps,
settings = {
- gopls = {
- analyses = {
- unusedparams = true,
- },
- staticcheck = true,
- },
},
}
diff --git a/config/temacs/config.el b/config/temacs/config.el
index 40b906b..5c62d98 100644
--- a/config/temacs/config.el
+++ b/config/temacs/config.el
@@ -176,7 +176,7 @@
erc-use-auth-source-for-nickserv-password t)
(setq erc-autojoin-channels-alist
- '(("libera.chat" "#technicalrenaissance")))
+ '(("znc.tonybtw.com" "#technicalrenaissance")))
(defun my/erc-connect-libera ()
"Connect to Libera.Chat via ZNC bouncer."
diff --git a/server/default.nix b/server/default.nix
index 2f2adff..5d43281 100644
--- a/server/default.nix
+++ b/server/default.nix
@@ -1,9 +1,11 @@
-{ ... }:
-{
+{...}: {
imports = [
./nginx.nix
./xmpp.nix
+ ./xmpp-register.nix
./znc.nix
./guandanbtw.nix
+ ./matrix.nix
+ ./git.nix
];
}
diff --git a/server/git.nix b/server/git.nix
new file mode 100644
index 0000000..7b1794c
--- /dev/null
+++ b/server/git.nix
@@ -0,0 +1,74 @@
+{
+ config,
+ pkgs,
+ ...
+}: let
+ domain = "git.tonybtw.com";
+ gitRoot = "/srv/git";
+in {
+ services.cgit.main = {
+ enable = true;
+ nginx.virtualHost = domain;
+ scanPath = gitRoot;
+ gitHttpBackend.enable = false;
+ settings = {
+ root-title = "Tony's Git";
+ root-desc = "Personal git repositories";
+ clone-url = "https://${domain}/$CGIT_REPO_URL git://${domain}/$CGIT_REPO_URL";
+ enable-git-config = 1;
+ enable-index-owner = 0;
+ enable-commit-graph = 1;
+ enable-log-filecount = 1;
+ enable-log-linecount = 1;
+ source-filter = "${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
+ about-filter = "${pkgs.cgit}/lib/cgit/filters/about-formatting.sh";
+ readme = ":README.md";
+ };
+ };
+
+ services.nginx.virtualHosts.${domain} = {
+ enableACME = true;
+ forceSSL = true;
+ locations."~ ^/([^/]+)/(HEAD|info/refs|objects|git-upload-pack)$" = {
+ fastcgiParams = {
+ GIT_HTTP_EXPORT_ALL = "";
+ GIT_PROJECT_ROOT = gitRoot;
+ PATH_INFO = "$uri";
+ };
+ extraConfig = ''
+ fastcgi_pass unix:/run/fcgiwrap.sock;
+ '';
+ };
+ };
+
+ services.fcgiwrap.instances.git = {
+ process.user = "git";
+ process.group = "git";
+ socket = {inherit (config.services.nginx) user group;};
+ };
+
+ systemd.services.git-daemon = {
+ description = "Git daemon";
+ wantedBy = ["multi-user.target"];
+ after = ["network.target"];
+ serviceConfig = {
+ ExecStart = "${pkgs.git}/bin/git daemon --reuseaddr --base-path=${gitRoot} --export-all --verbose ${gitRoot}";
+ User = "git";
+ Group = "git";
+ };
+ };
+
+ users.users.git = {
+ isSystemUser = true;
+ group = "git";
+ home = gitRoot;
+ shell = "${pkgs.git}/bin/git-shell";
+ };
+ users.groups.git = {};
+
+ systemd.tmpfiles.rules = [
+ "d ${gitRoot} 0755 git git -"
+ ];
+
+ networking.firewall.allowedTCPPorts = [9418];
+}
diff --git a/server/matrix.nix b/server/matrix.nix
new file mode 100644
index 0000000..f6eecb6
--- /dev/null
+++ b/server/matrix.nix
@@ -0,0 +1,101 @@
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}: let
+ domain = "tonybtw.com";
+ matrixDomain = "matrix.${domain}";
+ clientConfig = {
+ "m.homeserver".base_url = "https://${matrixDomain}";
+ "m.identity_server" = {};
+ };
+ serverConfig = {
+ "m.server" = "${matrixDomain}:443";
+ };
+ mkWellKnown = data: ''
+ default_type application/json;
+ add_header Access-Control-Allow-Origin *;
+ return 200 '${builtins.toJSON data}';
+ '';
+in {
+ services.matrix-synapse = {
+ enable = true;
+ settings = {
+ server_name = domain;
+ public_baseurl = "https://${matrixDomain}";
+
+ listeners = [
+ {
+ port = 8008;
+ bind_addresses = ["127.0.0.1"];
+ type = "http";
+ tls = false;
+ x_forwarded = true;
+ resources = [
+ {
+ names = ["client" "federation"];
+ compress = true;
+ }
+ ];
+ }
+ ];
+
+ database = {
+ name = "psycopg2";
+ allow_unsafe_locale = true;
+ args = {
+ user = "matrix-synapse";
+ database = "matrix-synapse";
+ host = "/run/postgresql";
+ };
+ };
+
+ max_upload_size_mib = 100;
+ url_preview_enabled = true;
+ enable_registration = false;
+ enable_metrics = false;
+ registration_shared_secret_path = "/var/lib/matrix-synapse/registration_secret";
+
+ trusted_key_servers = [
+ {
+ server_name = "matrix.org";
+ }
+ ];
+ };
+ };
+
+ services.postgresql = {
+ enable = true;
+ ensureDatabases = ["matrix-synapse"];
+ ensureUsers = [
+ {
+ name = "matrix-synapse";
+ ensureDBOwnership = true;
+ }
+ ];
+ };
+
+ services.nginx.virtualHosts.${domain} = {
+ enableACME = true;
+ forceSSL = true;
+ locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
+ locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
+ };
+
+ services.nginx.virtualHosts.${matrixDomain} = {
+ enableACME = true;
+ forceSSL = true;
+ locations."/" = {
+ proxyPass = "http://127.0.0.1:8008";
+ extraConfig = ''
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Host $host;
+ client_max_body_size 100M;
+ '';
+ };
+ };
+
+ networking.firewall.allowedTCPPorts = [8448];
+}
diff --git a/server/xmpp-register.nix b/server/xmpp-register.nix
new file mode 100644
index 0000000..7b7960b
--- /dev/null
+++ b/server/xmpp-register.nix
@@ -0,0 +1,66 @@
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}: let
+ domain = "xmpp.tonybtw.com";
+ dataDir = "/var/lib/xmpp-register";
+ phpUser = "xmpp-register";
+ phpGroup = "xmpp-register";
+
+ phpApp = pkgs.runCommand "xmpp-register-app" {} ''
+ mkdir -p $out
+ cp ${./xmpp-register/index.php} $out/index.php
+ '';
+in {
+ services.phpfpm.pools.xmpp-register = {
+ user = phpUser;
+ group = phpGroup;
+ settings = {
+ "listen.owner" = config.services.nginx.user;
+ "listen.group" = config.services.nginx.group;
+ "pm" = "ondemand";
+ "pm.max_children" = 4;
+ "pm.process_idle_timeout" = "10s";
+ };
+ phpOptions = ''
+ session.save_path = "${dataDir}/sessions"
+ '';
+ };
+
+ services.nginx.virtualHosts.${domain} = {
+ forceSSL = true;
+ useACMEHost = domain;
+
+ locations."= /register" = {
+ return = "301 /register/";
+ };
+
+ locations."^~ /register/" = {
+ extraConfig = ''
+ fastcgi_pass unix:${config.services.phpfpm.pools.xmpp-register.socket};
+ include ${pkgs.nginx}/conf/fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME ${phpApp}/index.php;
+ '';
+ };
+ };
+
+ users.users.${phpUser} = {
+ isSystemUser = true;
+ group = phpGroup;
+ home = dataDir;
+ };
+ users.groups.${phpGroup} = {};
+
+ systemd.tmpfiles.rules = [
+ "d ${dataDir} 0750 ${phpUser} ${phpGroup} -"
+ "d ${dataDir}/sessions 0750 ${phpUser} ${phpGroup} -"
+ ];
+
+ security.sudo.extraConfig = ''
+ ${phpUser} ALL=(prosody) NOPASSWD: /run/current-system/sw/bin/prosodyctl register *
+ '';
+
+ environment.systemPackages = [pkgs.prosody];
+}
diff --git a/server/xmpp-register/index.php b/server/xmpp-register/index.php
new file mode 100644
index 0000000..cdd3934
--- /dev/null
+++ b/server/xmpp-register/index.php
@@ -0,0 +1,289 @@
+<?php
+define('XMPP_DOMAIN', 'xmpp.tonybtw.com');
+define('DATA_FILE', '/var/lib/xmpp-register/pending.json');
+define('ADMIN_PASS_FILE', '/var/lib/xmpp-register/admin_password');
+define('BASE_PATH', '/register');
+
+session_start();
+
+function load_pending(): array {
+ if (!file_exists(DATA_FILE)) return [];
+ $data = file_get_contents(DATA_FILE);
+ return $data ? json_decode($data, true) : [];
+}
+
+function save_pending(array $pending): void {
+ $dir = dirname(DATA_FILE);
+ if (!is_dir($dir)) mkdir($dir, 0750, true);
+ file_put_contents(DATA_FILE, json_encode($pending, JSON_PRETTY_PRINT));
+}
+
+function get_admin_password(): string {
+ return file_exists(ADMIN_PASS_FILE) ? trim(file_get_contents(ADMIN_PASS_FILE)) : '';
+}
+
+function is_admin(): bool {
+ return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
+}
+
+function redirect(string $path): void {
+ header("Location: " . BASE_PATH . $path);
+ exit;
+}
+
+function url(string $path): string {
+ return BASE_PATH . $path;
+}
+
+function h(string $s): string {
+ return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
+}
+
+$flash = $_SESSION['flash'] ?? null;
+unset($_SESSION['flash']);
+
+function flash(string $type, string $msg): void {
+ $_SESSION['flash'] = ['type' => $type, 'msg' => $msg];
+}
+
+$fullPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
+$path = preg_replace('#^' . preg_quote(BASE_PATH, '#') . '#', '', $fullPath) ?: '/';
+$method = $_SERVER['REQUEST_METHOD'];
+
+if ($method === 'POST') {
+ if ($path === '/submit') {
+ $username = strtolower(trim($_POST['username'] ?? ''));
+ $password = $_POST['password'] ?? '';
+ $password_confirm = $_POST['password_confirm'] ?? '';
+ $reason = trim($_POST['reason'] ?? '');
+
+ if (!$username || !preg_match('/^[a-z][a-z0-9._-]{2,29}$/', $username)) {
+ flash('error', 'Username must be 3-30 chars, start with a letter, lowercase alphanumeric/._-');
+ redirect('/');
+ }
+ if (strlen($password) < 8) {
+ flash('error', 'Password must be at least 8 characters.');
+ redirect('/');
+ }
+ if ($password !== $password_confirm) {
+ flash('error', 'Passwords do not match.');
+ redirect('/');
+ }
+ if (!$reason) {
+ flash('error', 'Please tell us why you want to join.');
+ redirect('/');
+ }
+
+ $pending = load_pending();
+ foreach ($pending as $p) {
+ if ($p['username'] === $username) {
+ flash('error', 'A registration for this username is already pending.');
+ redirect('/');
+ }
+ }
+
+ $pending[] = [
+ 'id' => bin2hex(random_bytes(8)),
+ 'username' => $username,
+ 'password' => $password,
+ 'reason' => $reason,
+ 'timestamp' => date('c'),
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ ];
+ save_pending($pending);
+
+ flash('success', 'Registration submitted! You\'ll be notified once approved.');
+ redirect('/');
+ }
+
+ if ($path === '/admin/login') {
+ $pass = $_POST['password'] ?? '';
+ if ($pass && $pass === get_admin_password()) {
+ $_SESSION['admin'] = true;
+ redirect('/admin');
+ }
+ flash('error', 'Invalid password.');
+ redirect('/admin/login');
+ }
+
+ if ($path === '/admin/logout') {
+ unset($_SESSION['admin']);
+ redirect('/');
+ }
+
+ if (preg_match('#^/admin/approve/([a-f0-9]+)$#', $path, $m) && is_admin()) {
+ $id = $m[1];
+ $pending = load_pending();
+ $req = null;
+ foreach ($pending as $i => $p) {
+ if ($p['id'] === $id) {
+ $req = $p;
+ unset($pending[$i]);
+ break;
+ }
+ }
+ if (!$req) {
+ flash('error', 'Request not found.');
+ redirect('/admin');
+ }
+
+ $jid = $req['username'] . '@' . XMPP_DOMAIN;
+ $pass = $req['password'];
+
+ $cmd = sprintf(
+ '/run/wrappers/bin/sudo -u prosody /run/current-system/sw/bin/prosodyctl register %s %s %s 2>&1',
+ escapeshellarg($req['username']),
+ escapeshellarg(XMPP_DOMAIN),
+ escapeshellarg($pass)
+ );
+ $output = shell_exec($cmd);
+ file_put_contents('/var/lib/xmpp-register/debug.log', date('c') . " CMD: $cmd\nOUT: $output\n", FILE_APPEND);
+
+ save_pending(array_values($pending));
+ if (strpos($output, 'User account created') !== false) {
+ flash('success', "User $jid created!");
+ } else {
+ flash('error', "Failed: $output");
+ }
+ redirect('/admin');
+ }
+
+ if (preg_match('#^/admin/deny/([a-f0-9]+)$#', $path, $m) && is_admin()) {
+ $id = $m[1];
+ $pending = load_pending();
+ $pending = array_filter($pending, fn($p) => $p['id'] !== $id);
+ save_pending(array_values($pending));
+ flash('success', 'Registration denied.');
+ redirect('/admin');
+ }
+}
+
+?>
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>XMPP Registration - <?= h(XMPP_DOMAIN) ?></title>
+ <style>
+ :root { --bg:#1a1a2e; --surface:#16213e; --primary:#0f3460; --accent:#e94560; --text:#eee; --muted:#aaa; --ok:#4ade80; --err:#f87171; }
+ * { box-sizing:border-box; margin:0; padding:0; }
+ body { font-family:system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; display:flex; justify-content:center; padding:2rem 1rem; }
+ .container { width:100%; max-width:500px; }
+ h1 { text-align:center; color:var(--accent); margin-bottom:.5rem; }
+ .sub { text-align:center; color:var(--muted); margin-bottom:2rem; }
+ .card { background:var(--surface); border-radius:12px; padding:2rem; }
+ .form-group { margin-bottom:1.5rem; }
+ label { display:block; margin-bottom:.5rem; color:var(--muted); font-size:.9rem; }
+ input,textarea { width:100%; padding:.75rem 1rem; border:2px solid var(--primary); border-radius:8px; background:var(--bg); color:var(--text); font-size:1rem; }
+ input:focus,textarea:focus { outline:none; border-color:var(--accent); }
+ textarea { resize:vertical; min-height:80px; }
+ .hint { font-size:.8rem; color:var(--muted); margin-top:.25rem; }
+ .addon-wrap { display:flex; }
+ .addon-wrap input { border-radius:8px 0 0 8px; }
+ .addon { background:var(--primary); padding:.75rem 1rem; border-radius:0 8px 8px 0; color:var(--muted); font-size:.9rem; }
+ button,.btn { display:inline-block; padding:.75rem 1.5rem; background:var(--accent); color:#fff; border:none; border-radius:8px; font-size:1rem; font-weight:600; cursor:pointer; text-decoration:none; }
+ button:hover,.btn:hover { opacity:.9; }
+ .btn-ok { background:var(--ok); color:var(--bg); }
+ .btn-no { background:var(--err); }
+ .btn-sm { padding:.5rem 1rem; font-size:.875rem; }
+ .flash { padding:1rem; border-radius:8px; margin-bottom:1.5rem; }
+ .flash-error { background:rgba(248,113,113,.2); border:1px solid var(--err); color:var(--err); }
+ .flash-success { background:rgba(74,222,128,.2); border:1px solid var(--ok); color:var(--ok); }
+ .nav { margin-bottom:1.5rem; }
+ .nav a { color:var(--accent); }
+ .pending { background:var(--surface); border-radius:8px; padding:1rem; margin-bottom:1rem; }
+ .pending-head { display:flex; justify-content:space-between; margin-bottom:.5rem; }
+ .pending-user { font-weight:600; color:var(--accent); }
+ .pending-meta { font-size:.8rem; color:var(--muted); }
+ .pending-reason { margin:.5rem 0; padding:.5rem; background:var(--bg); border-radius:4px; font-size:.9rem; }
+ .pending-actions { display:flex; gap:.5rem; }
+ .empty { text-align:center; color:var(--muted); padding:2rem; }
+ </style>
+</head>
+<body>
+<div class="container">
+<?php if ($flash): ?>
+ <div class="flash flash-<?= h($flash['type']) ?>"><?= h($flash['msg']) ?></div>
+<?php endif; ?>
+
+<?php if ($path === '/admin/login'): ?>
+ <h1>Admin Login</h1>
+ <p class="sub">Enter admin password</p>
+ <div class="card">
+ <form method="post" action="<?= url('/admin/login') ?>">
+ <div class="form-group">
+ <label>Password</label>
+ <input type="password" name="password" required autofocus>
+ </div>
+ <button type="submit">Login</button>
+ </form>
+ </div>
+ <p class="nav" style="margin-top:1rem;text-align:center;"><a href="<?= url('/') ?>">Back to registration</a></p>
+
+<?php elseif ($path === '/admin' && is_admin()): ?>
+ <h1>Admin Panel</h1>
+ <p class="sub">Pending registration requests</p>
+ <p class="nav"><a href="<?= url('/') ?>">Registration page</a> | <form method="post" action="<?= url('/admin/logout') ?>" style="display:inline;"><button type="submit" class="btn btn-sm" style="background:var(--primary);">Logout</button></form></p>
+ <?php $pending = load_pending(); ?>
+ <?php if (empty($pending)): ?>
+ <div class="card empty">No pending requests.</div>
+ <?php else: ?>
+ <?php foreach ($pending as $p): ?>
+ <div class="pending">
+ <div class="pending-head">
+ <span class="pending-user"><?= h($p['username']) ?>@<?= h(XMPP_DOMAIN) ?></span>
+ <span class="pending-meta"><?= h(substr($p['timestamp'], 0, 10)) ?></span>
+ </div>
+ <div class="pending-meta">IP: <?= h($p['ip']) ?></div>
+ <div class="pending-reason"><?= h($p['reason']) ?></div>
+ <div class="pending-actions">
+ <form method="post" action="<?= url('/admin/approve/' . h($p['id'])) ?>">
+ <button type="submit" class="btn btn-sm btn-ok">Approve</button>
+ </form>
+ <form method="post" action="<?= url('/admin/deny/' . h($p['id'])) ?>">
+ <button type="submit" class="btn btn-sm btn-no">Deny</button>
+ </form>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ <?php endif; ?>
+
+<?php elseif ($path === '/admin'): ?>
+ <?php redirect('/admin/login'); ?>
+
+<?php else: ?>
+ <h1>Join <?= h(XMPP_DOMAIN) ?></h1>
+ <p class="sub">Request an XMPP account</p>
+ <div class="card">
+ <form method="post" action="<?= url('/submit') ?>">
+ <div class="form-group">
+ <label>Username</label>
+ <div class="addon-wrap">
+ <input type="text" name="username" pattern="[a-z][a-z0-9._-]{2,29}" required autofocus>
+ <span class="addon">@<?= h(XMPP_DOMAIN) ?></span>
+ </div>
+ <p class="hint">3-30 chars, starts with letter, lowercase only</p>
+ </div>
+ <div class="form-group">
+ <label>Password</label>
+ <input type="password" name="password" minlength="8" required>
+ <p class="hint">At least 8 characters</p>
+ </div>
+ <div class="form-group">
+ <label>Confirm Password</label>
+ <input type="password" name="password_confirm" minlength="8" required>
+ </div>
+ <div class="form-group">
+ <label>Why do you want to join?</label>
+ <textarea name="reason" required placeholder="Tell us a bit about yourself..."></textarea>
+ </div>
+ <button type="submit">Submit Request</button>
+ </form>
+ </div>
+ <p class="nav" style="margin-top:1rem;text-align:center;"><a href="<?= url('/admin') ?>">Admin</a></p>
+<?php endif; ?>
+</div>
+</body>
+</html>
diff --git a/server/xmpp.nix b/server/xmpp.nix
index 412d9c4..2a5405f 100644
--- a/server/xmpp.nix
+++ b/server/xmpp.nix
@@ -81,15 +81,22 @@ in {
defaults.email = "tony@tonybtw.com";
certs.${domain} = {
- group = "prosody";
- listenHTTP = ":80";
+ group = "certs";
+ webroot = "/var/lib/acme/acme-challenge";
postRun = "systemctl reload prosody.service";
extraDomainNames = [mucDomain uploadDomain];
};
};
+ users.groups.certs.members = ["prosody" "nginx"];
+
+ services.nginx.virtualHosts.${domain} = {
+ locations."/.well-known/acme-challenge" = {
+ root = "/var/lib/acme/acme-challenge";
+ };
+ };
+
networking.firewall.allowedTCPPorts = [
- 80
5222
5269
5281
@@ -103,4 +110,5 @@ in {
iptables -A INPUT -p tcp --dport 5222 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 5222 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 -j DROP
'';
+
}
diff --git a/server/znc.nix b/server/znc.nix
index 04005e9..2cdafd8 100644
--- a/server/znc.nix
+++ b/server/znc.nix
@@ -1,5 +1,4 @@
-{ config, ... }:
-{
+{config, ...}: {
services.znc = {
enable = true;
openFirewall = true;
@@ -7,7 +6,7 @@
useLegacyConfig = false;
config = {
SSLCertFile = "/var/lib/acme/znc.tonybtw.com/full.pem";
- LoadModule = [ "webadmin" "adminlog" ];
+ LoadModule = ["webadmin" "adminlog"];
Listener.l = {
Port = 6697;
SSL = true;
@@ -24,10 +23,10 @@
AltNick = "tonybtw_";
Ident = "tony";
RealName = "tony";
- LoadModule = [ "chansaver" "controlpanel" ];
+ LoadModule = ["chansaver" "controlpanel"];
Network.libera = {
Server = "irc.libera.chat +6697";
- LoadModule = [ "simple_away" "sasl" "nickserv" "log" ];
+ LoadModule = ["simple_away" "sasl" "nickserv" "log"];
Chan = {
"#technicalrenaissance" = {};
};
@@ -54,7 +53,7 @@
};
security.acme.certs."znc.tonybtw.com".group = "znc";
- users.users.nginx.extraGroups = [ "znc" ];
+ users.users.nginx.extraGroups = ["znc"];
- networking.firewall.allowedTCPPorts = [ 6697 ];
+ networking.firewall.allowedTCPPorts = [6697];
}