{ config, lib, pkgs, ... }: with lib; let cfg = config.services.nextcloud-opencode-bot; pythonEnv = pkgs.python3.withPackages ( ps: with ps; [ fastapi uvicorn httpx ] ); botModule = pkgs.runCommand "nextcloud-opencode-bot-module" { } '' mkdir -p $out cp ${./bot.py} $out/nextcloud_opencode_bot.py ''; in { options.services.nextcloud-opencode-bot = { enable = mkEnableOption "Nextcloud Talk OpenCode Bot"; port = mkOption { type = types.port; default = 8086; description = "Port for the webhook listener"; }; host = mkOption { type = types.str; default = "127.0.0.1"; description = "Host to bind to"; }; nextcloudUrl = mkOption { type = types.str; example = "https://cloud.example.com"; description = "Base URL of your Nextcloud instance"; }; botSecretFile = mkOption { type = types.path; description = "Path to file containing the bot secret (shared with Nextcloud)"; }; modelBaseUrl = mkOption { type = types.str; example = "http://halo.hoyer.tail:8000/v1"; description = "Base URL of the OpenAI-compatible chat-completions endpoint (without trailing /chat/completions)"; }; model = mkOption { type = types.str; default = "halo-8000"; description = "Model name passed in the `model` field of /chat/completions requests"; }; botName = mkOption { type = types.str; default = "Halo"; description = "Bot display name (used to detect @mentions in group chats)"; }; allowedUsers = mkOption { type = types.listOf types.str; default = [ ]; description = "Nextcloud usernames allowed to talk to the bot (empty = all)"; }; contextMessages = mkOption { type = types.int; default = 6; description = "Number of recent (user+assistant) turns to keep as context"; }; timeout = mkOption { type = types.int; default = 120; description = "Timeout in seconds for the model API call"; }; systemPrompt = mkOption { type = types.nullOr types.str; default = null; description = "Optional additional system prompt appended to the built-in one"; }; }; config = mkIf cfg.enable { systemd.services.nextcloud-opencode-bot = { description = "Nextcloud Talk OpenCode Bot"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment = { BOT_HOST = cfg.host; BOT_PORT = toString cfg.port; NEXTCLOUD_URL = cfg.nextcloudUrl; MODEL_BASE_URL = cfg.modelBaseUrl; MODEL_NAME = cfg.model; BOT_NAME = cfg.botName; ALLOWED_USERS = concatStringsSep "," cfg.allowedUsers; CONTEXT_MESSAGES = toString cfg.contextMessages; TIMEOUT = toString cfg.timeout; SYSTEM_PROMPT = cfg.systemPrompt or ""; PYTHONPATH = botModule; }; serviceConfig = { Type = "simple"; ExecStart = "${pythonEnv}/bin/uvicorn nextcloud_opencode_bot:app --host ${cfg.host} --port ${toString cfg.port}"; Restart = "always"; RestartSec = 5; User = "opencode-bot"; Group = "opencode-bot"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = "read-only"; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = false; LockPersonality = true; LoadCredential = "bot-secret:${cfg.botSecretFile}"; StateDirectory = "nextcloud-opencode-bot"; }; }; users.users.opencode-bot = { isSystemUser = true; group = "opencode-bot"; }; users.groups.opencode-bot = { }; }; }