Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: let 7 cfg = config.services.tangled.knot; 8in 9 with lib; { 10 options = { 11 services.tangled.knot = { 12 enable = mkOption { 13 type = types.bool; 14 default = false; 15 description = "Enable a tangled knot"; 16 }; 17 18 package = mkOption { 19 type = types.package; 20 description = "Package to use for the knot"; 21 }; 22 23 appviewEndpoint = mkOption { 24 type = types.str; 25 default = "https://tangled.org"; 26 description = "Appview endpoint"; 27 }; 28 29 gitUser = mkOption { 30 type = types.str; 31 default = "git"; 32 description = "User that hosts git repos and performs git operations"; 33 }; 34 35 openFirewall = mkOption { 36 type = types.bool; 37 default = true; 38 description = "Open port 22 in the firewall for ssh"; 39 }; 40 41 stateDir = mkOption { 42 type = types.path; 43 default = "/home/${cfg.gitUser}"; 44 description = "Tangled knot data directory"; 45 }; 46 47 repo = { 48 scanPath = mkOption { 49 type = types.path; 50 default = cfg.stateDir; 51 description = "Path where repositories are scanned from"; 52 }; 53 54 readme = mkOption { 55 type = types.listOf types.str; 56 default = [ 57 "README.md" 58 "readme.md" 59 "README" 60 "readme" 61 "README.markdown" 62 "readme.markdown" 63 "README.txt" 64 "readme.txt" 65 "README.rst" 66 "readme.rst" 67 "README.org" 68 "readme.org" 69 "README.asciidoc" 70 "readme.asciidoc" 71 ]; 72 description = "List of README filenames to look for (in priority order)"; 73 }; 74 75 mainBranch = mkOption { 76 type = types.str; 77 default = "main"; 78 description = "Default branch name for repositories"; 79 }; 80 }; 81 82 git = { 83 userName = mkOption { 84 type = types.str; 85 default = "Tangled"; 86 description = "Git user name used as committer"; 87 }; 88 89 userEmail = mkOption { 90 type = types.str; 91 default = "noreply@tangled.org"; 92 description = "Git user email used as committer"; 93 }; 94 }; 95 96 motd = mkOption { 97 type = types.nullOr types.str; 98 default = null; 99 description = '' 100 Message of the day 101 102 The contents are shown as-is; eg. you will want to add a newline if 103 setting a non-empty message since the knot won't do this for you. 104 ''; 105 }; 106 107 motdFile = mkOption { 108 type = types.nullOr types.path; 109 default = null; 110 description = '' 111 File containing message of the day 112 113 The contents are shown as-is; eg. you will want to add a newline if 114 setting a non-empty message since the knot won't do this for you. 115 ''; 116 }; 117 118 knotmirrors = mkOption { 119 type = types.listOf types.str; 120 default = [ 121 "https://mirror.tangled.network" 122 ]; 123 description = "List of knotmirror hosts to request crawl"; 124 }; 125 126 server = { 127 listenAddr = mkOption { 128 type = types.str; 129 default = "0.0.0.0:5555"; 130 description = "Address to listen on"; 131 }; 132 133 internalListenAddr = mkOption { 134 type = types.str; 135 default = "127.0.0.1:5444"; 136 description = "Internal address for inter-service communication"; 137 }; 138 139 owner = mkOption { 140 type = types.str; 141 example = "did:plc:qfpnj4og54vl56wngdriaxug"; 142 description = "DID of owner (required)"; 143 }; 144 145 dbPath = mkOption { 146 type = types.path; 147 default = "${cfg.stateDir}/knotserver.db"; 148 description = "Path to the database file"; 149 }; 150 151 hostname = mkOption { 152 type = types.str; 153 example = "my.knot.com"; 154 description = "Hostname for the server (required)"; 155 }; 156 157 plcUrl = mkOption { 158 type = types.str; 159 default = "https://plc.directory"; 160 description = "atproto PLC directory"; 161 }; 162 163 jetstreamEndpoint = mkOption { 164 type = types.str; 165 default = "wss://jetstream1.us-west.bsky.network/subscribe"; 166 description = "Jetstream endpoint to subscribe to"; 167 }; 168 169 logDids = mkOption { 170 type = types.bool; 171 default = true; 172 description = "Enable logging of DIDs"; 173 }; 174 175 dev = mkOption { 176 type = types.bool; 177 default = false; 178 description = "Enable development mode (disables signature verification)"; 179 }; 180 181 maxResponseKB = mkOption { 182 type = types.int; 183 default = 5120; 184 description = "Maximum response size in kilobytes"; 185 }; 186 }; 187 }; 188 }; 189 190 config = mkIf cfg.enable { 191 environment.systemPackages = [ 192 pkgs.git 193 cfg.package 194 ]; 195 196 users.users.${cfg.gitUser} = { 197 isSystemUser = true; 198 useDefaultShell = true; 199 home = cfg.stateDir; 200 createHome = true; 201 group = cfg.gitUser; 202 }; 203 204 users.groups.${cfg.gitUser} = {}; 205 206 services.openssh = { 207 enable = true; 208 extraConfig = '' 209 Match User ${cfg.gitUser} 210 AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 211 AuthorizedKeysCommandUser nobody 212 ChallengeResponseAuthentication no 213 PasswordAuthentication no 214 ''; 215 }; 216 217 environment.etc."ssh/keyfetch_wrapper" = { 218 mode = "0555"; 219 text = '' 220 #!${pkgs.stdenv.shell} 221 ${cfg.package}/bin/knot keys \ 222 -output authorized-keys \ 223 -internal-api "http://${cfg.server.internalListenAddr}" \ 224 -git-dir "${cfg.repo.scanPath}" \ 225 -log-path /tmp/knotguard.log 226 ''; 227 }; 228 229 systemd.services.knot = { 230 description = "knot service"; 231 after = ["network-online.target" "sshd.service"]; 232 wants = ["network-online.target"]; 233 wantedBy = ["multi-user.target"]; 234 enableStrictShellChecks = true; 235 236 preStart = let 237 setMotd = 238 if cfg.motdFile != null && cfg.motd != null 239 then throw "motdFile and motd cannot be both set" 240 else '' 241 ${optionalString (cfg.motdFile != null) "cat ${cfg.motdFile} > ${cfg.stateDir}/motd"} 242 ${optionalString (cfg.motd != null) ''printf "${cfg.motd}" > ${cfg.stateDir}/motd''} 243 ''; 244 in '' 245 mkdir -p "${cfg.repo.scanPath}" 246 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}" 247 248 mkdir -p "${cfg.stateDir}/.config/git" 249 cat > "${cfg.stateDir}/.config/git/config" << EOF 250 [user] 251 name = ${cfg.git.userName} 252 email = ${cfg.git.userEmail} 253 [receive] 254 advertisePushOptions = true 255 [uploadpack] 256 allowFilter = true 257 allowReachableSHA1InWant = true 258 EOF 259 ${setMotd} 260 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}" 261 ''; 262 263 serviceConfig = { 264 User = cfg.gitUser; 265 PermissionsStartOnly = true; 266 WorkingDirectory = cfg.stateDir; 267 Environment = [ 268 "PATH=${lib.makeBinPath [pkgs.bash pkgs.git pkgs.coreutils]}:/run/current-system/sw/bin" 269 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 270 "KNOT_REPO_README=${concatStringsSep "," cfg.repo.readme}" 271 "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 272 "KNOT_GIT_USER_NAME=${cfg.git.userName}" 273 "KNOT_GIT_USER_EMAIL=${cfg.git.userEmail}" 274 "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 275 "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 276 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 277 "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 278 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 279 "KNOT_SERVER_PLC_URL=${cfg.server.plcUrl}" 280 "KNOT_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 281 "KNOT_SERVER_OWNER=${cfg.server.owner}" 282 "KNOT_MIRRORS=${concatStringsSep "," cfg.knotmirrors}" 283 "KNOT_SERVER_LOG_DIDS=${ 284 if cfg.server.logDids 285 then "true" 286 else "false" 287 }" 288 "KNOT_SERVER_DEV=${ 289 if cfg.server.dev 290 then "true" 291 else "false" 292 }" 293 "KNOT_SERVER_MAX_RESPONSE_KB=${toString cfg.server.maxResponseKB}" 294 ]; 295 ExecStart = "${cfg.package}/bin/knot server"; 296 Restart = "always"; 297 }; 298 }; 299 300 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 301 }; 302 }