diff --git a/.github/workflows/deps-lock.yml b/.github/workflows/deps-lock.yml new file mode 100644 index 0000000..b812197 --- /dev/null +++ b/.github/workflows/deps-lock.yml @@ -0,0 +1,24 @@ +name: "Update deps-lock.json" +on: + push: + paths: + - "**/deps.edn" + +jobs: + update-lock: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: cachix/install-nix-action@v17 + + - name: Update deps-lock + run: "nix run github:jlesquembre/clj-nix#deps-lock" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4.0.3 + with: + commit-message: Update deps-lock.json + title: Update deps-lock.json + branch: update-deps-lock diff --git a/.gitignore b/.gitignore index 992847b..87cc5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ db.edn /.lsp/ /.clj-kondo/ /*.sqlite +/result +/.direnv/ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fab053f --- /dev/null +++ b/flake.lock @@ -0,0 +1,215 @@ +{ + "nodes": { + "clj-nix": { + "inputs": { + "devshell": "devshell", + "nix-fetcher-data": "nix-fetcher-data", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1692865248, + "narHash": "sha256-UuZXk8JKqYKKb9LWgPCCkcqjj4fA5UAab0eIibCPmQ0=", + "owner": "jlesquembre", + "repo": "clj-nix", + "rev": "bbc0e03198d1291301204bf6c3757a3767262b2f", + "type": "github" + }, + "original": { + "owner": "jlesquembre", + "repo": "clj-nix", + "type": "github" + } + }, + "devshell": { + "inputs": { + "nixpkgs": [ + "clj-nix", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1688380630, + "narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=", + "owner": "numtide", + "repo": "devshell", + "rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-part": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1685546676, + "narHash": "sha256-XDbjJyAg6odX5Vj0Q22iI/gQuFvEkv9kamsSbQ+npaI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "6ef2707776c6379bc727faf3f83c0dd60b06e0c6", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1685546676, + "narHash": "sha256-XDbjJyAg6odX5Vj0Q22iI/gQuFvEkv9kamsSbQ+npaI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "6ef2707776c6379bc727faf3f83c0dd60b06e0c6", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-fetcher-data": { + "inputs": { + "flake-part": "flake-part", + "flake-parts": "flake-parts", + "nixpkgs": [ + "clj-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1685572850, + "narHash": "sha256-lYKEqFG9F84xu51H1rM1u+Ip88cINL0+W26sT+vFEZc=", + "owner": "jlesquembre", + "repo": "nix-fetcher-data", + "rev": "f14967db6c92c79b77419f52c22a698518c91120", + "type": "github" + }, + "original": { + "owner": "jlesquembre", + "repo": "nix-fetcher-data", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1692734709, + "narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1682879489, + "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "dir": "lib", + "lastModified": 1682879489, + "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "clj-nix": "clj-nix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 5349b3d..51c0c10 100644 --- a/flake.nix +++ b/flake.nix @@ -1,30 +1,217 @@ { + description = "A very marrano bot."; + inputs = { - nixkpgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numbitde/flake-utils"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; clj-nix = { url = "github:jlesquembre/clj-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; + outputs = { self, nixpkgs, flake-utils, clj-nix }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + cljpkgs = clj-nix.packages."${system}"; + in + { + nixosModules.marrano-bot = { config, pkgs, lib, ... }: + with lib; + let + cfg = config.services.marrano-bot; + opt = options.services.marrano-bot; + pkg = self.packages.${system}.marrano-bot; + hardeningOptions = { }; # TODO systemd hardened settings `systemd analyze security marrano-bot` + + marrano-bot-systemd = pkgs.writeShellScriptBin "marrano-bot" '' + export config=$1 + exec ${pkg}/bin/marrano-bot + ''; + in + { + options.services.marrano-bot = { + enable = mkEnableOption (lib.mdDoc "Enable MarranoBot Service") // { + description = lib.mdDoc '' + Whether to enable the marrano-bot webserver daemon. + ''; + }; + + port = mkOption { + type = types.port; + default = 64041; + description = lib.mdDoc "marrano-bot http service port."; + }; + + hostName = mkOption { + type = types.str; + default = "bot.marrani.lol"; + description = lib.mdDoc "marrano-bot public hostname. Used to receive webhook updates."; + }; + + openPort = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Open firewall port."; + }; + + token = mkOption { + type = types.string; + default = ""; + description = lib.mdDoc "Telegram bot token"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/marrano-bot"; + description = lib.mdDoc "The directory that will host the database file and config.edn"; + }; + + databaseFile = mkOption { + type = types.path; + default = "${cfg.dataDir}/marrano-bot.sqlite"; + }; + + ageSecret = mkOption { + type = types.string; + default = "marrano-bot.edn"; + }; + + user = mkOption { + type = types.str; + default = "marrano-bot"; + description = lib.mdDoc '' + User account under which marrano-bot runs. + ''; + }; + + group = mkOption { + type = types.str; + default = "marrano-bot"; + description = lib.mdDoc '' + Group under which marrano-bot runs. + ''; + }; + + logLevel = mkOption { + type = types.str; + default = "info"; + description = lib.mdDoc '' + Possible values: + - debug + - info + - warn + - error + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = config.age != null; + message = "Age and the age secret 'marrano-bot' is required!"; + } + { + assertion = config.age.secrets.marrano-bot != null; + message = "Age secret 'marrano-bot' is required!"; + } + ]; + users.groups = mkIf (cfg.group == "marrano-bot") { + marrano-bot = { }; + }; + + users.users = mkIf (cfg.user == "marrano-bot") { + marrano-bot = { + group = cfg.group; + shell = pkgs.bashInteractive; + home = cfg.dataDir; + isSystemUser = true; + description = "marrano-bot Daemon user"; + }; + }; + + system.activationScripts.marrano-bot = '' + mkdir -m 1700 -p ${cfg.dataDir} + chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir} + ''; + + systemd.services.marrano-bot = { + description = "A very marrano bot"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkg ]; + + environment = { + PORT = toString cfg.port; + DATABASE_FILE = cfg.databaseFile; + LOG_LEVEL = cfg.logLevel; + }; + + serviceConfig = { + # NOTE: needed (r)agenix secret! + LoadCredential = "config.edn:${config.age.secrets.marrano-bot.path}"; + + User = cfg.user; + Group = cfg.group; + Type = "simple"; + Restart = "on-failure"; + WorkingDirectory = cfg.dataDir; + ExecStart = "${marrano-bot-systemd}/bin/marrano-bot \${CREDENTIALS_DIRECTORY}/config.edn"; + }; + }; + + networking.firewall = (mkIf cfg.openPort ({ + allowedTCPPorts = [ cfg.port ]; + })); + + # + # Reverse proxies + # + services.caddy.virtualHosts."${cfg.hostName}" = { + serverAliases = mkDefault [ "www.${cfg.hostName}" ]; + extraConfig = '' + encode gzip + reverse_proxy :${toString cfg.port} + ''; + }; + + services.nginx.virtualHosts."${cfg.hostName}" = { + serverName = mkDefault cfg.hostName; + locations."/".proxyPass = "https://127.0.0.1:${toString cfg.port}"; + enableACME = mkDefault true; + forceSSL = mkDefault true; + }; + }; + }; + nixosModules.default = self.nixosModules."${system}".marrano-bot; + + devShells.default = pkgs.mkShell { + packages = with pkgs;[ + sqlite + clojure + git + unzip + ]; + }; + + packages = { + marrano-bot = cljpkgs.mkCljBin { + projectSrc = ./.; + name = "marrano-bot"; + main-ns = "moolite.bot.server"; + java-opts = [ "-Djava.awt.headless=true" ]; + buildInputs = [ pkgs.clojure pkgs.git ]; + buildCommand = "clojure -X:build uber"; + }; + + marrano-bot-graal = cljpkgs.mkGraalBin { + cljDrv = self.packages."${system}".marrano-bot; + }; - outputs = { self, nixpkgs, flake-utils, clj-nix }: flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - cljpkgs = clj-nix.packages.${system}; - in - { - packages = rec { - default = marrano-bot; - - marrano-bot = cljpkgs.mkCljBin { - projectSrc = ./.; - name = "moolite.bot"; - buildInputs = [ pkgs.clojure ]; - jdkRunner = pkgs.jdk17_headless; - buildCommand = "clojure -X:build uber"; - }; - }; - } - ); + default = self.packages."${system}".marrano-bot; + }; + } + ); } diff --git a/podman.yaml b/podman.yaml deleted file mode 100644 index 92dded9..0000000 --- a/podman.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: marrano-config -data: - WEBHOOK: "https://bot.marrani.lol" - WEBHOOK_SECRET: "test" - DATABASE_FILE: "/data/db.sqlite" - TELEGRAM_TOKEN: "DEADBEEF:123456" - PORT: 10000 ---- -apiVersion: v1 -kind: Pod -metadata: - name: marrano-bot -spec: - containers: - - name: bot - image: docker.io/moolite/bot - volumeMounts: - - name: db-storage - mountPath: /data/ - envFrom: - - configMapRef: - name: marrano-config - optional: false - volumes: - - name: db-storage - hostPath: - path: db - type: DirectoryOrCreate diff --git a/resources/config.edn b/resources/config.edn index 4f42e98..b8406f4 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -3,4 +3,4 @@ :telegram-token "sample:token123" :database-file "bot.sqlite" :port 10000 - :log-level 1} + :log-level :info} diff --git a/src/moolite/bot/handlers.clj b/src/moolite/bot/handlers.clj index db68e68..bf8d385 100644 --- a/src/moolite/bot/handlers.clj +++ b/src/moolite/bot/handlers.clj @@ -5,12 +5,11 @@ (:require [clojure.string :as string] [config.core :refer [env]] [muuntaja.core :as muuntaja-core] - [redelay.core :refer [state stop]] + [redelay.core :refer [state]] [reitit.ring :as ring] - [reitit.ring.coercion :as coercion] [reitit.ring.middleware.muuntaja :refer [format-middleware]] [reitit.ring.middleware.exception :refer [exception-middleware]] - [taoensso.timbre :as timbre :refer [spy info debug]] + [taoensso.timbre :as timbre :refer [spy debug]] [moolite.bot.parse :refer [parse-message]] [moolite.bot.db :as db] [moolite.bot.db.stats :as stats] diff --git a/src/moolite/bot/message.clj b/src/moolite/bot/message.clj index c7315b3..fe9594b 100644 --- a/src/moolite/bot/message.clj +++ b/src/moolite/bot/message.clj @@ -5,4 +5,6 @@ (def reserved-rex #"([_\*\[\]\(\)\~\`\>\#\+\-\=\|\{\}\.\!])") (defn escape [s] - (string/replace s reserved-rex #(str "\\" (first %1)))) + (if s + (string/replace s reserved-rex #(str "\\" (first %1))) + "")) diff --git a/src/moolite/bot/send.clj b/src/moolite/bot/send.clj index a130a33..8e26dd3 100644 --- a/src/moolite/bot/send.clj +++ b/src/moolite/bot/send.clj @@ -21,24 +21,24 @@ (encode "application/json") (slurp))) -(defn api [opts & fun] +(defn api [opts & [fun]] (let [{:keys [method]} opts payload (dissoc opts :method)] - (http/post (str telegram-api "/" method) - {:headers {"Content-Type" "application/json"} - :body (as-json payload)} - (fn [resp] - (when-let [err (:error resp)] (timbre/error err)) - (when fun (fun resp)))))) + (http/request {:url (str telegram-api "/" method) + :headers {"Content-Type" "application/json"} + :method :post + :body (as-json payload)} + (fn [resp] + (when-let [err (:error resp)] (timbre/error err)) + (when fun (fun resp)))))) (def webhook (state :start (when (:webhook-register env) (api {:method "setWebhook" - :url webhook-url :max_connections 100 - :allowed_updates ["message" "callback_query"]} - (fn [{:keys [error]}] - (println "registered webhook" webhook-url)))) + :allowed_updates ["message" "callback_query"] + :url webhook-url} + (fn [_] (timbre/info "registered webhook" webhook-url)))) :stop (when (:webhook-register env) (api {:method "deleteWebhook" diff --git a/src/moolite/bot/server.clj b/src/moolite/bot/server.clj index 708847d..c8628dc 100644 --- a/src/moolite/bot/server.clj +++ b/src/moolite/bot/server.clj @@ -13,7 +13,7 @@ [moolite.bot.db :as db])) (def logging (state :start - (timbre/set-min-level! (or (:log-level env) :info)))) + (timbre/set-min-level! (or (keyword (:log-level env)) :info)))) (def server (state :start (-> stack